@tanstack/router-generator
Version:
Modern and scalable routing for React applications
958 lines (957 loc) • 37.6 kB
JavaScript
import path from "node:path";
import * as fsp from "node:fs/promises";
import { mkdtempSync } from "node:fs";
import crypto from "node:crypto";
import { deepEqual, rootRouteId } from "@tanstack/router-core";
import { logging } from "./logger.js";
import { getRouteNodes as getRouteNodes$1, isVirtualConfigFile } from "./filesystem/physical/getRouteNodes.js";
import { getRouteNodes } from "./filesystem/virtual/getRouteNodes.js";
import { rootPathId } from "./filesystem/physical/rootPathId.js";
import { multiSortBy, format, mergeImportDeclarations, buildImportString, replaceBackslash, removeExt, checkFileExists, resetRegex, hasParentRoute, determineNodePath, trimPathLeft, removeGroups, removeUnderscores, removeLayoutSegments, removeLastSegmentFromPath, routePathToVariable, buildRouteTreeConfig, findParent, createRouteNodesByFullPath, createRouteNodesByTo, createRouteNodesById, getResolvedRouteNodeVariableName, buildFileRoutesByPathInterface, lowerCaseFirstChar, isRouteNodeValidForAugmentation } from "./utils.js";
import { getTargetTemplate, fillTemplate } from "./template.js";
import { transform } from "./transform/transform.js";
import { defaultGeneratorPlugin } from "./plugin/default-generator-plugin.js";
const DefaultFileSystem = {
stat: (filePath) => fsp.stat(filePath, { bigint: true }),
mkdtempSync,
rename: (oldPath, newPath) => fsp.rename(oldPath, newPath),
writeFile: (filePath, content) => fsp.writeFile(filePath, content),
readFile: async (filePath) => {
try {
const fileHandle = await fsp.open(filePath, "r");
const stat = await fileHandle.stat({ bigint: true });
const fileContent = (await fileHandle.readFile()).toString();
await fileHandle.close();
return { stat, fileContent };
} catch (e) {
if ("code" in e) {
if (e.code === "ENOENT") {
return "file-not-existing";
}
}
throw e;
}
}
};
function rerun(opts) {
const { event, ...rest } = opts;
return { rerun: true, event: event ?? { type: "rerun" }, ...rest };
}
function isRerun(result) {
return typeof result === "object" && result !== null && "rerun" in result && result.rerun === true;
}
class Generator {
constructor(opts) {
this.routeNodeCache = /* @__PURE__ */ new Map();
this.routeNodeShadowCache = /* @__PURE__ */ new Map();
this.fileEventQueue = [];
this.plugins = [defaultGeneratorPlugin()];
this.pluginsWithTransform = [];
this.transformPlugins = [];
this.routeGroupPatternRegex = /\(.+\)/g;
this.physicalDirectories = [];
this.config = opts.config;
this.logger = logging({ disabled: this.config.disableLogging });
this.root = opts.root;
this.fs = opts.fs || DefaultFileSystem;
this.tmpDir = this.fs.mkdtempSync(
path.join(this.config.tmpDir, "router-generator-")
);
this.generatedRouteTreePath = path.resolve(this.config.generatedRouteTree);
this.targetTemplate = getTargetTemplate(this.config);
this.routesDirectoryPath = this.getRoutesDirectoryPath();
this.plugins.push(...opts.config.plugins || []);
this.plugins.forEach((plugin) => {
if ("transformPlugin" in plugin) {
if (this.pluginsWithTransform.find((p) => p.name === plugin.name)) {
throw new Error(
`Plugin with name "${plugin.name}" is already registered for export ${plugin.transformPlugin.exportName}!`
);
}
this.pluginsWithTransform.push(plugin);
this.transformPlugins.push(plugin.transformPlugin);
}
});
}
getRoutesDirectoryPath() {
return path.isAbsolute(this.config.routesDirectory) ? this.config.routesDirectory : path.resolve(this.root, this.config.routesDirectory);
}
getRoutesByFileMap() {
return new Map(
[...this.routeNodeCache.entries()].map(([filePath, cacheEntry]) => [
filePath,
{ routePath: cacheEntry.routeId }
])
);
}
async run(event) {
if (event && event.type !== "rerun" && !this.isFileRelevantForRouteTreeGeneration(event.path)) {
return;
}
this.fileEventQueue.push(event ?? { type: "rerun" });
if (this.runPromise) {
return this.runPromise;
}
this.runPromise = (async () => {
do {
const tempQueue = this.fileEventQueue;
this.fileEventQueue = [];
const remainingEvents = (await Promise.all(
tempQueue.map(async (e) => {
if (e.type === "update") {
let cacheEntry;
if (e.path === this.generatedRouteTreePath) {
cacheEntry = this.routeTreeFileCache;
} else {
cacheEntry = this.routeNodeCache.get(e.path);
}
const change = await this.didFileChangeComparedToCache(
{ path: e.path },
cacheEntry
);
if (change.result === false) {
return null;
}
}
return e;
})
)).filter((e) => e !== null);
if (remainingEvents.length === 0) {
break;
}
try {
const start = performance.now();
await this.generatorInternal();
const end = performance.now();
this.logger.info(
`Generated route tree in ${Math.round(end - start)}ms`
);
} catch (err) {
const errArray = !Array.isArray(err) ? [err] : err;
const recoverableErrors = errArray.filter((e) => isRerun(e));
if (recoverableErrors.length === errArray.length) {
this.fileEventQueue.push(...recoverableErrors.map((e) => e.event));
recoverableErrors.forEach((e) => {
if (e.msg) {
this.logger.info(e.msg);
}
});
} else {
const unrecoverableErrors = errArray.filter((e) => !isRerun(e));
this.runPromise = void 0;
throw new Error(
unrecoverableErrors.map((e) => e.message).join()
);
}
}
} while (this.fileEventQueue.length);
this.runPromise = void 0;
})();
return this.runPromise;
}
async generatorInternal() {
let writeRouteTreeFile = false;
let getRouteNodesResult;
if (this.config.virtualRouteConfig) {
getRouteNodesResult = await getRouteNodes(this.config, this.root);
} else {
getRouteNodesResult = await getRouteNodes$1(this.config, this.root);
}
const {
rootRouteNode,
routeNodes: beforeRouteNodes,
physicalDirectories
} = getRouteNodesResult;
if (rootRouteNode === void 0) {
let errorMessage = `rootRouteNode must not be undefined. Make sure you've added your root route into the route-tree.`;
if (!this.config.virtualRouteConfig) {
errorMessage += `
Make sure that you add a "${rootPathId}.${this.config.disableTypes ? "js" : "tsx"}" file to your routes directory.
Add the file in: "${this.config.routesDirectory}/${rootPathId}.${this.config.disableTypes ? "js" : "tsx"}"`;
}
throw new Error(errorMessage);
}
this.physicalDirectories = physicalDirectories;
writeRouteTreeFile = await this.handleRootNode(rootRouteNode);
const preRouteNodes = multiSortBy(beforeRouteNodes, [
(d) => d.routePath === "/" ? -1 : 1,
(d) => {
var _a;
return (_a = d.routePath) == null ? void 0 : _a.split("/").length;
},
(d) => d.filePath.match(new RegExp(`[./]${this.config.indexToken}[.]`)) ? 1 : -1,
(d) => d.filePath.match(
/[./](component|errorComponent|pendingComponent|loader|lazy)[.]/
) ? 1 : -1,
(d) => d.filePath.match(new RegExp(`[./]${this.config.routeToken}[.]`)) ? -1 : 1,
(d) => {
var _a;
return ((_a = d.routePath) == null ? void 0 : _a.endsWith("/")) ? -1 : 1;
},
(d) => d.routePath
]).filter((d) => ![`/${rootPathId}`].includes(d.routePath || ""));
const routeFileAllResult = await Promise.allSettled(
preRouteNodes.filter((n) => !n.isVirtualParentRoute && !n.isVirtual).map((n) => this.processRouteNodeFile(n))
);
const rejections = routeFileAllResult.filter(
(result) => result.status === "rejected"
);
if (rejections.length > 0) {
throw rejections.map((e) => e.reason);
}
const routeFileResult = routeFileAllResult.flatMap((result) => {
if (result.status === "fulfilled" && result.value !== null) {
return result.value;
}
return [];
});
routeFileResult.forEach((result) => {
var _a;
if (!((_a = result.node.exports) == null ? void 0 : _a.length)) {
this.logger.warn(
`Route file "${result.cacheEntry.fileContent}" does not export any route piece. This is likely a mistake.`
);
}
});
if (routeFileResult.find((r) => r.shouldWriteTree)) {
writeRouteTreeFile = true;
}
if (!this.routeTreeFileCache) {
const routeTreeFile = await this.fs.readFile(this.generatedRouteTreePath);
if (routeTreeFile !== "file-not-existing") {
this.routeTreeFileCache = {
fileContent: routeTreeFile.fileContent,
mtimeMs: routeTreeFile.stat.mtimeMs
};
}
writeRouteTreeFile = true;
} else {
const routeTreeFileChange = await this.didFileChangeComparedToCache(
{ path: this.generatedRouteTreePath },
this.routeTreeFileCache
);
if (routeTreeFileChange.result !== false) {
writeRouteTreeFile = "force";
if (routeTreeFileChange.result === true) {
const routeTreeFile = await this.fs.readFile(
this.generatedRouteTreePath
);
if (routeTreeFile !== "file-not-existing") {
this.routeTreeFileCache = {
fileContent: routeTreeFile.fileContent,
mtimeMs: routeTreeFile.stat.mtimeMs
};
}
}
}
}
if (!writeRouteTreeFile) {
for (const fullPath of this.routeNodeCache.keys()) {
if (!this.routeNodeShadowCache.has(fullPath)) {
writeRouteTreeFile = true;
break;
}
}
}
if (!writeRouteTreeFile) {
this.swapCaches();
return;
}
let routeTreeContent = this.buildRouteTreeFileContent(
rootRouteNode,
preRouteNodes,
routeFileResult
);
routeTreeContent = this.config.enableRouteTreeFormatting ? await format(routeTreeContent, this.config) : routeTreeContent;
let newMtimeMs;
if (this.routeTreeFileCache) {
if (writeRouteTreeFile !== "force" && this.routeTreeFileCache.fileContent === routeTreeContent) ;
else {
const newRouteTreeFileStat = await this.safeFileWrite({
filePath: this.generatedRouteTreePath,
newContent: routeTreeContent,
strategy: {
type: "mtime",
expectedMtimeMs: this.routeTreeFileCache.mtimeMs
}
});
newMtimeMs = newRouteTreeFileStat.mtimeMs;
}
} else {
const newRouteTreeFileStat = await this.safeFileWrite({
filePath: this.generatedRouteTreePath,
newContent: routeTreeContent,
strategy: {
type: "new-file"
}
});
newMtimeMs = newRouteTreeFileStat.mtimeMs;
}
if (newMtimeMs !== void 0) {
this.routeTreeFileCache = {
fileContent: routeTreeContent,
mtimeMs: newMtimeMs
};
}
this.swapCaches();
}
swapCaches() {
this.routeNodeCache = this.routeNodeShadowCache;
this.routeNodeShadowCache = /* @__PURE__ */ new Map();
}
buildRouteTreeFileContent(rootRouteNode, preRouteNodes, routeFileResult) {
const getImportForRouteNode = (node, exportName) => {
var _a;
if ((_a = node.exports) == null ? void 0 : _a.includes(exportName)) {
return {
source: `./${this.getImportPath(node)}`,
specifiers: [
{
imported: exportName,
local: `${node.variableName}${exportName}Import`
}
]
};
}
return void 0;
};
const buildRouteTreeForExport = (plugin) => {
var _a, _b, _c;
const exportName = plugin.transformPlugin.exportName;
const acc = {
routeTree: [],
routeNodes: [],
routePiecesByPath: {}
};
for (const node of preRouteNodes) {
if ((_a = node.exports) == null ? void 0 : _a.includes(plugin.transformPlugin.exportName)) {
this.handleNode(node, acc);
}
}
const sortedRouteNodes = multiSortBy(acc.routeNodes, [
(d) => {
var _a2;
return ((_a2 = d.routePath) == null ? void 0 : _a2.includes(`/${rootPathId}`)) ? -1 : 1;
},
(d) => {
var _a2;
return (_a2 = d.routePath) == null ? void 0 : _a2.split("/").length;
},
(d) => {
var _a2;
return ((_a2 = d.routePath) == null ? void 0 : _a2.endsWith(this.config.indexToken)) ? -1 : 1;
},
(d) => d
]);
const pluginConfig = plugin.config({
generator: this,
rootRouteNode,
sortedRouteNodes
});
const routeImports2 = sortedRouteNodes.filter((d) => !d.isVirtual).flatMap((node) => getImportForRouteNode(node, exportName) ?? []);
const hasMatchingRouteFiles = acc.routeNodes.length > 0 || ((_b = rootRouteNode.exports) == null ? void 0 : _b.includes(exportName));
const virtualRouteNodes = sortedRouteNodes.filter((d) => d.isVirtual).map((node) => {
return `const ${node.variableName}${exportName}Import = ${plugin.createVirtualRouteCode({ node })}`;
});
if (!((_c = rootRouteNode.exports) == null ? void 0 : _c.includes(exportName)) && pluginConfig.virtualRootRoute) {
virtualRouteNodes.unshift(
`const ${rootRouteNode.variableName}${exportName}Import = ${plugin.createRootRouteCode()}`
);
}
const imports = plugin.imports({
sortedRouteNodes,
acc,
generator: this,
rootRouteNode
});
const routeTreeConfig = buildRouteTreeConfig(
acc.routeTree,
exportName,
this.config.disableTypes
);
const createUpdateRoutes = sortedRouteNodes.map((node) => {
var _a2, _b2, _c2, _d, _e;
const loaderNode = (_a2 = acc.routePiecesByPath[node.routePath]) == null ? void 0 : _a2.loader;
const componentNode = (_b2 = acc.routePiecesByPath[node.routePath]) == null ? void 0 : _b2.component;
const errorComponentNode = (_c2 = acc.routePiecesByPath[node.routePath]) == null ? void 0 : _c2.errorComponent;
const pendingComponentNode = (_d = acc.routePiecesByPath[node.routePath]) == null ? void 0 : _d.pendingComponent;
const lazyComponentNode = (_e = acc.routePiecesByPath[node.routePath]) == null ? void 0 : _e.lazy;
return [
[
`const ${node.variableName}${exportName} = ${node.variableName}${exportName}Import.update({
${[
`id: '${node.path}'`,
!node.isNonPath ? `path: '${node.cleanedPath}'` : void 0,
`getParentRoute: () => ${findParent(node, exportName)}`
].filter(Boolean).join(",")}
}${this.config.disableTypes ? "" : "as any"})`,
loaderNode ? `.updateLoader({ loader: lazyFn(() => import('./${replaceBackslash(
removeExt(
path.relative(
path.dirname(this.config.generatedRouteTree),
path.resolve(
this.config.routesDirectory,
loaderNode.filePath
)
),
this.config.addExtensions
)
)}'), 'loader') })` : "",
componentNode || errorComponentNode || pendingComponentNode ? `.update({
${[
["component", componentNode],
["errorComponent", errorComponentNode],
["pendingComponent", pendingComponentNode]
].filter((d) => d[1]).map((d) => {
return `${d[0]}: lazyRouteComponent(() => import('./${replaceBackslash(
removeExt(
path.relative(
path.dirname(this.config.generatedRouteTree),
path.resolve(
this.config.routesDirectory,
d[1].filePath
)
),
this.config.addExtensions
)
)}'), '${d[0]}')`;
}).join("\n,")}
})` : "",
lazyComponentNode ? `.lazy(() => import('./${replaceBackslash(
removeExt(
path.relative(
path.dirname(this.config.generatedRouteTree),
path.resolve(
this.config.routesDirectory,
lazyComponentNode.filePath
)
),
this.config.addExtensions
)
)}').then((d) => d.${exportName}))` : ""
].join("")
].join("\n\n");
});
let fileRoutesByPathInterfacePerPlugin = "";
let fileRoutesByFullPathPerPlugin = "";
if (!this.config.disableTypes && hasMatchingRouteFiles) {
fileRoutesByFullPathPerPlugin = [
`export interface File${exportName}sByFullPath {
${[...createRouteNodesByFullPath(acc.routeNodes).entries()].filter(([fullPath]) => fullPath).map(([fullPath, routeNode]) => {
return `'${fullPath}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`;
})}
}`,
`export interface File${exportName}sByTo {
${[...createRouteNodesByTo(acc.routeNodes).entries()].filter(([to]) => to).map(([to, routeNode]) => {
return `'${to}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`;
})}
}`,
`export interface File${exportName}sById {
'${rootRouteId}': typeof root${exportName}Import,
${[...createRouteNodesById(acc.routeNodes).entries()].map(([id, routeNode]) => {
return `'${id}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`;
})}
}`,
`export interface File${exportName}Types {
file${exportName}sByFullPath: File${exportName}sByFullPath
fullPaths: ${acc.routeNodes.length > 0 ? [...createRouteNodesByFullPath(acc.routeNodes).keys()].filter((fullPath) => fullPath).map((fullPath) => `'${fullPath}'`).join("|") : "never"}
file${exportName}sByTo: File${exportName}sByTo
to: ${acc.routeNodes.length > 0 ? [...createRouteNodesByTo(acc.routeNodes).keys()].filter((to) => to).map((to) => `'${to}'`).join("|") : "never"}
id: ${[`'${rootRouteId}'`, ...[...createRouteNodesById(acc.routeNodes).keys()].map((id) => `'${id}'`)].join("|")}
file${exportName}sById: File${exportName}sById
}`,
`export interface Root${exportName}Children {
${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${getResolvedRouteNodeVariableName(child, exportName)}`).join(",")}
}`
].join("\n");
fileRoutesByPathInterfacePerPlugin = buildFileRoutesByPathInterface({
...plugin.moduleAugmentation({ generator: this }),
routeNodes: this.config.verboseFileRoutes !== false ? sortedRouteNodes : [
...routeFileResult.map(({ node }) => node),
...sortedRouteNodes.filter((d) => d.isVirtual)
],
exportName
});
}
let routeTree = "";
if (hasMatchingRouteFiles) {
routeTree = [
`const root${exportName}Children${this.config.disableTypes ? "" : `: Root${exportName}Children`} = {
${acc.routeTree.map(
(child) => `${child.variableName}${exportName}: ${getResolvedRouteNodeVariableName(child, exportName)}`
).join(",")}
}`,
`export const ${lowerCaseFirstChar(exportName)}Tree = root${exportName}Import._addFileChildren(root${exportName}Children)${this.config.disableTypes ? "" : `._addFileTypes<File${exportName}Types>()`}`
].join("\n");
}
return {
routeImports: routeImports2,
sortedRouteNodes,
acc,
virtualRouteNodes,
routeTreeConfig,
routeTree,
imports,
createUpdateRoutes,
fileRoutesByFullPathPerPlugin,
fileRoutesByPathInterfacePerPlugin
};
};
const routeTrees = this.pluginsWithTransform.map((plugin) => ({
exportName: plugin.transformPlugin.exportName,
...buildRouteTreeForExport(plugin)
}));
this.plugins.map((plugin) => {
var _a;
return (_a = plugin.onRouteTreesChanged) == null ? void 0 : _a.call(plugin, {
routeTrees,
rootRouteNode,
generator: this
});
});
let mergedImports = mergeImportDeclarations(
routeTrees.flatMap((d) => d.imports)
);
if (this.config.disableTypes) {
mergedImports = mergedImports.filter((d) => d.importKind !== "type");
}
const importStatements = mergedImports.map(buildImportString);
let moduleAugmentation = "";
if (this.config.verboseFileRoutes === false && !this.config.disableTypes) {
moduleAugmentation = routeFileResult.map(({ node }) => {
const getModuleDeclaration = (routeNode) => {
if (!isRouteNodeValidForAugmentation(routeNode)) {
return "";
}
const moduleAugmentation2 = this.pluginsWithTransform.map((plugin) => {
return plugin.routeModuleAugmentation({
routeNode
});
}).filter(Boolean).join("\n");
return `declare module './${this.getImportPath(routeNode)}' {
${moduleAugmentation2}
}`;
};
return getModuleDeclaration(node);
}).join("\n");
}
const routeImports = routeTrees.flatMap((t) => t.routeImports);
const rootRouteImports = this.pluginsWithTransform.flatMap(
(p) => getImportForRouteNode(rootRouteNode, p.transformPlugin.exportName) ?? []
);
if (rootRouteImports.length > 0) {
routeImports.unshift(...rootRouteImports);
}
const routeTreeContent = [
...this.config.routeTreeFileHeader,
`// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.`,
[...importStatements].join("\n"),
mergeImportDeclarations(routeImports).map(buildImportString).join("\n"),
routeTrees.flatMap((t) => t.virtualRouteNodes).join("\n"),
routeTrees.flatMap((t) => t.createUpdateRoutes).join("\n"),
routeTrees.map((t) => t.fileRoutesByFullPathPerPlugin).join("\n"),
routeTrees.map((t) => t.fileRoutesByPathInterfacePerPlugin).join("\n"),
moduleAugmentation,
routeTrees.flatMap((t) => t.routeTreeConfig).join("\n"),
routeTrees.map((t) => t.routeTree).join("\n"),
...this.config.routeTreeFileFooter
].filter(Boolean).join("\n\n");
return routeTreeContent;
}
getImportPath(node) {
return replaceBackslash(
removeExt(
path.relative(
path.dirname(this.config.generatedRouteTree),
path.resolve(this.config.routesDirectory, node.filePath)
),
this.config.addExtensions
)
);
}
async processRouteNodeFile(node) {
var _a, _b, _c, _d, _e;
const result = await this.isRouteFileCacheFresh(node);
if (result.status === "fresh") {
node.exports = result.cacheEntry.exports;
return {
node,
shouldWriteTree: result.exportsChanged,
cacheEntry: result.cacheEntry
};
}
const existingRouteFile = await this.fs.readFile(node.fullPath);
if (existingRouteFile === "file-not-existing") {
throw new Error(`⚠️ File ${node.fullPath} does not exist`);
}
const updatedCacheEntry = {
fileContent: existingRouteFile.fileContent,
mtimeMs: existingRouteFile.stat.mtimeMs,
exports: [],
routeId: node.routePath ?? "$$TSR_NO_ROUTE_PATH_ASSIGNED$$"
};
const escapedRoutePath = ((_a = node.routePath) == null ? void 0 : _a.replaceAll("$", "$$")) ?? "";
let shouldWriteRouteFile = false;
if (!existingRouteFile.fileContent) {
shouldWriteRouteFile = true;
if (node._fsRouteType === "lazy") {
const tLazyRouteTemplate = this.targetTemplate.lazyRoute;
updatedCacheEntry.fileContent = await fillTemplate(
this.config,
(((_b = this.config.customScaffolding) == null ? void 0 : _b.lazyRouteTemplate) || ((_c = this.config.customScaffolding) == null ? void 0 : _c.routeTemplate)) ?? tLazyRouteTemplate.template(),
{
tsrImports: tLazyRouteTemplate.imports.tsrImports(),
tsrPath: escapedRoutePath.replaceAll(/\{(.+)\}/gm, "$1"),
tsrExportStart: tLazyRouteTemplate.imports.tsrExportStart(escapedRoutePath),
tsrExportEnd: tLazyRouteTemplate.imports.tsrExportEnd()
}
);
updatedCacheEntry.exports = ["Route"];
} else if (
// Creating a new normal route file
["layout", "static"].some(
(d) => d === node._fsRouteType
) || [
"component",
"pendingComponent",
"errorComponent",
"loader"
].every((d) => d !== node._fsRouteType)
) {
const tRouteTemplate = this.targetTemplate.route;
updatedCacheEntry.fileContent = await fillTemplate(
this.config,
((_d = this.config.customScaffolding) == null ? void 0 : _d.routeTemplate) ?? tRouteTemplate.template(),
{
tsrImports: tRouteTemplate.imports.tsrImports(),
tsrPath: escapedRoutePath.replaceAll(/\{(.+)\}/gm, "$1"),
tsrExportStart: tRouteTemplate.imports.tsrExportStart(escapedRoutePath),
tsrExportEnd: tRouteTemplate.imports.tsrExportEnd()
}
);
updatedCacheEntry.exports = ["Route"];
} else {
return null;
}
} else {
const transformResult = await transform({
source: updatedCacheEntry.fileContent,
ctx: {
target: this.config.target,
routeId: escapedRoutePath,
lazy: node._fsRouteType === "lazy",
verboseFileRoutes: !(this.config.verboseFileRoutes === false)
},
plugins: this.transformPlugins
});
if (transformResult.result === "error") {
throw new Error(
`Error transforming route file ${node.fullPath}: ${transformResult.error}`
);
}
updatedCacheEntry.exports = transformResult.exports;
if (transformResult.result === "modified") {
updatedCacheEntry.fileContent = transformResult.output;
shouldWriteRouteFile = true;
}
}
if (shouldWriteRouteFile) {
const stats = await this.safeFileWrite({
filePath: node.fullPath,
newContent: updatedCacheEntry.fileContent,
strategy: {
type: "mtime",
expectedMtimeMs: updatedCacheEntry.mtimeMs
}
});
updatedCacheEntry.mtimeMs = stats.mtimeMs;
}
this.routeNodeShadowCache.set(node.fullPath, updatedCacheEntry);
node.exports = updatedCacheEntry.exports;
const shouldWriteTree = !deepEqual(
(_e = result.cacheEntry) == null ? void 0 : _e.exports,
updatedCacheEntry.exports
);
return {
node,
shouldWriteTree,
cacheEntry: updatedCacheEntry
};
}
async didRouteFileChangeComparedToCache(file, cache) {
const cacheEntry = this[cache].get(file.path);
return this.didFileChangeComparedToCache(file, cacheEntry);
}
async didFileChangeComparedToCache(file, cacheEntry) {
if (!cacheEntry) {
return { result: "file-not-in-cache" };
}
let mtimeMs = file.mtimeMs;
if (mtimeMs === void 0) {
try {
const currentStat = await this.fs.stat(file.path);
mtimeMs = currentStat.mtimeMs;
} catch {
return { result: "cannot-stat-file" };
}
}
return { result: mtimeMs !== cacheEntry.mtimeMs, mtimeMs, cacheEntry };
}
async safeFileWrite(opts) {
const tmpPath = this.getTempFileName(opts.filePath);
await this.fs.writeFile(tmpPath, opts.newContent);
if (opts.strategy.type === "mtime") {
const beforeStat = await this.fs.stat(opts.filePath);
if (beforeStat.mtimeMs !== opts.strategy.expectedMtimeMs) {
throw rerun({
msg: `File ${opts.filePath} was modified by another process during processing.`,
event: { type: "update", path: opts.filePath }
});
}
} else {
if (await checkFileExists(opts.filePath)) {
throw rerun({
msg: `File ${opts.filePath} already exists. Cannot overwrite.`,
event: { type: "update", path: opts.filePath }
});
}
}
const stat = await this.fs.stat(tmpPath);
await this.fs.rename(tmpPath, opts.filePath);
return stat;
}
getTempFileName(filePath) {
const absPath = path.resolve(filePath);
const hash = crypto.createHash("md5").update(absPath).digest("hex");
return path.join(this.tmpDir, hash);
}
async isRouteFileCacheFresh(node) {
const fileChangedCache = await this.didRouteFileChangeComparedToCache(
{ path: node.fullPath },
"routeNodeCache"
);
if (fileChangedCache.result === false) {
this.routeNodeShadowCache.set(node.fullPath, fileChangedCache.cacheEntry);
return {
status: "fresh",
exportsChanged: false,
cacheEntry: fileChangedCache.cacheEntry
};
}
if (fileChangedCache.result === "cannot-stat-file") {
throw new Error(`⚠️ expected route file to exist at ${node.fullPath}`);
}
const mtimeMs = fileChangedCache.result === true ? fileChangedCache.mtimeMs : void 0;
const shadowCacheFileChange = await this.didRouteFileChangeComparedToCache(
{ path: node.fullPath, mtimeMs },
"routeNodeShadowCache"
);
if (shadowCacheFileChange.result === "cannot-stat-file") {
throw new Error(`⚠️ expected route file to exist at ${node.fullPath}`);
}
if (shadowCacheFileChange.result === false) {
if (fileChangedCache.result === true) {
if (deepEqual(
fileChangedCache.cacheEntry.exports,
shadowCacheFileChange.cacheEntry.exports
)) {
return {
status: "fresh",
exportsChanged: false,
cacheEntry: shadowCacheFileChange.cacheEntry
};
}
return {
status: "fresh",
exportsChanged: true,
cacheEntry: shadowCacheFileChange.cacheEntry
};
}
}
if (fileChangedCache.result === "file-not-in-cache") {
return {
status: "stale"
};
}
return { status: "stale", cacheEntry: fileChangedCache.cacheEntry };
}
async handleRootNode(node) {
var _a;
const result = await this.isRouteFileCacheFresh(node);
if (result.status === "fresh") {
node.exports = result.cacheEntry.exports;
this.routeNodeShadowCache.set(node.fullPath, result.cacheEntry);
return result.exportsChanged;
}
const rootNodeFile = await this.fs.readFile(node.fullPath);
if (rootNodeFile === "file-not-existing") {
throw new Error(`⚠️ expected root route to exist at ${node.fullPath}`);
}
const updatedCacheEntry = {
fileContent: rootNodeFile.fileContent,
mtimeMs: rootNodeFile.stat.mtimeMs,
exports: [],
routeId: node.routePath ?? "$$TSR_NO_ROOT_ROUTE_PATH_ASSIGNED$$"
};
if (!rootNodeFile.fileContent) {
const rootTemplate = this.targetTemplate.rootRoute;
const rootRouteContent = await fillTemplate(
this.config,
rootTemplate.template(),
{
tsrImports: rootTemplate.imports.tsrImports(),
tsrPath: rootPathId,
tsrExportStart: rootTemplate.imports.tsrExportStart(),
tsrExportEnd: rootTemplate.imports.tsrExportEnd()
}
);
this.logger.log(`🟡 Creating ${node.fullPath}`);
const stats = await this.safeFileWrite({
filePath: node.fullPath,
newContent: rootRouteContent,
strategy: {
type: "mtime",
expectedMtimeMs: rootNodeFile.stat.mtimeMs
}
});
updatedCacheEntry.fileContent = rootRouteContent;
updatedCacheEntry.mtimeMs = stats.mtimeMs;
}
const rootRouteExports = [];
for (const plugin of this.pluginsWithTransform) {
const exportName = plugin.transformPlugin.exportName;
if (rootNodeFile.fileContent.includes(`export const ${exportName}`)) {
rootRouteExports.push(exportName);
}
}
updatedCacheEntry.exports = rootRouteExports;
node.exports = rootRouteExports;
this.routeNodeShadowCache.set(node.fullPath, updatedCacheEntry);
const shouldWriteTree = !deepEqual(
(_a = result.cacheEntry) == null ? void 0 : _a.exports,
rootRouteExports
);
return shouldWriteTree;
}
handleNode(node, acc) {
var _a;
resetRegex(this.routeGroupPatternRegex);
let parentRoute = hasParentRoute(acc.routeNodes, node, node.routePath);
if ((parentRoute == null ? void 0 : parentRoute.isVirtualParentRoute) && ((_a = parentRoute.children) == null ? void 0 : _a.length)) {
const possibleParentRoute = hasParentRoute(
parentRoute.children,
node,
node.routePath
);
if (possibleParentRoute) {
parentRoute = possibleParentRoute;
}
}
if (parentRoute) node.parent = parentRoute;
node.path = determineNodePath(node);
const trimmedPath = trimPathLeft(node.path ?? "");
const split = trimmedPath.split("/");
const lastRouteSegment = split[split.length - 1] ?? trimmedPath;
node.isNonPath = lastRouteSegment.startsWith("_") || this.routeGroupPatternRegex.test(lastRouteSegment);
node.cleanedPath = removeGroups(
removeUnderscores(removeLayoutSegments(node.path)) ?? ""
);
if (!node.isVirtual && [
"lazy",
"loader",
"component",
"pendingComponent",
"errorComponent"
].some((d) => d === node._fsRouteType)) {
acc.routePiecesByPath[node.routePath] = acc.routePiecesByPath[node.routePath] || {};
acc.routePiecesByPath[node.routePath][node._fsRouteType === "lazy" ? "lazy" : node._fsRouteType === "loader" ? "loader" : node._fsRouteType === "errorComponent" ? "errorComponent" : node._fsRouteType === "pendingComponent" ? "pendingComponent" : "component"] = node;
const anchorRoute = acc.routeNodes.find(
(d) => d.routePath === node.routePath
);
if (!anchorRoute) {
this.handleNode(
{
...node,
isVirtual: true,
_fsRouteType: "static"
},
acc
);
}
return;
}
const cleanedPathIsEmpty = (node.cleanedPath || "").length === 0;
const nonPathRoute = node._fsRouteType === "pathless_layout" && node.isNonPath;
node.isVirtualParentRequired = node._fsRouteType === "pathless_layout" || nonPathRoute ? !cleanedPathIsEmpty : false;
if (!node.isVirtual && node.isVirtualParentRequired) {
const parentRoutePath = removeLastSegmentFromPath(node.routePath) || "/";
const parentVariableName = routePathToVariable(parentRoutePath);
const anchorRoute = acc.routeNodes.find(
(d) => d.routePath === parentRoutePath
);
if (!anchorRoute) {
const parentNode = {
...node,
path: removeLastSegmentFromPath(node.path) || "/",
filePath: removeLastSegmentFromPath(node.filePath) || "/",
fullPath: removeLastSegmentFromPath(node.fullPath) || "/",
routePath: parentRoutePath,
variableName: parentVariableName,
isVirtual: true,
_fsRouteType: "layout",
// layout since this route will wrap other routes
isVirtualParentRoute: true,
isVirtualParentRequired: false
};
parentNode.children = parentNode.children ?? [];
parentNode.children.push(node);
node.parent = parentNode;
if (node._fsRouteType === "pathless_layout") {
node.path = determineNodePath(node);
}
this.handleNode(parentNode, acc);
} else {
anchorRoute.children = anchorRoute.children ?? [];
anchorRoute.children.push(node);
node.parent = anchorRoute;
}
}
if (node.parent) {
if (!node.isVirtualParentRequired) {
node.parent.children = node.parent.children ?? [];
node.parent.children.push(node);
}
} else {
acc.routeTree.push(node);
}
acc.routeNodes.push(node);
}
// only process files that are relevant for the route tree generation
isFileRelevantForRouteTreeGeneration(filePath) {
if (filePath === this.generatedRouteTreePath) {
return true;
}
if (filePath.startsWith(this.routesDirectoryPath)) {
return true;
}
if (typeof this.config.virtualRouteConfig === "string" && filePath === this.config.virtualRouteConfig) {
return true;
}
if (this.routeNodeCache.has(filePath)) {
return true;
}
if (isVirtualConfigFile(path.basename(filePath))) {
return true;
}
if (this.physicalDirectories.some((dir) => filePath.startsWith(dir))) {
return true;
}
return false;
}
}
export {
Generator
};
//# sourceMappingURL=generator.js.map