diff --git a/.changeset/afraid-cups-deliver.md b/.changeset/afraid-cups-deliver.md index 7ef9653f3f7c..05cfb56775b4 100644 --- a/.changeset/afraid-cups-deliver.md +++ b/.changeset/afraid-cups-deliver.md @@ -2,11 +2,14 @@ 'astro': patch --- -Adds a new option to the Container API to skip client side directives. This option should be used if you render a component that uses `client:*` directives. +Adds a new function called `addClientRenderer` to the Container API. + +This function should be used when rendering components using the `client:*` directives. The `addClientRenderer` API must be used +*after* the use of the `addServerRenderer`: ```js const container = await experimental_AstroContainer.create(); -return await container.renderToResponse(Component, { - skipClientDirectives: true, -}); +container.addServerRenderer({renderer}); +container.addClientRenderer({name: '@astrojs/react', entrypoint: '@astrojs/react/client.js'}); +const response = await container.renderToResponse(Component); ``` diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 7882abdf2092..e54c6e037c0f 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -3339,12 +3339,6 @@ export interface SSRResult { response: AstroGlobal['response']; request: AstroGlobal['request']; actionResult?: ReturnType; - // Metadata used to signal Astro renderer to skip any client hydration, such as: - // - client directories - // - client entrypoint - // - renderer-url - skipHydration: boolean; - renderers: SSRLoadedRenderer[]; /** * Map of directive name (e.g. `load`) to the directive script code diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts index c8cf40674be6..557f32dfb4db 100644 --- a/packages/astro/src/container/index.ts +++ b/packages/astro/src/container/index.ts @@ -84,13 +84,6 @@ export type ContainerRenderOptions = { * ``` */ props?: Props; - - /** - * Allows to bypass clientside hydration of components. - * - * If you're testing components that use `client:*` directives, you might want to use this option. - */ - skipClientDirectives?: boolean; }; export type AddServerRenderer = @@ -103,6 +96,11 @@ export type AddServerRenderer = name: string; }; +export type AddClientRenderer = { + name: string; + entrypoint: string; +}; + function createManifest( manifest?: AstroContainerManifest, renderers?: SSRLoadedRenderer[], @@ -289,7 +287,7 @@ export class experimental_AstroContainer { } /** - * Use this function to manually add a renderer to the container. + * Use this function to manually add a **server** renderer to the container. * * This function is preferred when you require to use the container with a renderer in environments such as on-demand pages. * @@ -332,6 +330,46 @@ export class experimental_AstroContainer { } } + /** + * Use this function to manually add a **client** renderer to the container. + * + * When rendering components that use the `client:*` directives, you need to use this function. + * + * ## Example + * + * ```js + * import reactRenderer from "@astrojs/react/server.js"; + * import { experimental_AstroContainer as AstroContainer } from "astro/container" + * + * const container = await AstroContainer.create(); + * container.addServerRenderer(reactRenderer); + * container.addClientRenderer({ + * name: "@astrojs/react", + * entrypoint: "@astrojs/react/client.js" + * }); + * ``` + * + * @param options {object} + * @param options.name The name of the renderer. The name **isn't** arbitrary, and it should match the name of the package. + * @param options.entrypoint The entrypoint of the client renderer. + */ + public addClientRenderer(options: AddClientRenderer): void { + const { entrypoint, name } = options; + + const rendererIndex = this.#pipeline.manifest.renderers.findIndex((r) => r.name === name); + if (rendererIndex === -1) { + throw new Error( + 'You tried to add the ' + + name + + " client renderer, but its server renderer wasn't added. You must add the server renderer first. Use the `addServerRenderer` function." + ); + } + const renderer = this.#pipeline.manifest.renderers[rendererIndex]; + renderer.clientEntrypoint = entrypoint; + + this.#pipeline.manifest.renderers[rendererIndex] = renderer; + } + // NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it. // @ematipico: I plan to use it for a possible integration that could help people private static async createFromManifest( @@ -443,9 +481,6 @@ export class experimental_AstroContainer { pathname: url.pathname, locals: options?.locals ?? {}, }); - if (options.skipClientDirectives === true) { - renderContext.skipHydration = true; - } if (options.params) { renderContext.params = options.params; } diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 07a877e57109..bc97212a86b0 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -44,9 +44,6 @@ export class RenderContext { // The first route that this instance of the context attempts to render originalRoute: RouteData; - // Metadata used to signal Astro renderer to skip any client hydration - skipHydration: boolean; - private constructor( readonly pipeline: Pipeline, public locals: App.Locals, @@ -61,7 +58,6 @@ export class RenderContext { public props: Props = {} ) { this.originalRoute = routeData; - this.skipHydration = false; } /** @@ -323,7 +319,7 @@ export class RenderContext { } async createResult(mod: ComponentInstance) { - const { cookies, pathname, pipeline, routeData, status, skipHydration } = this; + const { cookies, pathname, pipeline, routeData, status } = this; const { clientDirectives, inlinedScripts, compressHTML, manifest, renderers, resolve } = pipeline; const { links, scripts, styles } = await pipeline.headElements(routeData); @@ -370,7 +366,6 @@ export class RenderContext { request: this.request, scripts, styles, - skipHydration, actionResult, serverIslandNameMap: manifest.serverIslandNameMap ?? new Map(), _metadata: { diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index ca88b8bd642e..28b5ff674e19 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -156,7 +156,7 @@ export async function generateHydrateScript( island.props['component-url'] = await result.resolve(decodeURI(componentUrl)); // Add renderer url - if (renderer.clientEntrypoint && !result.skipHydration) { + if (renderer.clientEntrypoint) { island.props['component-export'] = componentExport.value; island.props['renderer-url'] = await result.resolve(decodeURI(renderer.clientEntrypoint)); island.props['props'] = escapeHTML(serializeProps(props, metadata)); diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts index 5be435a0dc80..e6c9f3acb486 100644 --- a/packages/astro/src/runtime/server/render/component.ts +++ b/packages/astro/src/runtime/server/render/component.ts @@ -296,8 +296,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr renderer && !renderer.clientEntrypoint && renderer.name !== '@astrojs/lit' && - metadata.hydrate && - !result.skipHydration + metadata.hydrate ) { throw new AstroError({ ...AstroErrorData.NoClientEntrypoint, @@ -522,7 +521,10 @@ export async function renderComponent( ); function handleCancellation(e: unknown) { - if (result.cancelled) return { render() {} }; + if (result.cancelled) + return { + render() {}, + }; throw e; } } diff --git a/packages/astro/test/container.test.js b/packages/astro/test/container.test.js index a054b819c8dc..6af5dc1f9a40 100644 --- a/packages/astro/test/container.test.js +++ b/packages/astro/test/container.test.js @@ -267,6 +267,7 @@ describe('Container with renderers', () => { const response = await app.render(request); const html = await response.text(); + assert.match(html, /Button not rendered/); assert.match(html, /I am a react button/); }); }); diff --git a/packages/astro/test/fixtures/container-custom-renderers/src/components/buttonDirective.astro b/packages/astro/test/fixtures/container-custom-renderers/src/components/buttonDirective.astro index 01b6a03dfa76..48c6cc386b1b 100644 --- a/packages/astro/test/fixtures/container-custom-renderers/src/components/buttonDirective.astro +++ b/packages/astro/test/fixtures/container-custom-renderers/src/components/buttonDirective.astro @@ -2,4 +2,7 @@ import Button from "./button.jsx" --- -