@tanstack/start-plugin-core 1.146.3 → 1.147.1
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 +9 -0
- package/dist/esm/dev-server-plugin/dev-styles.js +96 -0
- package/dist/esm/dev-server-plugin/dev-styles.js.map +1 -0
- package/dist/esm/dev-server-plugin/plugin.d.ts +2 -4
- package/dist/esm/dev-server-plugin/plugin.js +36 -0
- package/dist/esm/dev-server-plugin/plugin.js.map +1 -1
- package/package.json +6 -6
- package/src/dev-server-plugin/dev-styles.ts +176 -0
- package/src/dev-server-plugin/plugin.ts +58 -2
|
@@ -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 = viteDevServer.moduleGraph.getModuleById(normalizedPath);
|
|
21
|
+
if (!node) {
|
|
22
|
+
try {
|
|
23
|
+
await viteDevServer.transformRequest(normalizedPath);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
}
|
|
26
|
+
node = 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 = 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,7 +1,5 @@
|
|
|
1
1
|
import { PluginOption } from 'vite';
|
|
2
|
-
import {
|
|
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
|
}) {
|
|
@@ -33,6 +34,41 @@ function devServerPlugin({
|
|
|
33
34
|
`Server environment ${VITE_ENVIRONMENT_NAMES.server} not found`
|
|
34
35
|
);
|
|
35
36
|
}
|
|
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
|
+
});
|
|
36
72
|
const { startConfig } = getConfig();
|
|
37
73
|
const installMiddleware = startConfig.vite?.installDevServerMiddleware;
|
|
38
74
|
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 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\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/start-plugin-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.147.1",
|
|
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.
|
|
64
|
-
"@tanstack/
|
|
65
|
-
"@tanstack/router-plugin": "1.146.3",
|
|
62
|
+
"@tanstack/router-core": "1.147.1",
|
|
63
|
+
"@tanstack/router-generator": "1.147.1",
|
|
64
|
+
"@tanstack/router-plugin": "1.147.1",
|
|
66
65
|
"@tanstack/router-utils": "1.143.11",
|
|
67
|
-
"@tanstack/start-
|
|
66
|
+
"@tanstack/start-client-core": "1.147.1",
|
|
67
|
+
"@tanstack/start-server-core": "1.147.1"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/babel__code-frame": "^7.0.6",
|
|
@@ -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 = 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 = 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 {
|
|
9
|
+
import type { GetConfigFn } from '../types'
|
|
9
10
|
|
|
10
11
|
export function devServerPlugin({
|
|
11
12
|
getConfig,
|
|
12
13
|
}: {
|
|
13
|
-
getConfig:
|
|
14
|
+
getConfig: GetConfigFn
|
|
14
15
|
}): PluginOption {
|
|
15
16
|
let isTest = false
|
|
16
17
|
|
|
@@ -48,6 +49,61 @@ export function devServerPlugin({
|
|
|
48
49
|
`Server environment ${VITE_ENVIRONMENT_NAMES.server} not found`,
|
|
49
50
|
)
|
|
50
51
|
}
|
|
52
|
+
|
|
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
|
+
|
|
51
107
|
const { startConfig } = getConfig()
|
|
52
108
|
const installMiddleware = startConfig.vite?.installDevServerMiddleware
|
|
53
109
|
if (installMiddleware === false) {
|