summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/dull-carpets-breathe.md24
-rw-r--r--examples/container-with-vitest/test/ReactWrapper.test.ts2
-rw-r--r--packages/astro/src/@types/astro.ts44
-rw-r--r--packages/astro/src/container/index.ts36
-rw-r--r--packages/astro/test/container.test.js27
-rw-r--r--packages/astro/test/fixtures/container-react/astro.config.mjs7
-rw-r--r--packages/astro/test/fixtures/container-react/package.json12
-rw-r--r--packages/astro/test/fixtures/container-react/src/components/button.jsx5
-rw-r--r--packages/astro/test/fixtures/container-react/src/pages/api.ts10
-rw-r--r--packages/integrations/react/server.js1
-rw-r--r--packages/integrations/react/src/index.ts69
-rw-r--r--packages/integrations/react/src/version.ts34
-rw-r--r--packages/integrations/react/tsconfig.json2
-rw-r--r--pnpm-lock.yaml15
14 files changed, 213 insertions, 75 deletions
diff --git a/.changeset/dull-carpets-breathe.md b/.changeset/dull-carpets-breathe.md
new file mode 100644
index 000000000..63aca3cef
--- /dev/null
+++ b/.changeset/dull-carpets-breathe.md
@@ -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);
+}
+```
diff --git a/examples/container-with-vitest/test/ReactWrapper.test.ts b/examples/container-with-vitest/test/ReactWrapper.test.ts
index 6adbff6cf..099e0c8a3 100644
--- a/examples/container-with-vitest/test/ReactWrapper.test.ts
+++ b/examples/container-with-vitest/test/ReactWrapper.test.ts
@@ -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 () => {
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index d30b1b3ba..69c8f9368 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -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<
diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts
index 015d19272..47fac93a9 100644
--- a/packages/astro/src/container/index.ts
+++ b/packages/astro/src/container/index.ts
@@ -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(
diff --git a/packages/astro/test/container.test.js b/packages/astro/test/container.test.js
index e28506988..7f873fb19 100644
--- a/packages/astro/test/container.test.js
+++ b/packages/astro/test/container.test.js
@@ -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/)
+ })
+});
+
diff --git a/packages/astro/test/fixtures/container-react/astro.config.mjs b/packages/astro/test/fixtures/container-react/astro.config.mjs
new file mode 100644
index 000000000..e7ce274c0
--- /dev/null
+++ b/packages/astro/test/fixtures/container-react/astro.config.mjs
@@ -0,0 +1,7 @@
+import react from '@astrojs/react';
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [react()],
+});
diff --git a/packages/astro/test/fixtures/container-react/package.json b/packages/astro/test/fixtures/container-react/package.json
new file mode 100644
index 000000000..43d164ce8
--- /dev/null
+++ b/packages/astro/test/fixtures/container-react/package.json
@@ -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"
+ }
+}
diff --git a/packages/astro/test/fixtures/container-react/src/components/button.jsx b/packages/astro/test/fixtures/container-react/src/components/button.jsx
new file mode 100644
index 000000000..2eeffc334
--- /dev/null
+++ b/packages/astro/test/fixtures/container-react/src/components/button.jsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export default () => {
+ return <button id="arrow-fn-component">I am a react button</button>;
+}
diff --git a/packages/astro/test/fixtures/container-react/src/pages/api.ts b/packages/astro/test/fixtures/container-react/src/pages/api.ts
new file mode 100644
index 000000000..7fae87247
--- /dev/null
+++ b/packages/astro/test/fixtures/container-react/src/pages/api.ts
@@ -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);
+}
diff --git a/packages/integrations/react/server.js b/packages/integrations/react/server.js
index c2b255853..efdd72102 100644
--- a/packages/integrations/react/server.js
+++ b/packages/integrations/react/server.js
@@ -230,3 +230,4 @@ export default {
renderToStaticMarkup,
supportsAstroStaticSlot: true,
};
+
diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts
index 85d79eef8..c20146949 100644
--- a/packages/integrations/react/src/index.ts
+++ b/packages/integrations/react/src/index.ts
@@ -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,
+ };
+}
diff --git a/packages/integrations/react/src/version.ts b/packages/integrations/react/src/version.ts
new file mode 100644
index 000000000..dc3a7a85a
--- /dev/null
+++ b/packages/integrations/react/src/version.ts
@@ -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'],
+ },
+};
diff --git a/packages/integrations/react/tsconfig.json b/packages/integrations/react/tsconfig.json
index 1504b4b6d..3d1121296 100644
--- a/packages/integrations/react/tsconfig.json
+++ b/packages/integrations/react/tsconfig.json
@@ -3,5 +3,5 @@
"include": ["src"],
"compilerOptions": {
"outDir": "./dist"
- }
+ },
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ef034d50c..fea9345a6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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':