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
5 changes: 5 additions & 0 deletions .changeset/start-css-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

Add `start.css.filter` to control which module graphs are traversed while collecting development CSS.
29 changes: 29 additions & 0 deletions examples/turnkey/test/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -3111,6 +3138,7 @@ const ALL_MODES = [
'dev',
'prod',
'document',
'css-filter',
'entries',
'endpoint',
'configure',
Expand All @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions examples/turnkey/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 19 additions & 6 deletions src/dev-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { joinBase } from './http.js';

export type DevStyleDescriptor = { id: string; content: string; attrs?: Record<string, string> };
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[];
Expand Down Expand Up @@ -215,16 +218,19 @@ async function collectModuleDeps(
file: string,
deps: Set<EnvironmentModuleNode>,
crawled: Set<string>,
filter: DevStyleFilter,
onFile?: (file: string) => void,
importer?: string,
): Promise<void> {
crawled.add(file);
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(() => {});
Expand All @@ -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);
}
}

Expand All @@ -249,11 +255,12 @@ export async function collectDevStyleSources(
env: DevEnvironment,
files: string[],
onFile?: (file: string) => void,
filter: DevStyleFilter = defaultStyleFilter,
): Promise<DevStyleSource[]> {
const deps = new Set<EnvironmentModuleNode>();
const crawled = new Set<string>();
for (const file of files) {
await collectModuleDeps(env, file, deps, crawled, onFile);
await collectModuleDeps(env, file, deps, crawled, filter, onFile);
}

const css: DevStyleSource[] = [];
Expand Down Expand Up @@ -281,6 +288,7 @@ export async function collectDevStyleSources(
export async function collectDevStyles(
server: ViteDevServer,
files: string[],
filter: DevStyleFilter = defaultStyleFilter,
): Promise<DevStyleDescriptor[]> {
const ssrEnv = server.environments?.ssr;
const clientEnv = server.environments?.client;
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down
18 changes: 17 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -555,6 +556,12 @@ export default function solidPlugin(options: Partial<Options> = {}): 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;
Expand Down Expand Up @@ -845,6 +852,11 @@ export default function solidPlugin(options: Partial<Options> = {}): 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` ' +
Expand All @@ -870,7 +882,10 @@ export default function solidPlugin(options: Partial<Options> = {}): 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;
Expand Down Expand Up @@ -1187,6 +1202,7 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
serverFunctions: !!options.serverFunctions,
serverComponents,
ssr: !!options.ssr,
styleFilter: filterDevStyles,
}),
);
}
Expand Down
29 changes: 26 additions & 3 deletions src/ssr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import path from 'path';
import { pathToFileURL } from 'node:url';
import {
type DevEnvironment,
type FilterPattern,
type Plugin,
type PreviewServer,
type ViteDevServer,
Expand All @@ -55,6 +56,7 @@ import { getEnvironmentConsumer, isRunnableEnvironment } from '../environment.js
import {
collectDevStyles,
collectDevStyleSources,
type DevStyleFilter,
devStylePatch,
renderDevStyleTag,
} from '../dev-manifest.js';
Expand All @@ -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`.
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -466,7 +482,12 @@ export function startServe(
environment: DevEnvironment,
watchFile: (file: string) => void,
): Promise<string> {
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) => {
Expand Down Expand Up @@ -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
Expand Down
Loading