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:
parent
aa05be3313
commit
9c0c8492d9
8 changed files with 99 additions and 5 deletions
15
.changeset/afraid-cups-deliver.md
Normal file
15
.changeset/afraid-cups-deliver.md
Normal 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);
|
||||||
|
```
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
8
packages/astro/test/fixtures/container-custom-renderers/src/components/buttonDirective.astro
vendored
Normal file
8
packages/astro/test/fixtures/container-custom-renderers/src/components/buttonDirective.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
import Button from "./button.jsx"
|
||||||
|
---
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Button not rendered</p>
|
||||||
|
<Button client:idle/>
|
||||||
|
</div>
|
11
packages/astro/test/fixtures/container-custom-renderers/src/pages/button-directive.ts
vendored
Normal file
11
packages/astro/test/fixtures/container-custom-renderers/src/pages/button-directive.ts
vendored
Normal 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);
|
||||||
|
};
|
|
@ -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"
|
||||||
|
|
4
packages/integrations/react/server.d.ts
vendored
4
packages/integrations/react/server.d.ts
vendored
|
@ -1,2 +1,4 @@
|
||||||
import type { NamedSSRLoadedRendererValue } from 'astro';
|
import type { NamedSSRLoadedRendererValue } from 'astro';
|
||||||
export default NamedSSRLoadedRendererValue;
|
|
||||||
|
declare const renderer: NamedSSRLoadedRendererValue;
|
||||||
|
export default renderer;
|
||||||
|
|
Loading…
Add table
Reference in a new issue