0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-30 22:03:56 -05:00

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>
This commit is contained in:
Emanuele Stoppa 2024-06-05 11:39:42 +01:00 committed by GitHub
parent 587e75f47e
commit 803dd8061d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 233 additions and 59 deletions

View file

@ -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');
});
```

View file

@ -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`.

View file

@ -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"

View file

@ -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');

View file

@ -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';
}

View file

@ -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;
};

View file

@ -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;

View file

@ -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));
}

View file

@ -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',

View file

@ -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.

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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) {

View file

@ -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;

View file

@ -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