From ed69f8f98e66935ea2987db60e95f1bfa720eba0 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 26 Jun 2024 13:50:13 +0100 Subject: [PATCH] fix: prevent client hydration when rendering via Container API --- .changeset/afraid-cups-deliver.md | 5 +++++ .../src/components/ReactWrapper.astro | 2 +- packages/astro/src/@types/astro.ts | 6 ++++++ packages/astro/src/container/index.ts | 5 ++++- packages/astro/src/core/render-context.ts | 7 ++++++- packages/astro/src/runtime/server/hydration.ts | 2 +- packages/astro/src/runtime/server/render/component.ts | 3 ++- packages/astro/test/container.test.js | 8 ++++++++ .../src/components/buttonDirective.astro | 5 +++++ .../src/pages/button-directive.ts | 10 ++++++++++ .../container-custom-renderers/src/pages/react.ts | 2 +- packages/integrations/react/src/index.ts | 1 + 12 files changed, 50 insertions(+), 6 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..edbb27bfec --- /dev/null +++ b/.changeset/afraid-cups-deliver.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a bug in the Container API that was triggered when trying to render a client component with some `client:*` directives. diff --git a/examples/container-with-vitest/src/components/ReactWrapper.astro b/examples/container-with-vitest/src/components/ReactWrapper.astro index 73ac6baebd..07ee4c5b42 100644 --- a/examples/container-with-vitest/src/components/ReactWrapper.astro +++ b/examples/container-with-vitest/src/components/ReactWrapper.astro @@ -2,4 +2,4 @@ import Counter from './Counter.jsx'; --- - + diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 46490dfa8b..76ea2f01dc 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -3220,6 +3220,12 @@ 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 53bc33e4b9..7ce6d89b10 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'; @@ -114,7 +115,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(), @@ -435,6 +436,8 @@ export class experimental_AstroContainer { pathname: url.pathname, locals: options?.locals ?? {}, }); + // client directives aren't needed in this case + 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 e6ac423648..de9ffcc6e4 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -42,6 +42,9 @@ 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, @@ -56,6 +59,7 @@ export class RenderContext { public props: Props = {} ) { this.originalRoute = routeData; + this.skipHydration = false; } /** @@ -297,7 +301,7 @@ export class RenderContext { } async createResult(mod: ComponentInstance) { - const { cookies, pathname, pipeline, routeData, status } = this; + const { cookies, pathname, pipeline, routeData, status, skipHydration } = this; const { clientDirectives, inlinedScripts, compressHTML, manifest, renderers, resolve } = pipeline; const { links, scripts, styles } = await pipeline.headElements(routeData); @@ -343,6 +347,7 @@ export class RenderContext { request: this.request, scripts, styles, + skipHydration, actionResult, _metadata: { hasHydrationScript: false, diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index 28b5ff674e..ca88b8bd64 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) { + if (renderer.clientEntrypoint && !result.skipHydration) { 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 4473c7441f..6074282bbc 100644 --- a/packages/astro/src/runtime/server/render/component.ts +++ b/packages/astro/src/runtime/server/render/component.ts @@ -295,7 +295,8 @@ 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 + metadata.hydrate && + !result.skipHydration ) { throw new AstroError({ ...AstroErrorData.NoClientEntrypoint, diff --git a/packages/astro/test/container.test.js b/packages/astro/test/container.test.js index 72d233ce47..a054b819c8 100644 --- a/packages/astro/test/container.test.js +++ b/packages/astro/test/container.test.js @@ -261,4 +261,12 @@ 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, /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..01b6a03dfa --- /dev/null +++ b/packages/astro/test/fixtures/container-custom-renderers/src/components/buttonDirective.astro @@ -0,0 +1,5 @@ +--- +import Button from "./button.jsx" +--- + +