Skip to content

Commit 803dd80

Browse files
ematipicomatthewpsarah11918
authoredJun 5, 2024··
feat(container): provide a virtual module to load renderers (#11144)
* feat(container): provide a virtual module to load renderers * address feedback * chore: restore some default to allow to have PHP prototype working * Thread through renderers and manifest * Pass manifest too * update changeset * add diff * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * fix diff * rebase and update lock --------- Co-authored-by: Matthew Phillips <matthew@skypack.dev> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
1 parent 587e75f commit 803dd80

File tree

16 files changed

+233
-59
lines changed

16 files changed

+233
-59
lines changed
 

‎.changeset/fair-singers-reflect.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@astrojs/preact": minor
3+
"@astrojs/svelte": minor
4+
"@astrojs/react": minor
5+
"@astrojs/solid-js": minor
6+
"@astrojs/lit": minor
7+
"@astrojs/mdx": minor
8+
"@astrojs/vue": minor
9+
"astro": patch
10+
---
11+
12+
The integration now exposes a function called `getContainerRenderer`, that can be used inside the Container APIs to load the relative renderer.
13+
14+
```js
15+
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
16+
import ReactWrapper from '../src/components/ReactWrapper.astro';
17+
import { loadRenderers } from "astro:container";
18+
import { getContainerRenderer } from "@astrojs/react";
19+
20+
test('ReactWrapper with react renderer', async () => {
21+
const renderers = await loadRenderers([getContainerRenderer()])
22+
const container = await AstroContainer.create({
23+
renderers,
24+
});
25+
const result = await container.renderToString(ReactWrapper);
26+
27+
expect(result).toContain('Counter');
28+
expect(result).toContain('Count: <!-- -->5');
29+
});
30+
```

‎.changeset/gold-mayflies-beam.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"astro": patch
3+
---
4+
5+
**BREAKING CHANGE to the experimental Container API only**
6+
7+
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`).
8+
9+
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()`:
10+
11+
```diff
12+
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
13+
import ReactWrapper from '../src/components/ReactWrapper.astro';
14+
import { loadRenderers } from "astro:container";
15+
import { getContainerRenderer } from "@astrojs/react";
16+
17+
test('ReactWrapper with react renderer', async () => {
18+
+ const renderers = await loadRenderers([getContainerRenderer()])
19+
- const renderers = [
20+
- {
21+
- name: '@astrojs/react',
22+
- clientEntrypoint: '@astrojs/react/client.js',
23+
- serverEntrypoint: '@astrojs/react/server.js',
24+
- },
25+
- ];
26+
const container = await AstroContainer.create({
27+
renderers,
28+
});
29+
const result = await container.renderToString(ReactWrapper);
30+
31+
expect(result).toContain('Counter');
32+
expect(result).toContain('Count: <!-- -->5');
33+
});
34+
```
35+
36+
The new `loadRenderers()` helper function is available from `astro:container`, a virtual module that can be used when running the Astro container inside `vite`.

‎examples/container-with-vitest/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
"test": "vitest run"
1313
},
1414
"dependencies": {
15-
"astro": "^4.9.3",
16-
"@astrojs/react": "^3.4.0",
15+
"astro": "experimental--container",
16+
"@astrojs/react": "experimental--container",
1717
"react": "^18.3.1",
1818
"react-dom": "^18.3.1",
1919
"vitest": "^1.6.0"

‎examples/container-with-vitest/test/ReactWrapper.test.ts

+7-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
22
import { expect, test } from 'vitest';
33
import ReactWrapper from '../src/components/ReactWrapper.astro';
4+
import { loadRenderers } from 'astro:container';
5+
import { getContainerRenderer } from '@astrojs/react';
6+
7+
const renderers = await loadRenderers([getContainerRenderer()]);
8+
const container = await AstroContainer.create({
9+
renderers,
10+
});
411

512
test('ReactWrapper with react renderer', async () => {
6-
const container = await AstroContainer.create({
7-
renderers: [
8-
{
9-
name: '@astrojs/react',
10-
clientEntrypoint: '@astrojs/react/client.js',
11-
serverEntrypoint: '@astrojs/react/server.js',
12-
},
13-
],
14-
});
1513
const result = await container.renderToString(ReactWrapper);
1614

1715
expect(result).toContain('Counter');

‎packages/astro/client.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ declare module 'astro:i18n' {
152152
export * from 'astro/virtual-modules/i18n.js';
153153
}
154154

155+
declare module 'astro:container' {
156+
export * from 'astro/virtual-modules/container.js';
157+
}
158+
155159
declare module 'astro:middleware' {
156160
export * from 'astro/virtual-modules/middleware.js';
157161
}

‎packages/astro/src/@types/astro.ts

+16
Original file line numberDiff line numberDiff line change
@@ -3290,3 +3290,19 @@ declare global {
32903290
'astro:page-load': Event;
32913291
}
32923292
}
3293+
3294+
// Container types
3295+
export type ContainerImportRendererFn = (
3296+
containerRenderer: ContainerRenderer
3297+
) => Promise<SSRLoadedRenderer>;
3298+
3299+
export type ContainerRenderer = {
3300+
/**
3301+
* The name of the renderer.
3302+
*/
3303+
name: string;
3304+
/**
3305+
* The entrypoint that is used to render a component on the server
3306+
*/
3307+
serverEntrypoint: string;
3308+
};

‎packages/astro/src/container/index.ts

+37-39
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { posix } from 'node:path';
22
import type {
3+
AstroConfig,
34
AstroRenderer,
45
AstroUserConfig,
56
ComponentInstance,
7+
ContainerImportRendererFn,
8+
ContainerRenderer,
69
MiddlewareHandler,
710
Props,
811
RouteData,
@@ -83,8 +86,8 @@ export type ContainerRenderOptions = {
8386
};
8487

8588
function createManifest(
86-
renderers: SSRLoadedRenderer[],
8789
manifest?: AstroContainerManifest,
90+
renderers?: SSRLoadedRenderer[],
8891
middleware?: MiddlewareHandler
8992
): SSRManifest {
9093
const defaultMiddleware: MiddlewareHandler = (_, next) => {
@@ -102,7 +105,7 @@ function createManifest(
102105
routes: manifest?.routes ?? [],
103106
adapterName: '',
104107
clientDirectives: manifest?.clientDirectives ?? new Map(),
105-
renderers: manifest?.renderers ?? renderers,
108+
renderers: renderers ?? manifest?.renderers ?? [],
106109
base: manifest?.base ?? ASTRO_CONFIG_DEFAULTS.base,
107110
componentMetadata: manifest?.componentMetadata ?? new Map(),
108111
inlinedScripts: manifest?.inlinedScripts ?? new Map(),
@@ -138,21 +141,9 @@ export type AstroContainerOptions = {
138141
* @default []
139142
* @description
140143
*
141-
* List or renderers to use when rendering components. Usually they are entry points
142-
*
143-
* ## Example
144-
*
145-
* ```js
146-
* const container = await AstroContainer.create({
147-
* renderers: [{
148-
* name: "@astrojs/react"
149-
* client: "@astrojs/react/client.js"
150-
* server: "@astrojs/react/server.js"
151-
* }]
152-
* });
153-
* ```
144+
* List or renderers to use when rendering components. Usually, you want to pass these in an SSR context.
154145
*/
155-
renderers?: AstroRenderer[];
146+
renderers?: SSRLoadedRenderer[];
156147
/**
157148
* @default {}
158149
* @description
@@ -170,6 +161,17 @@ export type AstroContainerOptions = {
170161
* ```
171162
*/
172163
astroConfig?: AstroContainerUserConfig;
164+
165+
// TODO: document out of experimental
166+
resolve?: SSRResult['resolve'];
167+
168+
/**
169+
* @default {}
170+
* @description
171+
*
172+
* The raw manifest from the build output.
173+
*/
174+
manifest?: SSRManifest;
173175
};
174176

175177
type AstroContainerManifest = Pick<
@@ -195,6 +197,7 @@ type AstroContainerConstructor = {
195197
renderers?: SSRLoadedRenderer[];
196198
manifest?: AstroContainerManifest;
197199
resolve?: SSRResult['resolve'];
200+
astroConfig: AstroConfig;
198201
};
199202

200203
export class experimental_AstroContainer {
@@ -206,24 +209,31 @@ export class experimental_AstroContainer {
206209
*/
207210
#withManifest = false;
208211

212+
/**
213+
* Internal function responsible for importing a renderer
214+
* @private
215+
*/
216+
#getRenderer: ContainerImportRendererFn | undefined;
217+
209218
private constructor({
210219
streaming = false,
211-
renderers = [],
212220
manifest,
221+
renderers,
213222
resolve,
223+
astroConfig,
214224
}: AstroContainerConstructor) {
215225
this.#pipeline = ContainerPipeline.create({
216226
logger: new Logger({
217227
level: 'info',
218228
dest: nodeLogDestination,
219229
}),
220-
manifest: createManifest(renderers, manifest),
230+
manifest: createManifest(manifest, renderers),
221231
streaming,
222232
serverLike: true,
223-
renderers,
233+
renderers: renderers ?? manifest?.renderers ?? [],
224234
resolve: async (specifier: string) => {
225235
if (this.#withManifest) {
226-
return this.#containerResolve(specifier);
236+
return this.#containerResolve(specifier, astroConfig);
227237
} else if (resolve) {
228238
return resolve(specifier);
229239
}
@@ -232,10 +242,10 @@ export class experimental_AstroContainer {
232242
});
233243
}
234244

235-
async #containerResolve(specifier: string): Promise<string> {
245+
async #containerResolve(specifier: string, astroConfig: AstroConfig): Promise<string> {
236246
const found = this.#pipeline.manifest.entryModules[specifier];
237247
if (found) {
238-
return new URL(found, ASTRO_CONFIG_DEFAULTS.build.client).toString();
248+
return new URL(found, astroConfig.build.client).toString();
239249
}
240250
return found;
241251
}
@@ -248,32 +258,20 @@ export class experimental_AstroContainer {
248258
public static async create(
249259
containerOptions: AstroContainerOptions = {}
250260
): Promise<experimental_AstroContainer> {
251-
const { streaming = false, renderers = [] } = containerOptions;
252-
const loadedRenderers = await Promise.all(
253-
renderers.map(async (renderer) => {
254-
const mod = await import(renderer.serverEntrypoint);
255-
if (typeof mod.default !== 'undefined') {
256-
return {
257-
...renderer,
258-
ssr: mod.default,
259-
} as SSRLoadedRenderer;
260-
}
261-
return undefined;
262-
})
263-
);
264-
const finalRenderers = loadedRenderers.filter((r): r is SSRLoadedRenderer => Boolean(r));
265-
266-
return new experimental_AstroContainer({ streaming, renderers: finalRenderers });
261+
const { streaming = false, manifest, renderers = [], resolve } = containerOptions;
262+
const astroConfig = await validateConfig(ASTRO_CONFIG_DEFAULTS, process.cwd(), 'container');
263+
return new experimental_AstroContainer({ streaming, manifest, renderers, astroConfig, resolve });
267264
}
268265

269266
// NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it.
270267
// @ematipico: I plan to use it for a possible integration that could help people
271268
private static async createFromManifest(
272269
manifest: SSRManifest
273270
): Promise<experimental_AstroContainer> {
274-
const config = await validateConfig(ASTRO_CONFIG_DEFAULTS, process.cwd(), 'container');
271+
const astroConfig = await validateConfig(ASTRO_CONFIG_DEFAULTS, process.cwd(), 'container');
275272
const container = new experimental_AstroContainer({
276273
manifest,
274+
astroConfig,
277275
});
278276
container.#withManifest = true;
279277
return container;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { AstroRenderer, SSRLoadedRenderer } from '../@types/astro.js';
2+
3+
/**
4+
* Use this function to provide renderers to the `AstroContainer`:
5+
*
6+
* ```js
7+
* import { getContainerRenderer } from "@astrojs/react";
8+
* import { experimental_AstroContainer as AstroContainer } from "astro/container";
9+
* import { loadRenderers } from "astro:container"; // use this only when using vite/vitest
10+
*
11+
* const renderers = await loadRenderers([ getContainerRenderer ]);
12+
* const container = await AstroContainer.create({ renderers });
13+
*
14+
* ```
15+
* @param renderers
16+
*/
17+
export async function loadRenderers(renderers: AstroRenderer[]) {
18+
const loadedRenderers = await Promise.all(
19+
renderers.map(async (renderer) => {
20+
const mod = await import(renderer.serverEntrypoint);
21+
if (typeof mod.default !== 'undefined') {
22+
return {
23+
...renderer,
24+
ssr: mod.default,
25+
} as SSRLoadedRenderer;
26+
}
27+
return undefined;
28+
})
29+
);
30+
31+
return loadedRenderers.filter((r): r is SSRLoadedRenderer => Boolean(r));
32+
}

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { readFileSync } from 'node:fs';
2-
import type { AstroIntegration } from 'astro';
2+
import type { AstroIntegration, ContainerRenderer } from 'astro';
33

44
function getViteConfiguration() {
55
return {
@@ -19,6 +19,13 @@ function getViteConfiguration() {
1919
};
2020
}
2121

22+
export function getContainerRenderer(): ContainerRenderer {
23+
return {
24+
name: '@astrojs/lit',
25+
serverEntrypoint: '@astrojs/lit/server.js',
26+
};
27+
}
28+
2229
export default function (): AstroIntegration {
2330
return {
2431
name: '@astrojs/lit',

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'node:fs/promises';
22
import { fileURLToPath } from 'node:url';
33
import { markdownConfigDefaults } from '@astrojs/markdown-remark';
4-
import type { AstroIntegration, ContentEntryType, HookParameters } from 'astro';
4+
import type { AstroIntegration, ContainerRenderer, ContentEntryType, HookParameters } from 'astro';
55
import astroJSXRenderer from 'astro/jsx/renderer.js';
66
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
77
import type { PluggableList } from 'unified';
@@ -28,6 +28,13 @@ type SetupHookParams = HookParameters<'astro:config:setup'> & {
2828
addContentEntryType: (contentEntryType: ContentEntryType) => void;
2929
};
3030

31+
export function getContainerRenderer(): ContainerRenderer {
32+
return {
33+
name: 'astro:jsx',
34+
serverEntrypoint: 'astro/jsx/server.js',
35+
};
36+
}
37+
3138
export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroIntegration {
3239
// @ts-expect-error Temporarily assign an empty object here, which will be re-assigned by the
3340
// `astro:config:done` hook later. This is so that `vitePluginMdx` can get hold of a reference earlier.

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { fileURLToPath } from 'node:url';
22
import { type PreactPluginOptions as VitePreactPluginOptions, preact } from '@preact/preset-vite';
3-
import type { AstroIntegration, AstroRenderer, ViteUserConfig } from 'astro';
3+
import type { AstroIntegration, AstroRenderer, ContainerRenderer, ViteUserConfig } from 'astro';
44

55
const babelCwd = new URL('../', import.meta.url);
66

@@ -12,6 +12,13 @@ function getRenderer(development: boolean): AstroRenderer {
1212
};
1313
}
1414

15+
export function getContainerRenderer(): ContainerRenderer {
16+
return {
17+
name: '@astrojs/preact',
18+
serverEntrypoint: '@astrojs/preact/server.js',
19+
};
20+
}
21+
1522
export interface Options extends Pick<VitePreactPluginOptions, 'include' | 'exclude'> {
1623
compat?: boolean;
1724
devtools?: boolean;

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import react, { type Options as ViteReactPluginOptions } from '@vitejs/plugin-react';
2-
import type { AstroIntegration } from 'astro';
2+
import type { AstroIntegration, ContainerRenderer } from 'astro';
33
import { version as ReactVersion } from 'react-dom';
44
import type * as vite from 'vite';
55

@@ -53,6 +53,19 @@ function getRenderer(reactConfig: ReactVersionConfig) {
5353
};
5454
}
5555

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+
5669
function optionsPlugin(experimentalReactChildren: boolean): vite.Plugin {
5770
const virtualModule = 'astro:react:opts';
5871
const virtualModuleId = '\0' + virtualModule;

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { AstroIntegration, AstroIntegrationLogger, AstroRenderer } from 'astro';
1+
import type {
2+
AstroIntegration,
3+
AstroIntegrationLogger,
4+
AstroRenderer,
5+
ContainerRenderer,
6+
} from 'astro';
27
import type { PluginOption, UserConfig } from 'vite';
38
import solid, { type Options as ViteSolidPluginOptions } from 'vite-plugin-solid';
49

@@ -94,6 +99,13 @@ function getRenderer(): AstroRenderer {
9499
};
95100
}
96101

102+
export function getContainerRenderer(): ContainerRenderer {
103+
return {
104+
name: '@astrojs/solid',
105+
serverEntrypoint: '@astrojs/solid-js/server.js',
106+
};
107+
}
108+
97109
export interface Options extends Pick<ViteSolidPluginOptions, 'include' | 'exclude'> {
98110
devtools?: boolean;
99111
}

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { fileURLToPath } from 'node:url';
22
import type { Options } from '@sveltejs/vite-plugin-svelte';
33
import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
4-
import type { AstroIntegration, AstroRenderer } from 'astro';
4+
import type { AstroIntegration, AstroRenderer, ContainerRenderer } from 'astro';
55
import { VERSION } from 'svelte/compiler';
66
import type { UserConfig } from 'vite';
77

@@ -15,6 +15,13 @@ function getRenderer(): AstroRenderer {
1515
};
1616
}
1717

18+
export function getContainerRenderer(): ContainerRenderer {
19+
return {
20+
name: '@astrojs/svelte',
21+
serverEntrypoint: isSvelte5 ? '@astrojs/svelte/server-v5.js' : '@astrojs/svelte/server.js',
22+
};
23+
}
24+
1825
async function svelteConfigHasPreprocess(root: URL) {
1926
const svelteConfigFiles = ['./svelte.config.js', './svelte.config.cjs', './svelte.config.mjs'];
2027
for (const file of svelteConfigFiles) {

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Options as VueOptions } from '@vitejs/plugin-vue';
33
import vue from '@vitejs/plugin-vue';
44
import type { Options as VueJsxOptions } from '@vitejs/plugin-vue-jsx';
55
import { MagicString } from '@vue/compiler-sfc';
6-
import type { AstroIntegration, AstroRenderer, HookParameters } from 'astro';
6+
import type { AstroIntegration, AstroRenderer, ContainerRenderer, HookParameters } from 'astro';
77
import type { Plugin, UserConfig } from 'vite';
88
import type { VitePluginVueDevToolsOptions } from 'vite-plugin-vue-devtools';
99

@@ -32,6 +32,13 @@ function getJsxRenderer(): AstroRenderer {
3232
};
3333
}
3434

35+
export function getContainerRenderer(): ContainerRenderer {
36+
return {
37+
name: '@astrojs/vue',
38+
serverEntrypoint: '@astrojs/vue/server.js',
39+
};
40+
}
41+
3542
function virtualAppEntrypoint(options?: Options): Plugin {
3643
let isBuild: boolean;
3744
let root: string;

‎pnpm-lock.yaml

+2-2
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.