Skip to content
Merged
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
28 changes: 28 additions & 0 deletions .changeset/stale-goats-go.md
Original file line number Diff line number Diff line change
@@ -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(?:\/|$))/,
},
},
});
```
5 changes: 5 additions & 0 deletions .changeset/strong-geckos-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/start": patch
---

Fixed css from files with url sensitive characters such as `+` not being server-rendered.
5 changes: 5 additions & 0 deletions apps/fixtures/css/src/components/lazy+.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import "../styles/lazyPlus.css";

export default () => {
return <></>;
};
8 changes: 8 additions & 0 deletions apps/fixtures/css/src/components/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ export const CommonTests = (props: { routeModuleClass?: string }) => (
/>
<Test component="Route" file="notRendered.css" class="notRendered" integration="url" invert />
<Test component="Lazy" file="lazy.css" class="lazy" lazy />
<Test
component="Lazy+"
file="lazyPlus.css"
class="lazyPlus"
integration="import"
lazy
comment={<>Tests if files with special characters such as "+" are properly crawled.</>}
/>
<Test
component="LazyGlob"
file="lazyGlob.css"
Expand Down
3 changes: 3 additions & 0 deletions apps/fixtures/css/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const Lazy = lazy(() => 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);

Expand All @@ -36,6 +38,7 @@ export default function Home() {
<link rel="stylesheet" href={notRenderedInlineCSS} />
</Show>
<Lazy />
<LazyPlus />
<LazyGlob />
<LazyLink />
<Show when={!data()}>
Expand Down
3 changes: 3 additions & 0 deletions apps/fixtures/css/src/styles/lazyPlus.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.lazyPlus {
background-color: var(--color-success);
}
15 changes: 14 additions & 1 deletion packages/start/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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`.
*
Expand Down
35 changes: 35 additions & 0 deletions packages/start/src/config/manifest.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
15 changes: 13 additions & 2 deletions packages/start/src/config/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
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";
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<SolidStartOptions["css"]>["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",
Expand Down Expand Up @@ -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]) => `{
Expand Down
18 changes: 14 additions & 4 deletions packages/start/src/server/collect-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EnvironmentModuleNode>,
filter: StyleFilter,
crawledFiles = new Set<string>(),
importer?: string,
) {
Expand All @@ -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(() => {});
Expand All @@ -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);
}
}

Expand All @@ -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<EnvironmentModuleNode>();

try {
await findModuleDependencies(vite, absolute, dependencies);
await findModuleDependencies(vite, absolute, dependencies, filter);
} catch (e) {
console.error(e);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/start/src/server/manifest/dev-client-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion packages/start/src/server/manifest/dev-ssr-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading