Skip to content

Commit a620eeb

Browse files
authored
fix: collect virtual module CSS in vite dev (#2039)
1 parent 4a8d415 commit a620eeb

File tree

11 files changed

+86
-32
lines changed

11 files changed

+86
-32
lines changed

.changeset/wicked-laws-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/start": patch
3+
---
4+
5+
Fixed virtual module CSS not being collected in vite dev.

apps/fixtures/css/src/components/test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ export const CommonTests = (props: { routeModuleClass?: string }) => (
6060
class={props.routeModuleClass}
6161
integration="module"
6262
/>
63+
<Test
64+
component="Route"
65+
file="virtual:virtualModule.css"
66+
class="virtualCss"
67+
comment="CSS from virtual module (virtualCssPlugin.ts)."
68+
/>
6369
<Test
6470
component="Route"
6571
file="url.css"

apps/fixtures/css/src/routes/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createAsync, query } from "@solidjs/router";
22
import { lazy, Show } from "solid-js";
3+
import "virtual:virtualModule.css";
34
import Layout from "../components/layout";
45
import { CommonTests } from "../components/test";
56
import notRenderedInlineCSS from "../styles/notRendered.css?url";
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Plugin } from "vite";
2+
3+
const id = "virtual:virtualModule.css";
4+
const resolvedId = "\0" + id;
5+
6+
const virtualCSS = () =>
7+
({
8+
name: "css-fixture-virtual-css",
9+
resolveId(source) {
10+
if (source === id) return resolvedId;
11+
},
12+
load(id) {
13+
if (id.startsWith(resolvedId))
14+
return `.virtualCss { background-color: var(--color-success); }`;
15+
},
16+
}) satisfies Plugin;
17+
18+
export default virtualCSS;

apps/fixtures/css/vite.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import tailwindcss from "@tailwindcss/vite";
22
import { defineConfig } from "vite";
33
import { nitroV2Plugin } from "../../../packages/start-nitro-v2-vite-plugin/src";
44
import { solidStart } from "../../../packages/start/src/config";
5+
import virtualCSS from "./src/virtualCssPlugin";
56

67
export default defineConfig({
7-
plugins: [solidStart(), nitroV2Plugin(), tailwindcss()],
8+
plugins: [virtualCSS(), solidStart(), nitroV2Plugin(), tailwindcss()],
89
});

packages/start/src/config/manifest.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type PluginOption, type ViteDevServer } from "vite";
33
import { findStylesInModuleGraph } from "../server/collect-styles.ts";
44
import { VIRTUAL_MODULES } from "./constants.ts";
55
import { type SolidStartOptions } from "./index.ts";
6+
import { wrapId } from "./vite-utils.ts";
67

78
export function manifest(start: SolidStartOptions): PluginOption {
89
let devServer: ViteDevServer = undefined!;
@@ -69,10 +70,10 @@ export function manifest(start: SolidStartOptions): PluginOption {
6970
tag: "style",
7071
attrs: {
7172
type: "text/css",
72-
"data-vite-dev-id": "${key}",
73+
"data-vite-dev-id": "${wrapId(key)}",
7374
"data-vite-ref": "0",
7475
},
75-
children: () => import("${value}?inline").then(mod => mod.default),
76+
children: () => import("${wrapId(value)}?inline").then(mod => mod.default),
7677
}`,
7778
);
7879

packages/start/src/config/vite-utils.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const FS_PREFIX = `/@fs/`;
1212
export const VALID_ID_PREFIX = `/@id/`;
1313

1414
export const NULL_BYTE_PLACEHOLDER = `__x00__`;
15+
const NULL_BYTE_REGEX = /^\0/;
1516

1617
export function normalizeResolvedIdToUrl(
1718
environment: DevEnvironment,
@@ -52,10 +53,13 @@ export function normalizeResolvedIdToUrl(
5253
return url;
5354
}
5455

56+
/**
57+
* Inspired by:
58+
* https://github.com/withastro/astro/blob/fddde5fad81007795eb263c7fd0cea096b8e2cba/packages/astro/src/core/util.ts#L115
59+
* https://github.com/vitejs/vite/blob/130e7181a55c524383c63bbfb1749d0ff7185cad/packages/vite/src/shared/utils.ts#L11
60+
*/
5561
export function wrapId(id: string): string {
56-
return id.startsWith(VALID_ID_PREFIX)
57-
? id
58-
: VALID_ID_PREFIX + id.replace("\0", NULL_BYTE_PLACEHOLDER);
62+
return id.replace(NULL_BYTE_REGEX, `${VALID_ID_PREFIX}${NULL_BYTE_PLACEHOLDER}`);
5963
}
6064

6165
export function unwrapId(id: string): string {

packages/start/src/server/StartServer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import App from "solid-start:app";
55

66
import { ErrorBoundary, TopErrorBoundary } from "../shared/ErrorBoundary.tsx";
77
import { useAssets } from "./assets/index.ts";
8+
import PatchVirtualDevStyles from "./assets/PatchVirtualDevStyles.tsx";
89
import { getSsrManifest } from "./manifest/ssr-manifest.ts";
910
import type { DocumentComponentProps, PageEvent } from "./types.ts";
1011

@@ -29,6 +30,7 @@ export function StartServer(props: { document: Component<DocumentComponentProps>
2930
assets={<HydrationScript />}
3031
scripts={
3132
<>
33+
<PatchVirtualDevStyles nonce={nonce} />
3234
<script
3335
type="module"
3436
nonce={nonce}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Patches the data-vite-dev-id attribute for style tags of virtual modules
3+
*
4+
* Per Vite's convention, virtual module ids are prefixed with \0 (null byte):
5+
* https://vite.dev/guide/api-plugin#virtual-modules-convention
6+
*
7+
* However this null byte cannot be server rendered properly.
8+
* Vite client runtime then fails to find style's with wrong null bytes,
9+
* and instead inserts duplicate style's.
10+
*
11+
* This patch replaces the serializable /@id/__x00__ with the proper null byte,
12+
* and has to run before Vite's client runtime:
13+
* https://github.com/vitejs/vite/blob/130e7181a55c524383c63bbfb1749d0ff7185cad/packages/vite/src/client/client.ts#L529
14+
*
15+
* TODO: This should be solved in Vite directly!
16+
*/
17+
const patch = function () {
18+
document.querySelectorAll<HTMLElement>("style[data-vite-dev-id]").forEach(function (el) {
19+
el.setAttribute("data-vite-dev-id", el.dataset.viteDevId!.replace("/@id/__x00__", "\0"));
20+
});
21+
};
22+
23+
const serializedPatch = `(${patch.toString()})();`;
24+
25+
const PatchVirtualDevStyles = (props: { nonce?: string }) => {
26+
if (!import.meta.env.PROD) {
27+
return <script nonce={props.nonce} innerHTML={serializedPatch} />;
28+
}
29+
};
30+
31+
export default PatchVirtualDevStyles;

packages/start/src/server/collect-styles.ts

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,23 @@
11
import path from "node:path";
2-
import { resolve } from "pathe";
32
import type { DevEnvironment, EnvironmentModuleNode } from "vite";
43

5-
async function getViteModuleNode(vite: DevEnvironment, file: string) {
6-
let nodePath = file;
7-
let node = vite.moduleGraph.getModuleById(file);
8-
9-
if (!node) {
10-
const resolvedId = await vite.pluginContainer.resolveId(file, undefined);
11-
if (!resolvedId) return;
12-
13-
nodePath = resolvedId.id;
14-
node = vite.moduleGraph.getModuleById(file);
15-
}
16-
17-
if (!node) {
18-
nodePath = resolve(nodePath);
19-
node = await vite.moduleGraph.getModuleByUrl(file);
20-
}
21-
22-
if (!node) {
23-
await vite.moduleGraph.ensureEntryFromUrl(nodePath, false);
24-
node = vite.moduleGraph.getModuleById(nodePath);
25-
}
26-
27-
return node;
4+
async function getViteModuleNode(vite: DevEnvironment, file: string, importer?: string) {
5+
try {
6+
const res = await vite.fetchModule(file, importer);
7+
if (!("id" in res)) return;
8+
return vite.moduleGraph.getModuleById(res.id);
9+
} catch (err) {}
2810
}
2911

3012
async function findModuleDependencies(
3113
vite: DevEnvironment,
3214
file: string,
3315
deps: Set<EnvironmentModuleNode>,
3416
crawledFiles = new Set<string>(),
17+
importer?: string,
3518
) {
3619
crawledFiles.add(file);
37-
const module = await getViteModuleNode(vite, file);
20+
const module = await getViteModuleNode(vite, file, importer);
3821
if (!module?.id || deps.has(module)) return;
3922

4023
deps.add(module);
@@ -53,7 +36,7 @@ async function findModuleDependencies(
5336
if (crawledFiles.has(dep)) {
5437
continue;
5538
}
56-
await findModuleDependencies(vite, dep, deps, crawledFiles);
39+
await findModuleDependencies(vite, dep, deps, crawledFiles, module.id);
5740
}
5841
}
5942

0 commit comments

Comments
 (0)