Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wicked-laws-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/start": patch
---

Fixed virtual module CSS not being collected in vite dev.
6 changes: 6 additions & 0 deletions apps/fixtures/css/src/components/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ export const CommonTests = (props: { routeModuleClass?: string }) => (
class={props.routeModuleClass}
integration="module"
/>
<Test
component="Route"
file="virtual:virtualModule.css"
class="virtualCss"
comment="CSS from virtual module (virtualCssPlugin.ts)."
/>
<Test
component="Route"
file="url.css"
Expand Down
1 change: 1 addition & 0 deletions apps/fixtures/css/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createAsync, query } from "@solidjs/router";
import { lazy, Show } from "solid-js";
import "virtual:virtualModule.css";
import Layout from "../components/layout";
import { CommonTests } from "../components/test";
import notRenderedInlineCSS from "../styles/notRendered.css?url";
Expand Down
18 changes: 18 additions & 0 deletions apps/fixtures/css/src/virtualCssPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Plugin } from "vite";

const id = "virtual:virtualModule.css";
const resolvedId = "\0" + id;

const virtualCSS = () =>
({
name: "css-fixture-virtual-css",
resolveId(source) {
if (source === id) return resolvedId;
},
load(id) {
if (id.startsWith(resolvedId))
return `.virtualCss { background-color: var(--color-success); }`;
},
}) satisfies Plugin;

export default virtualCSS;
3 changes: 2 additions & 1 deletion apps/fixtures/css/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import { nitroV2Plugin } from "../../../packages/start-nitro-v2-vite-plugin/src";
import { solidStart } from "../../../packages/start/src/config";
import virtualCSS from "./src/virtualCssPlugin";

export default defineConfig({
plugins: [solidStart(), nitroV2Plugin(), tailwindcss()],
plugins: [virtualCSS(), solidStart(), nitroV2Plugin(), tailwindcss()],
});
5 changes: 3 additions & 2 deletions packages/start/src/config/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type PluginOption, type ViteDevServer } from "vite";
import { findStylesInModuleGraph } from "../server/collect-styles.ts";
import { VIRTUAL_MODULES } from "./constants.ts";
import { type SolidStartOptions } from "./index.ts";
import { wrapId } from "./vite-utils.ts";

export function manifest(start: SolidStartOptions): PluginOption {
let devServer: ViteDevServer = undefined!;
Expand Down Expand Up @@ -69,10 +70,10 @@ export function manifest(start: SolidStartOptions): PluginOption {
tag: "style",
attrs: {
type: "text/css",
"data-vite-dev-id": "${key}",
"data-vite-dev-id": "${wrapId(key)}",
"data-vite-ref": "0",
},
children: () => import("${value}?inline").then(mod => mod.default),
children: () => import("${wrapId(value)}?inline").then(mod => mod.default),
}`,
);

Expand Down
10 changes: 7 additions & 3 deletions packages/start/src/config/vite-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const FS_PREFIX = `/@fs/`;
export const VALID_ID_PREFIX = `/@id/`;

export const NULL_BYTE_PLACEHOLDER = `__x00__`;
const NULL_BYTE_REGEX = /^\0/;

export function normalizeResolvedIdToUrl(
environment: DevEnvironment,
Expand Down Expand Up @@ -52,10 +53,13 @@ export function normalizeResolvedIdToUrl(
return url;
}

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

export function unwrapId(id: string): string {
Expand Down
2 changes: 2 additions & 0 deletions packages/start/src/server/StartServer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import App from "solid-start:app";

import { ErrorBoundary, TopErrorBoundary } from "../shared/ErrorBoundary.tsx";
import { useAssets } from "./assets/index.ts";
import PatchVirtualDevStyles from "./assets/PatchVirtualDevStyles.tsx";
import { getSsrManifest } from "./manifest/ssr-manifest.ts";
import type { DocumentComponentProps, PageEvent } from "./types.ts";

Expand All @@ -29,6 +30,7 @@ export function StartServer(props: { document: Component<DocumentComponentProps>
assets={<HydrationScript />}
scripts={
<>
<PatchVirtualDevStyles nonce={nonce} />
<script
type="module"
nonce={nonce}
Expand Down
31 changes: 31 additions & 0 deletions packages/start/src/server/assets/PatchVirtualDevStyles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Patches the data-vite-dev-id attribute for style tags of virtual modules
*
* Per Vite's convention, virtual module ids are prefixed with \0 (null byte):
* https://vite.dev/guide/api-plugin#virtual-modules-convention
*
* However this null byte cannot be server rendered properly.
* Vite client runtime then fails to find style's with wrong null bytes,
* and instead inserts duplicate style's.
*
* This patch replaces the serializable /@id/__x00__ with the proper null byte,
* and has to run before Vite's client runtime:
* https://github.com/vitejs/vite/blob/130e7181a55c524383c63bbfb1749d0ff7185cad/packages/vite/src/client/client.ts#L529
*
* TODO: This should be solved in Vite directly!
*/
const patch = function () {
document.querySelectorAll<HTMLElement>("style[data-vite-dev-id]").forEach(function (el) {
el.setAttribute("data-vite-dev-id", el.dataset.viteDevId!.replace("/@id/__x00__", "\0"));
});
};

const serializedPatch = `(${patch.toString()})();`;

const PatchVirtualDevStyles = (props: { nonce?: string }) => {
if (!import.meta.env.PROD) {
return <script nonce={props.nonce} innerHTML={serializedPatch} />;
}
};

export default PatchVirtualDevStyles;
35 changes: 9 additions & 26 deletions packages/start/src/server/collect-styles.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,23 @@
import path from "node:path";
import { resolve } from "pathe";
import type { DevEnvironment, EnvironmentModuleNode } from "vite";

async function getViteModuleNode(vite: DevEnvironment, file: string) {
let nodePath = file;
let node = vite.moduleGraph.getModuleById(file);

if (!node) {
const resolvedId = await vite.pluginContainer.resolveId(file, undefined);
if (!resolvedId) return;

nodePath = resolvedId.id;
node = vite.moduleGraph.getModuleById(file);
}

if (!node) {
nodePath = resolve(nodePath);
node = await vite.moduleGraph.getModuleByUrl(file);
}

if (!node) {
await vite.moduleGraph.ensureEntryFromUrl(nodePath, false);
node = vite.moduleGraph.getModuleById(nodePath);
}

return node;
async function getViteModuleNode(vite: DevEnvironment, file: string, importer?: string) {
try {
const res = await vite.fetchModule(file, importer);
if (!("id" in res)) return;
return vite.moduleGraph.getModuleById(res.id);
} catch (err) {}
}

async function findModuleDependencies(
vite: DevEnvironment,
file: string,
deps: Set<EnvironmentModuleNode>,
crawledFiles = new Set<string>(),
importer?: string,
) {
crawledFiles.add(file);
const module = await getViteModuleNode(vite, file);
const module = await getViteModuleNode(vite, file, importer);
if (!module?.id || deps.has(module)) return;

deps.add(module);
Expand All @@ -53,7 +36,7 @@ async function findModuleDependencies(
if (crawledFiles.has(dep)) {
continue;
}
await findModuleDependencies(vite, dep, deps, crawledFiles);
await findModuleDependencies(vite, dep, deps, crawledFiles, module.id);
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/start/src/server/spa/StartServer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getSsrManifest } from "../manifest/ssr-manifest.ts";

import { TopErrorBoundary } from "../../shared/ErrorBoundary.tsx";
import { useAssets } from "../assets/index.ts";
import PatchVirtualDevStyles from "../assets/PatchVirtualDevStyles.tsx";
import type { DocumentComponentProps, PageEvent } from "../types.ts";

const docType = ssr("<!DOCTYPE html>");
Expand All @@ -27,6 +28,7 @@ export function StartServer(props: { document: Component<DocumentComponentProps>
<props.document
scripts={
<>
<PatchVirtualDevStyles nonce={nonce} />
<script
type="module"
src={getSsrManifest("client").path(import.meta.env.START_CLIENT_ENTRY)}
Expand Down
Loading