Skip to content

Commit 552aa70

Browse files
committed
feat: add Start CSS filter
1 parent c8615ed commit 552aa70

6 files changed

Lines changed: 99 additions & 10 deletions

File tree

.changeset/start-css-filter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@solidjs/vite-plugin': patch
3+
---
4+
5+
Add `start.css.filter` to control which module graphs are traversed while collecting development CSS.

examples/turnkey/test/run.mjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,33 @@ async function runDocumentMode() {
11191119
}
11201120
}
11211121

1122+
async function runCssFilterMode() {
1123+
const mode = 'css-filter';
1124+
console.log(`\n=== ${mode.toUpperCase()} ===`);
1125+
const port = 3172;
1126+
const origin = `http://localhost:${port}`;
1127+
const server = startProcess('pnpm', ['exec', 'vite', '--port', String(port), '--strictPort'], {
1128+
cwd: exampleDir,
1129+
env: { ...process.env, CSS_FILTER: '1' },
1130+
});
1131+
let serverLog = '';
1132+
server.stdout.on('data', (d) => (serverLog += d));
1133+
server.stderr.on('data', (d) => (serverLog += d));
1134+
1135+
try {
1136+
await waitForHttp(origin + '/src/api.ts', 30000);
1137+
const { html } = await fetchStreamed(origin + '/');
1138+
record(mode, 'css', 'excluded module graph is not crawled for CSS', !html.includes(APP_CSS_COLOR));
1139+
record(mode, 'ssr', 'filter does not prevent app rendering', html.includes('SSR Start Mode'));
1140+
} catch (error) {
1141+
record(mode, 'run', 'mode completed', false, String(error) + serverLog.slice(-2000));
1142+
} finally {
1143+
try {
1144+
process.kill(-server.pid, 'SIGTERM');
1145+
} catch {}
1146+
}
1147+
}
1148+
11221149
// Conventional entries: authored src/entry-server.tsx / src/entry-client.tsx
11231150
// (written temporarily) take precedence over the generated ones. Dev serves
11241151
// them as-is; the prod handler rewrites the authored `/src/entry-client.tsx`
@@ -3103,6 +3130,7 @@ const ALL_MODES = [
31033130
'dev',
31043131
'prod',
31053132
'document',
3133+
'css-filter',
31063134
'entries',
31073135
'endpoint',
31083136
'configure',
@@ -3124,6 +3152,7 @@ for (const mode of modes) {
31243152
if (mode === 'dev') await runDevMode();
31253153
else if (mode === 'prod') await runProdMode();
31263154
else if (mode === 'document') await runDocumentMode();
3155+
else if (mode === 'css-filter') await runCssFilterMode();
31273156
else if (mode === 'entries') await runEntriesMode();
31283157
else if (mode === 'endpoint') await runEndpointMode();
31293158
else if (mode === 'configure') await runConfigureMode();

examples/turnkey/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ export default defineConfig({
109109
? { document: process.env.SSR_DOCUMENT }
110110
: {
111111
external: !!process.env.SOLID_EXTERNAL,
112+
...(process.env.CSS_FILTER
113+
? { css: { filter: { exclude: /App\.tsx$/ } } }
114+
: {}),
112115
// SSR_MIDDLEWARE=1 (middleware/preview modes): a fetch-style
113116
// chain fronting every dispatch path — page SSR, /_server,
114117
// preview — with getRequestEvent() live inside it.

src/dev-manifest.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import { joinBase } from './http.js';
2020

2121
export type DevStyleDescriptor = { id: string; content: string; attrs?: Record<string, string> };
2222
export type DevStyleSource = { id: string; url: string };
23+
export type DevStyleFilter = (id: string) => boolean;
24+
25+
const defaultStyleFilter: DevStyleFilter = (id) => !id.includes('node_modules');
2326

2427
export type ResolvedAssets = {
2528
js: string[];
@@ -215,16 +218,19 @@ async function collectModuleDeps(
215218
file: string,
216219
deps: Set<EnvironmentModuleNode>,
217220
crawled: Set<string>,
221+
filter: DevStyleFilter,
218222
onFile?: (file: string) => void,
219223
importer?: string,
220224
): Promise<void> {
221225
crawled.add(file);
222226
const node = await getModuleNode(env, file, importer);
223227
if (!node?.id || deps.has(node)) return;
224228
deps.add(node);
225-
if (node.file && !node.id.includes('node_modules')) onFile?.(node.file);
226229

227-
if (cssFileRegExp.test(node.url.split('?')[0]) || node.id.includes('node_modules')) return;
230+
const isCss = cssFileRegExp.test(node.url.split('?')[0]);
231+
if (!isCss && node.file && !node.id.startsWith('\0') && !filter(node.file)) return;
232+
if (node.file) onFile?.(node.file);
233+
if (isCss) return;
228234

229235
if (!node.transformResult) {
230236
await env.transformRequest(node.url).catch(() => {});
@@ -236,7 +242,7 @@ async function collectModuleDeps(
236242
// from dynamicDeps — dynamic imports load their own styles when rendered.
237243
for (const dep of directDeps) {
238244
if (crawled.has(dep)) continue;
239-
await collectModuleDeps(env, dep, deps, crawled, onFile, node.id);
245+
await collectModuleDeps(env, dep, deps, crawled, filter, onFile, node.id);
240246
}
241247
}
242248

@@ -249,11 +255,12 @@ export async function collectDevStyleSources(
249255
env: DevEnvironment,
250256
files: string[],
251257
onFile?: (file: string) => void,
258+
filter: DevStyleFilter = defaultStyleFilter,
252259
): Promise<DevStyleSource[]> {
253260
const deps = new Set<EnvironmentModuleNode>();
254261
const crawled = new Set<string>();
255262
for (const file of files) {
256-
await collectModuleDeps(env, file, deps, crawled, onFile);
263+
await collectModuleDeps(env, file, deps, crawled, filter, onFile);
257264
}
258265

259266
const css: DevStyleSource[] = [];
@@ -281,6 +288,7 @@ export async function collectDevStyleSources(
281288
export async function collectDevStyles(
282289
server: ViteDevServer,
283290
files: string[],
291+
filter: DevStyleFilter = defaultStyleFilter,
284292
): Promise<DevStyleDescriptor[]> {
285293
const ssrEnv = server.environments?.ssr;
286294
const clientEnv = server.environments?.client;
@@ -289,6 +297,8 @@ export async function collectDevStyles(
289297
const sources = await collectDevStyleSources(
290298
ssrEnv,
291299
files.map((file) => path.resolve(server.config.root, file)),
300+
undefined,
301+
filter,
292302
);
293303

294304
const css: DevStyleDescriptor[] = [];
@@ -348,7 +358,10 @@ export function devModuleUrl(root: string, base: string, key: string): string {
348358
return joinBase(base, '/@fs/' + absolute.replace(/^\//, '') + query);
349359
}
350360

351-
export function createDevAssetResolver(server: ViteDevServer): DevAssetResolver {
361+
export function createDevAssetResolver(
362+
server: ViteDevServer,
363+
filter: DevStyleFilter = defaultStyleFilter,
364+
): DevAssetResolver {
352365
// Server-side lazy() re-requests a module's assets on every retry of a
353366
// suspended render pass (retries re-create the component). The build
354367
// manifest answers those repeats synchronously and the pass converges; an
@@ -382,7 +395,7 @@ export function createDevAssetResolver(server: ViteDevServer): DevAssetResolver
382395
// The module's dev URL doubles as its client entry: modulepreload
383396
// hint and hydration module-map value.
384397
const js = [devModuleUrl(root, base, key)];
385-
const css = await collectDevStyles(server, [key]);
398+
const css = await collectDevStyles(server, [key], filter);
386399
return { js, css };
387400
})().then(
388401
(assets) => {

src/index.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const REFRESH_RUNTIME_SOURCE = 'solid-js/refresh';
5252

5353
const viteVersionMajor = +version.split('.')[0];
5454
const isVite8 = viteVersionMajor >= 8;
55+
const DEFAULT_STYLE_EXCLUDE = /node_modules/;
5556

5657
const VIRTUAL_MANIFEST_ID = 'virtual:solid-manifest';
5758
const RESOLVED_VIRTUAL_MANIFEST_ID = '\0' + VIRTUAL_MANIFEST_ID;
@@ -555,6 +556,12 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
555556
// single shape (`false` behaves exactly like omission).
556557
const turnkey: StartOptions | null =
557558
options.start === true ? {} : options.start || null;
559+
const styleFilterOptions = turnkey?.css?.filter;
560+
let styleFilter = createFilter(
561+
styleFilterOptions?.include,
562+
styleFilterOptions?.exclude ?? DEFAULT_STYLE_EXCLUDE,
563+
);
564+
const filterDevStyles = (id: string) => styleFilter(id);
558565
// `start.external` only means something when a server side exists to hand
559566
// over (SSR start mode); in client mode it is a documented no-op.
560567
const externalDevServer = !!options.ssr && !!turnkey?.external;
@@ -845,6 +852,11 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
845852
base = config.base;
846853
projectRoot = config.root;
847854
filter = createFilter(options.include, options.exclude, { resolve: projectRoot });
855+
styleFilter = createFilter(
856+
styleFilterOptions?.include,
857+
styleFilterOptions?.exclude ?? DEFAULT_STYLE_EXCLUDE,
858+
{ resolve: projectRoot },
859+
);
848860
if (serverComponents && !(options.start && options.ssr)) {
849861
config.logger.warn(
850862
'[@solidjs/vite-plugin] serverFunctions.components is set without SSR start mode (the `start` ' +
@@ -870,7 +882,10 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
870882
// that don't share globals with this process, through the HTTP bridge
871883
// endpoint the middleware serves.
872884
if (options.ssr || options.start) {
873-
registerDevAssetResolver(server.config.root, createDevAssetResolver(server));
885+
registerDevAssetResolver(
886+
server.config.root,
887+
createDevAssetResolver(server, filterDevStyles),
888+
);
874889
installDevManifestBridge(server);
875890
}
876891
if (!needHmr) return;
@@ -1187,6 +1202,7 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
11871202
serverFunctions: !!options.serverFunctions,
11881203
serverComponents,
11891204
ssr: !!options.ssr,
1205+
styleFilter: filterDevStyles,
11901206
}),
11911207
);
11921208
}

src/ssr/index.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import path from 'path';
4747
import { pathToFileURL } from 'node:url';
4848
import {
4949
type DevEnvironment,
50+
type FilterPattern,
5051
type Plugin,
5152
type PreviewServer,
5253
type ViteDevServer,
@@ -55,6 +56,7 @@ import { getEnvironmentConsumer, isRunnableEnvironment } from '../environment.js
5556
import {
5657
collectDevStyles,
5758
collectDevStyleSources,
59+
type DevStyleFilter,
5860
devStylePatch,
5961
renderDevStyleTag,
6062
} from '../dev-manifest.js';
@@ -77,6 +79,14 @@ export interface StartOptions {
7779
* @default "src/App.{tsx,jsx,ts,js}" (also probes lowercase "src/app.*")
7880
*/
7981
app?: string;
82+
/** Options for development CSS crawling. */
83+
css?: {
84+
/** Filter files traversed while collecting CSS. */
85+
filter?: {
86+
include?: FilterPattern;
87+
exclude?: FilterPattern;
88+
};
89+
};
8090
/**
8191
* Server entry module. Must export `render(request?, context?)` returning
8292
* a `renderToStream` result, an HTML string, or a `Response`.
@@ -380,7 +390,12 @@ function resolveEntries(root: string, options: StartOptions, clientMode: boolean
380390

381391
export function startServe(
382392
options: StartOptions,
383-
internal: { serverFunctions?: boolean; serverComponents?: boolean; ssr?: boolean } = {},
393+
internal: {
394+
serverFunctions?: boolean;
395+
serverComponents?: boolean;
396+
ssr?: boolean;
397+
styleFilter?: DevStyleFilter;
398+
} = {},
384399
): Plugin[] {
385400
// Client mode (the `start` option without `ssr: true`) rides this exact
386401
// plugin with three deltas: the generated server entry renders the
@@ -401,6 +416,7 @@ export function startServe(
401416
// the server-function handler module either way). Everything is gated
402417
// codegen: with the option off, none of these imports exist anywhere.
403418
const serverComponents = !!internal.serverComponents;
419+
const styleFilter = internal.styleFilter;
404420
// `external` is server-mode-only (documented no-op in client mode, so a
405421
// host-integrated config survives the `ssr` boolean flip untouched).
406422
const externalServer = !clientMode && !!options.external;
@@ -456,7 +472,12 @@ export function startServe(
456472
environment: DevEnvironment,
457473
watchFile: (file: string) => void,
458474
): Promise<string> {
459-
const styles = await collectDevStyleSources(environment, styleRoots(), watchFile);
475+
const styles = await collectDevStyleSources(
476+
environment,
477+
styleRoots(),
478+
watchFile,
479+
styleFilter,
480+
);
460481
if (!styles.length) return `export default '';`;
461482

462483
const imports = styles.map((style, index) => {
@@ -1142,7 +1163,9 @@ export function startServe(
11421163
// Loaded through the SSR environment so the app, the request
11431164
// event storage, and the handler share one module registry.
11441165
const handler = await server.ssrLoadModule(HANDLER_ID);
1145-
const styles = pageRequest ? await collectDevStyles(server, styleRoots()) : [];
1166+
const styles = pageRequest
1167+
? await collectDevStyles(server, styleRoots(), styleFilter)
1168+
: [];
11461169
const devHead = styles.map(renderDevStyleTag).join('');
11471170
// Post middlewares run after Vite's base middleware stripped
11481171
// the configured `base` from req.url; restore it so the app

0 commit comments

Comments
 (0)