1- import { resolve } from "node:path" ;
1+ import { isAbsolute , resolve } from "node:path" ;
22import type { Plugin } from "vite" ;
33import 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" ;
59import { parse } from "devalue" ;
610
711type 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 } ,
0 commit comments