Skip to content

Commit 4385bf7

Browse files
authored
feat(@astrojs/react): export renderer for easy loading (#11234)
* wip * feat(@astrojs/react): export `renderer` for easy loading * restore change * chore: address feedback * revert changes * revert changes to react integration * update changeset
1 parent d07d2f7 commit 4385bf7

File tree

14 files changed

+213
-75
lines changed

14 files changed

+213
-75
lines changed

.changeset/dull-carpets-breathe.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Adds a new function called `addServerRenderer` to the Container API. Use this function to manually store renderers inside the instance of your container.
6+
7+
This new function should be preferred when using the Container API in environments like on-demand pages:
8+
9+
```ts
10+
import type {APIRoute} from "astro";
11+
import { experimental_AstroContainer } from "astro/container";
12+
import reactRenderer from '@astrojs/react/server.js';
13+
import vueRenderer from '@astrojs/vue/server.js';
14+
import ReactComponent from "../components/button.jsx"
15+
import VueComponent from "../components/button.vue"
16+
17+
export const GET: APIRoute = async (ctx) => {
18+
const container = await experimental_AstroContainer.create();
19+
container.addServerRenderer("@astrojs/react", reactRenderer);
20+
container.addServerRenderer("@astrojs/vue", vueRenderer);
21+
const vueComponent = await container.renderToString(VueComponent)
22+
return await container.renderToResponse(Component);
23+
}
24+
```

examples/container-with-vitest/test/ReactWrapper.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import ReactWrapper from '../src/components/ReactWrapper.astro';
66

77
const renderers = await loadRenderers([getContainerRenderer()]);
88
const container = await AstroContainer.create({
9-
renderers,
9+
renderers
1010
});
1111

1212
test('ReactWrapper with react renderer', async () => {

packages/astro/src/@types/astro.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2977,27 +2977,29 @@ export interface AstroRenderer {
29772977
jsxTransformOptions?: JSXTransformFn;
29782978
}
29792979

2980-
export interface SSRLoadedRenderer extends AstroRenderer {
2981-
ssr: {
2982-
check: AsyncRendererComponentFn<boolean>;
2983-
renderToStaticMarkup: AsyncRendererComponentFn<{
2984-
html: string;
2985-
attrs?: Record<string, string>;
2986-
}>;
2987-
supportsAstroStaticSlot?: boolean;
2988-
/**
2989-
* If provided, Astro will call this function and inject the returned
2990-
* script in the HTML before the first component handled by this renderer.
2991-
*
2992-
* This feature is needed by some renderers (in particular, by Solid). The
2993-
* Solid official hydration script sets up a page-level data structure.
2994-
* It is mainly used to transfer data between the server side render phase
2995-
* and the browser application state. Solid Components rendered later in
2996-
* the HTML may inject tiny scripts into the HTML that call into this
2997-
* page-level data structure.
2998-
*/
2999-
renderHydrationScript?: () => string;
3000-
};
2980+
export type SSRLoadedRendererValue = {
2981+
check: AsyncRendererComponentFn<boolean>;
2982+
renderToStaticMarkup: AsyncRendererComponentFn<{
2983+
html: string;
2984+
attrs?: Record<string, string>;
2985+
}>;
2986+
supportsAstroStaticSlot?: boolean;
2987+
/**
2988+
* If provided, Astro will call this function and inject the returned
2989+
* script in the HTML before the first component handled by this renderer.
2990+
*
2991+
* This feature is needed by some renderers (in particular, by Solid). The
2992+
* Solid official hydration script sets up a page-level data structure.
2993+
* It is mainly used to transfer data between the server side render phase
2994+
* and the browser application state. Solid Components rendered later in
2995+
* the HTML may inject tiny scripts into the HTML that call into this
2996+
* page-level data structure.
2997+
*/
2998+
renderHydrationScript?: () => string;
2999+
}
3000+
3001+
export interface SSRLoadedRenderer extends Pick<AstroRenderer, 'name' | 'clientEntrypoint'> {
3002+
ssr: SSRLoadedRendererValue;
30013003
}
30023004

30033005
export type HookParameters<

packages/astro/src/container/index.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import { posix } from 'node:path';
22
import type {
33
AstroConfig,
4-
AstroRenderer,
54
AstroUserConfig,
65
ComponentInstance,
76
ContainerImportRendererFn,
8-
ContainerRenderer,
97
MiddlewareHandler,
108
Props,
119
RouteData,
1210
RouteType,
13-
SSRLoadedRenderer,
11+
SSRLoadedRenderer, SSRLoadedRendererValue,
1412
SSRManifest,
1513
SSRResult,
1614
} from '../@types/astro.js';
@@ -270,6 +268,38 @@ export class experimental_AstroContainer {
270268
});
271269
}
272270

271+
/**
272+
* Use this function to manually add a renderer to the container.
273+
*
274+
* This function is preferred when you require to use the container with a renderer in environments such as on-demand pages.
275+
*
276+
* ## Example
277+
*
278+
* ```js
279+
* import reactRenderer from "@astrojs/react/server.js";
280+
* import vueRenderer from "@astrojs/vue/server.js";
281+
* import { experimental_AstroContainer as AstroContainer } from "astro/container"
282+
*
283+
* const container = await AstroContainer.create();
284+
* container.addServerRenderer("@astrojs/react", reactRenderer);
285+
* container.addServerRenderer("@astrojs/vue", vueRenderer);
286+
* ```
287+
*
288+
* @param name The name of the renderer. The name **isn't** arbitrary, and it should match the name of the package.
289+
* @param renderer The server renderer exported by integration.
290+
*/
291+
public addServerRenderer(name: string, renderer: SSRLoadedRendererValue) {
292+
if (!renderer.check || !renderer.renderToStaticMarkup) {
293+
throw new Error("The renderer you passed isn't valid. A renderer is usually an object that exposes the `check` and `renderToStaticMarkup` functions.\n" +
294+
"Usually, the renderer is exported by a /server.js entrypoint e.g. `import renderer from '@astrojs/react/server.js'`")
295+
}
296+
297+
this.#pipeline.manifest.renderers.push({
298+
name,
299+
ssr: renderer
300+
})
301+
}
302+
273303
// NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it.
274304
// @ematipico: I plan to use it for a possible integration that could help people
275305
private static async createFromManifest(

packages/astro/test/container.test.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import assert from 'node:assert/strict';
2-
import { describe, it } from 'node:test';
2+
import { describe, it, before } from 'node:test';
33
import { experimental_AstroContainer } from '../dist/container/index.js';
44
import {
55
Fragment,
@@ -12,6 +12,8 @@ import {
1212
renderSlot,
1313
renderTemplate,
1414
} from '../dist/runtime/server/index.js';
15+
import {loadFixture} from "./test-utils.js";
16+
import testAdapter from "./test-adapter.js";
1517

1618
const BaseLayout = createComponent((result, _props, slots) => {
1719
return render`<html>
@@ -230,3 +232,26 @@ describe('Container', () => {
230232
assert.match(result, /Is open/);
231233
});
232234
});
235+
236+
describe('Container with renderers', () => {
237+
let fixture
238+
let app;
239+
before(async () => {
240+
fixture = await loadFixture({
241+
root: new URL('./fixtures/container-react/', import.meta.url),
242+
output: "server",
243+
adapter: testAdapter()
244+
});
245+
await fixture.build();
246+
app = await fixture.loadTestAdapterApp();
247+
});
248+
249+
it("the endpoint should return the HTML of the React component", async () => {
250+
const request = new Request("https://example.com/api");
251+
const response = await app.render(request)
252+
const html = await response.text()
253+
254+
assert.match(html, /I am a react button/)
255+
})
256+
});
257+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import react from '@astrojs/react';
2+
import { defineConfig } from 'astro/config';
3+
4+
// https://astro.build/config
5+
export default defineConfig({
6+
integrations: [react()],
7+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@test/react-container",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"dependencies": {
7+
"@astrojs/react": "workspace:*",
8+
"astro": "workspace:*",
9+
"react": "^18.3.1",
10+
"react-dom": "^18.3.1"
11+
}
12+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React from 'react';
2+
3+
export default () => {
4+
return <button id="arrow-fn-component">I am a react button</button>;
5+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type {APIRoute, SSRLoadedRenderer} from "astro";
2+
import { experimental_AstroContainer } from "astro/container";
3+
import server from '@astrojs/react/server.js';
4+
import Component from "../components/button.jsx"
5+
6+
export const GET: APIRoute = async (ctx) => {
7+
const container = await experimental_AstroContainer.create();
8+
container.addServerRenderer("@astrojs/react", server);
9+
return await container.renderToResponse(Component);
10+
}

packages/integrations/react/server.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,4 @@ export default {
230230
renderToStaticMarkup,
231231
supportsAstroStaticSlot: true,
232232
};
233+

0 commit comments

Comments
 (0)