Skip to content

Commit 4331210

Browse files
committed
Better handling of client hydration
1 parent a044a33 commit 4331210

5 files changed

Lines changed: 94 additions & 98 deletions

File tree

demos/example/package.json

Lines changed: 0 additions & 16 deletions
This file was deleted.

packages/vitest-browser-astro/README.md

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Test Astro components in real browsers with [Vitest Browser Mode](https://vitest.dev/guide/browser/).
44

5-
Astro components render server-side using the Container API. This library enables browser testing by rendering components on the server and injecting the HTML into the browser DOM. Tests run with real browser APIs.
5+
Astro components render server-side using the [Container API](https://docs.astro.build/en/reference/container-reference/). This library enables browser testing by rendering components on the server and injecting the HTML into the browser DOM. Tests run with real browser APIs.
66

77
## Installation
88

@@ -43,6 +43,8 @@ export default getViteConfig({
4343
});
4444
```
4545

46+
For Astro pages that contain framework components (React, Vue, etc.), add renderers using `getContainerRenderer()` from your framework integration packages - see [Plugin options](#plugin-options).
47+
4648
Write a test file:
4749

4850
```ts
@@ -68,34 +70,30 @@ Run tests:
6870
npx vitest
6971
```
7072

73+
See the [test fixture](https://github.com/ascorbic/vitest-browser-astro/tree/main/packages/vitest-browser-astro/test/fixtures/astro-site) for a complete working example.
74+
7175
## Configuration
7276

7377
### Plugin options
7478

75-
The `astroRenderer` plugin accepts optional configuration for framework support:
79+
The `astroRenderer` plugin uses the [Astro Container API](https://docs.astro.build/en/reference/container-reference/) (experimental) for framework support. Configure it with `getContainerRenderer()` from your framework integration packages:
7680

7781
```ts
82+
import { getContainerRenderer as getReactRenderer } from "@astrojs/react";
83+
import { getContainerRenderer as getVueRenderer } from "@astrojs/vue";
84+
7885
astroRenderer({
79-
serverRenderers: [
80-
{ module: "@astrojs/react/server.js" },
81-
{ module: "@astrojs/vue/server.js" },
82-
{ module: "@astrojs/svelte/server.js" },
83-
],
84-
clientRenderers: [
85-
{ name: "@astrojs/react", entrypoint: "@astrojs/react/client.js" },
86-
{ name: "@astrojs/vue", entrypoint: "@astrojs/vue/client.js" },
87-
{ name: "@astrojs/svelte", entrypoint: "@astrojs/svelte/client.js" },
88-
],
86+
renderers: [getReactRenderer(), getVueRenderer()],
8987
});
9088
```
9189

9290
**Options:**
9391

94-
- `serverRenderers` - Array of server renderer configurations for SSR. Each entry requires:
95-
- `module` - Path to the server renderer module
96-
- `clientRenderers` - Array of client renderer configurations for hydration. Each entry requires:
97-
- `name` - Integration name
98-
- `entrypoint` - Path to the client renderer entrypoint
92+
- `renderers` - Array of framework renderers obtained from `getContainerRenderer()`. Pass these if your Astro components use framework integrations.
93+
94+
**Note:** Only one JSX-based framework (React, Preact, or Solid) can be used at a time. Non-JSX frameworks (Vue, Svelte) can be combined with any JSX framework.
95+
96+
See the [Container API renderers documentation](https://docs.astro.build/en/reference/container-reference/#renderers-option) for more details.
9997

10098
### Browser providers
10199

@@ -245,7 +243,29 @@ export default defineConfig({
245243
});
246244
```
247245

248-
Configure both server and client renderers in `vitest.config.ts` (see [Configuration](#plugin-options) for details).
246+
Configure framework renderers in `vitest.config.ts` using `getContainerRenderer()`:
247+
248+
```ts
249+
import { getViteConfig } from "astro/config";
250+
import { astroRenderer } from "vitest-browser-astro/plugin";
251+
import { getContainerRenderer as getReactRenderer } from "@astrojs/react";
252+
253+
export default getViteConfig({
254+
plugins: [
255+
astroRenderer({
256+
renderers: [getReactRenderer()],
257+
}),
258+
],
259+
test: {
260+
browser: {
261+
enabled: true,
262+
name: "chromium",
263+
provider: "playwright",
264+
headless: true,
265+
},
266+
},
267+
});
268+
```
249269

250270
### Testing hydrated components
251271

@@ -368,7 +388,7 @@ Then restart the TypeScript server.
368388

369389
### Framework components not hydrating
370390

371-
1. Add the framework to both `serverRenderers` and `clientRenderers` in plugin options
391+
1. Add the framework renderer using `getContainerRenderer()` in plugin options
372392
2. Add the corresponding integration to `astro.config.mjs`
373393
3. Use `client:load` directive on framework components
374394
4. Call `waitForHydration()` before interacting with the component

packages/vitest-browser-astro/src/plugin.ts

Lines changed: 51 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import { isAbsolute, resolve } from "node:path";
22
import type { Plugin } from "vite";
33
import type { BrowserCommand } from "vitest/node";
44
import type { ViteDevServer } from "vite";
5-
import {
6-
experimental_AstroContainer as AstroContainer,
7-
type AddClientRenderer,
8-
} from "astro/container";
5+
import { experimental_AstroContainer as AstroContainer } from "astro/container";
96
import { parse } from "devalue";
7+
import type { AstroRenderer, SSRLoadedRenderer } from "astro";
108

119
type RenderAstroCommand = BrowserCommand<
1210
[
@@ -18,63 +16,75 @@ type RenderAstroCommand = BrowserCommand<
1816
>;
1917

2018
/**
21-
* Server renderer configuration - path to renderer module
19+
* Loads renderer modules using Vite's SSR loader and adds them to the container
20+
* Mimics the behavior of loadRenderers() from astro:container
2221
*/
23-
interface ServerRendererConfig {
24-
/** Path to server renderer module (e.g., '@astrojs/react/server.js') */
25-
module: string;
22+
async function loadRenderers(
23+
renderers: AstroRenderer[],
24+
server: ViteDevServer,
25+
) {
26+
const loadedRenderers = await Promise.all(
27+
renderers.map(async (renderer) => {
28+
const mod = await server.ssrLoadModule(
29+
renderer.serverEntrypoint.toString(),
30+
);
31+
let { clientEntrypoint, name } = renderer;
32+
if (
33+
!clientEntrypoint &&
34+
name.startsWith("@astrojs/") &&
35+
name !== "@astrojs/mdx"
36+
) {
37+
// Hacky workaround because astro < 5.16.0 doesn't provide clientEntrypoint for official renderers
38+
clientEntrypoint = renderer.serverEntrypoint
39+
.toString()
40+
.replace("/server.js", "/client.js");
41+
}
42+
if (typeof mod.default !== "undefined") {
43+
return {
44+
...renderer,
45+
clientEntrypoint,
46+
ssr: mod.default,
47+
} as SSRLoadedRenderer;
48+
}
49+
return undefined;
50+
}),
51+
);
52+
53+
return loadedRenderers.filter((r): r is SSRLoadedRenderer => Boolean(r));
2654
}
2755

2856
/**
2957
* Creates the browser command with a pre-configured container
3058
*/
3159
async function createRenderAstroCommand(
32-
serverRenderers: ServerRendererConfig[],
33-
clientRenderers: AddClientRenderer[],
34-
viteServer: ViteDevServer,
3560
container: AstroContainer,
3661
): Promise<RenderAstroCommand> {
37-
// Load and add server renderers using Vite's SSR loader (must be added before client renderers)
38-
for (const { module: modulePath } of serverRenderers) {
39-
const rendererModule = await viteServer.ssrLoadModule(modulePath);
40-
const renderer = rendererModule.default || rendererModule;
41-
container.addServerRenderer({ renderer });
42-
}
43-
44-
// Add client renderers for hydration
45-
for (const clientRenderer of clientRenderers) {
46-
container.addClientRenderer(clientRenderer);
47-
}
48-
4962
return async (
5063
ctx,
5164
componentPath: string,
5265
componentName: string,
5366
serializedProps?: string,
5467
slots?: Record<string, string>,
5568
) => {
56-
const projectRoot = process.cwd();
69+
const projectRoot = ctx.project.config.root;
5770
const absolutePath = resolve(projectRoot, componentPath);
5871

5972
// Use Vitest's Vite server which already has Astro configured
6073
const viteServer = ctx.project.vite;
6174

62-
// Load the component directly (astro-head-inject will be auto-injected during SSR)
6375
const componentModule = await viteServer.ssrLoadModule(absolutePath);
6476

65-
// Get the component
6677
const Component = componentModule.default || componentModule[componentName];
6778

6879
if (!Component) {
6980
throw new Error(
70-
`Component not found for ${absolutePath}. Available exports: ${Object.keys(componentModule).join(", ")}`,
81+
`Component ${componentName} not found for ${absolutePath}. Available exports: ${Object.keys(componentModule).join(", ")}`,
7182
);
7283
}
7384

7485
// Deserialize props using devalue to restore Dates, RegExps, etc.
7586
const props = serializedProps ? parse(serializedProps) : undefined;
7687

77-
// Render the component (which will include scripts due to astro-head-inject)
7888
const html = await container.renderToString(Component, {
7989
props,
8090
slots,
@@ -90,39 +100,32 @@ async function createRenderAstroCommand(
90100
*/
91101
export interface AstroRendererOptions {
92102
/**
93-
* Server renderers for SSR (React, Vue, Svelte, etc.)
94-
* Specify module paths - they will be loaded using Vite's SSR loader
103+
* Framework renderers for SSR and hydration
104+
* Use getContainerRenderer() from your framework integration packages
95105
* @example
96-
* serverRenderers: [{ module: '@astrojs/react/server.js' }]
106+
* import { getContainerRenderer as reactRenderer } from '@astrojs/react';
107+
* import { getContainerRenderer as vueRenderer } from '@astrojs/vue';
108+
*
109+
* renderers: [reactRenderer(), vueRenderer()]
97110
*/
98-
serverRenderers?: ServerRendererConfig[];
99-
100-
/**
101-
* Client renderers for hydration
102-
* Specify the integration name and client entrypoint
103-
* @example
104-
* clientRenderers: [{ name: '@astrojs/react', entrypoint: '@astrojs/react/client.js' }]
105-
*/
106-
clientRenderers?: AddClientRenderer[];
111+
renderers?: AstroRenderer[];
107112
}
108-
const VALID_ID_PREFIX = `/@id/`;
109113

110114
/**
111115
* Vite plugin that intercepts .astro imports and provides browser command
112116
* Returns array of two plugins: one for pre-processing, one for post-processing
113117
*/
114118
export function astroRenderer(options: AstroRendererOptions = {}): Plugin {
115-
const { serverRenderers = [], clientRenderers = [] } = options;
116119
let renderAstroCommand: RenderAstroCommand | null = null;
117-
let container: AstroContainer | null = null;
118120

119121
return {
120122
name: "vitest:astro-renderer",
121123
enforce: "post",
122124

123125
async configureServer(server) {
124-
// Create Astro container once during initialization
125-
container = await AstroContainer.create({
126+
const renderers = await loadRenderers(options.renderers || [], server);
127+
const container = await AstroContainer.create({
128+
renderers,
126129
resolve: async (id) => {
127130
const resolved = await server.pluginContainer.resolveId(
128131
id,
@@ -134,13 +137,9 @@ export function astroRenderer(options: AstroRendererOptions = {}): Plugin {
134137
return `/@id/${resolved?.id ?? id}`;
135138
},
136139
});
137-
// Create container with renderers during server startup
138-
renderAstroCommand = await createRenderAstroCommand(
139-
serverRenderers,
140-
clientRenderers,
141-
server,
142-
container,
143-
);
140+
141+
// Create browser command
142+
renderAstroCommand = await createRenderAstroCommand(container);
144143
},
145144

146145
config() {

packages/vitest-browser-astro/test/fixtures/astro-site/vitest.browser.config.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
/// <reference types="vitest" />
22
import { getViteConfig } from "astro/config";
33
import { astroRenderer } from "vitest-browser-astro/plugin";
4+
import { getContainerRenderer as getReactRenderer } from "@astrojs/react";
5+
import { getContainerRenderer as getVueRenderer } from "@astrojs/vue";
6+
import { getContainerRenderer as getSvelteRenderer } from "@astrojs/svelte";
47

58
export default getViteConfig({
69
plugins: [
710
astroRenderer({
8-
serverRenderers: [
9-
{ module: "@astrojs/react/server.js" },
10-
{ module: "@astrojs/vue/server.js" },
11-
{ module: "@astrojs/svelte/server.js" },
12-
],
13-
clientRenderers: [
14-
{ name: "@astrojs/react", entrypoint: "@astrojs/react/client.js" },
15-
{ name: "@astrojs/vue", entrypoint: "@astrojs/vue/client.js" },
16-
{ name: "@astrojs/svelte", entrypoint: "@astrojs/svelte/client.js" },
17-
],
11+
renderers: [getReactRenderer(), getVueRenderer(), getSvelteRenderer()],
1812
}),
1913
],
2014
test: {

pnpm-workspace.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
packages:
22
- "packages/*"
33
- "packages/vitest-browser-astro/test/fixtures/astro-site"
4-
- "demos/*"

0 commit comments

Comments
 (0)