summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/fair-singers-reflect.md30
-rw-r--r--.changeset/gold-mayflies-beam.md36
-rw-r--r--examples/container-with-vitest/package.json4
-rw-r--r--examples/container-with-vitest/test/ReactWrapper.test.ts16
-rw-r--r--packages/astro/client.d.ts4
-rw-r--r--packages/astro/src/@types/astro.ts16
-rw-r--r--packages/astro/src/container/index.ts76
-rw-r--r--packages/astro/src/virtual-modules/container.ts32
-rw-r--r--packages/integrations/lit/src/index.ts9
-rw-r--r--packages/integrations/mdx/src/index.ts9
-rw-r--r--packages/integrations/preact/src/index.ts9
-rw-r--r--packages/integrations/react/src/index.ts15
-rw-r--r--packages/integrations/solid/src/index.ts14
-rw-r--r--packages/integrations/svelte/src/index.ts9
-rw-r--r--packages/integrations/vue/src/index.ts9
-rw-r--r--pnpm-lock.yaml4
16 files changed, 233 insertions, 59 deletions
diff --git a/.changeset/fair-singers-reflect.md b/.changeset/fair-singers-reflect.md
new file mode 100644
index 000000000..e18ec42f9
--- /dev/null
+++ b/.changeset/fair-singers-reflect.md
@@ -0,0 +1,30 @@
+---
+"@astrojs/preact": minor
+"@astrojs/svelte": minor
+"@astrojs/react": minor
+"@astrojs/solid-js": minor
+"@astrojs/lit": minor
+"@astrojs/mdx": minor
+"@astrojs/vue": minor
+"astro": patch
+---
+
+The integration now exposes a function called `getContainerRenderer`, that can be used inside the Container APIs to load the relative renderer.
+
+```js
+import { experimental_AstroContainer as AstroContainer } from 'astro/container';
+import ReactWrapper from '../src/components/ReactWrapper.astro';
+import { loadRenderers } from "astro:container";
+import { getContainerRenderer } from "@astrojs/react";
+
+test('ReactWrapper with react renderer', async () => {
+ const renderers = await loadRenderers([getContainerRenderer()])
+ const container = await AstroContainer.create({
+ renderers,
+ });
+ const result = await container.renderToString(ReactWrapper);
+
+ expect(result).toContain('Counter');
+ expect(result).toContain('Count: <!-- -->5');
+});
+```
diff --git a/.changeset/gold-mayflies-beam.md b/.changeset/gold-mayflies-beam.md
new file mode 100644
index 000000000..d500b30b4
--- /dev/null
+++ b/.changeset/gold-mayflies-beam.md
@@ -0,0 +1,36 @@
+---
+"astro": patch
+---
+
+**BREAKING CHANGE to the experimental Container API only**
+
+Changes the **type** of the `renderers` option of the `AstroContainer::create` function and adds a dedicated function `loadRenderers()` to load the rendering scripts from renderer integration packages (`@astrojs/react`, `@astrojs/preact`, `@astrojs/solid-js`, `@astrojs/svelte`, `@astrojs/vue`, `@astrojs/lit`, and `@astrojs/mdx`).
+
+You no longer need to know the individual, direct file paths to the client and server rendering scripts for each renderer integration package. Now, there is a dedicated function to load the renderer from each package, which is available from `getContainerRenderer()`:
+
+```diff
+import { experimental_AstroContainer as AstroContainer } from 'astro/container';
+import ReactWrapper from '../src/components/ReactWrapper.astro';
+import { loadRenderers } from "astro:container";
+import { getContainerRenderer } from "@astrojs/react";
+
+test('ReactWrapper with react renderer', async () => {
++ const renderers = await loadRenderers([getContainerRenderer()])
+- const renderers = [
+- {
+- name: '@astrojs/react',
+- clientEntrypoint: '@astrojs/react/client.js',
+- serverEntrypoint: '@astrojs/react/server.js',
+- },
+- ];
+ const container = await AstroContainer.create({
+ renderers,
+ });
+ const result = await container.renderToString(ReactWrapper);
+
+ expect(result).toContain('Counter');
+ expect(result).toContain('Count: <!-- -->5');
+});
+```
+
+The new `loadRenderers()` helper function is available from `astro:container`, a virtual module that can be used when running the Astro container inside `vite`.
diff --git a/examples/container-with-vitest/package.json b/examples/container-with-vitest/package.json
index 8f07d620c..e8cb1536a 100644
--- a/examples/container-with-vitest/package.json
+++ b/examples/container-with-vitest/package.json
@@ -12,8 +12,8 @@
"test": "vitest run"
},
"dependencies": {
- "astro": "^4.9.3",
- "@astrojs/react": "^3.4.0",
+ "astro": "experimental--container",
+ "@astrojs/react": "experimental--container",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"vitest": "^1.6.0"
diff --git a/examples/container-with-vitest/test/ReactWrapper.test.ts b/examples/container-with-vitest/test/ReactWrapper.test.ts
index 91e3dd09d..70b938708 100644
--- a/examples/container-with-vitest/test/ReactWrapper.test.ts
+++ b/examples/container-with-vitest/test/ReactWrapper.test.ts
@@ -1,17 +1,15 @@
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import ReactWrapper from '../src/components/ReactWrapper.astro';
+import { loadRenderers } from 'astro:container';
+import { getContainerRenderer } from '@astrojs/react';
+
+const renderers = await loadRenderers([getContainerRenderer()]);
+const container = await AstroContainer.create({
+ renderers,
+});
test('ReactWrapper with react renderer', async () => {
- const container = await AstroContainer.create({
- renderers: [
- {
- name: '@astrojs/react',
- clientEntrypoint: '@astrojs/react/client.js',
- serverEntrypoint: '@astrojs/react/server.js',
- },
- ],
- });
const result = await container.renderToString(ReactWrapper);
expect(result).toContain('Counter');
diff --git a/packages/astro/client.d.ts b/packages/astro/client.d.ts
index 3083247cf..0870d3dcc 100644
--- a/packages/astro/client.d.ts
+++ b/packages/astro/client.d.ts
@@ -152,6 +152,10 @@ declare module 'astro:i18n' {
export * from 'astro/virtual-modules/i18n.js';
}
+declare module 'astro:container' {
+ export * from 'astro/virtual-modules/container.js';
+}
+
declare module 'astro:middleware' {
export * from 'astro/virtual-modules/middleware.js';
}
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 877b77bd9..ab1af4048 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -3290,3 +3290,19 @@ declare global {
'astro:page-load': Event;
}
}
+
+// Container types
+export type ContainerImportRendererFn = (
+ containerRenderer: ContainerRenderer
+) => Promise<SSRLoadedRenderer>;
+
+export type ContainerRenderer = {
+ /**
+ * The name of the renderer.
+ */
+ name: string;
+ /**
+ * The entrypoint that is used to render a component on the server
+ */
+ serverEntrypoint: string;
+};
diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts
index 724c426f3..5a2606853 100644
--- a/packages/astro/src/container/index.ts
+++ b/packages/astro/src/container/index.ts
@@ -1,8 +1,11 @@
import { posix } from 'node:path';
import type {
+ AstroConfig,
AstroRenderer,
AstroUserConfig,
ComponentInstance,
+ ContainerImportRendererFn,
+ ContainerRenderer,
MiddlewareHandler,
Props,
RouteData,
@@ -83,8 +86,8 @@ export type ContainerRenderOptions = {
};
function createManifest(
- renderers: SSRLoadedRenderer[],
manifest?: AstroContainerManifest,
+ renderers?: SSRLoadedRenderer[],
middleware?: MiddlewareHandler
): SSRManifest {
const defaultMiddleware: MiddlewareHandler = (_, next) => {
@@ -102,7 +105,7 @@ function createManifest(
routes: manifest?.routes ?? [],
adapterName: '',
clientDirectives: manifest?.clientDirectives ?? new Map(),
- renderers: manifest?.renderers ?? renderers,
+ renderers: renderers ?? manifest?.renderers ?? [],
base: manifest?.base ?? ASTRO_CONFIG_DEFAULTS.base,
componentMetadata: manifest?.componentMetadata ?? new Map(),
inlinedScripts: manifest?.inlinedScripts ?? new Map(),
@@ -138,21 +141,9 @@ export type AstroContainerOptions = {
* @default []
* @description
*
- * List or renderers to use when rendering components. Usually they are entry points
- *
- * ## Example
- *
- * ```js
- * const container = await AstroContainer.create({
- * renderers: [{
- * name: "@astrojs/react"
- * client: "@astrojs/react/client.js"
- * server: "@astrojs/react/server.js"
- * }]
- * });
- * ```
+ * List or renderers to use when rendering components. Usually, you want to pass these in an SSR context.
*/
- renderers?: AstroRenderer[];
+ renderers?: SSRLoadedRenderer[];
/**
* @default {}
* @description
@@ -170,6 +161,17 @@ export type AstroContainerOptions = {
* ```
*/
astroConfig?: AstroContainerUserConfig;
+
+ // TODO: document out of experimental
+ resolve?: SSRResult['resolve'];
+
+ /**
+ * @default {}
+ * @description
+ *
+ * The raw manifest from the build output.
+ */
+ manifest?: SSRManifest;
};
type AstroContainerManifest = Pick<
@@ -195,6 +197,7 @@ type AstroContainerConstructor = {
renderers?: SSRLoadedRenderer[];
manifest?: AstroContainerManifest;
resolve?: SSRResult['resolve'];
+ astroConfig: AstroConfig;
};
export class experimental_AstroContainer {
@@ -206,24 +209,31 @@ export class experimental_AstroContainer {
*/
#withManifest = false;
+ /**
+ * Internal function responsible for importing a renderer
+ * @private
+ */
+ #getRenderer: ContainerImportRendererFn | undefined;
+
private constructor({
streaming = false,
- renderers = [],
manifest,
+ renderers,
resolve,
+ astroConfig,
}: AstroContainerConstructor) {
this.#pipeline = ContainerPipeline.create({
logger: new Logger({
level: 'info',
dest: nodeLogDestination,
}),
- manifest: createManifest(renderers, manifest),
+ manifest: createManifest(manifest, renderers),
streaming,
serverLike: true,
- renderers,
+ renderers: renderers ?? manifest?.renderers ?? [],
resolve: async (specifier: string) => {
if (this.#withManifest) {
- return this.#containerResolve(specifier);
+ return this.#containerResolve(specifier, astroConfig);
} else if (resolve) {
return resolve(specifier);
}
@@ -232,10 +242,10 @@ export class experimental_AstroContainer {
});
}
- async #containerResolve(specifier: string): Promise<string> {
+ async #containerResolve(specifier: string, astroConfig: AstroConfig): Promise<string> {
const found = this.#pipeline.manifest.entryModules[specifier];
if (found) {
- return new URL(found, ASTRO_CONFIG_DEFAULTS.build.client).toString();
+ return new URL(found, astroConfig.build.client).toString();
}
return found;
}
@@ -248,22 +258,9 @@ export class experimental_AstroContainer {
public static async create(
containerOptions: AstroContainerOptions = {}
): Promise<experimental_AstroContainer> {
- const { streaming = false, renderers = [] } = containerOptions;
- const loadedRenderers = await Promise.all(
- renderers.map(async (renderer) => {
- const mod = await import(renderer.serverEntrypoint);
- if (typeof mod.default !== 'undefined') {
- return {
- ...renderer,
- ssr: mod.default,
- } as SSRLoadedRenderer;
- }
- return undefined;
- })
- );
- const finalRenderers = loadedRenderers.filter((r): r is SSRLoadedRenderer => Boolean(r));
-
- return new experimental_AstroContainer({ streaming, renderers: finalRenderers });
+ const { streaming = false, manifest, renderers = [], resolve } = containerOptions;
+ const astroConfig = await validateConfig(ASTRO_CONFIG_DEFAULTS, process.cwd(), 'container');
+ return new experimental_AstroContainer({ streaming, manifest, renderers, astroConfig, resolve });
}
// NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it.
@@ -271,9 +268,10 @@ export class experimental_AstroContainer {
private static async createFromManifest(
manifest: SSRManifest
): Promise<experimental_AstroContainer> {
- const config = await validateConfig(ASTRO_CONFIG_DEFAULTS, process.cwd(), 'container');
+ const astroConfig = await validateConfig(ASTRO_CONFIG_DEFAULTS, process.cwd(), 'container');
const container = new experimental_AstroContainer({
manifest,
+ astroConfig,
});
container.#withManifest = true;
return container;
diff --git a/packages/astro/src/virtual-modules/container.ts b/packages/astro/src/virtual-modules/container.ts
new file mode 100644
index 000000000..a4da62972
--- /dev/null
+++ b/packages/astro/src/virtual-modules/container.ts
@@ -0,0 +1,32 @@
+import type { AstroRenderer, SSRLoadedRenderer } from '../@types/astro.js';
+
+/**
+ * Use this function to provide renderers to the `AstroContainer`:
+ *
+ * ```js
+ * import { getContainerRenderer } from "@astrojs/react";
+ * import { experimental_AstroContainer as AstroContainer } from "astro/container";
+ * import { loadRenderers } from "astro:container"; // use this only when using vite/vitest
+ *
+ * const renderers = await loadRenderers([ getContainerRenderer ]);
+ * const container = await AstroContainer.create({ renderers });
+ *
+ * ```
+ * @param renderers
+ */
+export async function loadRenderers(renderers: AstroRenderer[]) {
+ const loadedRenderers = await Promise.all(
+ renderers.map(async (renderer) => {
+ const mod = await import(renderer.serverEntrypoint);
+ if (typeof mod.default !== 'undefined') {
+ return {
+ ...renderer,
+ ssr: mod.default,
+ } as SSRLoadedRenderer;
+ }
+ return undefined;
+ })
+ );
+
+ return loadedRenderers.filter((r): r is SSRLoadedRenderer => Boolean(r));
+}
diff --git a/packages/integrations/lit/src/index.ts b/packages/integrations/lit/src/index.ts
index 6c86bd740..33aaf727d 100644
--- a/packages/integrations/lit/src/index.ts
+++ b/packages/integrations/lit/src/index.ts
@@ -1,5 +1,5 @@
import { readFileSync } from 'node:fs';
-import type { AstroIntegration } from 'astro';
+import type { AstroIntegration, ContainerRenderer } from 'astro';
function getViteConfiguration() {
return {
@@ -19,6 +19,13 @@ function getViteConfiguration() {
};
}
+export function getContainerRenderer(): ContainerRenderer {
+ return {
+ name: '@astrojs/lit',
+ serverEntrypoint: '@astrojs/lit/server.js',
+ };
+}
+
export default function (): AstroIntegration {
return {
name: '@astrojs/lit',
diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts
index 3aaed8787..bd0278933 100644
--- a/packages/integrations/mdx/src/index.ts
+++ b/packages/integrations/mdx/src/index.ts
@@ -1,7 +1,7 @@
import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { markdownConfigDefaults } from '@astrojs/markdown-remark';
-import type { AstroIntegration, ContentEntryType, HookParameters } from 'astro';
+import type { AstroIntegration, ContainerRenderer, ContentEntryType, HookParameters } from 'astro';
import astroJSXRenderer from 'astro/jsx/renderer.js';
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
import type { PluggableList } from 'unified';
@@ -28,6 +28,13 @@ type SetupHookParams = HookParameters<'astro:config:setup'> & {
addContentEntryType: (contentEntryType: ContentEntryType) => void;
};
+export function getContainerRenderer(): ContainerRenderer {
+ return {
+ name: 'astro:jsx',
+ serverEntrypoint: 'astro/jsx/server.js',
+ };
+}
+
export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroIntegration {
// @ts-expect-error Temporarily assign an empty object here, which will be re-assigned by the
// `astro:config:done` hook later. This is so that `vitePluginMdx` can get hold of a reference earlier.
diff --git a/packages/integrations/preact/src/index.ts b/packages/integrations/preact/src/index.ts
index bcca01dd0..aab4f9925 100644
--- a/packages/integrations/preact/src/index.ts
+++ b/packages/integrations/preact/src/index.ts
@@ -1,6 +1,6 @@
import { fileURLToPath } from 'node:url';
import { type PreactPluginOptions as VitePreactPluginOptions, preact } from '@preact/preset-vite';
-import type { AstroIntegration, AstroRenderer, ViteUserConfig } from 'astro';
+import type { AstroIntegration, AstroRenderer, ContainerRenderer, ViteUserConfig } from 'astro';
const babelCwd = new URL('../', import.meta.url);
@@ -12,6 +12,13 @@ function getRenderer(development: boolean): AstroRenderer {
};
}
+export function getContainerRenderer(): ContainerRenderer {
+ return {
+ name: '@astrojs/preact',
+ serverEntrypoint: '@astrojs/preact/server.js',
+ };
+}
+
export interface Options extends Pick<VitePreactPluginOptions, 'include' | 'exclude'> {
compat?: boolean;
devtools?: boolean;
diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts
index 838640239..85d79eef8 100644
--- a/packages/integrations/react/src/index.ts
+++ b/packages/integrations/react/src/index.ts
@@ -1,5 +1,5 @@
import react, { type Options as ViteReactPluginOptions } from '@vitejs/plugin-react';
-import type { AstroIntegration } from 'astro';
+import type { AstroIntegration, ContainerRenderer } from 'astro';
import { version as ReactVersion } from 'react-dom';
import type * as vite from 'vite';
@@ -53,6 +53,19 @@ 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;
diff --git a/packages/integrations/solid/src/index.ts b/packages/integrations/solid/src/index.ts
index a779dea60..1bbfa741f 100644
--- a/packages/integrations/solid/src/index.ts
+++ b/packages/integrations/solid/src/index.ts
@@ -1,4 +1,9 @@
-import type { AstroIntegration, AstroIntegrationLogger, AstroRenderer } from 'astro';
+import type {
+ AstroIntegration,
+ AstroIntegrationLogger,
+ AstroRenderer,
+ ContainerRenderer,
+} from 'astro';
import type { PluginOption, UserConfig } from 'vite';
import solid, { type Options as ViteSolidPluginOptions } from 'vite-plugin-solid';
@@ -94,6 +99,13 @@ function getRenderer(): AstroRenderer {
};
}
+export function getContainerRenderer(): ContainerRenderer {
+ return {
+ name: '@astrojs/solid',
+ serverEntrypoint: '@astrojs/solid-js/server.js',
+ };
+}
+
export interface Options extends Pick<ViteSolidPluginOptions, 'include' | 'exclude'> {
devtools?: boolean;
}
diff --git a/packages/integrations/svelte/src/index.ts b/packages/integrations/svelte/src/index.ts
index 9c38b9d05..b0db3505c 100644
--- a/packages/integrations/svelte/src/index.ts
+++ b/packages/integrations/svelte/src/index.ts
@@ -1,7 +1,7 @@
import { fileURLToPath } from 'node:url';
import type { Options } from '@sveltejs/vite-plugin-svelte';
import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
-import type { AstroIntegration, AstroRenderer } from 'astro';
+import type { AstroIntegration, AstroRenderer, ContainerRenderer } from 'astro';
import { VERSION } from 'svelte/compiler';
import type { UserConfig } from 'vite';
@@ -15,6 +15,13 @@ function getRenderer(): AstroRenderer {
};
}
+export function getContainerRenderer(): ContainerRenderer {
+ return {
+ name: '@astrojs/svelte',
+ serverEntrypoint: isSvelte5 ? '@astrojs/svelte/server-v5.js' : '@astrojs/svelte/server.js',
+ };
+}
+
async function svelteConfigHasPreprocess(root: URL) {
const svelteConfigFiles = ['./svelte.config.js', './svelte.config.cjs', './svelte.config.mjs'];
for (const file of svelteConfigFiles) {
diff --git a/packages/integrations/vue/src/index.ts b/packages/integrations/vue/src/index.ts
index 6edb82526..81afe3a2e 100644
--- a/packages/integrations/vue/src/index.ts
+++ b/packages/integrations/vue/src/index.ts
@@ -3,7 +3,7 @@ import type { Options as VueOptions } from '@vitejs/plugin-vue';
import vue from '@vitejs/plugin-vue';
import type { Options as VueJsxOptions } from '@vitejs/plugin-vue-jsx';
import { MagicString } from '@vue/compiler-sfc';
-import type { AstroIntegration, AstroRenderer, HookParameters } from 'astro';
+import type { AstroIntegration, AstroRenderer, ContainerRenderer, HookParameters } from 'astro';
import type { Plugin, UserConfig } from 'vite';
import type { VitePluginVueDevToolsOptions } from 'vite-plugin-vue-devtools';
@@ -32,6 +32,13 @@ function getJsxRenderer(): AstroRenderer {
};
}
+export function getContainerRenderer(): ContainerRenderer {
+ return {
+ name: '@astrojs/vue',
+ serverEntrypoint: '@astrojs/vue/server.js',
+ };
+}
+
function virtualAppEntrypoint(options?: Options): Plugin {
let isBuild: boolean;
let root: string;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2d24ea4f9..72df592a7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -155,10 +155,10 @@ importers:
examples/container-with-vitest:
dependencies:
'@astrojs/react':
- specifier: ^3.4.0
+ specifier: experimental--container
version: link:../../packages/integrations/react
astro:
- specifier: ^4.9.3
+ specifier: experimental--container
version: link:../../packages/astro
react:
specifier: ^18.3.1