diff --git a/.changeset/start-css-filter.md b/.changeset/start-css-filter.md new file mode 100644 index 0000000..df65c09 --- /dev/null +++ b/.changeset/start-css-filter.md @@ -0,0 +1,5 @@ +--- +'@solidjs/vite-plugin': patch +--- + +Add `start.css.filter` to control which module graphs are traversed while collecting development CSS. diff --git a/examples/turnkey/test/run.mjs b/examples/turnkey/test/run.mjs index 57d735a..e342c0e 100644 --- a/examples/turnkey/test/run.mjs +++ b/examples/turnkey/test/run.mjs @@ -1127,6 +1127,33 @@ async function runDocumentMode() { } } +async function runCssFilterMode() { + const mode = 'css-filter'; + console.log(`\n=== ${mode.toUpperCase()} ===`); + const port = 3172; + const origin = `http://localhost:${port}`; + const server = startProcess('pnpm', ['exec', 'vite', '--port', String(port), '--strictPort'], { + cwd: exampleDir, + env: { ...process.env, CSS_FILTER: '1' }, + }); + let serverLog = ''; + server.stdout.on('data', (d) => (serverLog += d)); + server.stderr.on('data', (d) => (serverLog += d)); + + try { + await waitForHttp(origin + '/src/api.ts', 30000); + const { html } = await fetchStreamed(origin + '/'); + record(mode, 'css', 'excluded module graph is not crawled for CSS', !html.includes(APP_CSS_COLOR)); + record(mode, 'ssr', 'filter does not prevent app rendering', html.includes('SSR Start Mode')); + } catch (error) { + record(mode, 'run', 'mode completed', false, String(error) + serverLog.slice(-2000)); + } finally { + try { + process.kill(-server.pid, 'SIGTERM'); + } catch {} + } +} + // Conventional entries: authored src/entry-server.tsx / src/entry-client.tsx // (written temporarily) take precedence over the generated ones. Dev serves // them as-is; the prod handler rewrites the authored `/src/entry-client.tsx` @@ -3111,6 +3138,7 @@ const ALL_MODES = [ 'dev', 'prod', 'document', + 'css-filter', 'entries', 'endpoint', 'configure', @@ -3132,6 +3160,7 @@ for (const mode of modes) { if (mode === 'dev') await runDevMode(); else if (mode === 'prod') await runProdMode(); else if (mode === 'document') await runDocumentMode(); + else if (mode === 'css-filter') await runCssFilterMode(); else if (mode === 'entries') await runEntriesMode(); else if (mode === 'endpoint') await runEndpointMode(); else if (mode === 'configure') await runConfigureMode(); diff --git a/examples/turnkey/vite.config.ts b/examples/turnkey/vite.config.ts index 8a594d8..a7c9ce4 100644 --- a/examples/turnkey/vite.config.ts +++ b/examples/turnkey/vite.config.ts @@ -109,6 +109,9 @@ export default defineConfig({ ? { document: process.env.SSR_DOCUMENT } : { external: !!process.env.SOLID_EXTERNAL, + ...(process.env.CSS_FILTER + ? { css: { filter: { exclude: /App\.tsx$/ } } } + : {}), // SSR_MIDDLEWARE=1 (middleware/preview modes): a fetch-style // chain fronting every dispatch path — page SSR, /_server, // preview — with getRequestEvent() live inside it. diff --git a/src/dev-manifest.ts b/src/dev-manifest.ts index d720c29..fb0a92f 100644 --- a/src/dev-manifest.ts +++ b/src/dev-manifest.ts @@ -20,6 +20,9 @@ import { joinBase } from './http.js'; export type DevStyleDescriptor = { id: string; content: string; attrs?: Record }; export type DevStyleSource = { id: string; url: string }; +export type DevStyleFilter = (id: string) => boolean; + +const defaultStyleFilter: DevStyleFilter = (id) => !id.includes('node_modules'); export type ResolvedAssets = { js: string[]; @@ -215,6 +218,7 @@ async function collectModuleDeps( file: string, deps: Set, crawled: Set, + filter: DevStyleFilter, onFile?: (file: string) => void, importer?: string, ): Promise { @@ -222,9 +226,11 @@ async function collectModuleDeps( const node = await getModuleNode(env, file, importer); if (!node?.id || deps.has(node)) return; deps.add(node); - if (node.file && !node.id.includes('node_modules')) onFile?.(node.file); - if (cssFileRegExp.test(node.url.split('?')[0]) || node.id.includes('node_modules')) return; + const isCss = cssFileRegExp.test(node.url.split('?')[0]); + if (!isCss && node.file && !node.id.startsWith('\0') && !filter(node.file)) return; + if (node.file) onFile?.(node.file); + if (isCss) return; if (!node.transformResult) { await env.transformRequest(node.url).catch(() => {}); @@ -236,7 +242,7 @@ async function collectModuleDeps( // from dynamicDeps — dynamic imports load their own styles when rendered. for (const dep of directDeps) { if (crawled.has(dep)) continue; - await collectModuleDeps(env, dep, deps, crawled, onFile, node.id); + await collectModuleDeps(env, dep, deps, crawled, filter, onFile, node.id); } } @@ -249,11 +255,12 @@ export async function collectDevStyleSources( env: DevEnvironment, files: string[], onFile?: (file: string) => void, + filter: DevStyleFilter = defaultStyleFilter, ): Promise { const deps = new Set(); const crawled = new Set(); for (const file of files) { - await collectModuleDeps(env, file, deps, crawled, onFile); + await collectModuleDeps(env, file, deps, crawled, filter, onFile); } const css: DevStyleSource[] = []; @@ -281,6 +288,7 @@ export async function collectDevStyleSources( export async function collectDevStyles( server: ViteDevServer, files: string[], + filter: DevStyleFilter = defaultStyleFilter, ): Promise { const ssrEnv = server.environments?.ssr; const clientEnv = server.environments?.client; @@ -289,6 +297,8 @@ export async function collectDevStyles( const sources = await collectDevStyleSources( ssrEnv, files.map((file) => path.resolve(server.config.root, file)), + undefined, + filter, ); const css: DevStyleDescriptor[] = []; @@ -348,7 +358,10 @@ export function devModuleUrl(root: string, base: string, key: string): string { return joinBase(base, '/@fs/' + absolute.replace(/^\//, '') + query); } -export function createDevAssetResolver(server: ViteDevServer): DevAssetResolver { +export function createDevAssetResolver( + server: ViteDevServer, + filter: DevStyleFilter = defaultStyleFilter, +): DevAssetResolver { // Server-side lazy() re-requests a module's assets on every retry of a // suspended render pass (retries re-create the component). The build // manifest answers those repeats synchronously and the pass converges; an @@ -382,7 +395,7 @@ export function createDevAssetResolver(server: ViteDevServer): DevAssetResolver // The module's dev URL doubles as its client entry: modulepreload // hint and hydration module-map value. const js = [devModuleUrl(root, base, key)]; - const css = await collectDevStyles(server, [key]); + const css = await collectDevStyles(server, [key], filter); return { js, css }; })().then( (assets) => { diff --git a/src/index.ts b/src/index.ts index 88818a4..60043fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,6 +52,7 @@ const REFRESH_RUNTIME_SOURCE = 'solid-js/refresh'; const viteVersionMajor = +version.split('.')[0]; const isVite8 = viteVersionMajor >= 8; +const DEFAULT_STYLE_EXCLUDE = /node_modules/; const VIRTUAL_MANIFEST_ID = 'virtual:solid-manifest'; const RESOLVED_VIRTUAL_MANIFEST_ID = '\0' + VIRTUAL_MANIFEST_ID; @@ -555,6 +556,12 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { // single shape (`false` behaves exactly like omission). const turnkey: StartOptions | null = options.start === true ? {} : options.start || null; + const styleFilterOptions = turnkey?.css?.filter; + let styleFilter = createFilter( + styleFilterOptions?.include, + styleFilterOptions?.exclude ?? DEFAULT_STYLE_EXCLUDE, + ); + const filterDevStyles = (id: string) => styleFilter(id); // `start.external` only means something when a server side exists to hand // over (SSR start mode); in client mode it is a documented no-op. const externalDevServer = !!options.ssr && !!turnkey?.external; @@ -845,6 +852,11 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { base = config.base; projectRoot = config.root; filter = createFilter(options.include, options.exclude, { resolve: projectRoot }); + styleFilter = createFilter( + styleFilterOptions?.include, + styleFilterOptions?.exclude ?? DEFAULT_STYLE_EXCLUDE, + { resolve: projectRoot }, + ); if (serverComponents && !(options.start && options.ssr)) { config.logger.warn( '[@solidjs/vite-plugin] serverFunctions.components is set without SSR start mode (the `start` ' + @@ -870,7 +882,10 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { // that don't share globals with this process, through the HTTP bridge // endpoint the middleware serves. if (options.ssr || options.start) { - registerDevAssetResolver(server.config.root, createDevAssetResolver(server)); + registerDevAssetResolver( + server.config.root, + createDevAssetResolver(server, filterDevStyles), + ); installDevManifestBridge(server); } if (!needHmr) return; @@ -1187,6 +1202,7 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { serverFunctions: !!options.serverFunctions, serverComponents, ssr: !!options.ssr, + styleFilter: filterDevStyles, }), ); } diff --git a/src/ssr/index.ts b/src/ssr/index.ts index 0ff3d3c..e98c55b 100644 --- a/src/ssr/index.ts +++ b/src/ssr/index.ts @@ -47,6 +47,7 @@ import path from 'path'; import { pathToFileURL } from 'node:url'; import { type DevEnvironment, + type FilterPattern, type Plugin, type PreviewServer, type ViteDevServer, @@ -55,6 +56,7 @@ import { getEnvironmentConsumer, isRunnableEnvironment } from '../environment.js import { collectDevStyles, collectDevStyleSources, + type DevStyleFilter, devStylePatch, renderDevStyleTag, } from '../dev-manifest.js'; @@ -77,6 +79,14 @@ export interface StartOptions { * @default "src/App.{tsx,jsx,ts,js}" (also probes lowercase "src/app.*") */ app?: string; + /** Options for development CSS crawling. */ + css?: { + /** Filter files traversed while collecting CSS. */ + filter?: { + include?: FilterPattern; + exclude?: FilterPattern; + }; + }; /** * Server entry module. Must export `render(request?, context?)` returning * a `renderToStream` result, an HTML string, or a `Response`. @@ -389,7 +399,12 @@ function resolveEntries(root: string, options: StartOptions, clientMode: boolean export function startServe( options: StartOptions, - internal: { serverFunctions?: boolean; serverComponents?: boolean; ssr?: boolean } = {}, + internal: { + serverFunctions?: boolean; + serverComponents?: boolean; + ssr?: boolean; + styleFilter?: DevStyleFilter; + } = {}, ): Plugin[] { // Client mode (the `start` option without `ssr: true`) rides this exact // plugin with three deltas: the generated server entry renders the @@ -411,6 +426,7 @@ export function startServe( // codegen: with the option off, none of these imports exist anywhere. const serverComponents = !!internal.serverComponents; const errorBoundary = options.errorBoundary !== false; + const styleFilter = internal.styleFilter; // `external` is server-mode-only (documented no-op in client mode, so a // host-integrated config survives the `ssr` boolean flip untouched). const externalServer = !clientMode && !!options.external; @@ -466,7 +482,12 @@ export function startServe( environment: DevEnvironment, watchFile: (file: string) => void, ): Promise { - const styles = await collectDevStyleSources(environment, styleRoots(), watchFile); + const styles = await collectDevStyleSources( + environment, + styleRoots(), + watchFile, + styleFilter, + ); if (!styles.length) return `export default '';`; const imports = styles.map((style, index) => { @@ -1211,7 +1232,9 @@ export function startServe( // Loaded through the SSR environment so the app, the request // event storage, and the handler share one module registry. const handler = await server.ssrLoadModule(HANDLER_ID); - const styles = pageRequest ? await collectDevStyles(server, styleRoots()) : []; + const styles = pageRequest + ? await collectDevStyles(server, styleRoots(), styleFilter) + : []; const devHead = styles.map(renderDevStyleTag).join(''); // Post middlewares run after Vite's base middleware stripped // the configured `base` from req.url; restore it so the app