0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-01-20 22:12:38 -05:00

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 <matthew@skypack.dev>
This commit is contained in:
Emanuele Stoppa 2024-07-18 16:28:52 +01:00 committed by GitHub
parent aa05be3313
commit 9c0c8492d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 99 additions and 5 deletions

View file

@ -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);
```

View file

@ -14,6 +14,7 @@ import type {
SSRManifest, SSRManifest,
SSRResult, SSRResult,
} from '../@types/astro.js'; } from '../@types/astro.js';
import { getDefaultClientDirectives } from '../core/client-directive/index.js';
import { ASTRO_CONFIG_DEFAULTS } from '../core/config/schema.js'; import { ASTRO_CONFIG_DEFAULTS } from '../core/config/schema.js';
import { validateConfig } from '../core/config/validate.js'; import { validateConfig } from '../core/config/validate.js';
import { Logger } from '../core/logger/core.js'; import { Logger } from '../core/logger/core.js';
@ -96,6 +97,11 @@ export type AddServerRenderer =
name: string; name: string;
}; };
export type AddClientRenderer = {
name: string;
entrypoint: string;
};
function createManifest( function createManifest(
manifest?: AstroContainerManifest, manifest?: AstroContainerManifest,
renderers?: SSRLoadedRenderer[], renderers?: SSRLoadedRenderer[],
@ -116,7 +122,7 @@ function createManifest(
entryModules: manifest?.entryModules ?? {}, entryModules: manifest?.entryModules ?? {},
routes: manifest?.routes ?? [], routes: manifest?.routes ?? [],
adapterName: '', adapterName: '',
clientDirectives: manifest?.clientDirectives ?? new Map(), clientDirectives: manifest?.clientDirectives ?? getDefaultClientDirectives(),
renderers: renderers ?? manifest?.renderers ?? [], renderers: renderers ?? manifest?.renderers ?? [],
base: manifest?.base ?? ASTRO_CONFIG_DEFAULTS.base, base: manifest?.base ?? ASTRO_CONFIG_DEFAULTS.base,
componentMetadata: manifest?.componentMetadata ?? new Map(), 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. * 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. // 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 // @ematipico: I plan to use it for a possible integration that could help people
private static async createFromManifest( private static async createFromManifest(

View file

@ -521,7 +521,10 @@ export async function renderComponent(
); );
function handleCancellation(e: unknown) { function handleCancellation(e: unknown) {
if (result.cancelled) return { render() {} }; if (result.cancelled)
return {
render() {},
};
throw e; throw e;
} }
} }

View file

@ -261,4 +261,13 @@ describe('Container with renderers', () => {
assert.match(html, /I am a vue button/); 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/);
});
}); });

View file

@ -0,0 +1,8 @@
---
import Button from "./button.jsx"
---
<div>
<p>Button not rendered</p>
<Button client:idle/>
</div>

View file

@ -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);
};

View file

@ -1,4 +1,4 @@
import type {APIRoute, SSRLoadedRenderer} from "astro"; import type {APIRoute} from "astro";
import { experimental_AstroContainer } from "astro/container"; import { experimental_AstroContainer } from "astro/container";
import renderer from '@astrojs/react/server.js'; import renderer from '@astrojs/react/server.js';
import Component from "../components/button.jsx" import Component from "../components/button.jsx"

View file

@ -1,2 +1,4 @@
import type { NamedSSRLoadedRendererValue } from 'astro'; import type { NamedSSRLoadedRendererValue } from 'astro';
export default NamedSSRLoadedRendererValue;
declare const renderer: NamedSSRLoadedRendererValue;
export default renderer;