From 7943f83eb6eb4f871908fa997137d69b1f77e59e Mon Sep 17 00:00:00 2001 From: Katja Lutz Date: Sat, 1 Aug 2026 16:07:45 +0200 Subject: [PATCH 1/4] fix: url encode asset id in dev manifests --- .changeset/strong-geckos-rescue.md | 5 +++++ apps/fixtures/css/src/components/lazy+.tsx | 5 +++++ apps/fixtures/css/src/components/test.tsx | 8 ++++++++ apps/fixtures/css/src/routes/index.tsx | 3 +++ apps/fixtures/css/src/styles/lazyPlus.css | 3 +++ packages/start/src/server/manifest/dev-client-manifest.ts | 2 +- packages/start/src/server/manifest/dev-ssr-manifest.ts | 2 +- 7 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 .changeset/strong-geckos-rescue.md create mode 100644 apps/fixtures/css/src/components/lazy+.tsx create mode 100644 apps/fixtures/css/src/styles/lazyPlus.css diff --git a/.changeset/strong-geckos-rescue.md b/.changeset/strong-geckos-rescue.md new file mode 100644 index 000000000..0c45abcc3 --- /dev/null +++ b/.changeset/strong-geckos-rescue.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +Fixed css from files with url sensitive characters such as `+` not being server-rendered. diff --git a/apps/fixtures/css/src/components/lazy+.tsx b/apps/fixtures/css/src/components/lazy+.tsx new file mode 100644 index 000000000..5dd50f094 --- /dev/null +++ b/apps/fixtures/css/src/components/lazy+.tsx @@ -0,0 +1,5 @@ +import "../styles/lazyPlus.css"; + +export default () => { + return <>; +}; diff --git a/apps/fixtures/css/src/components/test.tsx b/apps/fixtures/css/src/components/test.tsx index 21defb028..f5b9ecdb1 100644 --- a/apps/fixtures/css/src/components/test.tsx +++ b/apps/fixtures/css/src/components/test.tsx @@ -76,6 +76,14 @@ export const CommonTests = (props: { routeModuleClass?: string }) => ( /> + Tests if files with special characters such as "+" are properly crawled.} + /> import("../components/lazy")); const LazyLink = lazy(() => import("../components/lazyLink")); const LazyLinkTmp = lazy(() => import("../components/lazyLinkTmp")); +const LazyPlus = lazy(() => import("../components/lazy+")); + const entries = import.meta.glob("../components/lazyG*.tsx"); const LazyGlob = lazy(Object.values(entries)[0] as any); @@ -36,6 +38,7 @@ export default function Home() { + diff --git a/apps/fixtures/css/src/styles/lazyPlus.css b/apps/fixtures/css/src/styles/lazyPlus.css new file mode 100644 index 000000000..28cb0ee9d --- /dev/null +++ b/apps/fixtures/css/src/styles/lazyPlus.css @@ -0,0 +1,3 @@ +.lazyPlus { + background-color: var(--color-success); +} diff --git a/packages/start/src/server/manifest/dev-client-manifest.ts b/packages/start/src/server/manifest/dev-client-manifest.ts index 480693955..b3aadb3ac 100644 --- a/packages/start/src/server/manifest/dev-client-manifest.ts +++ b/packages/start/src/server/manifest/dev-client-manifest.ts @@ -6,7 +6,7 @@ export function getClientDevManifest() { return import(/* @vite-ignore */ join("/", id)); }, async getAssets(id) { - const assetsPath = `/@manifest/client/${Date.now()}/assets?id=${id}`; + const assetsPath = `/@manifest/client/${Date.now()}/assets?id=${encodeURIComponent(id)}`; const assets = (await import(/* @vite-ignore */ assetsPath)).default; diff --git a/packages/start/src/server/manifest/dev-ssr-manifest.ts b/packages/start/src/server/manifest/dev-ssr-manifest.ts index 4fefff1c5..8e98f0bb0 100644 --- a/packages/start/src/server/manifest/dev-ssr-manifest.ts +++ b/packages/start/src/server/manifest/dev-ssr-manifest.ts @@ -4,7 +4,7 @@ export function getSsrDevManifest(environment: "client" | "ssr") { return { path: (id: string) => normalize(join(import.meta.env.BASE_URL, id)), async getAssets(id) { - const assetsPath = `/@manifest/${environment}/${Date.now()}/assets?id=${id}`; + const assetsPath = `/@manifest/${environment}/${Date.now()}/assets?id=${encodeURIComponent(id)}`; const assets = (await import(/* @vite-ignore */ assetsPath)).default; From de5a27dc3e5deb88d540179282ae0921cc1e9c61 Mon Sep 17 00:00:00 2001 From: Katja Lutz Date: Sat, 1 Aug 2026 23:34:49 +0200 Subject: [PATCH 2/4] feat: implement `css.filter` plugin option --- .changeset/stale-goats-go.md | 16 ++++++++++++++++ packages/start/src/config/index.ts | 15 ++++++++++++++- packages/start/src/config/manifest.ts | 12 ++++++++++-- packages/start/src/server/collect-styles.ts | 18 ++++++++++++++---- 4 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 .changeset/stale-goats-go.md diff --git a/.changeset/stale-goats-go.md b/.changeset/stale-goats-go.md new file mode 100644 index 000000000..ba1783a0a --- /dev/null +++ b/.changeset/stale-goats-go.md @@ -0,0 +1,16 @@ +--- +"@solidjs/start": minor +--- + +The file filter logic used for CSS crawling in development can now be configured with the vite plugin option `css.filter` analog to `serverFunctions.filter`: + +```ts +solidStart({ + css: { + filter: { + // Exclude all node_modules expect "my-dependency" + exclude: "node_modules/!(my-dependency)/**/*", + }, + }, +}); +``` diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index ea9a00428..c7ab08d65 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -1,7 +1,7 @@ import { defu } from "defu"; import { globSync } from "node:fs"; import { basename, extname, isAbsolute, join } from "node:path"; -import type { PluginOption } from "vite"; +import type { PluginOption, FilterPattern } from "vite"; import solid, { type Options as SolidOptions } from "vite-plugin-solid"; import { type ServerFunctionsOptions, serverFunctionsPlugin } from "../directives/index.ts"; import { appRootAlias } from "./app-root-alias.ts"; @@ -30,6 +30,19 @@ export interface SolidStartOptions { */ appRoot?: string; + /** + * Options related to the css crawling logic + */ + css?: { + /** + * Filter files included during css crawling in development. + */ + filter?: { + include?: FilterPattern; + exclude?: FilterPattern; + }; + }; + /** * Options forwarded to `vite-plugin-solid`. * diff --git a/packages/start/src/config/manifest.ts b/packages/start/src/config/manifest.ts index 6f90c2f54..a71c346e6 100644 --- a/packages/start/src/config/manifest.ts +++ b/packages/start/src/config/manifest.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { type PluginOption, type ViteDevServer, version as viteVersion } from "vite"; +import { createFilter, type PluginOption, type ViteDevServer, version as viteVersion } from "vite"; import { fileURLToPath } from "node:url"; import { findStylesInModuleGraph } from "../server/collect-styles.ts"; @@ -8,8 +8,16 @@ import { VIRTUAL_MODULES } from "./constants.ts"; import { type SolidStartOptions } from "./index.ts"; import { wrapId } from "./vite-utils.ts"; +const DEFAULT_STYLE_EXCLUDE = /node_modules/; + export function manifest(start: SolidStartOptions): PluginOption { let devServer: ViteDevServer = undefined!; + + const styleFilter = createFilter( + start.css?.filter?.include || [], + start.css?.filter?.exclude || DEFAULT_STYLE_EXCLUDE, + ); + return { name: "solid-start:manifest-plugin", enforce: "pre", @@ -118,7 +126,7 @@ export function manifest(start: SolidStartOptions): PluginOption { // Client env does not have css dependencies in mod.transformResult // Aalways use ssr env instead, to prevent hydration mismatches const env = devServer.environments["ssr"]; - const styles = await findStylesInModuleGraph(env, id); + const styles = await findStylesInModuleGraph(env, id, styleFilter); const cssAssets = Object.entries(styles).map( ([key, value]) => `{ diff --git a/packages/start/src/server/collect-styles.ts b/packages/start/src/server/collect-styles.ts index 4586ead7b..bb9510bde 100644 --- a/packages/start/src/server/collect-styles.ts +++ b/packages/start/src/server/collect-styles.ts @@ -9,10 +9,13 @@ async function getViteModuleNode(vite: DevEnvironment, file: string, importer?: } catch (err) {} } +type StyleFilter = (id: string) => boolean; + async function findModuleDependencies( vite: DevEnvironment, file: string, deps: Set, + filter: StyleFilter, crawledFiles = new Set(), importer?: string, ) { @@ -22,7 +25,10 @@ async function findModuleDependencies( deps.add(module); - if (module.url.endsWith(".css") || module.url.includes("node_modules")) return; + if (module.url.endsWith(".css")) return; + + // Apply user-config file filters only to real files (virtual modules should always be included) + if (module.file && !module.id.startsWith("\0") && !filter(module.file)) return; if (!module.transformResult) { await vite.transformRequest(module.id).catch(() => {}); @@ -36,7 +42,7 @@ async function findModuleDependencies( if (crawledFiles.has(dep)) { continue; } - await findModuleDependencies(vite, dep, deps, crawledFiles, module.id); + await findModuleDependencies(vite, dep, deps, filter, crawledFiles, module.id); } } @@ -49,12 +55,16 @@ const cssModulesRegExp = new RegExp(`\\.module${cssFileRegExp.source}`); const isCssFile = (file: string) => cssFileRegExp.test(file); export const isCssModulesFile = (file: string) => cssModulesRegExp.test(file); -export async function findStylesInModuleGraph(vite: DevEnvironment, id: string) { +export async function findStylesInModuleGraph( + vite: DevEnvironment, + id: string, + filter: StyleFilter, +) { const absolute = path.resolve(process.cwd(), id); const dependencies = new Set(); try { - await findModuleDependencies(vite, absolute, dependencies); + await findModuleDependencies(vite, absolute, dependencies, filter); } catch (e) { console.error(e); } From 03b977aa671343efbf16e66b7bd2b764d9b1e5a1 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sun, 2 Aug 2026 01:58:20 +0200 Subject: [PATCH 3/4] pnpm example --- .changeset/stale-goats-go.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.changeset/stale-goats-go.md b/.changeset/stale-goats-go.md index ba1783a0a..0f9262f03 100644 --- a/.changeset/stale-goats-go.md +++ b/.changeset/stale-goats-go.md @@ -8,9 +8,21 @@ The file filter logic used for CSS crawling in development can now be configured solidStart({ css: { filter: { - // Exclude all node_modules expect "my-dependency" + // Exclude all node_modules except "my-dependency" with a flat node_modules layout exclude: "node_modules/!(my-dependency)/**/*", }, }, }); ``` + +With pnpm, Vite may resolve dependencies through the nested `.pnpm` directory. Use a regular expression that accounts for that layout: + +```ts +solidStart({ + css: { + filter: { + exclude: /node_modules\/(?!(?:\.pnpm\/[^/]+\/node_modules\/)?my-dependency(?:\/|$))/, + }, + }, +}); +``` From 8fd2c57c381ccc716e28ce4de268d0d879058899 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sun, 2 Aug 2026 02:00:07 +0200 Subject: [PATCH 4/4] test createStyleFilter --- packages/start/src/config/manifest.spec.ts | 35 ++++++++++++++++++++++ packages/start/src/config/manifest.ts | 11 ++++--- 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 packages/start/src/config/manifest.spec.ts diff --git a/packages/start/src/config/manifest.spec.ts b/packages/start/src/config/manifest.spec.ts new file mode 100644 index 000000000..d26b04891 --- /dev/null +++ b/packages/start/src/config/manifest.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; + +import { createStyleFilter } from "./manifest.ts"; + +describe("createStyleFilter", () => { + it("excludes node_modules by default", () => { + const filter = createStyleFilter(); + + expect(filter("/app/src/app.tsx")).toBe(true); + expect(filter("/app/node_modules/dependency/index.js")).toBe(false); + expect( + filter("/app/node_modules/.pnpm/dependency@1.0.0/node_modules/dependency/index.js"), + ).toBe(false); + }); + + it("can include a dependency from npm and pnpm layouts", () => { + const filter = createStyleFilter({ + exclude: /node_modules\/(?!(?:\.pnpm\/[^/]+\/node_modules\/)?my-dependency(?:\/|$))/, + }); + + const included = [ + "/app/node_modules/my-dependency/index.js", + "/app/node_modules/.pnpm/my-dependency@1.0.0/node_modules/my-dependency/index.js", + "/workspace/node_modules/.pnpm/my-dependency@1.0.0/node_modules/my-dependency/index.js", + ]; + const excluded = [ + "/app/node_modules/other-dependency/index.js", + "/app/node_modules/.pnpm/other-dependency@1.0.0/node_modules/other-dependency/index.js", + "/workspace/node_modules/.pnpm/other-dependency@1.0.0/node_modules/other-dependency/index.js", + ]; + + for (const id of included) expect(filter(id)).toBe(true); + for (const id of excluded) expect(filter(id)).toBe(false); + }); +}); diff --git a/packages/start/src/config/manifest.ts b/packages/start/src/config/manifest.ts index a71c346e6..eb40351b8 100644 --- a/packages/start/src/config/manifest.ts +++ b/packages/start/src/config/manifest.ts @@ -10,13 +10,16 @@ import { wrapId } from "./vite-utils.ts"; const DEFAULT_STYLE_EXCLUDE = /node_modules/; +type StyleFilterOptions = NonNullable["filter"]; + +export function createStyleFilter(options?: StyleFilterOptions) { + return createFilter(options?.include || [], options?.exclude || DEFAULT_STYLE_EXCLUDE); +} + export function manifest(start: SolidStartOptions): PluginOption { let devServer: ViteDevServer = undefined!; - const styleFilter = createFilter( - start.css?.filter?.include || [], - start.css?.filter?.exclude || DEFAULT_STYLE_EXCLUDE, - ); + const styleFilter = createStyleFilter(start.css?.filter); return { name: "solid-start:manifest-plugin",