@tanstack/start-plugin-core 1.146.3 → 1.147.0

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.
@@ -0,0 +1,9 @@
1
+ import { ViteDevServer } from 'vite';
2
+ export interface CollectDevStylesOptions {
3
+ viteDevServer: ViteDevServer;
4
+ entries: Array<string>;
5
+ }
6
+ /**
7
+ * Collect CSS content from the module graph starting from the given entry points.
8
+ */
9
+ export declare function collectDevStyles(opts: CollectDevStylesOptions): Promise<string | undefined>;
@@ -0,0 +1,96 @@
1
+ const CSS_FILE_REGEX = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
2
+ const CSS_SIDE_EFFECT_FREE_PARAMS = ["url", "inline", "raw", "inline-css"];
3
+ function isCssFile(file) {
4
+ return CSS_FILE_REGEX.test(file);
5
+ }
6
+ function hasCssSideEffectFreeParam(url) {
7
+ const queryString = url.split("?")[1];
8
+ if (!queryString) return false;
9
+ const params = new URLSearchParams(queryString);
10
+ return CSS_SIDE_EFFECT_FREE_PARAMS.some(
11
+ (param) => params.get(param) === "" && !url.includes(`?${param}=`) && !url.includes(`&${param}=`)
12
+ );
13
+ }
14
+ async function collectDevStyles(opts) {
15
+ const { viteDevServer, entries } = opts;
16
+ const styles = /* @__PURE__ */ new Map();
17
+ const visited = /* @__PURE__ */ new Set();
18
+ for (const entry of entries) {
19
+ const normalizedPath = entry.replace(/\\/g, "/");
20
+ let node = await viteDevServer.moduleGraph.getModuleById(normalizedPath);
21
+ if (!node) {
22
+ try {
23
+ await viteDevServer.transformRequest(normalizedPath);
24
+ } catch (err) {
25
+ }
26
+ node = await viteDevServer.moduleGraph.getModuleById(normalizedPath);
27
+ }
28
+ if (node) {
29
+ await crawlModuleForCss(viteDevServer, node, visited, styles);
30
+ }
31
+ }
32
+ 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 `
36
+ /* ${escapedFileName} */
37
+ ${css}`;
38
+ }).join("\n");
39
+ }
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) {
45
+ 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
+ }
51
+ } catch {
52
+ }
53
+ }
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
+ }
59
+ }
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);
67
+ }
68
+ }
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
+ );
78
+ }
79
+ await Promise.all(branches);
80
+ }
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, "\\");
90
+ }
91
+ return void 0;
92
+ }
93
+ export {
94
+ collectDevStyles
95
+ };
96
+ //# sourceMappingURL=dev-styles.js.map
@@ -0,0 +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 = await 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 = await 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,7 +1,5 @@
1
1
  import { PluginOption } from 'vite';
2
- import { TanStackStartOutputConfig } from '../schema.js';
2
+ import { GetConfigFn } from '../types.js';
3
3
  export declare function devServerPlugin({ getConfig, }: {
4
- getConfig: () => {
5
- startConfig: TanStackStartOutputConfig;
6
- };
4
+ getConfig: GetConfigFn;
7
5
  }): PluginOption;
@@ -4,6 +4,7 @@ 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
8
  function devServerPlugin({
8
9
  getConfig
9
10
  }) {
@@ -52,6 +53,32 @@ function devServerPlugin({
52
53
  "cannot install vite dev server middleware for TanStack Start since the SSR environment is not a RunnableDevEnvironment"
53
54
  );
54
55
  }
56
+ viteDevServer.middlewares.use(async (req, res, next) => {
57
+ const url = req.url ?? "";
58
+ if (!url.startsWith("/@tanstack-start/styles.css")) {
59
+ return next();
60
+ }
61
+ const urlObj = new URL(url, "http://localhost");
62
+ const routesParam = urlObj.searchParams.get("routes");
63
+ const routeIds = routesParam ? routesParam.split(",") : [];
64
+ const entries = [];
65
+ const routesManifest = globalThis.TSS_ROUTES_MANIFEST;
66
+ if (routesManifest && routeIds.length > 0) {
67
+ for (const routeId of routeIds) {
68
+ const route = routesManifest[routeId];
69
+ if (route?.filePath) {
70
+ entries.push(route.filePath);
71
+ }
72
+ }
73
+ }
74
+ const css = entries.length > 0 ? await collectDevStyles({
75
+ viteDevServer,
76
+ entries
77
+ }) : void 0;
78
+ res.setHeader("Content-Type", "text/css");
79
+ res.setHeader("Cache-Control", "no-store");
80
+ res.end(css ?? "");
81
+ });
55
82
  viteDevServer.middlewares.use(async (req, res) => {
56
83
  if (req.originalUrl) {
57
84
  req.url = req.originalUrl;
@@ -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 type { Connect, DevEnvironment, PluginOption } from 'vite'\nimport type { TanStackStartOutputConfig } from '../schema'\n\nexport function devServerPlugin({\n getConfig,\n}: {\n getConfig: () => { startConfig: TanStackStartOutputConfig }\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 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 { 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 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 // Middleware to serve collected CSS for dev mode\n // Security: Route IDs from query params are validated against TSS_ROUTES_MANIFEST.\n // Only routes that exist in the manifest will have their CSS collected.\n // Arbitrary file paths cannot be injected.\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 // 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 })\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.146.3",
3
+ "version": "1.147.0",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -59,11 +59,11 @@
59
59
  "vitefu": "^1.1.1",
60
60
  "xmlbuilder2": "^4.0.3",
61
61
  "zod": "^3.24.2",
62
- "@tanstack/router-core": "1.146.2",
63
62
  "@tanstack/router-generator": "1.146.2",
64
- "@tanstack/start-client-core": "1.146.2",
65
- "@tanstack/router-plugin": "1.146.3",
63
+ "@tanstack/router-core": "1.146.2",
66
64
  "@tanstack/router-utils": "1.143.11",
65
+ "@tanstack/start-client-core": "1.146.2",
66
+ "@tanstack/router-plugin": "1.147.0",
67
67
  "@tanstack/start-server-core": "1.146.2"
68
68
  },
69
69
  "devDependencies": {
@@ -0,0 +1,176 @@
1
+ /**
2
+ * CSS collection for dev mode.
3
+ * Crawls the Vite module graph to collect CSS from the router entry and all its dependencies.
4
+ */
5
+ import type { ModuleNode, ViteDevServer } from 'vite'
6
+
7
+ // CSS file extensions supported by Vite
8
+ const CSS_FILE_REGEX =
9
+ /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/
10
+ // URL params that indicate CSS should not be injected (e.g., ?url, ?inline)
11
+ const CSS_SIDE_EFFECT_FREE_PARAMS = ['url', 'inline', 'raw', 'inline-css']
12
+
13
+ function isCssFile(file: string): boolean {
14
+ return CSS_FILE_REGEX.test(file)
15
+ }
16
+
17
+ function hasCssSideEffectFreeParam(url: string): boolean {
18
+ const queryString = url.split('?')[1]
19
+ if (!queryString) return false
20
+
21
+ const params = new URLSearchParams(queryString)
22
+ return CSS_SIDE_EFFECT_FREE_PARAMS.some(
23
+ (param) =>
24
+ params.get(param) === '' &&
25
+ !url.includes(`?${param}=`) &&
26
+ !url.includes(`&${param}=`),
27
+ )
28
+ }
29
+
30
+ export interface CollectDevStylesOptions {
31
+ viteDevServer: ViteDevServer
32
+ entries: Array<string>
33
+ }
34
+
35
+ /**
36
+ * Collect CSS content from the module graph starting from the given entry points.
37
+ */
38
+ export async function collectDevStyles(
39
+ opts: CollectDevStylesOptions,
40
+ ): Promise<string | undefined> {
41
+ const { viteDevServer, entries } = opts
42
+ const styles: Map<string, string> = new Map()
43
+ const visited = new Set<ModuleNode>()
44
+
45
+ for (const entry of entries) {
46
+ const normalizedPath = entry.replace(/\\/g, '/')
47
+ let node = await viteDevServer.moduleGraph.getModuleById(normalizedPath)
48
+
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
55
+ }
56
+ node = await viteDevServer.moduleGraph.getModuleById(normalizedPath)
57
+ }
58
+
59
+ if (node) {
60
+ await crawlModuleForCss(viteDevServer, node, visited, styles)
61
+ }
62
+ }
63
+
64
+ if (styles.size === 0) return undefined
65
+
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')
74
+ }
75
+
76
+ async function crawlModuleForCss(
77
+ vite: ViteDevServer,
78
+ node: ModuleNode,
79
+ visited: Set<ModuleNode>,
80
+ styles: Map<string, string>,
81
+ ): Promise<void> {
82
+ if (visited.has(node)) return
83
+ visited.add(node)
84
+
85
+ const branches: Array<Promise<void>> = []
86
+
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) {
90
+ 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
+ }
97
+ } catch {
98
+ // Ignore transform errors - the module might not be transformable
99
+ }
100
+ }
101
+
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
+ }
112
+ }
113
+
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)
129
+ }
130
+ }
131
+
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
+ )
141
+ }
142
+
143
+ await Promise.all(branches)
144
+ }
145
+
146
+ async function loadCssContent(
147
+ vite: ViteDevServer,
148
+ node: ModuleNode,
149
+ ): 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
+ }
159
+
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, '\\')
174
+ }
175
+ return undefined
176
+ }
@@ -4,13 +4,14 @@ 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
8
  import type { Connect, DevEnvironment, PluginOption } from 'vite'
8
- import type { TanStackStartOutputConfig } from '../schema'
9
+ import type { GetConfigFn } from '../types'
9
10
 
10
11
  export function devServerPlugin({
11
12
  getConfig,
12
13
  }: {
13
- getConfig: () => { startConfig: TanStackStartOutputConfig }
14
+ getConfig: GetConfigFn
14
15
  }): PluginOption {
15
16
  let isTest = false
16
17
 
@@ -75,6 +76,52 @@ export function devServerPlugin({
75
76
  )
76
77
  }
77
78
 
79
+ // Middleware to serve collected CSS for dev mode
80
+ // Security: Route IDs from query params are validated against TSS_ROUTES_MANIFEST.
81
+ // Only routes that exist in the manifest will have their CSS collected.
82
+ // Arbitrary file paths cannot be injected.
83
+ viteDevServer.middlewares.use(async (req, res, next) => {
84
+ const url = req.url ?? ''
85
+ if (!url.startsWith('/@tanstack-start/styles.css')) {
86
+ return next()
87
+ }
88
+
89
+ // Parse route IDs from query param
90
+ const urlObj = new URL(url, 'http://localhost')
91
+ const routesParam = urlObj.searchParams.get('routes')
92
+ const routeIds = routesParam ? routesParam.split(',') : []
93
+
94
+ // Build entries list from route file paths
95
+ const entries: Array<string> = []
96
+
97
+ // Look up route file paths from manifest
98
+ // Only routes registered in the manifest are used - this prevents path injection
99
+ const routesManifest = (globalThis as any).TSS_ROUTES_MANIFEST as
100
+ | Record<string, { filePath: string; children?: Array<string> }>
101
+ | undefined
102
+
103
+ if (routesManifest && routeIds.length > 0) {
104
+ for (const routeId of routeIds) {
105
+ const route = routesManifest[routeId]
106
+ if (route?.filePath) {
107
+ entries.push(route.filePath)
108
+ }
109
+ }
110
+ }
111
+
112
+ const css =
113
+ entries.length > 0
114
+ ? await collectDevStyles({
115
+ viteDevServer,
116
+ entries,
117
+ })
118
+ : undefined
119
+
120
+ res.setHeader('Content-Type', 'text/css')
121
+ res.setHeader('Cache-Control', 'no-store')
122
+ res.end(css ?? '')
123
+ })
124
+
78
125
  viteDevServer.middlewares.use(async (req, res) => {
79
126
  // fix the request URL to match the original URL
80
127
  // otherwise, the request URL will '/index.html'