Skip to content

Commit d0e6af6

Browse files
committed
Getting frameworks working>
1 parent ae232b6 commit d0e6af6

13 files changed

Lines changed: 729 additions & 74 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ Uses strict TypeScript configuration with:
5151
- Module: preserve (for bundler compatibility)
5252
- Strict mode with additional safety checks (`noUncheckedIndexedAccess`, `noImplicitOverride`)
5353
- Library-focused settings (declaration files, declaration maps)
54+
- run pnpm format after every edit

packages/vitest-browser-astro/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"prepublishOnly": "node --run build",
2929
"check": "publint && attw --pack --ignore-rules=cjs-resolves-to-esm --profile=node16",
3030
"test": "vitest run",
31-
"test:browser": "vitest run --config vitest.browser.config.ts",
31+
"test:browser": "cd test/fixtures/astro-site && vitest run --config vitest.browser.config.ts",
3232
"test:all": "pnpm test && pnpm test:browser",
3333
"test:watch": "vitest"
3434
},
@@ -38,10 +38,15 @@
3838
},
3939
"devDependencies": {
4040
"@arethetypeswrong/cli": "^0.18.2",
41+
"@astrojs/react": "^4.4.0",
4142
"@playwright/test": "^1.49.0",
4243
"@types/node": "^22.18.10",
44+
"@types/react": "^19.2.2",
45+
"@types/react-dom": "^19.2.2",
4346
"astro": "^5.14.4",
4447
"publint": "^0.3.14",
48+
"react": "^19.2.0",
49+
"react-dom": "^19.2.0",
4550
"tsdown": "^0.15.6",
4651
"typescript": "^5.9.3",
4752
"vite": "^6.0.11",

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ export async function render(
6464
options.slots,
6565
);
6666

67+
console.log(html);
68+
6769
// Inject the HTML into the browser DOM
6870
return injectHTML(html, {
6971
container: options.container,

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

Lines changed: 124 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { resolve } from "node:path";
1+
import { isAbsolute, resolve } from "node:path";
22
import type { Plugin } from "vite";
33
import type { BrowserCommand } from "vitest/node";
4-
import { experimental_AstroContainer as AstroContainer } from "astro/container";
4+
import type { ViteDevServer } from "vite";
5+
import {
6+
experimental_AstroContainer as AstroContainer,
7+
type AddClientRenderer,
8+
} from "astro/container";
59
import { parse } from "devalue";
610

711
type RenderAstroCommand = BrowserCommand<
@@ -14,54 +18,104 @@ type RenderAstroCommand = BrowserCommand<
1418
>;
1519

1620
/**
17-
* Browser command that runs in Node.js to render Astro components
21+
* Server renderer configuration - path to renderer module
1822
*/
19-
const renderAstroCommand: RenderAstroCommand = async (
20-
ctx,
21-
componentPath: string,
22-
componentName: string,
23-
serializedProps?: string,
24-
slots?: Record<string, string>,
25-
) => {
26-
const projectRoot = process.cwd();
27-
const absolutePath = resolve(projectRoot, componentPath);
28-
29-
// Use Vitest's Vite server which already has Astro configured
30-
const viteServer = ctx.project.vite;
31-
32-
// Load the component directly (astro-head-inject will be auto-injected during SSR)
33-
const componentModule = await viteServer.ssrLoadModule(absolutePath);
34-
35-
// Get the component
36-
const Component = componentModule.default || componentModule[componentName];
37-
38-
if (!Component) {
39-
throw new Error(
40-
`Component not found for ${absolutePath}. Available exports: ${Object.keys(componentModule).join(", ")}`,
41-
);
23+
interface ServerRendererConfig {
24+
/** Path to server renderer module (e.g., '@astrojs/react/server.js') */
25+
module: string;
26+
}
27+
28+
/**
29+
* Creates the browser command with a pre-configured container
30+
*/
31+
async function createRenderAstroCommand(
32+
serverRenderers: ServerRendererConfig[],
33+
clientRenderers: AddClientRenderer[],
34+
viteServer: ViteDevServer,
35+
container: AstroContainer,
36+
): 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 });
4242
}
4343

44-
// Deserialize props using devalue to restore Dates, RegExps, etc.
45-
const props = serializedProps ? parse(serializedProps) : undefined;
44+
// Add client renderers for hydration
45+
for (const clientRenderer of clientRenderers) {
46+
container.addClientRenderer(clientRenderer);
47+
}
4648

47-
// Create Astro container for rendering
48-
const container = await AstroContainer.create();
49+
return async (
50+
ctx,
51+
componentPath: string,
52+
componentName: string,
53+
serializedProps?: string,
54+
slots?: Record<string, string>,
55+
) => {
56+
const projectRoot = process.cwd();
57+
const absolutePath = resolve(projectRoot, componentPath);
4958

50-
// Render the component (which will include scripts due to astro-head-inject)
51-
const html = await container.renderToString(Component, {
52-
props,
53-
slots,
54-
request: new Request("http://localhost:3000/test"),
55-
});
59+
// Use Vitest's Vite server which already has Astro configured
60+
const viteServer = ctx.project.vite;
5661

57-
return { html };
58-
};
62+
// Load the component directly (astro-head-inject will be auto-injected during SSR)
63+
const componentModule = await viteServer.ssrLoadModule(absolutePath);
64+
65+
// Get the component
66+
const Component = componentModule.default || componentModule[componentName];
67+
68+
if (!Component) {
69+
throw new Error(
70+
`Component not found for ${absolutePath}. Available exports: ${Object.keys(componentModule).join(", ")}`,
71+
);
72+
}
73+
74+
// Deserialize props using devalue to restore Dates, RegExps, etc.
75+
const props = serializedProps ? parse(serializedProps) : undefined;
76+
77+
// Render the component (which will include scripts due to astro-head-inject)
78+
const html = await container.renderToString(Component, {
79+
props,
80+
slots,
81+
request: new Request("http://localhost:4321/"),
82+
});
83+
84+
return { html };
85+
};
86+
}
87+
88+
/**
89+
* Options for configuring the Astro renderer plugin
90+
*/
91+
export interface AstroRendererOptions {
92+
/**
93+
* Server renderers for SSR (React, Vue, Svelte, etc.)
94+
* Specify module paths - they will be loaded using Vite's SSR loader
95+
* @example
96+
* serverRenderers: [{ module: '@astrojs/react/server.js' }]
97+
*/
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[];
107+
}
108+
const VALID_ID_PREFIX = `/@id/`;
59109

60110
/**
61111
* Vite plugin that intercepts .astro imports and provides browser command
62112
* Returns array of two plugins: one for pre-processing, one for post-processing
63113
*/
64-
export function astroRenderer(): Plugin[] {
114+
export function astroRenderer(options: AstroRendererOptions = {}): Plugin[] {
115+
const { serverRenderers = [], clientRenderers = [] } = options;
116+
let renderAstroCommand: RenderAstroCommand | null = null;
117+
let container: AstroContainer | null = null;
118+
65119
return [
66120
{
67121
name: "vitest:astro-renderer:pre",
@@ -80,12 +134,42 @@ export function astroRenderer(): Plugin[] {
80134
name: "vitest:astro-renderer",
81135
enforce: "post",
82136

137+
async configureServer(server) {
138+
// Create Astro container once during initialization
139+
container = await AstroContainer.create({
140+
resolve: async (id) => {
141+
console.log("Resolving:", id);
142+
const resolved = await server.pluginContainer.resolveId(
143+
id,
144+
undefined,
145+
);
146+
console.log("Resolved to:", resolved);
147+
if (resolved && isAbsolute(resolved?.id)) {
148+
return `/@fs${resolved.id}`;
149+
}
150+
return `/@id/${resolved?.id ?? id}`;
151+
},
152+
});
153+
// Create container with renderers during server startup
154+
renderAstroCommand = await createRenderAstroCommand(
155+
serverRenderers,
156+
clientRenderers,
157+
server,
158+
container,
159+
);
160+
},
161+
83162
config() {
84163
return {
85164
test: {
86165
browser: {
87166
commands: {
88-
renderAstro: renderAstroCommand,
167+
renderAstro: ((...args) => {
168+
if (!renderAstroCommand) {
169+
throw new Error("renderAstroCommand not initialized");
170+
}
171+
return renderAstroCommand(...args);
172+
}) as RenderAstroCommand,
89173
},
90174
},
91175
},

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { defineConfig } from "astro/config";
2+
import react from "@astrojs/react";
23

34
export default defineConfig({
45
// Minimal config for testing
6+
integrations: [react()],
57
vite: {
68
ssr: {
79
// Exclude Vitest browser files from SSR processing

packages/vitest-browser-astro/test/fixtures/astro-site/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
"preview": "astro preview"
1010
},
1111
"dependencies": {
12-
"astro": "^5.14.4"
12+
"@astrojs/react": "^4.4.0",
13+
"astro": "^5.14.4",
14+
"react": "^19.2.0",
15+
"react-dom": "^19.2.0",
16+
"vitest-browser-astro": "workspace:*"
17+
},
18+
"devDependencies": {
19+
"@vitest/browser": "^3.2.4",
20+
"@playwright/test": "^1.49.0",
21+
"vitest": "^3.2.4"
1322
}
1423
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useState } from 'react';
2+
3+
interface ReactCounterProps {
4+
initialCount?: number;
5+
label?: string;
6+
}
7+
8+
export default function ReactCounter({ initialCount = 0, label = 'React Count' }: ReactCounterProps) {
9+
const [count, setCount] = useState(initialCount);
10+
11+
return (
12+
<div data-testid="react-counter">
13+
<h2 data-testid="react-label">{label}</h2>
14+
<p data-testid="react-count">{count}</p>
15+
<button
16+
data-testid="react-increment"
17+
onClick={() => setCount(count + 1)}
18+
>
19+
Increment
20+
</button>
21+
<button
22+
data-testid="react-decrement"
23+
onClick={() => setCount(count - 1)}
24+
>
25+
Decrement
26+
</button>
27+
</div>
28+
);
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
import ReactCounter from './ReactCounter.tsx';
3+
4+
interface Props {
5+
initialCount?: number;
6+
label?: string;
7+
}
8+
9+
const { initialCount = 0, label = 'React Counter' } = Astro.props;
10+
---
11+
12+
<div class="with-react" data-testid="with-react">
13+
<h1>Astro Component with React</h1>
14+
<ReactCounter client:load initialCount={initialCount} label={label} />
15+
</div>
16+
17+
<style>
18+
.with-react {
19+
padding: 1rem;
20+
border: 2px solid #61dafb;
21+
border-radius: 8px;
22+
}
23+
24+
h1 {
25+
color: #61dafb;
26+
margin: 0 0 1rem 0;
27+
}
28+
</style>

packages/vitest-browser-astro/test/fixtures/astro-site/src/pages/index.astro

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
---
2-
import SimpleCard from '../components/SimpleCard.astro';
3-
import WithSlots from '../components/WithSlots.astro';
4-
import ComplexProps from '../components/ComplexProps.astro';
5-
import Button from '../components/Button.astro';
2+
import SimpleCard from "../components/SimpleCard.astro";
3+
import WithSlots from "../components/WithSlots.astro";
4+
import ComplexProps from "../components/ComplexProps.astro";
5+
import Button from "../components/Button.astro";
6+
import ReactCounter from "../components/ReactCounter";
67
---
78

89
<html lang="en">
@@ -46,11 +47,17 @@ import Button from '../components/Button.astro';
4647
</head>
4748
<body>
4849
<h1>Astro Test Fixture Site</h1>
49-
<p>This is a minimal Astro site used for testing vitest-browser-astro components.</p>
50+
<p>
51+
This is a minimal Astro site used for testing vitest-browser-astro
52+
components.
53+
</p>
5054

5155
<section>
5256
<h2>SimpleCard Component</h2>
53-
<SimpleCard title="Hello World" description="This is a test card with props" />
57+
<SimpleCard
58+
title="Hello World"
59+
description="This is a test card with props"
60+
/>
5461
<SimpleCard title="No Description" />
5562
</section>
5663

@@ -66,15 +73,20 @@ import Button from '../components/Button.astro';
6673
<h2>ComplexProps Component</h2>
6774
<ComplexProps
6875
count={42}
69-
tags={['astro', 'vitest', 'testing']}
70-
config={{ enabled: true, theme: 'light' }}
71-
createdAt={new Date('2024-01-01')}
76+
tags={["astro", "vitest", "testing"]}
77+
config={{ enabled: true, theme: "light" }}
78+
createdAt={new Date("2024-01-01")}
7279
/>
7380
</section>
7481

7582
<section>
7683
<h2>Button Component</h2>
7784
<Button label="Click Me" />
7885
</section>
86+
<section>
87+
<h2>WithReact Component</h2>
88+
<!-- This component uses React and should be tested for hydration -->
89+
<ReactCounter client:load />
90+
</section>
7991
</body>
8092
</html>

0 commit comments

Comments
 (0)