@tanstack/start-plugin-core 1.149.1 → 1.149.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,12 @@
1
1
  import { ViteDevServer } from 'vite';
2
+ export declare const CSS_MODULES_REGEX: RegExp;
3
+ export declare function normalizeCssModuleCacheKey(idOrFile: string): string;
4
+ export declare function isCssModulesFile(file: string): boolean;
2
5
  export interface CollectDevStylesOptions {
3
6
  viteDevServer: ViteDevServer;
4
7
  entries: Array<string>;
8
+ /** Cache of CSS modules content captured during transform hook */
9
+ cssModulesCache?: Record<string, string>;
5
10
  }
6
11
  /**
7
12
  * Collect CSS content from the module graph starting from the given entry points.
@@ -1,8 +1,20 @@
1
+ import path from "node:path";
1
2
  const CSS_FILE_REGEX = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
3
+ const CSS_MODULES_REGEX = /\.module\.(css|less|sass|scss|styl|stylus)(?:$|[?#])/i;
4
+ function normalizeCssModuleCacheKey(idOrFile) {
5
+ const baseId = idOrFile.split("?")[0].split("#")[0];
6
+ return baseId.replace(/\\/g, "/");
7
+ }
2
8
  const CSS_SIDE_EFFECT_FREE_PARAMS = ["url", "inline", "raw", "inline-css"];
9
+ const VITE_CSS_REGEX = /const\s+__vite__css\s*=\s*["'`]([\s\S]*?)["'`]/;
10
+ const ESCAPE_CSS_COMMENT_START_REGEX = /\/\*/g;
11
+ const ESCAPE_CSS_COMMENT_END_REGEX = /\*\//g;
3
12
  function isCssFile(file) {
4
13
  return CSS_FILE_REGEX.test(file);
5
14
  }
15
+ function isCssModulesFile(file) {
16
+ return CSS_MODULES_REGEX.test(file);
17
+ }
6
18
  function hasCssSideEffectFreeParam(url) {
7
19
  const queryString = url.split("?")[1];
8
20
  if (!queryString) return false;
@@ -11,86 +23,131 @@ function hasCssSideEffectFreeParam(url) {
11
23
  (param) => params.get(param) === "" && !url.includes(`?${param}=`) && !url.includes(`&${param}=`)
12
24
  );
13
25
  }
26
+ function resolveDevUrl(rootDirectory, filePath) {
27
+ const normalizedPath = filePath.replace(/\\/g, "/");
28
+ const relativePath = path.posix.relative(
29
+ rootDirectory.replace(/\\/g, "/"),
30
+ normalizedPath
31
+ );
32
+ const isWithinRoot = !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
33
+ if (isWithinRoot) {
34
+ return path.posix.join("/", relativePath);
35
+ }
36
+ return path.posix.join("/@fs", normalizedPath);
37
+ }
14
38
  async function collectDevStyles(opts) {
15
- const { viteDevServer, entries } = opts;
39
+ const { viteDevServer, entries, cssModulesCache = {} } = opts;
16
40
  const styles = /* @__PURE__ */ new Map();
17
41
  const visited = /* @__PURE__ */ new Set();
18
- for (const entry of entries) {
19
- const normalizedPath = entry.replace(/\\/g, "/");
20
- let node = viteDevServer.moduleGraph.getModuleById(normalizedPath);
21
- if (!node) {
22
- try {
23
- await viteDevServer.transformRequest(normalizedPath);
24
- } catch (err) {
42
+ const rootDirectory = viteDevServer.config.root;
43
+ await Promise.all(
44
+ entries.map(
45
+ (entry) => processEntry(viteDevServer, resolveDevUrl(rootDirectory, entry), visited)
46
+ )
47
+ );
48
+ const cssPromises = [];
49
+ for (const dep of visited) {
50
+ if (hasCssSideEffectFreeParam(dep.url)) {
51
+ continue;
52
+ }
53
+ if (dep.file && isCssModulesFile(dep.file)) {
54
+ const css = cssModulesCache[normalizeCssModuleCacheKey(dep.file)];
55
+ if (!css) {
56
+ throw new Error(
57
+ `[tanstack-start] Missing CSS module in cache: ${dep.file}`
58
+ );
25
59
  }
26
- node = viteDevServer.moduleGraph.getModuleById(normalizedPath);
60
+ styles.set(dep.url, css);
61
+ continue;
62
+ }
63
+ const fileOrUrl = dep.file ?? dep.url;
64
+ if (!isCssFile(fileOrUrl)) {
65
+ continue;
27
66
  }
28
- if (node) {
29
- await crawlModuleForCss(viteDevServer, node, visited, styles);
67
+ cssPromises.push(
68
+ fetchCssFromModule(viteDevServer, dep).then(
69
+ (css) => css ? [dep.url, css] : null
70
+ )
71
+ );
72
+ }
73
+ const cssResults = await Promise.all(cssPromises);
74
+ for (const result of cssResults) {
75
+ if (result) {
76
+ styles.set(result[0], result[1]);
30
77
  }
31
78
  }
32
79
  if (styles.size === 0) return void 0;
33
- return Array.from(styles.entries()).map(([fileName, css]) => {
34
- const escapedFileName = fileName.replace(/\/\*/g, "/\\*").replace(/\*\//g, "*\\/");
35
- return `
80
+ const parts = [];
81
+ for (const [fileName, css] of styles.entries()) {
82
+ const escapedFileName = fileName.replace(ESCAPE_CSS_COMMENT_START_REGEX, "/\\*").replace(ESCAPE_CSS_COMMENT_END_REGEX, "*\\/");
83
+ parts.push(`
36
84
  /* ${escapedFileName} */
37
- ${css}`;
38
- }).join("\n");
85
+ ${css}`);
86
+ }
87
+ return parts.join("\n");
39
88
  }
40
- async function crawlModuleForCss(vite, node, visited, styles) {
41
- if (visited.has(node)) return;
42
- visited.add(node);
43
- const branches = [];
44
- if (!node.ssrTransformResult) {
89
+ async function processEntry(viteDevServer, entryUrl, visited) {
90
+ let node = await viteDevServer.moduleGraph.getModuleByUrl(entryUrl);
91
+ if (!node?.ssrTransformResult) {
45
92
  try {
46
- await vite.transformRequest(node.url, { ssr: true });
47
- const updatedNode = await vite.moduleGraph.getModuleByUrl(node.url);
48
- if (updatedNode) {
49
- node = updatedNode;
50
- }
93
+ await viteDevServer.transformRequest(entryUrl);
51
94
  } catch {
52
95
  }
96
+ node = await viteDevServer.moduleGraph.getModuleByUrl(entryUrl);
53
97
  }
54
- if (node.file && isCssFile(node.file) && !hasCssSideEffectFreeParam(node.url)) {
55
- const css = await loadCssContent(vite, node);
56
- if (css) {
57
- styles.set(node.url, css);
58
- }
98
+ if (!node || visited.has(node)) return;
99
+ visited.add(node);
100
+ await findModuleDeps(viteDevServer, node, visited);
101
+ }
102
+ async function findModuleDeps(viteDevServer, node, visited) {
103
+ const deps = node.ssrTransformResult?.deps ?? node.transformResult?.deps ?? null;
104
+ const importedModules = node.importedModules;
105
+ if ((!deps || deps.length === 0) && importedModules.size === 0) {
106
+ return;
59
107
  }
60
- const depsFromSsr = node.ssrTransformResult?.deps ?? [];
61
- const urlsToVisit = new Set(depsFromSsr);
62
- for (const importedNode of node.importedModules) {
63
- if (importedNode.file && isCssFile(importedNode.file)) {
64
- branches.push(crawlModuleForCss(vite, importedNode, visited, styles));
65
- } else if (!urlsToVisit.has(importedNode.url)) {
66
- urlsToVisit.add(importedNode.url);
108
+ const branches = [];
109
+ if (deps) {
110
+ for (const depUrl of deps) {
111
+ const dep = await viteDevServer.moduleGraph.getModuleByUrl(depUrl);
112
+ if (!dep) continue;
113
+ if (visited.has(dep)) continue;
114
+ visited.add(dep);
115
+ branches.push(findModuleDeps(viteDevServer, dep, visited));
67
116
  }
68
117
  }
69
- for (const depUrl of urlsToVisit) {
70
- branches.push(
71
- (async () => {
72
- const depNode = await vite.moduleGraph.getModuleByUrl(depUrl);
73
- if (depNode) {
74
- await crawlModuleForCss(vite, depNode, visited, styles);
75
- }
76
- })()
77
- );
118
+ for (const depNode of importedModules) {
119
+ if (visited.has(depNode)) continue;
120
+ visited.add(depNode);
121
+ branches.push(findModuleDeps(viteDevServer, depNode, visited));
122
+ }
123
+ if (branches.length === 1) {
124
+ await branches[0];
125
+ return;
78
126
  }
79
127
  await Promise.all(branches);
80
128
  }
81
- async function loadCssContent(vite, node) {
82
- const transformResult = await vite.transformRequest(node.url);
83
- if (!transformResult?.code) return void 0;
84
- return extractCssFromViteModule(transformResult.code);
85
- }
86
- function extractCssFromViteModule(code) {
87
- const match = code.match(/const\s+__vite__css\s*=\s*["'`]([\s\S]*?)["'`]/);
88
- if (match?.[1]) {
89
- return match[1].replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
129
+ async function fetchCssFromModule(viteDevServer, node) {
130
+ const cachedCode = node.transformResult?.code ?? node.ssrTransformResult?.code;
131
+ if (cachedCode) {
132
+ return extractCssFromCode(cachedCode);
90
133
  }
91
- return void 0;
134
+ try {
135
+ const transformResult = await viteDevServer.transformRequest(node.url);
136
+ if (!transformResult?.code) return void 0;
137
+ return extractCssFromCode(transformResult.code);
138
+ } catch {
139
+ return void 0;
140
+ }
141
+ }
142
+ function extractCssFromCode(code) {
143
+ const match = VITE_CSS_REGEX.exec(code);
144
+ if (!match?.[1]) return void 0;
145
+ return match[1].replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
92
146
  }
93
147
  export {
94
- collectDevStyles
148
+ CSS_MODULES_REGEX,
149
+ collectDevStyles,
150
+ isCssModulesFile,
151
+ normalizeCssModuleCacheKey
95
152
  };
96
153
  //# sourceMappingURL=dev-styles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dev-styles.js","sources":["../../../src/dev-server-plugin/dev-styles.ts"],"sourcesContent":["/**\n * CSS collection for dev mode.\n * Crawls the Vite module graph to collect CSS from the router entry and all its dependencies.\n */\nimport type { ModuleNode, ViteDevServer } from 'vite'\n\n// CSS file extensions supported by Vite\nconst CSS_FILE_REGEX =\n /\\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\\?)/\n// URL params that indicate CSS should not be injected (e.g., ?url, ?inline)\nconst CSS_SIDE_EFFECT_FREE_PARAMS = ['url', 'inline', 'raw', 'inline-css']\n\nfunction isCssFile(file: string): boolean {\n return CSS_FILE_REGEX.test(file)\n}\n\nfunction hasCssSideEffectFreeParam(url: string): boolean {\n const queryString = url.split('?')[1]\n if (!queryString) return false\n\n const params = new URLSearchParams(queryString)\n return CSS_SIDE_EFFECT_FREE_PARAMS.some(\n (param) =>\n params.get(param) === '' &&\n !url.includes(`?${param}=`) &&\n !url.includes(`&${param}=`),\n )\n}\n\nexport interface CollectDevStylesOptions {\n viteDevServer: ViteDevServer\n entries: Array<string>\n}\n\n/**\n * Collect CSS content from the module graph starting from the given entry points.\n */\nexport async function collectDevStyles(\n opts: CollectDevStylesOptions,\n): Promise<string | undefined> {\n const { viteDevServer, entries } = opts\n const styles: Map<string, string> = new Map()\n const visited = new Set<ModuleNode>()\n\n for (const entry of entries) {\n const normalizedPath = entry.replace(/\\\\/g, '/')\n let node = viteDevServer.moduleGraph.getModuleById(normalizedPath)\n\n // If module isn't in the graph yet, request it to trigger transform\n if (!node) {\n try {\n await viteDevServer.transformRequest(normalizedPath)\n } catch (err) {\n // Ignore - the module might not exist yet\n }\n node = viteDevServer.moduleGraph.getModuleById(normalizedPath)\n }\n\n if (node) {\n await crawlModuleForCss(viteDevServer, node, visited, styles)\n }\n }\n\n if (styles.size === 0) return undefined\n\n return Array.from(styles.entries())\n .map(([fileName, css]) => {\n const escapedFileName = fileName\n .replace(/\\/\\*/g, '/\\\\*')\n .replace(/\\*\\//g, '*\\\\/')\n return `\\n/* ${escapedFileName} */\\n${css}`\n })\n .join('\\n')\n}\n\nasync function crawlModuleForCss(\n vite: ViteDevServer,\n node: ModuleNode,\n visited: Set<ModuleNode>,\n styles: Map<string, string>,\n): Promise<void> {\n if (visited.has(node)) return\n visited.add(node)\n\n const branches: Array<Promise<void>> = []\n\n // Ensure the module has been transformed to populate its deps\n // This is important for code-split modules that may not have been processed yet\n if (!node.ssrTransformResult) {\n try {\n await vite.transformRequest(node.url, { ssr: true })\n // Re-fetch the node to get updated state\n const updatedNode = await vite.moduleGraph.getModuleByUrl(node.url)\n if (updatedNode) {\n node = updatedNode\n }\n } catch {\n // Ignore transform errors - the module might not be transformable\n }\n }\n\n // Check if this is a CSS file\n if (\n node.file &&\n isCssFile(node.file) &&\n !hasCssSideEffectFreeParam(node.url)\n ) {\n const css = await loadCssContent(vite, node)\n if (css) {\n styles.set(node.url, css)\n }\n }\n\n // Crawl dependencies using ssrTransformResult.deps and importedModules\n // We need both because:\n // 1. ssrTransformResult.deps has resolved URLs for SSR dependencies\n // 2. importedModules may contain CSS files and code-split modules not in SSR deps\n const depsFromSsr = node.ssrTransformResult?.deps ?? []\n const urlsToVisit = new Set<string>(depsFromSsr)\n\n // Check importedModules for CSS files and additional modules\n for (const importedNode of node.importedModules) {\n if (importedNode.file && isCssFile(importedNode.file)) {\n // CSS files often don't appear in ssrTransformResult.deps, add them explicitly\n branches.push(crawlModuleForCss(vite, importedNode, visited, styles))\n } else if (!urlsToVisit.has(importedNode.url)) {\n // Also add non-CSS imports that aren't in SSR deps (e.g., code-split modules)\n urlsToVisit.add(importedNode.url)\n }\n }\n\n for (const depUrl of urlsToVisit) {\n branches.push(\n (async () => {\n const depNode = await vite.moduleGraph.getModuleByUrl(depUrl)\n if (depNode) {\n await crawlModuleForCss(vite, depNode, visited, styles)\n }\n })(),\n )\n }\n\n await Promise.all(branches)\n}\n\nasync function loadCssContent(\n vite: ViteDevServer,\n node: ModuleNode,\n): Promise<string | undefined> {\n // For ALL CSS files (including CSS modules), get the transformed content\n // and extract __vite__css. Vite's transform puts the final CSS (with hashed\n // class names for modules) into the __vite__css variable.\n const transformResult = await vite.transformRequest(node.url)\n if (!transformResult?.code) return undefined\n\n // Extract CSS content from Vite's transformed module\n return extractCssFromViteModule(transformResult.code)\n}\n\n/**\n * Extract CSS string from Vite's transformed CSS module code.\n * Vite wraps CSS content in a JS module with __vite__css variable.\n */\nfunction extractCssFromViteModule(code: string): string | undefined {\n // Match: const __vite__css = \"...\"\n const match = code.match(/const\\s+__vite__css\\s*=\\s*[\"'`]([\\s\\S]*?)[\"'`]/)\n if (match?.[1]) {\n // Unescape the string\n return match[1]\n .replace(/\\\\n/g, '\\n')\n .replace(/\\\\t/g, '\\t')\n .replace(/\\\\\"/g, '\"')\n .replace(/\\\\\\\\/g, '\\\\')\n }\n return undefined\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dev-styles.js","sources":["../../../src/dev-server-plugin/dev-styles.ts"],"sourcesContent":["/**\n * CSS collection for dev mode.\n * Crawls the Vite module graph to collect CSS from the router entry and all its dependencies.\n */\nimport path from 'node:path'\nimport type { ModuleNode, ViteDevServer } from 'vite'\n\n// CSS file extensions supported by Vite\nconst CSS_FILE_REGEX =\n /\\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\\?)/\n// CSS modules file pattern - exported for use in plugin hook filters\n// Note: allow query/hash suffix since Vite ids often include them.\nexport const CSS_MODULES_REGEX =\n /\\.module\\.(css|less|sass|scss|styl|stylus)(?:$|[?#])/i\n\nexport function normalizeCssModuleCacheKey(idOrFile: string): string {\n const baseId = idOrFile.split('?')[0]!.split('#')[0]!\n return baseId.replace(/\\\\/g, '/')\n}\n// URL params that indicate CSS should not be injected (e.g., ?url, ?inline)\nconst CSS_SIDE_EFFECT_FREE_PARAMS = ['url', 'inline', 'raw', 'inline-css']\n\nconst VITE_CSS_REGEX = /const\\s+__vite__css\\s*=\\s*[\"'`]([\\s\\S]*?)[\"'`]/\n\nconst ESCAPE_CSS_COMMENT_START_REGEX = /\\/\\*/g\nconst ESCAPE_CSS_COMMENT_END_REGEX = /\\*\\//g\n\nfunction isCssFile(file: string): boolean {\n return CSS_FILE_REGEX.test(file)\n}\n\nexport function isCssModulesFile(file: string): boolean {\n return CSS_MODULES_REGEX.test(file)\n}\n\nfunction hasCssSideEffectFreeParam(url: string): boolean {\n const queryString = url.split('?')[1]\n if (!queryString) return false\n\n const params = new URLSearchParams(queryString)\n return CSS_SIDE_EFFECT_FREE_PARAMS.some(\n (param) =>\n params.get(param) === '' &&\n !url.includes(`?${param}=`) &&\n !url.includes(`&${param}=`),\n )\n}\n\n/**\n * Resolve a file path to a Vite dev server URL.\n * Files within the root directory use relative paths, files outside use /@fs prefix.\n */\nfunction resolveDevUrl(rootDirectory: string, filePath: string): string {\n const normalizedPath = filePath.replace(/\\\\/g, '/')\n const relativePath = path.posix.relative(\n rootDirectory.replace(/\\\\/g, '/'),\n normalizedPath,\n )\n const isWithinRoot =\n !relativePath.startsWith('..') && !path.isAbsolute(relativePath)\n\n if (isWithinRoot) {\n return path.posix.join('/', relativePath)\n }\n // Files outside root need /@fs prefix\n return path.posix.join('/@fs', normalizedPath)\n}\n\nexport interface CollectDevStylesOptions {\n viteDevServer: ViteDevServer\n entries: Array<string>\n /** Cache of CSS modules content captured during transform hook */\n cssModulesCache?: Record<string, string>\n}\n\n/**\n * Collect CSS content from the module graph starting from the given entry points.\n */\nexport async function collectDevStyles(\n opts: CollectDevStylesOptions,\n): Promise<string | undefined> {\n const { viteDevServer, entries, cssModulesCache = {} } = opts\n const styles: Map<string, string> = new Map()\n const visited = new Set<ModuleNode>()\n\n const rootDirectory = viteDevServer.config.root\n\n // Process entries in parallel - each entry is independent\n await Promise.all(\n entries.map((entry) =>\n processEntry(viteDevServer, resolveDevUrl(rootDirectory, entry), visited),\n ),\n )\n\n // Collect CSS from visited modules in parallel\n const cssPromises: Array<Promise<readonly [string, string] | null>> = []\n\n for (const dep of visited) {\n if (hasCssSideEffectFreeParam(dep.url)) {\n continue\n }\n\n if (dep.file && isCssModulesFile(dep.file)) {\n const css = cssModulesCache[normalizeCssModuleCacheKey(dep.file)]\n if (!css) {\n throw new Error(\n `[tanstack-start] Missing CSS module in cache: ${dep.file}`,\n )\n }\n styles.set(dep.url, css)\n continue\n }\n\n const fileOrUrl = dep.file ?? dep.url\n if (!isCssFile(fileOrUrl)) {\n continue\n }\n\n // Load regular CSS files in parallel\n cssPromises.push(\n fetchCssFromModule(viteDevServer, dep).then((css) =>\n css ? ([dep.url, css] as const) : null,\n ),\n )\n }\n\n // Wait for all CSS loads to complete\n const cssResults = await Promise.all(cssPromises)\n for (const result of cssResults) {\n if (result) {\n styles.set(result[0], result[1])\n }\n }\n\n if (styles.size === 0) return undefined\n\n const parts: Array<string> = []\n for (const [fileName, css] of styles.entries()) {\n const escapedFileName = fileName\n .replace(ESCAPE_CSS_COMMENT_START_REGEX, '/\\\\*')\n .replace(ESCAPE_CSS_COMMENT_END_REGEX, '*\\\\/')\n parts.push(`\\n/* ${escapedFileName} */\\n${css}`)\n }\n return parts.join('\\n')\n}\n\n/**\n * Process an entry URL: transform it if needed, get the module node, and crawl its dependencies.\n */\nasync function processEntry(\n viteDevServer: ViteDevServer,\n entryUrl: string,\n visited: Set<ModuleNode>,\n): Promise<void> {\n let node = await viteDevServer.moduleGraph.getModuleByUrl(entryUrl)\n\n // Only transform if not yet SSR-transformed (need ssrTransformResult.deps for crawling)\n if (!node?.ssrTransformResult) {\n try {\n await viteDevServer.transformRequest(entryUrl)\n } catch {\n // ignore - module might not exist yet\n }\n node = await viteDevServer.moduleGraph.getModuleByUrl(entryUrl)\n }\n\n if (!node || visited.has(node)) return\n\n visited.add(node)\n await findModuleDeps(viteDevServer, node, visited)\n}\n\n/**\n * Find all module dependencies by crawling the module graph.\n * Uses transformResult.deps for URL-based lookups (parallel) and\n * importedModules for already-resolved nodes (parallel).\n */\nasync function findModuleDeps(\n viteDevServer: ViteDevServer,\n node: ModuleNode,\n visited: Set<ModuleNode>,\n): Promise<void> {\n // Note: caller must add node to visited BEFORE calling this function\n // to prevent race conditions with parallel traversal\n\n // Process deps from transformResult if available (URLs including bare imports)\n const deps =\n node.ssrTransformResult?.deps ?? node.transformResult?.deps ?? null\n\n const importedModules = node.importedModules\n\n // Fast path: no deps and no imports\n if ((!deps || deps.length === 0) && importedModules.size === 0) {\n return\n }\n\n // Build branches only when needed (avoid array allocation on leaf nodes)\n const branches: Array<Promise<void>> = []\n\n if (deps) {\n for (const depUrl of deps) {\n const dep = await viteDevServer.moduleGraph.getModuleByUrl(depUrl)\n if (!dep) continue\n\n if (visited.has(dep)) continue\n visited.add(dep)\n branches.push(findModuleDeps(viteDevServer, dep, visited))\n }\n }\n\n // ALWAYS also traverse importedModules - this catches:\n // - Code-split chunks (e.g. ?tsr-split=component) not in deps\n // - Already-resolved nodes\n for (const depNode of importedModules) {\n if (visited.has(depNode)) continue\n visited.add(depNode)\n branches.push(findModuleDeps(viteDevServer, depNode, visited))\n }\n\n if (branches.length === 1) {\n await branches[0]\n return\n }\n\n await Promise.all(branches)\n}\n\nasync function fetchCssFromModule(\n viteDevServer: ViteDevServer,\n node: ModuleNode,\n): Promise<string | undefined> {\n // Use cached transform result if available\n const cachedCode = node.transformResult?.code ?? node.ssrTransformResult?.code\n if (cachedCode) {\n return extractCssFromCode(cachedCode)\n }\n\n // Otherwise request a fresh transform\n try {\n const transformResult = await viteDevServer.transformRequest(node.url)\n if (!transformResult?.code) return undefined\n\n return extractCssFromCode(transformResult.code)\n } catch {\n // Preprocessor partials (e.g., Sass files with mixins) can't compile in isolation.\n // The root stylesheet that @imports them will contain the compiled CSS.\n return undefined\n }\n}\n\nfunction extractCssFromCode(code: string): string | undefined {\n const match = VITE_CSS_REGEX.exec(code)\n if (!match?.[1]) return undefined\n\n return match[1]\n .replace(/\\\\n/g, '\\n')\n .replace(/\\\\t/g, '\\t')\n .replace(/\\\\\"/g, '\"')\n .replace(/\\\\\\\\/g, '\\\\')\n}\n"],"names":[],"mappings":"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"}
@@ -4,18 +4,28 @@ import { NodeRequest, sendNodeResponse } from "srvx/node";
4
4
  import { VITE_ENVIRONMENT_NAMES, ENTRY_POINTS } from "../constants.js";
5
5
  import { resolveViteId } from "../utils.js";
6
6
  import { extractHtmlScripts } from "./extract-html-scripts.js";
7
- import { collectDevStyles } from "./dev-styles.js";
7
+ import { CSS_MODULES_REGEX, collectDevStyles, normalizeCssModuleCacheKey } from "./dev-styles.js";
8
8
  function devServerPlugin({
9
9
  getConfig
10
10
  }) {
11
11
  let isTest = false;
12
12
  let injectedHeadScripts;
13
+ const cssModulesCache = {};
13
14
  return [
14
15
  {
15
16
  name: "tanstack-start-core:dev-server",
16
17
  config(_userConfig, { mode }) {
17
18
  isTest = isTest ? isTest : mode === "test";
18
19
  },
20
+ // Capture CSS modules content during transform
21
+ transform: {
22
+ filter: {
23
+ id: CSS_MODULES_REGEX
24
+ },
25
+ handler(code, id) {
26
+ cssModulesCache[normalizeCssModuleCacheKey(id)] = code;
27
+ }
28
+ },
19
29
  async configureServer(viteDevServer) {
20
30
  if (isTest) {
21
31
  return;
@@ -27,6 +37,43 @@ function devServerPlugin({
27
37
  );
28
38
  const scripts = extractHtmlScripts(transformedHtml);
29
39
  injectedHeadScripts = scripts.flatMap((script) => script.content ?? []).join(";");
40
+ viteDevServer.middlewares.use(async (req, res, next) => {
41
+ const url = req.url ?? "";
42
+ const pathname = url.split("?")[0];
43
+ if (!pathname?.endsWith("/@tanstack-start/styles.css")) {
44
+ return next();
45
+ }
46
+ try {
47
+ const urlObj = new URL(url, "http://localhost");
48
+ const routesParam = urlObj.searchParams.get("routes");
49
+ const routeIds = routesParam ? routesParam.split(",") : [];
50
+ const entries = [];
51
+ const routesManifest = globalThis.TSS_ROUTES_MANIFEST;
52
+ if (routesManifest && routeIds.length > 0) {
53
+ for (const routeId of routeIds) {
54
+ const route = routesManifest[routeId];
55
+ if (route?.filePath) {
56
+ entries.push(route.filePath);
57
+ }
58
+ }
59
+ }
60
+ const css = entries.length > 0 ? await collectDevStyles({
61
+ viteDevServer,
62
+ entries,
63
+ cssModulesCache
64
+ }) : void 0;
65
+ res.setHeader("Content-Type", "text/css");
66
+ res.setHeader("Cache-Control", "no-store");
67
+ res.end(css ?? "");
68
+ } catch (e) {
69
+ console.error("[tanstack-start] Error collecting dev styles:", e);
70
+ res.setHeader("Content-Type", "text/css");
71
+ res.setHeader("Cache-Control", "no-store");
72
+ res.end(
73
+ `/* Error collecting styles: ${e instanceof Error ? e.message : String(e)} */`
74
+ );
75
+ }
76
+ });
30
77
  return () => {
31
78
  const serverEnv = viteDevServer.environments[VITE_ENVIRONMENT_NAMES.server];
32
79
  if (!serverEnv) {
@@ -34,41 +81,6 @@ function devServerPlugin({
34
81
  `Server environment ${VITE_ENVIRONMENT_NAMES.server} not found`
35
82
  );
36
83
  }
37
- viteDevServer.middlewares.use(async (req, res, next) => {
38
- const url = req.url ?? "";
39
- if (!url.startsWith("/@tanstack-start/styles.css")) {
40
- return next();
41
- }
42
- try {
43
- const urlObj = new URL(url, "http://localhost");
44
- const routesParam = urlObj.searchParams.get("routes");
45
- const routeIds = routesParam ? routesParam.split(",") : [];
46
- const entries = [];
47
- const routesManifest = globalThis.TSS_ROUTES_MANIFEST;
48
- if (routesManifest && routeIds.length > 0) {
49
- for (const routeId of routeIds) {
50
- const route = routesManifest[routeId];
51
- if (route?.filePath) {
52
- entries.push(route.filePath);
53
- }
54
- }
55
- }
56
- const css = entries.length > 0 ? await collectDevStyles({
57
- viteDevServer,
58
- entries
59
- }) : void 0;
60
- res.setHeader("Content-Type", "text/css");
61
- res.setHeader("Cache-Control", "no-store");
62
- res.end(css ?? "");
63
- } catch (e) {
64
- console.error("[tanstack-start] Error collecting dev styles:", e);
65
- res.setHeader("Content-Type", "text/css");
66
- res.setHeader("Cache-Control", "no-store");
67
- res.end(
68
- `/* Error collecting styles: ${e instanceof Error ? e.message : String(e)} */`
69
- );
70
- }
71
- });
72
84
  const { startConfig } = getConfig();
73
85
  const installMiddleware = startConfig.vite?.installDevServerMiddleware;
74
86
  if (installMiddleware === false) {
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sources":["../../../src/dev-server-plugin/plugin.ts"],"sourcesContent":["import { isRunnableDevEnvironment } from 'vite'\nimport { VIRTUAL_MODULES } from '@tanstack/start-server-core'\nimport { NodeRequest, sendNodeResponse } from 'srvx/node'\nimport { ENTRY_POINTS, VITE_ENVIRONMENT_NAMES } from '../constants'\nimport { resolveViteId } from '../utils'\nimport { extractHtmlScripts } from './extract-html-scripts'\nimport { collectDevStyles } from './dev-styles'\nimport type { Connect, DevEnvironment, PluginOption } from 'vite'\nimport type { GetConfigFn } from '../types'\n\nexport function devServerPlugin({\n getConfig,\n}: {\n getConfig: GetConfigFn\n}): PluginOption {\n let isTest = false\n\n let injectedHeadScripts: string | undefined\n\n return [\n {\n name: 'tanstack-start-core:dev-server',\n config(_userConfig, { mode }) {\n isTest = isTest ? isTest : mode === 'test'\n },\n async configureServer(viteDevServer) {\n if (isTest) {\n return\n }\n\n // Extract the scripts that Vite plugins would inject into the initial HTML\n const templateHtml = `<html><head></head><body></body></html>`\n const transformedHtml = await viteDevServer.transformIndexHtml(\n '/',\n templateHtml,\n )\n const scripts = extractHtmlScripts(transformedHtml)\n injectedHeadScripts = scripts\n .flatMap((script) => script.content ?? [])\n .join(';')\n\n return () => {\n const serverEnv = viteDevServer.environments[\n VITE_ENVIRONMENT_NAMES.server\n ] as DevEnvironment | undefined\n\n if (!serverEnv) {\n throw new Error(\n `Server environment ${VITE_ENVIRONMENT_NAMES.server} not found`,\n )\n }\n\n // CSS middleware is always installed - it doesn't depend on the server environment type\n // This ensures dev styles work with nitro, cloudflare, and other environments\n viteDevServer.middlewares.use(async (req, res, next) => {\n const url = req.url ?? ''\n if (!url.startsWith('/@tanstack-start/styles.css')) {\n return next()\n }\n\n try {\n // Parse route IDs from query param\n const urlObj = new URL(url, 'http://localhost')\n const routesParam = urlObj.searchParams.get('routes')\n const routeIds = routesParam ? routesParam.split(',') : []\n\n // Build entries list from route file paths\n const entries: Array<string> = []\n\n // Look up route file paths from manifest\n // Only routes registered in the manifest are used - this prevents path injection\n const routesManifest = (globalThis as any).TSS_ROUTES_MANIFEST as\n | Record<string, { filePath: string; children?: Array<string> }>\n | undefined\n\n if (routesManifest && routeIds.length > 0) {\n for (const routeId of routeIds) {\n const route = routesManifest[routeId]\n if (route?.filePath) {\n entries.push(route.filePath)\n }\n }\n }\n\n const css =\n entries.length > 0\n ? await collectDevStyles({\n viteDevServer,\n entries,\n })\n : undefined\n\n res.setHeader('Content-Type', 'text/css')\n res.setHeader('Cache-Control', 'no-store')\n res.end(css ?? '')\n } catch (e) {\n // Log error but still return valid CSS response to avoid MIME type issues\n console.error('[tanstack-start] Error collecting dev styles:', e)\n res.setHeader('Content-Type', 'text/css')\n res.setHeader('Cache-Control', 'no-store')\n res.end(\n `/* Error collecting styles: ${e instanceof Error ? e.message : String(e)} */`,\n )\n }\n })\n\n const { startConfig } = getConfig()\n const installMiddleware = startConfig.vite?.installDevServerMiddleware\n if (installMiddleware === false) {\n return\n }\n if (installMiddleware == undefined) {\n // do not install middleware in middlewareMode by default\n if (viteDevServer.config.server.middlewareMode) {\n return\n }\n\n // do not install middleware if SSR env in case another plugin already did\n if (\n !isRunnableDevEnvironment(serverEnv) ||\n // do not check via `isFetchableDevEnvironment` since nitro does implement the `FetchableDevEnvironment` interface but not via inheritance (which this helper checks)\n 'dispatchFetch' in serverEnv\n ) {\n return\n }\n }\n\n if (!isRunnableDevEnvironment(serverEnv)) {\n throw new Error(\n 'cannot install vite dev server middleware for TanStack Start since the SSR environment is not a RunnableDevEnvironment',\n )\n }\n\n viteDevServer.middlewares.use(async (req, res) => {\n // fix the request URL to match the original URL\n // otherwise, the request URL will '/index.html'\n if (req.originalUrl) {\n req.url = req.originalUrl\n }\n const webReq = new NodeRequest({ req, res })\n\n try {\n // Import and resolve the request by running the server request entry point\n // this request entry point must implement the `fetch` API as follows:\n /**\n * export default {\n * fetch(req: Request): Promise<Response>\n * }\n */\n const serverEntry = await serverEnv.runner.import(\n ENTRY_POINTS.server,\n )\n const webRes = await serverEntry['default'].fetch(webReq)\n\n return sendNodeResponse(res, webRes)\n } catch (e) {\n console.error(e)\n try {\n viteDevServer.ssrFixStacktrace(e as Error)\n } catch (_e) {}\n\n if (\n webReq.headers.get('content-type')?.includes('application/json')\n ) {\n return sendNodeResponse(\n res,\n new Response(\n JSON.stringify(\n {\n status: 500,\n error: 'Internal Server Error',\n message:\n 'An unexpected error occurred. Please try again later.',\n timestamp: new Date().toISOString(),\n },\n null,\n 2,\n ),\n {\n status: 500,\n headers: {\n 'Content-Type': 'application/json',\n },\n },\n ),\n )\n }\n\n return sendNodeResponse(\n res,\n new Response(\n `\n <!DOCTYPE html>\n <html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Error</title>\n <script type=\"module\">\n import { ErrorOverlay } from '/@vite/client'\n document.body.appendChild(new ErrorOverlay(${JSON.stringify(\n prepareError(req, e),\n ).replace(/</g, '\\\\u003c')}))\n </script>\n </head>\n <body>\n </body>\n </html>\n `,\n {\n status: 500,\n headers: {\n 'Content-Type': 'text/html',\n },\n },\n ),\n )\n }\n })\n }\n },\n },\n {\n name: 'tanstack-start-core:dev-server:injected-head-scripts',\n sharedDuringBuild: true,\n applyToEnvironment: (env) => env.config.consumer === 'server',\n resolveId: {\n filter: { id: new RegExp(VIRTUAL_MODULES.injectedHeadScripts) },\n handler(_id) {\n return resolveViteId(VIRTUAL_MODULES.injectedHeadScripts)\n },\n },\n load: {\n filter: {\n id: new RegExp(resolveViteId(VIRTUAL_MODULES.injectedHeadScripts)),\n },\n handler() {\n const mod = `\n export const injectedHeadScripts = ${JSON.stringify(injectedHeadScripts) || 'undefined'}`\n return mod\n },\n },\n },\n ]\n}\n\n/**\n * Formats error for SSR message in error overlay\n * @param req\n * @param error\n * @returns\n */\nfunction prepareError(req: Connect.IncomingMessage, error: unknown) {\n const e = error as Error\n return {\n message: `An error occurred while server rendering ${req.url}:\\n\\n\\t${\n typeof e === 'string' ? e : e.message\n } `,\n stack: typeof e === 'string' ? '' : e.stack,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugin.js","sources":["../../../src/dev-server-plugin/plugin.ts"],"sourcesContent":["import { isRunnableDevEnvironment } from 'vite'\nimport { VIRTUAL_MODULES } from '@tanstack/start-server-core'\nimport { NodeRequest, sendNodeResponse } from 'srvx/node'\nimport { ENTRY_POINTS, VITE_ENVIRONMENT_NAMES } from '../constants'\nimport { resolveViteId } from '../utils'\nimport { extractHtmlScripts } from './extract-html-scripts'\nimport {\n CSS_MODULES_REGEX,\n collectDevStyles,\n normalizeCssModuleCacheKey,\n} from './dev-styles'\nimport type { Connect, DevEnvironment, PluginOption } from 'vite'\nimport type { GetConfigFn } from '../types'\n\nexport function devServerPlugin({\n getConfig,\n}: {\n getConfig: GetConfigFn\n}): PluginOption {\n let isTest = false\n\n let injectedHeadScripts: string | undefined\n\n // Cache CSS modules content during transform hook.\n // For CSS modules, the transform hook receives the raw CSS content before\n // Vite wraps it in JS. We capture this to use during SSR style collection.\n const cssModulesCache: Record<string, string> = {}\n\n return [\n {\n name: 'tanstack-start-core:dev-server',\n config(_userConfig, { mode }) {\n isTest = isTest ? isTest : mode === 'test'\n },\n // Capture CSS modules content during transform\n transform: {\n filter: {\n id: CSS_MODULES_REGEX,\n },\n handler(code, id) {\n cssModulesCache[normalizeCssModuleCacheKey(id)] = code\n },\n },\n async configureServer(viteDevServer) {\n if (isTest) {\n return\n }\n\n // Extract the scripts that Vite plugins would inject into the initial HTML\n const templateHtml = `<html><head></head><body></body></html>`\n const transformedHtml = await viteDevServer.transformIndexHtml(\n '/',\n templateHtml,\n )\n const scripts = extractHtmlScripts(transformedHtml)\n injectedHeadScripts = scripts\n .flatMap((script) => script.content ?? [])\n .join(';')\n\n // CSS middleware registered in PRE-PHASE (before Vite's internal middlewares)\n // This ensures it handles /@tanstack-start/styles.css before any catch-all middleware\n // from other plugins (like nitro) that may be registered in the post-phase.\n // This makes the CSS endpoint work regardless of plugin order in the Vite config.\n // We check pathname.endsWith() to handle basepaths (e.g., /my-app/@tanstack-start/styles.css)\n // since pre-phase runs before Vite's base middleware strips the basepath.\n viteDevServer.middlewares.use(async (req, res, next) => {\n const url = req.url ?? ''\n const pathname = url.split('?')[0]\n if (!pathname?.endsWith('/@tanstack-start/styles.css')) {\n return next()\n }\n\n try {\n // Parse route IDs from query param\n const urlObj = new URL(url, 'http://localhost')\n const routesParam = urlObj.searchParams.get('routes')\n const routeIds = routesParam ? routesParam.split(',') : []\n\n // Build entries list from route file paths\n const entries: Array<string> = []\n\n // Look up route file paths from manifest\n // Only routes registered in the manifest are used - this prevents path injection\n const routesManifest = (globalThis as any).TSS_ROUTES_MANIFEST as\n | Record<string, { filePath: string; children?: Array<string> }>\n | undefined\n\n if (routesManifest && routeIds.length > 0) {\n for (const routeId of routeIds) {\n const route = routesManifest[routeId]\n if (route?.filePath) {\n entries.push(route.filePath)\n }\n }\n }\n\n const css =\n entries.length > 0\n ? await collectDevStyles({\n viteDevServer,\n entries,\n cssModulesCache,\n })\n : undefined\n\n res.setHeader('Content-Type', 'text/css')\n res.setHeader('Cache-Control', 'no-store')\n res.end(css ?? '')\n } catch (e) {\n // Log error but still return valid CSS response to avoid MIME type issues\n console.error('[tanstack-start] Error collecting dev styles:', e)\n res.setHeader('Content-Type', 'text/css')\n res.setHeader('Cache-Control', 'no-store')\n res.end(\n `/* Error collecting styles: ${e instanceof Error ? e.message : String(e)} */`,\n )\n }\n })\n\n return () => {\n const serverEnv = viteDevServer.environments[\n VITE_ENVIRONMENT_NAMES.server\n ] as DevEnvironment | undefined\n\n if (!serverEnv) {\n throw new Error(\n `Server environment ${VITE_ENVIRONMENT_NAMES.server} not found`,\n )\n }\n\n const { startConfig } = getConfig()\n const installMiddleware = startConfig.vite?.installDevServerMiddleware\n if (installMiddleware === false) {\n return\n }\n if (installMiddleware == undefined) {\n // do not install middleware in middlewareMode by default\n if (viteDevServer.config.server.middlewareMode) {\n return\n }\n\n // do not install middleware if SSR env in case another plugin already did\n if (\n !isRunnableDevEnvironment(serverEnv) ||\n // do not check via `isFetchableDevEnvironment` since nitro does implement the `FetchableDevEnvironment` interface but not via inheritance (which this helper checks)\n 'dispatchFetch' in serverEnv\n ) {\n return\n }\n }\n\n if (!isRunnableDevEnvironment(serverEnv)) {\n throw new Error(\n 'cannot install vite dev server middleware for TanStack Start since the SSR environment is not a RunnableDevEnvironment',\n )\n }\n\n viteDevServer.middlewares.use(async (req, res) => {\n // fix the request URL to match the original URL\n // otherwise, the request URL will '/index.html'\n if (req.originalUrl) {\n req.url = req.originalUrl\n }\n const webReq = new NodeRequest({ req, res })\n\n try {\n // Import and resolve the request by running the server request entry point\n // this request entry point must implement the `fetch` API as follows:\n /**\n * export default {\n * fetch(req: Request): Promise<Response>\n * }\n */\n const serverEntry = await serverEnv.runner.import(\n ENTRY_POINTS.server,\n )\n const webRes = await serverEntry['default'].fetch(webReq)\n\n return sendNodeResponse(res, webRes)\n } catch (e) {\n console.error(e)\n try {\n viteDevServer.ssrFixStacktrace(e as Error)\n } catch (_e) {}\n\n if (\n webReq.headers.get('content-type')?.includes('application/json')\n ) {\n return sendNodeResponse(\n res,\n new Response(\n JSON.stringify(\n {\n status: 500,\n error: 'Internal Server Error',\n message:\n 'An unexpected error occurred. Please try again later.',\n timestamp: new Date().toISOString(),\n },\n null,\n 2,\n ),\n {\n status: 500,\n headers: {\n 'Content-Type': 'application/json',\n },\n },\n ),\n )\n }\n\n return sendNodeResponse(\n res,\n new Response(\n `\n <!DOCTYPE html>\n <html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Error</title>\n <script type=\"module\">\n import { ErrorOverlay } from '/@vite/client'\n document.body.appendChild(new ErrorOverlay(${JSON.stringify(\n prepareError(req, e),\n ).replace(/</g, '\\\\u003c')}))\n </script>\n </head>\n <body>\n </body>\n </html>\n `,\n {\n status: 500,\n headers: {\n 'Content-Type': 'text/html',\n },\n },\n ),\n )\n }\n })\n }\n },\n },\n {\n name: 'tanstack-start-core:dev-server:injected-head-scripts',\n sharedDuringBuild: true,\n applyToEnvironment: (env) => env.config.consumer === 'server',\n resolveId: {\n filter: { id: new RegExp(VIRTUAL_MODULES.injectedHeadScripts) },\n handler(_id) {\n return resolveViteId(VIRTUAL_MODULES.injectedHeadScripts)\n },\n },\n load: {\n filter: {\n id: new RegExp(resolveViteId(VIRTUAL_MODULES.injectedHeadScripts)),\n },\n handler() {\n const mod = `\n export const injectedHeadScripts = ${JSON.stringify(injectedHeadScripts) || 'undefined'}`\n return mod\n },\n },\n },\n ]\n}\n\n/**\n * Formats error for SSR message in error overlay\n * @param req\n * @param error\n * @returns\n */\nfunction prepareError(req: Connect.IncomingMessage, error: unknown) {\n const e = error as Error\n return {\n message: `An error occurred while server rendering ${req.url}:\\n\\n\\t${\n typeof e === 'string' ? e : e.message\n } `,\n stack: typeof e === 'string' ? '' : e.stack,\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/start-plugin-core",
3
- "version": "1.149.1",
3
+ "version": "1.149.3",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -59,12 +59,12 @@
59
59
  "vitefu": "^1.1.1",
60
60
  "xmlbuilder2": "^4.0.3",
61
61
  "zod": "^3.24.2",
62
- "@tanstack/router-core": "1.147.1",
63
- "@tanstack/router-generator": "1.149.0",
64
- "@tanstack/router-plugin": "1.149.0",
62
+ "@tanstack/router-core": "1.149.3",
63
+ "@tanstack/router-generator": "1.149.3",
64
+ "@tanstack/router-plugin": "1.149.3",
65
+ "@tanstack/start-client-core": "1.149.3",
65
66
  "@tanstack/router-utils": "1.143.11",
66
- "@tanstack/start-client-core": "1.149.1",
67
- "@tanstack/start-server-core": "1.149.1"
67
+ "@tanstack/start-server-core": "1.149.3"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/babel__code-frame": "^7.0.6",
@@ -2,18 +2,37 @@
2
2
  * CSS collection for dev mode.
3
3
  * Crawls the Vite module graph to collect CSS from the router entry and all its dependencies.
4
4
  */
5
+ import path from 'node:path'
5
6
  import type { ModuleNode, ViteDevServer } from 'vite'
6
7
 
7
8
  // CSS file extensions supported by Vite
8
9
  const CSS_FILE_REGEX =
9
10
  /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/
11
+ // CSS modules file pattern - exported for use in plugin hook filters
12
+ // Note: allow query/hash suffix since Vite ids often include them.
13
+ export const CSS_MODULES_REGEX =
14
+ /\.module\.(css|less|sass|scss|styl|stylus)(?:$|[?#])/i
15
+
16
+ export function normalizeCssModuleCacheKey(idOrFile: string): string {
17
+ const baseId = idOrFile.split('?')[0]!.split('#')[0]!
18
+ return baseId.replace(/\\/g, '/')
19
+ }
10
20
  // URL params that indicate CSS should not be injected (e.g., ?url, ?inline)
11
21
  const CSS_SIDE_EFFECT_FREE_PARAMS = ['url', 'inline', 'raw', 'inline-css']
12
22
 
23
+ const VITE_CSS_REGEX = /const\s+__vite__css\s*=\s*["'`]([\s\S]*?)["'`]/
24
+
25
+ const ESCAPE_CSS_COMMENT_START_REGEX = /\/\*/g
26
+ const ESCAPE_CSS_COMMENT_END_REGEX = /\*\//g
27
+
13
28
  function isCssFile(file: string): boolean {
14
29
  return CSS_FILE_REGEX.test(file)
15
30
  }
16
31
 
32
+ export function isCssModulesFile(file: string): boolean {
33
+ return CSS_MODULES_REGEX.test(file)
34
+ }
35
+
17
36
  function hasCssSideEffectFreeParam(url: string): boolean {
18
37
  const queryString = url.split('?')[1]
19
38
  if (!queryString) return false
@@ -27,9 +46,31 @@ function hasCssSideEffectFreeParam(url: string): boolean {
27
46
  )
28
47
  }
29
48
 
49
+ /**
50
+ * Resolve a file path to a Vite dev server URL.
51
+ * Files within the root directory use relative paths, files outside use /@fs prefix.
52
+ */
53
+ function resolveDevUrl(rootDirectory: string, filePath: string): string {
54
+ const normalizedPath = filePath.replace(/\\/g, '/')
55
+ const relativePath = path.posix.relative(
56
+ rootDirectory.replace(/\\/g, '/'),
57
+ normalizedPath,
58
+ )
59
+ const isWithinRoot =
60
+ !relativePath.startsWith('..') && !path.isAbsolute(relativePath)
61
+
62
+ if (isWithinRoot) {
63
+ return path.posix.join('/', relativePath)
64
+ }
65
+ // Files outside root need /@fs prefix
66
+ return path.posix.join('/@fs', normalizedPath)
67
+ }
68
+
30
69
  export interface CollectDevStylesOptions {
31
70
  viteDevServer: ViteDevServer
32
71
  entries: Array<string>
72
+ /** Cache of CSS modules content captured during transform hook */
73
+ cssModulesCache?: Record<string, string>
33
74
  }
34
75
 
35
76
  /**
@@ -38,139 +79,182 @@ export interface CollectDevStylesOptions {
38
79
  export async function collectDevStyles(
39
80
  opts: CollectDevStylesOptions,
40
81
  ): Promise<string | undefined> {
41
- const { viteDevServer, entries } = opts
82
+ const { viteDevServer, entries, cssModulesCache = {} } = opts
42
83
  const styles: Map<string, string> = new Map()
43
84
  const visited = new Set<ModuleNode>()
44
85
 
45
- for (const entry of entries) {
46
- const normalizedPath = entry.replace(/\\/g, '/')
47
- let node = viteDevServer.moduleGraph.getModuleById(normalizedPath)
86
+ const rootDirectory = viteDevServer.config.root
87
+
88
+ // Process entries in parallel - each entry is independent
89
+ await Promise.all(
90
+ entries.map((entry) =>
91
+ processEntry(viteDevServer, resolveDevUrl(rootDirectory, entry), visited),
92
+ ),
93
+ )
48
94
 
49
- // If module isn't in the graph yet, request it to trigger transform
50
- if (!node) {
51
- try {
52
- await viteDevServer.transformRequest(normalizedPath)
53
- } catch (err) {
54
- // Ignore - the module might not exist yet
95
+ // Collect CSS from visited modules in parallel
96
+ const cssPromises: Array<Promise<readonly [string, string] | null>> = []
97
+
98
+ for (const dep of visited) {
99
+ if (hasCssSideEffectFreeParam(dep.url)) {
100
+ continue
101
+ }
102
+
103
+ if (dep.file && isCssModulesFile(dep.file)) {
104
+ const css = cssModulesCache[normalizeCssModuleCacheKey(dep.file)]
105
+ if (!css) {
106
+ throw new Error(
107
+ `[tanstack-start] Missing CSS module in cache: ${dep.file}`,
108
+ )
55
109
  }
56
- node = viteDevServer.moduleGraph.getModuleById(normalizedPath)
110
+ styles.set(dep.url, css)
111
+ continue
57
112
  }
58
113
 
59
- if (node) {
60
- await crawlModuleForCss(viteDevServer, node, visited, styles)
114
+ const fileOrUrl = dep.file ?? dep.url
115
+ if (!isCssFile(fileOrUrl)) {
116
+ continue
117
+ }
118
+
119
+ // Load regular CSS files in parallel
120
+ cssPromises.push(
121
+ fetchCssFromModule(viteDevServer, dep).then((css) =>
122
+ css ? ([dep.url, css] as const) : null,
123
+ ),
124
+ )
125
+ }
126
+
127
+ // Wait for all CSS loads to complete
128
+ const cssResults = await Promise.all(cssPromises)
129
+ for (const result of cssResults) {
130
+ if (result) {
131
+ styles.set(result[0], result[1])
61
132
  }
62
133
  }
63
134
 
64
135
  if (styles.size === 0) return undefined
65
136
 
66
- return Array.from(styles.entries())
67
- .map(([fileName, css]) => {
68
- const escapedFileName = fileName
69
- .replace(/\/\*/g, '/\\*')
70
- .replace(/\*\//g, '*\\/')
71
- return `\n/* ${escapedFileName} */\n${css}`
72
- })
73
- .join('\n')
137
+ const parts: Array<string> = []
138
+ for (const [fileName, css] of styles.entries()) {
139
+ const escapedFileName = fileName
140
+ .replace(ESCAPE_CSS_COMMENT_START_REGEX, '/\\*')
141
+ .replace(ESCAPE_CSS_COMMENT_END_REGEX, '*\\/')
142
+ parts.push(`\n/* ${escapedFileName} */\n${css}`)
143
+ }
144
+ return parts.join('\n')
74
145
  }
75
146
 
76
- async function crawlModuleForCss(
77
- vite: ViteDevServer,
78
- node: ModuleNode,
147
+ /**
148
+ * Process an entry URL: transform it if needed, get the module node, and crawl its dependencies.
149
+ */
150
+ async function processEntry(
151
+ viteDevServer: ViteDevServer,
152
+ entryUrl: string,
79
153
  visited: Set<ModuleNode>,
80
- styles: Map<string, string>,
81
154
  ): Promise<void> {
82
- if (visited.has(node)) return
83
- visited.add(node)
84
-
85
- const branches: Array<Promise<void>> = []
155
+ let node = await viteDevServer.moduleGraph.getModuleByUrl(entryUrl)
86
156
 
87
- // Ensure the module has been transformed to populate its deps
88
- // This is important for code-split modules that may not have been processed yet
89
- if (!node.ssrTransformResult) {
157
+ // Only transform if not yet SSR-transformed (need ssrTransformResult.deps for crawling)
158
+ if (!node?.ssrTransformResult) {
90
159
  try {
91
- await vite.transformRequest(node.url, { ssr: true })
92
- // Re-fetch the node to get updated state
93
- const updatedNode = await vite.moduleGraph.getModuleByUrl(node.url)
94
- if (updatedNode) {
95
- node = updatedNode
96
- }
160
+ await viteDevServer.transformRequest(entryUrl)
97
161
  } catch {
98
- // Ignore transform errors - the module might not be transformable
162
+ // ignore - module might not exist yet
99
163
  }
164
+ node = await viteDevServer.moduleGraph.getModuleByUrl(entryUrl)
100
165
  }
101
166
 
102
- // Check if this is a CSS file
103
- if (
104
- node.file &&
105
- isCssFile(node.file) &&
106
- !hasCssSideEffectFreeParam(node.url)
107
- ) {
108
- const css = await loadCssContent(vite, node)
109
- if (css) {
110
- styles.set(node.url, css)
111
- }
167
+ if (!node || visited.has(node)) return
168
+
169
+ visited.add(node)
170
+ await findModuleDeps(viteDevServer, node, visited)
171
+ }
172
+
173
+ /**
174
+ * Find all module dependencies by crawling the module graph.
175
+ * Uses transformResult.deps for URL-based lookups (parallel) and
176
+ * importedModules for already-resolved nodes (parallel).
177
+ */
178
+ async function findModuleDeps(
179
+ viteDevServer: ViteDevServer,
180
+ node: ModuleNode,
181
+ visited: Set<ModuleNode>,
182
+ ): Promise<void> {
183
+ // Note: caller must add node to visited BEFORE calling this function
184
+ // to prevent race conditions with parallel traversal
185
+
186
+ // Process deps from transformResult if available (URLs including bare imports)
187
+ const deps =
188
+ node.ssrTransformResult?.deps ?? node.transformResult?.deps ?? null
189
+
190
+ const importedModules = node.importedModules
191
+
192
+ // Fast path: no deps and no imports
193
+ if ((!deps || deps.length === 0) && importedModules.size === 0) {
194
+ return
112
195
  }
113
196
 
114
- // Crawl dependencies using ssrTransformResult.deps and importedModules
115
- // We need both because:
116
- // 1. ssrTransformResult.deps has resolved URLs for SSR dependencies
117
- // 2. importedModules may contain CSS files and code-split modules not in SSR deps
118
- const depsFromSsr = node.ssrTransformResult?.deps ?? []
119
- const urlsToVisit = new Set<string>(depsFromSsr)
120
-
121
- // Check importedModules for CSS files and additional modules
122
- for (const importedNode of node.importedModules) {
123
- if (importedNode.file && isCssFile(importedNode.file)) {
124
- // CSS files often don't appear in ssrTransformResult.deps, add them explicitly
125
- branches.push(crawlModuleForCss(vite, importedNode, visited, styles))
126
- } else if (!urlsToVisit.has(importedNode.url)) {
127
- // Also add non-CSS imports that aren't in SSR deps (e.g., code-split modules)
128
- urlsToVisit.add(importedNode.url)
197
+ // Build branches only when needed (avoid array allocation on leaf nodes)
198
+ const branches: Array<Promise<void>> = []
199
+
200
+ if (deps) {
201
+ for (const depUrl of deps) {
202
+ const dep = await viteDevServer.moduleGraph.getModuleByUrl(depUrl)
203
+ if (!dep) continue
204
+
205
+ if (visited.has(dep)) continue
206
+ visited.add(dep)
207
+ branches.push(findModuleDeps(viteDevServer, dep, visited))
129
208
  }
130
209
  }
131
210
 
132
- for (const depUrl of urlsToVisit) {
133
- branches.push(
134
- (async () => {
135
- const depNode = await vite.moduleGraph.getModuleByUrl(depUrl)
136
- if (depNode) {
137
- await crawlModuleForCss(vite, depNode, visited, styles)
138
- }
139
- })(),
140
- )
211
+ // ALWAYS also traverse importedModules - this catches:
212
+ // - Code-split chunks (e.g. ?tsr-split=component) not in deps
213
+ // - Already-resolved nodes
214
+ for (const depNode of importedModules) {
215
+ if (visited.has(depNode)) continue
216
+ visited.add(depNode)
217
+ branches.push(findModuleDeps(viteDevServer, depNode, visited))
218
+ }
219
+
220
+ if (branches.length === 1) {
221
+ await branches[0]
222
+ return
141
223
  }
142
224
 
143
225
  await Promise.all(branches)
144
226
  }
145
227
 
146
- async function loadCssContent(
147
- vite: ViteDevServer,
228
+ async function fetchCssFromModule(
229
+ viteDevServer: ViteDevServer,
148
230
  node: ModuleNode,
149
231
  ): Promise<string | undefined> {
150
- // For ALL CSS files (including CSS modules), get the transformed content
151
- // and extract __vite__css. Vite's transform puts the final CSS (with hashed
152
- // class names for modules) into the __vite__css variable.
153
- const transformResult = await vite.transformRequest(node.url)
154
- if (!transformResult?.code) return undefined
155
-
156
- // Extract CSS content from Vite's transformed module
157
- return extractCssFromViteModule(transformResult.code)
158
- }
232
+ // Use cached transform result if available
233
+ const cachedCode = node.transformResult?.code ?? node.ssrTransformResult?.code
234
+ if (cachedCode) {
235
+ return extractCssFromCode(cachedCode)
236
+ }
159
237
 
160
- /**
161
- * Extract CSS string from Vite's transformed CSS module code.
162
- * Vite wraps CSS content in a JS module with __vite__css variable.
163
- */
164
- function extractCssFromViteModule(code: string): string | undefined {
165
- // Match: const __vite__css = "..."
166
- const match = code.match(/const\s+__vite__css\s*=\s*["'`]([\s\S]*?)["'`]/)
167
- if (match?.[1]) {
168
- // Unescape the string
169
- return match[1]
170
- .replace(/\\n/g, '\n')
171
- .replace(/\\t/g, '\t')
172
- .replace(/\\"/g, '"')
173
- .replace(/\\\\/g, '\\')
238
+ // Otherwise request a fresh transform
239
+ try {
240
+ const transformResult = await viteDevServer.transformRequest(node.url)
241
+ if (!transformResult?.code) return undefined
242
+
243
+ return extractCssFromCode(transformResult.code)
244
+ } catch {
245
+ // Preprocessor partials (e.g., Sass files with mixins) can't compile in isolation.
246
+ // The root stylesheet that @imports them will contain the compiled CSS.
247
+ return undefined
174
248
  }
175
- return undefined
249
+ }
250
+
251
+ function extractCssFromCode(code: string): string | undefined {
252
+ const match = VITE_CSS_REGEX.exec(code)
253
+ if (!match?.[1]) return undefined
254
+
255
+ return match[1]
256
+ .replace(/\\n/g, '\n')
257
+ .replace(/\\t/g, '\t')
258
+ .replace(/\\"/g, '"')
259
+ .replace(/\\\\/g, '\\')
176
260
  }
@@ -4,7 +4,11 @@ import { NodeRequest, sendNodeResponse } from 'srvx/node'
4
4
  import { ENTRY_POINTS, VITE_ENVIRONMENT_NAMES } from '../constants'
5
5
  import { resolveViteId } from '../utils'
6
6
  import { extractHtmlScripts } from './extract-html-scripts'
7
- import { collectDevStyles } from './dev-styles'
7
+ import {
8
+ CSS_MODULES_REGEX,
9
+ collectDevStyles,
10
+ normalizeCssModuleCacheKey,
11
+ } from './dev-styles'
8
12
  import type { Connect, DevEnvironment, PluginOption } from 'vite'
9
13
  import type { GetConfigFn } from '../types'
10
14
 
@@ -17,12 +21,26 @@ export function devServerPlugin({
17
21
 
18
22
  let injectedHeadScripts: string | undefined
19
23
 
24
+ // Cache CSS modules content during transform hook.
25
+ // For CSS modules, the transform hook receives the raw CSS content before
26
+ // Vite wraps it in JS. We capture this to use during SSR style collection.
27
+ const cssModulesCache: Record<string, string> = {}
28
+
20
29
  return [
21
30
  {
22
31
  name: 'tanstack-start-core:dev-server',
23
32
  config(_userConfig, { mode }) {
24
33
  isTest = isTest ? isTest : mode === 'test'
25
34
  },
35
+ // Capture CSS modules content during transform
36
+ transform: {
37
+ filter: {
38
+ id: CSS_MODULES_REGEX,
39
+ },
40
+ handler(code, id) {
41
+ cssModulesCache[normalizeCssModuleCacheKey(id)] = code
42
+ },
43
+ },
26
44
  async configureServer(viteDevServer) {
27
45
  if (isTest) {
28
46
  return
@@ -39,6 +57,66 @@ export function devServerPlugin({
39
57
  .flatMap((script) => script.content ?? [])
40
58
  .join(';')
41
59
 
60
+ // CSS middleware registered in PRE-PHASE (before Vite's internal middlewares)
61
+ // This ensures it handles /@tanstack-start/styles.css before any catch-all middleware
62
+ // from other plugins (like nitro) that may be registered in the post-phase.
63
+ // This makes the CSS endpoint work regardless of plugin order in the Vite config.
64
+ // We check pathname.endsWith() to handle basepaths (e.g., /my-app/@tanstack-start/styles.css)
65
+ // since pre-phase runs before Vite's base middleware strips the basepath.
66
+ viteDevServer.middlewares.use(async (req, res, next) => {
67
+ const url = req.url ?? ''
68
+ const pathname = url.split('?')[0]
69
+ if (!pathname?.endsWith('/@tanstack-start/styles.css')) {
70
+ return next()
71
+ }
72
+
73
+ try {
74
+ // Parse route IDs from query param
75
+ const urlObj = new URL(url, 'http://localhost')
76
+ const routesParam = urlObj.searchParams.get('routes')
77
+ const routeIds = routesParam ? routesParam.split(',') : []
78
+
79
+ // Build entries list from route file paths
80
+ const entries: Array<string> = []
81
+
82
+ // Look up route file paths from manifest
83
+ // Only routes registered in the manifest are used - this prevents path injection
84
+ const routesManifest = (globalThis as any).TSS_ROUTES_MANIFEST as
85
+ | Record<string, { filePath: string; children?: Array<string> }>
86
+ | undefined
87
+
88
+ if (routesManifest && routeIds.length > 0) {
89
+ for (const routeId of routeIds) {
90
+ const route = routesManifest[routeId]
91
+ if (route?.filePath) {
92
+ entries.push(route.filePath)
93
+ }
94
+ }
95
+ }
96
+
97
+ const css =
98
+ entries.length > 0
99
+ ? await collectDevStyles({
100
+ viteDevServer,
101
+ entries,
102
+ cssModulesCache,
103
+ })
104
+ : undefined
105
+
106
+ res.setHeader('Content-Type', 'text/css')
107
+ res.setHeader('Cache-Control', 'no-store')
108
+ res.end(css ?? '')
109
+ } catch (e) {
110
+ // Log error but still return valid CSS response to avoid MIME type issues
111
+ console.error('[tanstack-start] Error collecting dev styles:', e)
112
+ res.setHeader('Content-Type', 'text/css')
113
+ res.setHeader('Cache-Control', 'no-store')
114
+ res.end(
115
+ `/* Error collecting styles: ${e instanceof Error ? e.message : String(e)} */`,
116
+ )
117
+ }
118
+ })
119
+
42
120
  return () => {
43
121
  const serverEnv = viteDevServer.environments[
44
122
  VITE_ENVIRONMENT_NAMES.server
@@ -50,60 +128,6 @@ export function devServerPlugin({
50
128
  )
51
129
  }
52
130
 
53
- // CSS middleware is always installed - it doesn't depend on the server environment type
54
- // This ensures dev styles work with nitro, cloudflare, and other environments
55
- viteDevServer.middlewares.use(async (req, res, next) => {
56
- const url = req.url ?? ''
57
- if (!url.startsWith('/@tanstack-start/styles.css')) {
58
- return next()
59
- }
60
-
61
- try {
62
- // Parse route IDs from query param
63
- const urlObj = new URL(url, 'http://localhost')
64
- const routesParam = urlObj.searchParams.get('routes')
65
- const routeIds = routesParam ? routesParam.split(',') : []
66
-
67
- // Build entries list from route file paths
68
- const entries: Array<string> = []
69
-
70
- // Look up route file paths from manifest
71
- // Only routes registered in the manifest are used - this prevents path injection
72
- const routesManifest = (globalThis as any).TSS_ROUTES_MANIFEST as
73
- | Record<string, { filePath: string; children?: Array<string> }>
74
- | undefined
75
-
76
- if (routesManifest && routeIds.length > 0) {
77
- for (const routeId of routeIds) {
78
- const route = routesManifest[routeId]
79
- if (route?.filePath) {
80
- entries.push(route.filePath)
81
- }
82
- }
83
- }
84
-
85
- const css =
86
- entries.length > 0
87
- ? await collectDevStyles({
88
- viteDevServer,
89
- entries,
90
- })
91
- : undefined
92
-
93
- res.setHeader('Content-Type', 'text/css')
94
- res.setHeader('Cache-Control', 'no-store')
95
- res.end(css ?? '')
96
- } catch (e) {
97
- // Log error but still return valid CSS response to avoid MIME type issues
98
- console.error('[tanstack-start] Error collecting dev styles:', e)
99
- res.setHeader('Content-Type', 'text/css')
100
- res.setHeader('Cache-Control', 'no-store')
101
- res.end(
102
- `/* Error collecting styles: ${e instanceof Error ? e.message : String(e)} */`,
103
- )
104
- }
105
- })
106
-
107
131
  const { startConfig } = getConfig()
108
132
  const installMiddleware = startConfig.vite?.installDevServerMiddleware
109
133
  if (installMiddleware === false) {