mirror of
https://github.com/withastro/astro.git
synced 2025-03-17 23:11:29 -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 container = await AstroContainer.create({
|
||||
renderers,
|
||||
renderers
|
||||
});
|
||||
|
||||
test('ReactWrapper with react renderer', async () => {
|
||||
|
|
|
@ -2977,27 +2977,29 @@ export interface AstroRenderer {
|
|||
jsxTransformOptions?: JSXTransformFn;
|
||||
}
|
||||
|
||||
export interface SSRLoadedRenderer extends AstroRenderer {
|
||||
ssr: {
|
||||
check: AsyncRendererComponentFn<boolean>;
|
||||
renderToStaticMarkup: AsyncRendererComponentFn<{
|
||||
html: string;
|
||||
attrs?: Record<string, string>;
|
||||
}>;
|
||||
supportsAstroStaticSlot?: boolean;
|
||||
/**
|
||||
* If provided, Astro will call this function and inject the returned
|
||||
* script in the HTML before the first component handled by this renderer.
|
||||
*
|
||||
* This feature is needed by some renderers (in particular, by Solid). The
|
||||
* Solid official hydration script sets up a page-level data structure.
|
||||
* It is mainly used to transfer data between the server side render phase
|
||||
* and the browser application state. Solid Components rendered later in
|
||||
* the HTML may inject tiny scripts into the HTML that call into this
|
||||
* page-level data structure.
|
||||
*/
|
||||
renderHydrationScript?: () => string;
|
||||
};
|
||||
export type SSRLoadedRendererValue = {
|
||||
check: AsyncRendererComponentFn<boolean>;
|
||||
renderToStaticMarkup: AsyncRendererComponentFn<{
|
||||
html: string;
|
||||
attrs?: Record<string, string>;
|
||||
}>;
|
||||
supportsAstroStaticSlot?: boolean;
|
||||
/**
|
||||
* If provided, Astro will call this function and inject the returned
|
||||
* script in the HTML before the first component handled by this renderer.
|
||||
*
|
||||
* This feature is needed by some renderers (in particular, by Solid). The
|
||||
* Solid official hydration script sets up a page-level data structure.
|
||||
* It is mainly used to transfer data between the server side render phase
|
||||
* and the browser application state. Solid Components rendered later in
|
||||
* the HTML may inject tiny scripts into the HTML that call into this
|
||||
* page-level data structure.
|
||||
*/
|
||||
renderHydrationScript?: () => string;
|
||||
}
|
||||
|
||||
export interface SSRLoadedRenderer extends Pick<AstroRenderer, 'name' | 'clientEntrypoint'> {
|
||||
ssr: SSRLoadedRendererValue;
|
||||
}
|
||||
|
||||
export type HookParameters<
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
import { posix } from 'node:path';
|
||||
import type {
|
||||
AstroConfig,
|
||||
AstroRenderer,
|
||||
AstroUserConfig,
|
||||
ComponentInstance,
|
||||
ContainerImportRendererFn,
|
||||
ContainerRenderer,
|
||||
MiddlewareHandler,
|
||||
Props,
|
||||
RouteData,
|
||||
RouteType,
|
||||
SSRLoadedRenderer,
|
||||
SSRLoadedRenderer, SSRLoadedRendererValue,
|
||||
SSRManifest,
|
||||
SSRResult,
|
||||
} 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.
|
||||
// @ematipico: I plan to use it for a possible integration that could help people
|
||||
private static async createFromManifest(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 {
|
||||
Fragment,
|
||||
|
@ -12,6 +12,8 @@ import {
|
|||
renderSlot,
|
||||
renderTemplate,
|
||||
} from '../dist/runtime/server/index.js';
|
||||
import {loadFixture} from "./test-utils.js";
|
||||
import testAdapter from "./test-adapter.js";
|
||||
|
||||
const BaseLayout = createComponent((result, _props, slots) => {
|
||||
return render`<html>
|
||||
|
@ -230,3 +232,26 @@ describe('Container', () => {
|
|||
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,
|
||||
supportsAstroStaticSlot: true,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import react, { type Options as ViteReactPluginOptions } from '@vitejs/plugin-react';
|
||||
import type { AstroIntegration, ContainerRenderer } from 'astro';
|
||||
import { version as ReactVersion } from 'react-dom';
|
||||
import type {AstroIntegration, ContainerRenderer} from 'astro';
|
||||
import type * as vite from 'vite';
|
||||
import {
|
||||
getReactMajorVersion,
|
||||
isUnsupportedVersion,
|
||||
versionsConfig,
|
||||
type ReactVersionConfig,
|
||||
type SupportedReactVersion,
|
||||
} from './version.js';
|
||||
|
||||
export type ReactIntegrationOptions = Pick<
|
||||
ViteReactPluginOptions,
|
||||
|
@ -12,39 +18,6 @@ export type ReactIntegrationOptions = Pick<
|
|||
|
||||
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) {
|
||||
return {
|
||||
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 {
|
||||
const virtualModule = 'astro:react:opts';
|
||||
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"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
|
@ -2539,6 +2539,21 @@ importers:
|
|||
specifier: workspace:*
|
||||
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:
|
||||
dependencies:
|
||||
'@astrojs/mdx':
|
||||
|
|
Loading…
Add table
Reference in a new issue