mirror of
https://github.com/withastro/astro.git
synced 2025-03-24 23:21:57 -05:00
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
This commit is contained in:
parent
d07d2f7ac9
commit
4385bf7a4d
14 changed files with 213 additions and 75 deletions
24
.changeset/dull-carpets-breathe.md
Normal file
24
.changeset/dull-carpets-breathe.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Adds a new function called `addServerRenderer` to the Container API. Use this function to manually store renderers inside the instance of your container.
|
||||||
|
|
||||||
|
This new function should be preferred when using the Container API in environments like on-demand pages:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type {APIRoute} from "astro";
|
||||||
|
import { experimental_AstroContainer } from "astro/container";
|
||||||
|
import reactRenderer from '@astrojs/react/server.js';
|
||||||
|
import vueRenderer from '@astrojs/vue/server.js';
|
||||||
|
import ReactComponent from "../components/button.jsx"
|
||||||
|
import VueComponent from "../components/button.vue"
|
||||||
|
|
||||||
|
export const GET: APIRoute = async (ctx) => {
|
||||||
|
const container = await experimental_AstroContainer.create();
|
||||||
|
container.addServerRenderer("@astrojs/react", reactRenderer);
|
||||||
|
container.addServerRenderer("@astrojs/vue", vueRenderer);
|
||||||
|
const vueComponent = await container.renderToString(VueComponent)
|
||||||
|
return await container.renderToResponse(Component);
|
||||||
|
}
|
||||||
|
```
|
|
@ -6,7 +6,7 @@ import ReactWrapper from '../src/components/ReactWrapper.astro';
|
||||||
|
|
||||||
const renderers = await loadRenderers([getContainerRenderer()]);
|
const renderers = await loadRenderers([getContainerRenderer()]);
|
||||||
const container = await AstroContainer.create({
|
const container = await AstroContainer.create({
|
||||||
renderers,
|
renderers
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ReactWrapper with react renderer', async () => {
|
test('ReactWrapper with react renderer', async () => {
|
||||||
|
|
|
@ -2977,27 +2977,29 @@ export interface AstroRenderer {
|
||||||
jsxTransformOptions?: JSXTransformFn;
|
jsxTransformOptions?: JSXTransformFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SSRLoadedRenderer extends AstroRenderer {
|
export type SSRLoadedRendererValue = {
|
||||||
ssr: {
|
check: AsyncRendererComponentFn<boolean>;
|
||||||
check: AsyncRendererComponentFn<boolean>;
|
renderToStaticMarkup: AsyncRendererComponentFn<{
|
||||||
renderToStaticMarkup: AsyncRendererComponentFn<{
|
html: string;
|
||||||
html: string;
|
attrs?: Record<string, string>;
|
||||||
attrs?: Record<string, string>;
|
}>;
|
||||||
}>;
|
supportsAstroStaticSlot?: boolean;
|
||||||
supportsAstroStaticSlot?: boolean;
|
/**
|
||||||
/**
|
* If provided, Astro will call this function and inject the returned
|
||||||
* If provided, Astro will call this function and inject the returned
|
* script in the HTML before the first component handled by this renderer.
|
||||||
* script in the HTML before the first component handled by this renderer.
|
*
|
||||||
*
|
* This feature is needed by some renderers (in particular, by Solid). The
|
||||||
* This feature is needed by some renderers (in particular, by Solid). The
|
* Solid official hydration script sets up a page-level data structure.
|
||||||
* Solid official hydration script sets up a page-level data structure.
|
* It is mainly used to transfer data between the server side render phase
|
||||||
* It is mainly used to transfer data between the server side render phase
|
* and the browser application state. Solid Components rendered later in
|
||||||
* and the browser application state. Solid Components rendered later in
|
* the HTML may inject tiny scripts into the HTML that call into this
|
||||||
* the HTML may inject tiny scripts into the HTML that call into this
|
* page-level data structure.
|
||||||
* page-level data structure.
|
*/
|
||||||
*/
|
renderHydrationScript?: () => string;
|
||||||
renderHydrationScript?: () => string;
|
}
|
||||||
};
|
|
||||||
|
export interface SSRLoadedRenderer extends Pick<AstroRenderer, 'name' | 'clientEntrypoint'> {
|
||||||
|
ssr: SSRLoadedRendererValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HookParameters<
|
export type HookParameters<
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
import { posix } from 'node:path';
|
import { posix } from 'node:path';
|
||||||
import type {
|
import type {
|
||||||
AstroConfig,
|
AstroConfig,
|
||||||
AstroRenderer,
|
|
||||||
AstroUserConfig,
|
AstroUserConfig,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
ContainerImportRendererFn,
|
ContainerImportRendererFn,
|
||||||
ContainerRenderer,
|
|
||||||
MiddlewareHandler,
|
MiddlewareHandler,
|
||||||
Props,
|
Props,
|
||||||
RouteData,
|
RouteData,
|
||||||
RouteType,
|
RouteType,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer, SSRLoadedRendererValue,
|
||||||
SSRManifest,
|
SSRManifest,
|
||||||
SSRResult,
|
SSRResult,
|
||||||
} from '../@types/astro.js';
|
} from '../@types/astro.js';
|
||||||
|
@ -270,6 +268,38 @@ export class experimental_AstroContainer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this function to manually add a 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.
|
||||||
|
*
|
||||||
|
* ## Example
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* import reactRenderer from "@astrojs/react/server.js";
|
||||||
|
* import vueRenderer from "@astrojs/vue/server.js";
|
||||||
|
* import { experimental_AstroContainer as AstroContainer } from "astro/container"
|
||||||
|
*
|
||||||
|
* const container = await AstroContainer.create();
|
||||||
|
* container.addServerRenderer("@astrojs/react", reactRenderer);
|
||||||
|
* container.addServerRenderer("@astrojs/vue", vueRenderer);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param name The name of the renderer. The name **isn't** arbitrary, and it should match the name of the package.
|
||||||
|
* @param renderer The server renderer exported by integration.
|
||||||
|
*/
|
||||||
|
public addServerRenderer(name: string, renderer: SSRLoadedRendererValue) {
|
||||||
|
if (!renderer.check || !renderer.renderToStaticMarkup) {
|
||||||
|
throw new Error("The renderer you passed isn't valid. A renderer is usually an object that exposes the `check` and `renderToStaticMarkup` functions.\n" +
|
||||||
|
"Usually, the renderer is exported by a /server.js entrypoint e.g. `import renderer from '@astrojs/react/server.js'`")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#pipeline.manifest.renderers.push({
|
||||||
|
name,
|
||||||
|
ssr: 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(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { describe, it } from 'node:test';
|
import { describe, it, before } from 'node:test';
|
||||||
import { experimental_AstroContainer } from '../dist/container/index.js';
|
import { experimental_AstroContainer } from '../dist/container/index.js';
|
||||||
import {
|
import {
|
||||||
Fragment,
|
Fragment,
|
||||||
|
@ -12,6 +12,8 @@ import {
|
||||||
renderSlot,
|
renderSlot,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
} from '../dist/runtime/server/index.js';
|
} from '../dist/runtime/server/index.js';
|
||||||
|
import {loadFixture} from "./test-utils.js";
|
||||||
|
import testAdapter from "./test-adapter.js";
|
||||||
|
|
||||||
const BaseLayout = createComponent((result, _props, slots) => {
|
const BaseLayout = createComponent((result, _props, slots) => {
|
||||||
return render`<html>
|
return render`<html>
|
||||||
|
@ -230,3 +232,26 @@ describe('Container', () => {
|
||||||
assert.match(result, /Is open/);
|
assert.match(result, /Is open/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Container with renderers', () => {
|
||||||
|
let fixture
|
||||||
|
let app;
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: new URL('./fixtures/container-react/', import.meta.url),
|
||||||
|
output: "server",
|
||||||
|
adapter: testAdapter()
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
app = await fixture.loadTestAdapterApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the endpoint should return the HTML of the React component", async () => {
|
||||||
|
const request = new Request("https://example.com/api");
|
||||||
|
const response = await app.render(request)
|
||||||
|
const html = await response.text()
|
||||||
|
|
||||||
|
assert.match(html, /I am a react button/)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
7
packages/astro/test/fixtures/container-react/astro.config.mjs
vendored
Normal file
7
packages/astro/test/fixtures/container-react/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import react from '@astrojs/react';
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [react()],
|
||||||
|
});
|
12
packages/astro/test/fixtures/container-react/package.json
vendored
Normal file
12
packages/astro/test/fixtures/container-react/package.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "@test/react-container",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/react": "workspace:*",
|
||||||
|
"astro": "workspace:*",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
}
|
||||||
|
}
|
5
packages/astro/test/fixtures/container-react/src/components/button.jsx
vendored
Normal file
5
packages/astro/test/fixtures/container-react/src/components/button.jsx
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return <button id="arrow-fn-component">I am a react button</button>;
|
||||||
|
}
|
10
packages/astro/test/fixtures/container-react/src/pages/api.ts
vendored
Normal file
10
packages/astro/test/fixtures/container-react/src/pages/api.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import type {APIRoute, SSRLoadedRenderer} from "astro";
|
||||||
|
import { experimental_AstroContainer } from "astro/container";
|
||||||
|
import server from '@astrojs/react/server.js';
|
||||||
|
import Component from "../components/button.jsx"
|
||||||
|
|
||||||
|
export const GET: APIRoute = async (ctx) => {
|
||||||
|
const container = await experimental_AstroContainer.create();
|
||||||
|
container.addServerRenderer("@astrojs/react", server);
|
||||||
|
return await container.renderToResponse(Component);
|
||||||
|
}
|
|
@ -230,3 +230,4 @@ export default {
|
||||||
renderToStaticMarkup,
|
renderToStaticMarkup,
|
||||||
supportsAstroStaticSlot: true,
|
supportsAstroStaticSlot: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import react, { type Options as ViteReactPluginOptions } from '@vitejs/plugin-react';
|
import react, { type Options as ViteReactPluginOptions } from '@vitejs/plugin-react';
|
||||||
import type { AstroIntegration, ContainerRenderer } from 'astro';
|
import type {AstroIntegration, ContainerRenderer} from 'astro';
|
||||||
import { version as ReactVersion } from 'react-dom';
|
|
||||||
import type * as vite from 'vite';
|
import type * as vite from 'vite';
|
||||||
|
import {
|
||||||
|
getReactMajorVersion,
|
||||||
|
isUnsupportedVersion,
|
||||||
|
versionsConfig,
|
||||||
|
type ReactVersionConfig,
|
||||||
|
type SupportedReactVersion,
|
||||||
|
} from './version.js';
|
||||||
|
|
||||||
export type ReactIntegrationOptions = Pick<
|
export type ReactIntegrationOptions = Pick<
|
||||||
ViteReactPluginOptions,
|
ViteReactPluginOptions,
|
||||||
|
@ -12,39 +18,6 @@ export type ReactIntegrationOptions = Pick<
|
||||||
|
|
||||||
const FAST_REFRESH_PREAMBLE = react.preambleCode;
|
const FAST_REFRESH_PREAMBLE = react.preambleCode;
|
||||||
|
|
||||||
const versionsConfig = {
|
|
||||||
17: {
|
|
||||||
server: '@astrojs/react/server-v17.js',
|
|
||||||
client: '@astrojs/react/client-v17.js',
|
|
||||||
externals: ['react-dom/server.js', 'react-dom/client.js'],
|
|
||||||
},
|
|
||||||
18: {
|
|
||||||
server: '@astrojs/react/server.js',
|
|
||||||
client: '@astrojs/react/client.js',
|
|
||||||
externals: ['react-dom/server', 'react-dom/client'],
|
|
||||||
},
|
|
||||||
19: {
|
|
||||||
server: '@astrojs/react/server.js',
|
|
||||||
client: '@astrojs/react/client.js',
|
|
||||||
externals: ['react-dom/server', 'react-dom/client'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type SupportedReactVersion = keyof typeof versionsConfig;
|
|
||||||
type ReactVersionConfig = (typeof versionsConfig)[SupportedReactVersion];
|
|
||||||
|
|
||||||
function getReactMajorVersion(): number {
|
|
||||||
const matches = /\d+\./.exec(ReactVersion);
|
|
||||||
if (!matches) {
|
|
||||||
return NaN;
|
|
||||||
}
|
|
||||||
return Number(matches[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isUnsupportedVersion(majorVersion: number) {
|
|
||||||
return majorVersion < 17 || majorVersion > 19 || Number.isNaN(majorVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRenderer(reactConfig: ReactVersionConfig) {
|
function getRenderer(reactConfig: ReactVersionConfig) {
|
||||||
return {
|
return {
|
||||||
name: '@astrojs/react',
|
name: '@astrojs/react',
|
||||||
|
@ -53,19 +26,6 @@ function getRenderer(reactConfig: ReactVersionConfig) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContainerRenderer(): ContainerRenderer {
|
|
||||||
const majorVersion = getReactMajorVersion();
|
|
||||||
if (isUnsupportedVersion(majorVersion)) {
|
|
||||||
throw new Error(`Unsupported React version: ${majorVersion}.`);
|
|
||||||
}
|
|
||||||
const versionConfig = versionsConfig[majorVersion as SupportedReactVersion];
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: '@astrojs/react',
|
|
||||||
serverEntrypoint: versionConfig.server,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionsPlugin(experimentalReactChildren: boolean): vite.Plugin {
|
function optionsPlugin(experimentalReactChildren: boolean): vite.Plugin {
|
||||||
const virtualModule = 'astro:react:opts';
|
const virtualModule = 'astro:react:opts';
|
||||||
const virtualModuleId = '\0' + virtualModule;
|
const virtualModuleId = '\0' + virtualModule;
|
||||||
|
@ -152,3 +112,16 @@ export default function ({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getContainerRenderer(): ContainerRenderer {
|
||||||
|
const majorVersion = getReactMajorVersion();
|
||||||
|
if (isUnsupportedVersion(majorVersion)) {
|
||||||
|
throw new Error(`Unsupported React version: ${majorVersion}.`);
|
||||||
|
}
|
||||||
|
const versionConfig = versionsConfig[majorVersion as SupportedReactVersion];
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: '@astrojs/react',
|
||||||
|
serverEntrypoint: versionConfig.server,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
34
packages/integrations/react/src/version.ts
Normal file
34
packages/integrations/react/src/version.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { version as ReactVersion } from 'react-dom';
|
||||||
|
|
||||||
|
export type SupportedReactVersion = keyof typeof versionsConfig;
|
||||||
|
export type ReactVersionConfig = (typeof versionsConfig)[SupportedReactVersion];
|
||||||
|
|
||||||
|
export function getReactMajorVersion(): number {
|
||||||
|
const matches = /\d+\./.exec(ReactVersion);
|
||||||
|
if (!matches) {
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
return Number(matches[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUnsupportedVersion(majorVersion: number) {
|
||||||
|
return majorVersion < 17 || majorVersion > 19 || Number.isNaN(majorVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const versionsConfig = {
|
||||||
|
17: {
|
||||||
|
server: '@astrojs/react/server-v17.js',
|
||||||
|
client: '@astrojs/react/client-v17.js',
|
||||||
|
externals: ['react-dom/server.js', 'react-dom/client.js'],
|
||||||
|
},
|
||||||
|
18: {
|
||||||
|
server: '@astrojs/react/server.js',
|
||||||
|
client: '@astrojs/react/client.js',
|
||||||
|
externals: ['react-dom/server', 'react-dom/client'],
|
||||||
|
},
|
||||||
|
19: {
|
||||||
|
server: '@astrojs/react/server.js',
|
||||||
|
client: '@astrojs/react/client.js',
|
||||||
|
externals: ['react-dom/server', 'react-dom/client'],
|
||||||
|
},
|
||||||
|
};
|
|
@ -3,5 +3,5 @@
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist"
|
"outDir": "./dist"
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
|
@ -2539,6 +2539,21 @@ importers:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../..
|
version: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/container-react:
|
||||||
|
dependencies:
|
||||||
|
'@astrojs/react':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../../integrations/react
|
||||||
|
astro:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../..
|
||||||
|
react:
|
||||||
|
specifier: ^18.3.1
|
||||||
|
version: 18.3.1
|
||||||
|
react-dom:
|
||||||
|
specifier: ^18.3.1
|
||||||
|
version: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
packages/astro/test/fixtures/content:
|
packages/astro/test/fixtures/content:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/mdx':
|
'@astrojs/mdx':
|
||||||
|
|
Loading…
Add table
Reference in a new issue