@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.
- package/dist/esm/dev-server-plugin/dev-styles.d.ts +5 -0
- package/dist/esm/dev-server-plugin/dev-styles.js +115 -58
- package/dist/esm/dev-server-plugin/dev-styles.js.map +1 -1
- package/dist/esm/dev-server-plugin/plugin.js +48 -36
- package/dist/esm/dev-server-plugin/plugin.js.map +1 -1
- package/package.json +6 -6
- package/src/dev-server-plugin/dev-styles.ts +183 -99
- package/src/dev-server-plugin/plugin.ts +79 -55
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
}
|
|
85
|
+
${css}`);
|
|
86
|
+
}
|
|
87
|
+
return parts.join("\n");
|
|
39
88
|
}
|
|
40
|
-
async function
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
63
|
-
"@tanstack/router-generator": "1.149.
|
|
64
|
-
"@tanstack/router-plugin": "1.149.
|
|
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-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
110
|
+
styles.set(dep.url, css)
|
|
111
|
+
continue
|
|
57
112
|
}
|
|
58
113
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
83
|
-
visited.add(node)
|
|
84
|
-
|
|
85
|
-
const branches: Array<Promise<void>> = []
|
|
155
|
+
let node = await viteDevServer.moduleGraph.getModuleByUrl(entryUrl)
|
|
86
156
|
|
|
87
|
-
//
|
|
88
|
-
|
|
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
|
|
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
|
-
//
|
|
162
|
+
// ignore - module might not exist yet
|
|
99
163
|
}
|
|
164
|
+
node = await viteDevServer.moduleGraph.getModuleByUrl(entryUrl)
|
|
100
165
|
}
|
|
101
166
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
147
|
-
|
|
228
|
+
async function fetchCssFromModule(
|
|
229
|
+
viteDevServer: ViteDevServer,
|
|
148
230
|
node: ModuleNode,
|
|
149
231
|
): Promise<string | undefined> {
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
return
|
|
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
|
-
|
|
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 {
|
|
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) {
|