Skip to content

Commit 4385bf7

Browse files
authoredJun 13, 2024··
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

+24
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

+1-1
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

+23-21
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

+33-3
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

+26-1
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+
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+
});
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+
}
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+
}
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

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

‎packages/integrations/react/src/index.ts

+21-48
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import react, { type Options as ViteReactPluginOptions } from '@vitejs/plugin-react';
2-
import type { AstroIntegration, ContainerRenderer } from 'astro';
3-
import { version as ReactVersion } from 'react-dom';
2+
import type {AstroIntegration, ContainerRenderer} from 'astro';
43
import type * as vite from 'vite';
4+
import {
5+
getReactMajorVersion,
6+
isUnsupportedVersion,
7+
versionsConfig,
8+
type ReactVersionConfig,
9+
type SupportedReactVersion,
10+
} from './version.js';
511

612
export type ReactIntegrationOptions = Pick<
713
ViteReactPluginOptions,
@@ -12,39 +18,6 @@ export type ReactIntegrationOptions = Pick<
1218

1319
const FAST_REFRESH_PREAMBLE = react.preambleCode;
1420

15-
const versionsConfig = {
16-
17: {
17-
server: '@astrojs/react/server-v17.js',
18-
client: '@astrojs/react/client-v17.js',
19-
externals: ['react-dom/server.js', 'react-dom/client.js'],
20-
},
21-
18: {
22-
server: '@astrojs/react/server.js',
23-
client: '@astrojs/react/client.js',
24-
externals: ['react-dom/server', 'react-dom/client'],
25-
},
26-
19: {
27-
server: '@astrojs/react/server.js',
28-
client: '@astrojs/react/client.js',
29-
externals: ['react-dom/server', 'react-dom/client'],
30-
},
31-
};
32-
33-
type SupportedReactVersion = keyof typeof versionsConfig;
34-
type ReactVersionConfig = (typeof versionsConfig)[SupportedReactVersion];
35-
36-
function getReactMajorVersion(): number {
37-
const matches = /\d+\./.exec(ReactVersion);
38-
if (!matches) {
39-
return NaN;
40-
}
41-
return Number(matches[0]);
42-
}
43-
44-
function isUnsupportedVersion(majorVersion: number) {
45-
return majorVersion < 17 || majorVersion > 19 || Number.isNaN(majorVersion);
46-
}
47-
4821
function getRenderer(reactConfig: ReactVersionConfig) {
4922
return {
5023
name: '@astrojs/react',
@@ -53,19 +26,6 @@ function getRenderer(reactConfig: ReactVersionConfig) {
5326
};
5427
}
5528

56-
export function getContainerRenderer(): ContainerRenderer {
57-
const majorVersion = getReactMajorVersion();
58-
if (isUnsupportedVersion(majorVersion)) {
59-
throw new Error(`Unsupported React version: ${majorVersion}.`);
60-
}
61-
const versionConfig = versionsConfig[majorVersion as SupportedReactVersion];
62-
63-
return {
64-
name: '@astrojs/react',
65-
serverEntrypoint: versionConfig.server,
66-
};
67-
}
68-
6929
function optionsPlugin(experimentalReactChildren: boolean): vite.Plugin {
7030
const virtualModule = 'astro:react:opts';
7131
const virtualModuleId = '\0' + virtualModule;
@@ -152,3 +112,16 @@ export default function ({
152112
},
153113
};
154114
}
115+
116+
export function getContainerRenderer(): ContainerRenderer {
117+
const majorVersion = getReactMajorVersion();
118+
if (isUnsupportedVersion(majorVersion)) {
119+
throw new Error(`Unsupported React version: ${majorVersion}.`);
120+
}
121+
const versionConfig = versionsConfig[majorVersion as SupportedReactVersion];
122+
123+
return {
124+
name: '@astrojs/react',
125+
serverEntrypoint: versionConfig.server,
126+
};
127+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { version as ReactVersion } from 'react-dom';
2+
3+
export type SupportedReactVersion = keyof typeof versionsConfig;
4+
export type ReactVersionConfig = (typeof versionsConfig)[SupportedReactVersion];
5+
6+
export function getReactMajorVersion(): number {
7+
const matches = /\d+\./.exec(ReactVersion);
8+
if (!matches) {
9+
return NaN;
10+
}
11+
return Number(matches[0]);
12+
}
13+
14+
export function isUnsupportedVersion(majorVersion: number) {
15+
return majorVersion < 17 || majorVersion > 19 || Number.isNaN(majorVersion);
16+
}
17+
18+
export const versionsConfig = {
19+
17: {
20+
server: '@astrojs/react/server-v17.js',
21+
client: '@astrojs/react/client-v17.js',
22+
externals: ['react-dom/server.js', 'react-dom/client.js'],
23+
},
24+
18: {
25+
server: '@astrojs/react/server.js',
26+
client: '@astrojs/react/client.js',
27+
externals: ['react-dom/server', 'react-dom/client'],
28+
},
29+
19: {
30+
server: '@astrojs/react/server.js',
31+
client: '@astrojs/react/client.js',
32+
externals: ['react-dom/server', 'react-dom/client'],
33+
},
34+
};

‎packages/integrations/react/tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"include": ["src"],
44
"compilerOptions": {
55
"outDir": "./dist"
6-
}
6+
},
77
}

‎pnpm-lock.yaml

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.