Skip to content

Commit ee1713f

Browse files
committed
wip: dev server css link
1 parent 8866b1f commit ee1713f

File tree

5 files changed

+147
-94
lines changed

5 files changed

+147
-94
lines changed

packages/fullstack/examples/basic/src/entry.server.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ function Root() {
2525
<html>
2626
<head>
2727
<title>Vite Fullstack</title>
28-
{assets.css.map((href) => (
28+
{/* TODO: dedupe style via data-vite-dev-id https://github.com/vitejs/vite/pull/20767 */}
29+
{[...assets.css, ...serverAssets.css].map((href) => (
2930
<link key={href} rel="stylesheet" href={href} crossOrigin="" />
3031
))}
31-
{assets.js.map((href) => (
32+
{[...assets.js, ...serverAssets.js].map((href) => (
3233
<link key={href} rel="modulepreload" href={href} crossOrigin="" />
3334
))}
34-
{assets.entry && <script type="module" src={assets.entry}></script>}
35+
<script type="module" src={assets.entry}></script>
3536
</head>
3637
<body>
3738
<div>SSR at {new Date().toISOString()}</div>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
.test-client {
1+
.test-client-style {
22
color: orange;
33
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
.test-client {
1+
.test-server-style {
22
color: lightseagreen;
33
}

packages/fullstack/src/plugin.ts

Lines changed: 125 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import assert from "node:assert";
22
import MagicString from "magic-string";
33
import { 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";
511
import 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
922
type 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
/import\.meta\.vite\.assets\(([\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 /[?&](url|inline|raw)(\b|=|&|$)/.test(id);
224+
}
Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,3 @@
1-
type CssVirtual = {
2-
id: string;
3-
type: "ssr" | "rsc";
4-
};
5-
6-
export function toCssVirtual({ id, type }: CssVirtual) {
7-
// ensure other plugins treat it as a plain js file
8-
// e.g. https://github.com/vitejs/rolldown-vite/issues/372#issuecomment-3193401601
9-
return `virtual:vite-rsc/css?type=${type}&id=${encodeURIComponent(id)}&lang.js`;
10-
}
11-
12-
export function parseCssVirtual(id: string): CssVirtual | undefined {
13-
if (id.startsWith("\0virtual:vite-rsc/css?")) {
14-
return parseIdQuery(id).query as any;
15-
}
16-
}
17-
181
// https://github.com/vitejs/vite-plugin-vue/blob/06931b1ea2b9299267374cb8eb4db27c0626774a/packages/plugin-vue/src/utils/query.ts#L13
192
export function parseIdQuery(id: string): {
203
filename: string;
@@ -27,3 +10,19 @@ export function parseIdQuery(id: string): {
2710
const query = Object.fromEntries(new URLSearchParams(rawQuery));
2811
return { filename, query };
2912
}
13+
14+
type AssetsVirtual = {
15+
import: string;
16+
importer: string;
17+
environment: string;
18+
};
19+
20+
export function toAssetsVirtual(options: AssetsVirtual) {
21+
return `virtual:fullstack/assets?${new URLSearchParams(options)}&lang.js`;
22+
}
23+
24+
export function parseAssetsVirtual(id: string): AssetsVirtual | undefined {
25+
if (id.startsWith("\0virtual:fullstack/assets?")) {
26+
return parseIdQuery(id).query as any;
27+
}
28+
}

0 commit comments

Comments
 (0)