diff --git a/.changeset/stale-goats-go.md b/.changeset/stale-goats-go.md
new file mode 100644
index 000000000..0f9262f03
--- /dev/null
+++ b/.changeset/stale-goats-go.md
@@ -0,0 +1,28 @@
+---
+"@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 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(?:\/|$))/,
+ },
+ },
+});
+```
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/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.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 6f90c2f54..eb40351b8 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,19 @@ import { VIRTUAL_MODULES } from "./constants.ts";
import { type SolidStartOptions } from "./index.ts";
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 = createStyleFilter(start.css?.filter);
+
return {
name: "solid-start:manifest-plugin",
enforce: "pre",
@@ -118,7 +129,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);
}
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;