Skip to content

Commit

Permalink
feat(container): provide a virtual module to load renderers (#11144)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people committed Jun 5, 2024
1 parent 587e75f commit 803dd80
Show file tree
Hide file tree
Showing 16 changed files with 233 additions and 59 deletions.
30 changes: 30 additions & 0 deletions .changeset/fair-singers-reflect.md
Original file line number Diff line number Diff line change
@@ -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');
});
```
36 changes: 36 additions & 0 deletions .changeset/gold-mayflies-beam.md
Original file line number Diff line number Diff line change
@@ -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`.
4 changes: 2 additions & 2 deletions examples/container-with-vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
16 changes: 7 additions & 9 deletions examples/container-with-vitest/test/ReactWrapper.test.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
4 changes: 4 additions & 0 deletions packages/astro/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down
16 changes: 16 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
76 changes: 37 additions & 39 deletions packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { posix } from 'node:path';
import type {
AstroConfig,
AstroRenderer,
AstroUserConfig,
ComponentInstance,
ContainerImportRendererFn,
ContainerRenderer,
MiddlewareHandler,
Props,
RouteData,
Expand Down Expand Up @@ -83,8 +86,8 @@ export type ContainerRenderOptions = {
};

function createManifest(
renderers: SSRLoadedRenderer[],
manifest?: AstroContainerManifest,
renderers?: SSRLoadedRenderer[],
middleware?: MiddlewareHandler
): SSRManifest {
const defaultMiddleware: MiddlewareHandler = (_, next) => {
Expand All @@ -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(),
Expand Down Expand Up @@ -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
Expand All @@ -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<
Expand All @@ -195,6 +197,7 @@ type AstroContainerConstructor = {
renderers?: SSRLoadedRenderer[];
manifest?: AstroContainerManifest;
resolve?: SSRResult['resolve'];
astroConfig: AstroConfig;
};

export class experimental_AstroContainer {
Expand All @@ -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);
}
Expand All @@ -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;
}
Expand All @@ -248,32 +258,20 @@ 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.
// @ematipico: I plan to use it for a possible integration that could help people
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;
Expand Down
32 changes: 32 additions & 0 deletions packages/astro/src/virtual-modules/container.ts
Original file line number Diff line number Diff line change
@@ -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));
}
9 changes: 8 additions & 1 deletion packages/integrations/lit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFileSync } from 'node:fs';
import type { AstroIntegration } from 'astro';
import type { AstroIntegration, ContainerRenderer } from 'astro';

function getViteConfiguration() {
return {
Expand All @@ -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',
Expand Down
9 changes: 8 additions & 1 deletion packages/integrations/mdx/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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.
Expand Down
Loading

0 comments on commit 803dd80

Please sign in to comment.