From 9c0c8492d987cd9214ed53e71fb29599c206966a Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 18 Jul 2024 16:28:52 +0100 Subject: [PATCH] feat(container): client hydration (#11486) * fix: prevent client hydration when rendering via Container API * revert change that is not needed * skip client directives via option * reword changeset * Fix types of react server.d.ts * add new API --------- Co-authored-by: Matthew Phillips --- .changeset/afraid-cups-deliver.md | 15 ++++++ packages/astro/src/container/index.ts | 50 ++++++++++++++++++- .../src/runtime/server/render/component.ts | 5 +- packages/astro/test/container.test.js | 9 ++++ .../src/components/buttonDirective.astro | 8 +++ .../src/pages/button-directive.ts | 11 ++++ .../src/pages/react.ts | 2 +- packages/integrations/react/server.d.ts | 4 +- 8 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 .changeset/afraid-cups-deliver.md create mode 100644 packages/astro/test/fixtures/container-custom-renderers/src/components/buttonDirective.astro create mode 100644 packages/astro/test/fixtures/container-custom-renderers/src/pages/button-directive.ts diff --git a/.changeset/afraid-cups-deliver.md b/.changeset/afraid-cups-deliver.md new file mode 100644 index 0000000000..05cfb56775 --- /dev/null +++ b/.changeset/afraid-cups-deliver.md @@ -0,0 +1,15 @@ +--- +'astro': patch +--- + +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(); +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/container/index.ts b/packages/astro/src/container/index.ts index a8641b5eab..7e88f00e58 100644 --- a/packages/astro/src/container/index.ts +++ b/packages/astro/src/container/index.ts @@ -14,6 +14,7 @@ import type { SSRManifest, SSRResult, } from '../@types/astro.js'; +import { getDefaultClientDirectives } from '../core/client-directive/index.js'; import { ASTRO_CONFIG_DEFAULTS } from '../core/config/schema.js'; import { validateConfig } from '../core/config/validate.js'; import { Logger } from '../core/logger/core.js'; @@ -96,6 +97,11 @@ export type AddServerRenderer = name: string; }; +export type AddClientRenderer = { + name: string; + entrypoint: string; +}; + function createManifest( manifest?: AstroContainerManifest, renderers?: SSRLoadedRenderer[], @@ -116,7 +122,7 @@ function createManifest( entryModules: manifest?.entryModules ?? {}, routes: manifest?.routes ?? [], adapterName: '', - clientDirectives: manifest?.clientDirectives ?? new Map(), + clientDirectives: manifest?.clientDirectives ?? getDefaultClientDirectives(), renderers: renderers ?? manifest?.renderers ?? [], base: manifest?.base ?? ASTRO_CONFIG_DEFAULTS.base, componentMetadata: manifest?.componentMetadata ?? new Map(), @@ -283,7 +289,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. * @@ -326,6 +332,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( diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts index 0b552b9506..e6c9f3acb4 100644 --- a/packages/astro/src/runtime/server/render/component.ts +++ b/packages/astro/src/runtime/server/render/component.ts @@ -521,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 72d233ce47..6af5dc1f9a 100644 --- a/packages/astro/test/container.test.js +++ b/packages/astro/test/container.test.js @@ -261,4 +261,13 @@ describe('Container with renderers', () => { assert.match(html, /I am a vue button/); }); + + it('Should render a component with directives', async () => { + const request = new Request('https://example.com/button-directive'); + 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 new file mode 100644 index 0000000000..48c6cc386b --- /dev/null +++ b/packages/astro/test/fixtures/container-custom-renderers/src/components/buttonDirective.astro @@ -0,0 +1,8 @@ +--- +import Button from "./button.jsx" +--- + +
+

Button not rendered

+
diff --git a/packages/astro/test/fixtures/container-custom-renderers/src/pages/button-directive.ts b/packages/astro/test/fixtures/container-custom-renderers/src/pages/button-directive.ts new file mode 100644 index 0000000000..c3a2d377f6 --- /dev/null +++ b/packages/astro/test/fixtures/container-custom-renderers/src/pages/button-directive.ts @@ -0,0 +1,11 @@ +import type { APIRoute, SSRLoadedRenderer } from 'astro'; +import { experimental_AstroContainer } from 'astro/container'; +import renderer from '@astrojs/react/server.js'; +import Component from '../components/buttonDirective.astro'; + +export const GET: APIRoute = async (ctx) => { + const container = await experimental_AstroContainer.create(); + container.addServerRenderer({ renderer }); + container.addClientRenderer({ name: '@astrojs/react', entrypoint: '@astrojs/react/client.js' }); + return await container.renderToResponse(Component); +}; diff --git a/packages/astro/test/fixtures/container-custom-renderers/src/pages/react.ts b/packages/astro/test/fixtures/container-custom-renderers/src/pages/react.ts index d77eaead11..b8d44a3551 100644 --- a/packages/astro/test/fixtures/container-custom-renderers/src/pages/react.ts +++ b/packages/astro/test/fixtures/container-custom-renderers/src/pages/react.ts @@ -1,4 +1,4 @@ -import type {APIRoute, SSRLoadedRenderer} from "astro"; +import type {APIRoute} from "astro"; import { experimental_AstroContainer } from "astro/container"; import renderer from '@astrojs/react/server.js'; import Component from "../components/button.jsx" diff --git a/packages/integrations/react/server.d.ts b/packages/integrations/react/server.d.ts index bb2f29556c..75cc3eb64d 100644 --- a/packages/integrations/react/server.d.ts +++ b/packages/integrations/react/server.d.ts @@ -1,2 +1,4 @@ import type { NamedSSRLoadedRendererValue } from 'astro'; -export default NamedSSRLoadedRendererValue; + +declare const renderer: NamedSSRLoadedRendererValue; +export default renderer;