Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(container): client hydration #11486

Merged
merged 6 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/afraid-cups-deliver.md
Original file line number Diff line number Diff line change
@@ -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);
```
50 changes: 48 additions & 2 deletions packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -95,6 +96,11 @@ export type AddServerRenderer =
name: string;
};

export type AddClientRenderer = {
name: string;
entrypoint: string;
};

function createManifest(
manifest?: AstroContainerManifest,
renderers?: SSRLoadedRenderer[],
Expand All @@ -114,7 +120,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(),
Expand Down Expand Up @@ -281,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.
*
Expand Down Expand Up @@ -324,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(
Expand Down
5 changes: 4 additions & 1 deletion packages/astro/src/runtime/server/render/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,10 @@ export async function renderComponent(
);

function handleCancellation(e: unknown) {
if (result.cancelled) return { render() {} };
if (result.cancelled)
return {
render() {},
};
throw e;
}
}
Expand Down
9 changes: 9 additions & 0 deletions packages/astro/test/container.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
import Button from "./button.jsx"
---

<div>
<p>Button not rendered</p>
<Button client:idle/>
</div>
Original file line number Diff line number Diff line change
@@ -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);
};
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
4 changes: 3 additions & 1 deletion packages/integrations/react/server.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
import type { NamedSSRLoadedRendererValue } from 'astro';
export default NamedSSRLoadedRendererValue;

declare const renderer: NamedSSRLoadedRendererValue;
export default renderer;
Loading