11import assert from "node:assert" ;
22import MagicString from "magic-string" ;
33import { toNodeHandler } from "srvx/node" ;
4- import { type Plugin , isRunnableDevEnvironment } from "vite" ;
4+ import {
5+ DevEnvironment ,
6+ type Plugin ,
7+ type ViteDevServer ,
8+ isCSSRequest ,
9+ isRunnableDevEnvironment ,
10+ } from "vite" ;
511import type { ImportAssetsOptions , ImportAssetsResult } from "../types/shared" ;
6- import { getEntrySource } from "./plugins/utils" ;
7- import { evalValue } from "./plugins/vite-utils" ;
12+ import { parseAssetsVirtual , toAssetsVirtual } from "./plugins/shared" ;
13+ import { getEntrySource , hashString } from "./plugins/utils" ;
14+ import {
15+ evalValue ,
16+ normalizeViteImportAnalysisUrl ,
17+ } from "./plugins/vite-utils" ;
818
19+ // TODO: split plugins?
20+ // - assets
21+ // - server handler
922type FullstackPluginOptions = {
1023 serverHandler ?: boolean ;
1124} ;
@@ -14,6 +27,8 @@ export default function vitePluginFullstack(
1427 customOptions ?: FullstackPluginOptions ,
1528) : Plugin [ ] {
1629 customOptions ;
30+ let server : ViteDevServer ;
31+
1732 return [
1833 {
1934 name : "fullstack" ,
@@ -25,7 +40,8 @@ export default function vitePluginFullstack(
2540 } ,
2641 } ;
2742 } ,
28- configureServer ( server ) {
43+ configureServer ( server_ ) {
44+ server = server_ ;
2945 if ( customOptions ?. serverHandler === false ) return ;
3046 assert ( isRunnableDevEnvironment ( server . environments . ssr ) ) ;
3147 const environment = server . environments . ssr ;
@@ -45,19 +61,27 @@ export default function vitePluginFullstack(
4561 } ,
4662 {
4763 name : "fullstack:assets" ,
64+ /**
65+ * [Transform input]
66+ * const assets = import.meta.vite.assets(...)
67+ *
68+ * [Transform output]
69+ * import __assets_xxx from "virtual:fullstack/assets?..."
70+ * const assets = __assets_xxx
71+ */
4872 transform : {
4973 async handler ( code , id , _options ) {
5074 if ( ! code . includes ( "import.meta.vite.assets" ) ) return ;
5175
5276 const output = new MagicString ( code ) ;
53- // let importAdded = false;
5477
5578 const emptyResult : ImportAssetsResult = {
56- entry : undefined ,
5779 js : [ ] ,
5880 css : [ ] ,
5981 } ;
6082
83+ const newImports = new Set < string > ( ) ;
84+
6185 for ( const match of code . matchAll (
6286 / i m p o r t \. m e t a \. v i t e \. a s s e t s \( ( [ \s \S ] * ?) \) / dg,
6387 ) ) {
@@ -86,86 +110,115 @@ export default function vitePluginFullstack(
86110 }
87111 }
88112
89- // const importId = toCssVirtual({ id: importer, type: "rsc" });
90-
91- // // use dynamic import during dev to delay crawling and discover css correctly.
92- // let replacement: string;
93- // if (this.environment.mode === "dev") {
94- // replacement = `__vite_rsc_react__.createElement(async () => {
95- // const __m = await import(${JSON.stringify(importId)});
96- // return __vite_rsc_react__.createElement(__m.Resources);
97- // })`;
98- // } else {
99- // const hash = hashString(importId);
100- // if (
101- // !importAdded &&
102- // !code.includes(`__vite_rsc_importer_resources_${hash}`)
103- // ) {
104- // importAdded = true;
105- // output.prepend(
106- // `import * as __vite_rsc_importer_resources_${hash} from ${JSON.stringify(
107- // importId,
108- // )};`,
109- // );
110- // }
111- // replacement = `__vite_rsc_react__.createElement(__vite_rsc_importer_resources_${hash}.Resources)`;
112- // }
113-
114- const result : ImportAssetsResult = {
115- entry : undefined ,
116- js : [ ] ,
117- css : [ ] ,
118- } ;
119- const replacement = `(${ JSON . stringify ( result ) } )` ;
120- output . update ( start , end , replacement ) ;
113+ const importSource = toAssetsVirtual ( {
114+ import : options . import ,
115+ importer : id ,
116+ environment : options . environment ,
117+ } ) ;
118+ const hash = hashString ( importSource ) ;
119+ const importedName = `__assets_${ hash } ` ;
120+ newImports . add (
121+ `;import ${ importedName } from ${ JSON . stringify ( importSource ) } ;\n` ,
122+ ) ;
123+ output . update ( start , end , `(${ importedName } )` ) ;
121124 }
122125
123126 if ( output . hasChanged ( ) ) {
124- // if (!code.includes("__vite_rsc_react__")) {
125- // output.prepend(`import __vite_rsc_react__ from "react";`);
126- // }
127+ // add virtual imports at the end so that other imports are already processed
128+ // and css already exists in server module graph.
129+ // TODO: forgot to do this on `@vitejs/plugin-rsc`
130+ for ( const newImport of newImports ) {
131+ output . append ( newImport ) ;
132+ }
127133 return {
128134 code : output . toString ( ) ,
129135 map : output . generateMap ( { hires : "boundary" } ) ,
130136 } ;
131137 }
132138 } ,
133139 } ,
140+ resolveId : {
141+ handler ( source ) {
142+ if ( source . startsWith ( "virtual:fullstack/assets?" ) ) {
143+ return "\0" + source ;
144+ }
145+ } ,
146+ } ,
134147 load : {
135148 async handler ( id ) {
136- id ;
137- // const { server } = manager;
138- // const parsed = parseCssVirtual(id);
139- // if (parsed?.type === "rsc") {
140- // assert(this.environment.name === "rsc");
141- // const importer = parsed.id;
142- // if (this.environment.mode === "dev") {
143- // const result = collectCss(server.environments.rsc!, importer);
144- // for (const file of [importer, ...result.visitedFiles]) {
145- // this.addWatchFile(file);
146- // }
147- // const cssHrefs = result.hrefs.map((href) => href.slice(1));
148- // const deps = assetsURLOfDeps({ css: cssHrefs, js: [] }, manager);
149- // return generateResourcesCode(
150- // serializeValueWithRuntime(deps),
151- // manager,
152- // );
153- // } else {
154- // const key = manager.toRelativeId(importer);
155- // manager.serverResourcesMetaMap[importer] = { key };
156- // return `
157- // import __vite_rsc_assets_manifest__ from "virtual:vite-rsc/assets-manifest";
158- // ${generateResourcesCode(
159- // `__vite_rsc_assets_manifest__.serverResources[${JSON.stringify(
160- // key,
161- // )}]`,
162- // manager,
163- // )}
164- // `;
165- // }
166- // }
149+ const parsed = parseAssetsVirtual ( id ) ;
150+ if ( ! parsed ) return ;
151+
152+ // TODO: shouldn't resolve in different environment?
153+ // we can avoid this by another virtual but only dev.
154+ const resolved = await this . resolve ( parsed . import , parsed . importer ) ;
155+ assert ( resolved , `Failed to resolve: ${ parsed . import } ` ) ;
156+
157+ if ( this . environment . mode === "dev" ) {
158+ const result : ImportAssetsResult = {
159+ entry : undefined , // defined only on client
160+ js : [ ] , // always empty
161+ css : [ ] , // defined only on server
162+ } ;
163+ const environment = server . environments [ parsed . environment ] ;
164+ assert ( environment , `Unknown environment: ${ parsed . environment } ` ) ;
165+ if ( parsed . environment === "client" ) {
166+ result . entry = normalizeViteImportAnalysisUrl (
167+ environment ,
168+ resolved . id ,
169+ ) ;
170+ }
171+ if ( environment . name !== "client" ) {
172+ const collected = collectCss ( environment , resolved . id ) ;
173+ result . css = collected . hrefs ;
174+ }
175+ return `export default ${ JSON . stringify ( result ) } ` ;
176+ } else {
177+ // TODO: build
178+ }
167179 } ,
168180 } ,
169181 } ,
170182 ] ;
171183}
184+
185+ function collectCss ( environment : DevEnvironment , entryId : string ) {
186+ const visited = new Set < string > ( ) ;
187+ const cssIds = new Set < string > ( ) ;
188+ const visitedFiles = new Set < string > ( ) ;
189+
190+ function recurse ( id : string ) {
191+ if ( visited . has ( id ) ) {
192+ return ;
193+ }
194+ visited . add ( id ) ;
195+ const mod = environment . moduleGraph . getModuleById ( id ) ;
196+ if ( mod ?. file ) {
197+ visitedFiles . add ( mod . file ) ;
198+ }
199+ for ( const next of mod ?. importedModules ?? [ ] ) {
200+ if ( next . id ) {
201+ if ( isCSSRequest ( next . id ) ) {
202+ if ( hasSpecialCssQuery ( next . id ) ) {
203+ continue ;
204+ }
205+ cssIds . add ( next . id ) ;
206+ } else {
207+ recurse ( next . id ) ;
208+ }
209+ }
210+ }
211+ }
212+
213+ recurse ( entryId ) ;
214+
215+ // this doesn't include ?t= query so that RSC <link /> won't keep adding styles.
216+ const hrefs = [ ...cssIds ] . map ( ( id ) =>
217+ normalizeViteImportAnalysisUrl ( environment , id ) ,
218+ ) ;
219+ return { ids : [ ...cssIds ] , hrefs, visitedFiles : [ ...visitedFiles ] } ;
220+ }
221+
222+ function hasSpecialCssQuery ( id : string ) : boolean {
223+ return / [ ? & ] ( u r l | i n l i n e | r a w ) ( \b | = | & | $ ) / . test ( id ) ;
224+ }
0 commit comments