@tanstack/router-generator
Version:
Modern and scalable routing for React applications
1 lines • 70.5 kB
Source Map (JSON)
{"version":3,"file":"generator.cjs","sources":["../../src/generator.ts"],"sourcesContent":["import path from 'node:path'\nimport * as fsp from 'node:fs/promises'\nimport { mkdtempSync } from 'node:fs'\nimport crypto from 'node:crypto'\nimport { deepEqual, rootRouteId } from '@tanstack/router-core'\nimport { logging } from './logger'\nimport {\n isVirtualConfigFile,\n getRouteNodes as physicalGetRouteNodes,\n} from './filesystem/physical/getRouteNodes'\nimport { getRouteNodes as virtualGetRouteNodes } from './filesystem/virtual/getRouteNodes'\nimport { rootPathId } from './filesystem/physical/rootPathId'\nimport {\n buildFileRoutesByPathInterface,\n buildImportString,\n buildRouteTreeConfig,\n checkFileExists,\n createRouteNodesByFullPath,\n createRouteNodesById,\n createRouteNodesByTo,\n determineNodePath,\n findParent,\n format,\n getResolvedRouteNodeVariableName,\n hasParentRoute,\n isRouteNodeValidForAugmentation,\n lowerCaseFirstChar,\n mergeImportDeclarations,\n multiSortBy,\n removeExt,\n removeGroups,\n removeLastSegmentFromPath,\n removeLayoutSegments,\n removeUnderscores,\n replaceBackslash,\n resetRegex,\n routePathToVariable,\n trimPathLeft,\n} from './utils'\nimport { fillTemplate, getTargetTemplate } from './template'\nimport { transform } from './transform/transform'\nimport { defaultGeneratorPlugin } from './plugin/default-generator-plugin'\nimport type {\n GeneratorPlugin,\n GeneratorPluginWithTransform,\n} from './plugin/types'\nimport type { TargetTemplate } from './template'\nimport type {\n FsRouteType,\n GetRouteNodesResult,\n GetRoutesByFileMapResult,\n HandleNodeAccumulator,\n ImportDeclaration,\n RouteNode,\n} from './types'\nimport type { Config } from './config'\nimport type { Logger } from './logger'\nimport type { TransformPlugin } from './transform/types'\n\ninterface fs {\n stat: (filePath: string) => Promise<{ mtimeMs: bigint }>\n mkdtempSync: (prefix: string) => string\n rename: (oldPath: string, newPath: string) => Promise<void>\n writeFile: (filePath: string, content: string) => Promise<void>\n readFile: (\n filePath: string,\n ) => Promise<\n { stat: { mtimeMs: bigint }; fileContent: string } | 'file-not-existing'\n >\n}\n\nconst DefaultFileSystem: fs = {\n stat: (filePath) => fsp.stat(filePath, { bigint: true }),\n mkdtempSync: mkdtempSync,\n rename: (oldPath, newPath) => fsp.rename(oldPath, newPath),\n writeFile: (filePath, content) => fsp.writeFile(filePath, content),\n readFile: async (filePath: string) => {\n try {\n const fileHandle = await fsp.open(filePath, 'r')\n const stat = await fileHandle.stat({ bigint: true })\n const fileContent = (await fileHandle.readFile()).toString()\n await fileHandle.close()\n return { stat, fileContent }\n } catch (e: any) {\n if ('code' in e) {\n if (e.code === 'ENOENT') {\n return 'file-not-existing'\n }\n }\n throw e\n }\n },\n}\n\ninterface Rerun {\n rerun: true\n msg?: string\n event: GeneratorEvent\n}\nfunction rerun(opts: { msg?: string; event?: GeneratorEvent }): Rerun {\n const { event, ...rest } = opts\n return { rerun: true, event: event ?? { type: 'rerun' }, ...rest }\n}\n\nfunction isRerun(result: unknown): result is Rerun {\n return (\n typeof result === 'object' &&\n result !== null &&\n 'rerun' in result &&\n result.rerun === true\n )\n}\n\nexport type FileEventType = 'create' | 'update' | 'delete'\nexport type FileEvent = {\n type: FileEventType\n path: string\n}\nexport type GeneratorEvent = FileEvent | { type: 'rerun' }\n\ntype FileCacheChange<TCacheEntry extends GeneratorCacheEntry> =\n | {\n result: false\n cacheEntry: TCacheEntry\n }\n | { result: true; mtimeMs: bigint; cacheEntry: TCacheEntry }\n | {\n result: 'file-not-in-cache'\n }\n | {\n result: 'cannot-stat-file'\n }\n\ninterface GeneratorCacheEntry {\n mtimeMs: bigint\n fileContent: string\n}\n\ninterface RouteNodeCacheEntry extends GeneratorCacheEntry {\n exports: Array<string>\n routeId: string\n}\n\ntype GeneratorRouteNodeCache = Map</** filePath **/ string, RouteNodeCacheEntry>\n\nexport class Generator {\n /**\n * why do we have two caches for the route files?\n * During processing, we READ from the cache and WRITE to the shadow cache.\n *\n * After a route file is processed, we write to the shadow cache.\n * If during processing we bail out and re-run, we don't lose this modification\n * but still can track whether the file contributed changes and thus the route tree file needs to be regenerated.\n * After all files are processed, we swap the shadow cache with the main cache and initialize a new shadow cache.\n * That way we also ensure deleted/renamed files don't stay in the cache forever.\n */\n private routeNodeCache: GeneratorRouteNodeCache = new Map()\n private routeNodeShadowCache: GeneratorRouteNodeCache = new Map()\n\n private routeTreeFileCache: GeneratorCacheEntry | undefined\n\n public config: Config\n public targetTemplate: TargetTemplate\n\n private root: string\n private routesDirectoryPath: string\n private tmpDir: string\n private fs: fs\n private logger: Logger\n private generatedRouteTreePath: string\n private runPromise: Promise<void> | undefined\n private fileEventQueue: Array<GeneratorEvent> = []\n private plugins: Array<GeneratorPlugin> = [defaultGeneratorPlugin()]\n private pluginsWithTransform: Array<GeneratorPluginWithTransform> = []\n // this is just a cache for the transform plugins since we need them for each route file that is to be processed\n private transformPlugins: Array<TransformPlugin> = []\n private routeGroupPatternRegex = /\\(.+\\)/g\n private physicalDirectories: Array<string> = []\n\n constructor(opts: { config: Config; root: string; fs?: fs }) {\n this.config = opts.config\n this.logger = logging({ disabled: this.config.disableLogging })\n this.root = opts.root\n this.fs = opts.fs || DefaultFileSystem\n this.tmpDir = this.fs.mkdtempSync(\n path.join(this.config.tmpDir, 'router-generator-'),\n )\n this.generatedRouteTreePath = path.resolve(this.config.generatedRouteTree)\n this.targetTemplate = getTargetTemplate(this.config)\n\n this.routesDirectoryPath = this.getRoutesDirectoryPath()\n this.plugins.push(...(opts.config.plugins || []))\n this.plugins.forEach((plugin) => {\n if ('transformPlugin' in plugin) {\n if (this.pluginsWithTransform.find((p) => p.name === plugin.name)) {\n throw new Error(\n `Plugin with name \"${plugin.name}\" is already registered for export ${plugin.transformPlugin.exportName}!`,\n )\n }\n this.pluginsWithTransform.push(plugin)\n this.transformPlugins.push(plugin.transformPlugin)\n }\n })\n }\n\n private getRoutesDirectoryPath() {\n return path.isAbsolute(this.config.routesDirectory)\n ? this.config.routesDirectory\n : path.resolve(this.root, this.config.routesDirectory)\n }\n\n public getRoutesByFileMap(): GetRoutesByFileMapResult {\n return new Map(\n [...this.routeNodeCache.entries()].map(([filePath, cacheEntry]) => [\n filePath,\n { routePath: cacheEntry.routeId },\n ]),\n )\n }\n\n public async run(event?: GeneratorEvent): Promise<void> {\n if (\n event &&\n event.type !== 'rerun' &&\n !this.isFileRelevantForRouteTreeGeneration(event.path)\n ) {\n return\n }\n this.fileEventQueue.push(event ?? { type: 'rerun' })\n // only allow a single run at a time\n if (this.runPromise) {\n return this.runPromise\n }\n\n this.runPromise = (async () => {\n do {\n // synchronously copy and clear the queue since we are going to iterate asynchronously over it\n // and while we do so, a new event could be put into the queue\n const tempQueue = this.fileEventQueue\n this.fileEventQueue = []\n // if we only have 'update' events in the queue\n // and we already have the affected files' latest state in our cache, we can exit early\n const remainingEvents = (\n await Promise.all(\n tempQueue.map(async (e) => {\n if (e.type === 'update') {\n let cacheEntry: GeneratorCacheEntry | undefined\n if (e.path === this.generatedRouteTreePath) {\n cacheEntry = this.routeTreeFileCache\n } else {\n // we only check the routeNodeCache here\n // if the file's state is only up-to-date in the shadow cache we need to re-run\n cacheEntry = this.routeNodeCache.get(e.path)\n }\n const change = await this.didFileChangeComparedToCache(\n { path: e.path },\n cacheEntry,\n )\n if (change.result === false) {\n return null\n }\n }\n return e\n }),\n )\n ).filter((e) => e !== null)\n\n if (remainingEvents.length === 0) {\n break\n }\n\n try {\n const start = performance.now()\n await this.generatorInternal()\n const end = performance.now()\n this.logger.info(\n `Generated route tree in ${Math.round(end - start)}ms`,\n )\n } catch (err) {\n const errArray = !Array.isArray(err) ? [err] : err\n\n const recoverableErrors = errArray.filter((e) => isRerun(e))\n if (recoverableErrors.length === errArray.length) {\n this.fileEventQueue.push(...recoverableErrors.map((e) => e.event))\n recoverableErrors.forEach((e) => {\n if (e.msg) {\n this.logger.info(e.msg)\n }\n })\n } else {\n const unrecoverableErrors = errArray.filter((e) => !isRerun(e))\n this.runPromise = undefined\n throw new Error(\n unrecoverableErrors.map((e) => (e as Error).message).join(),\n )\n }\n }\n } while (this.fileEventQueue.length)\n this.runPromise = undefined\n })()\n return this.runPromise\n }\n\n private async generatorInternal() {\n let writeRouteTreeFile: boolean | 'force' = false\n\n let getRouteNodesResult: GetRouteNodesResult\n\n if (this.config.virtualRouteConfig) {\n getRouteNodesResult = await virtualGetRouteNodes(this.config, this.root)\n } else {\n getRouteNodesResult = await physicalGetRouteNodes(this.config, this.root)\n }\n\n const {\n rootRouteNode,\n routeNodes: beforeRouteNodes,\n physicalDirectories,\n } = getRouteNodesResult\n if (rootRouteNode === undefined) {\n let errorMessage = `rootRouteNode must not be undefined. Make sure you've added your root route into the route-tree.`\n if (!this.config.virtualRouteConfig) {\n errorMessage += `\\nMake sure that you add a \"${rootPathId}.${this.config.disableTypes ? 'js' : 'tsx'}\" file to your routes directory.\\nAdd the file in: \"${this.config.routesDirectory}/${rootPathId}.${this.config.disableTypes ? 'js' : 'tsx'}\"`\n }\n throw new Error(errorMessage)\n }\n this.physicalDirectories = physicalDirectories\n\n writeRouteTreeFile = await this.handleRootNode(rootRouteNode)\n\n const preRouteNodes = multiSortBy(beforeRouteNodes, [\n (d) => (d.routePath === '/' ? -1 : 1),\n (d) => d.routePath?.split('/').length,\n (d) =>\n d.filePath.match(new RegExp(`[./]${this.config.indexToken}[.]`))\n ? 1\n : -1,\n (d) =>\n d.filePath.match(\n /[./](component|errorComponent|pendingComponent|loader|lazy)[.]/,\n )\n ? 1\n : -1,\n (d) =>\n d.filePath.match(new RegExp(`[./]${this.config.routeToken}[.]`))\n ? -1\n : 1,\n (d) => (d.routePath?.endsWith('/') ? -1 : 1),\n (d) => d.routePath,\n ]).filter((d) => ![`/${rootPathId}`].includes(d.routePath || ''))\n\n const routeFileAllResult = await Promise.allSettled(\n preRouteNodes\n // only process routes that are backed by an actual file\n .filter((n) => !n.isVirtualParentRoute && !n.isVirtual)\n .map((n) => this.processRouteNodeFile(n)),\n )\n\n const rejections = routeFileAllResult.filter(\n (result) => result.status === 'rejected',\n )\n if (rejections.length > 0) {\n throw rejections.map((e) => e.reason)\n }\n\n const routeFileResult = routeFileAllResult.flatMap((result) => {\n if (result.status === 'fulfilled' && result.value !== null) {\n return result.value\n }\n return []\n })\n\n routeFileResult.forEach((result) => {\n if (!result.node.exports?.length) {\n this.logger.warn(\n `Route file \"${result.cacheEntry.fileContent}\" does not export any route piece. This is likely a mistake.`,\n )\n }\n })\n if (routeFileResult.find((r) => r.shouldWriteTree)) {\n writeRouteTreeFile = true\n }\n\n // this is the first time the generator runs, so read in the route tree file if it exists yet\n if (!this.routeTreeFileCache) {\n const routeTreeFile = await this.fs.readFile(this.generatedRouteTreePath)\n if (routeTreeFile !== 'file-not-existing') {\n this.routeTreeFileCache = {\n fileContent: routeTreeFile.fileContent,\n mtimeMs: routeTreeFile.stat.mtimeMs,\n }\n }\n writeRouteTreeFile = true\n } else {\n const routeTreeFileChange = await this.didFileChangeComparedToCache(\n { path: this.generatedRouteTreePath },\n this.routeTreeFileCache,\n )\n if (routeTreeFileChange.result !== false) {\n writeRouteTreeFile = 'force'\n if (routeTreeFileChange.result === true) {\n const routeTreeFile = await this.fs.readFile(\n this.generatedRouteTreePath,\n )\n if (routeTreeFile !== 'file-not-existing') {\n this.routeTreeFileCache = {\n fileContent: routeTreeFile.fileContent,\n mtimeMs: routeTreeFile.stat.mtimeMs,\n }\n }\n }\n }\n }\n\n if (!writeRouteTreeFile) {\n // only needs to be done if no other changes have been detected yet\n // compare shadowCache and cache to identify deleted routes\n for (const fullPath of this.routeNodeCache.keys()) {\n if (!this.routeNodeShadowCache.has(fullPath)) {\n writeRouteTreeFile = true\n break\n }\n }\n }\n\n if (!writeRouteTreeFile) {\n this.swapCaches()\n return\n }\n\n let routeTreeContent = this.buildRouteTreeFileContent(\n rootRouteNode,\n preRouteNodes,\n routeFileResult,\n )\n routeTreeContent = this.config.enableRouteTreeFormatting\n ? await format(routeTreeContent, this.config)\n : routeTreeContent\n\n let newMtimeMs: bigint | undefined\n if (this.routeTreeFileCache) {\n if (\n writeRouteTreeFile !== 'force' &&\n this.routeTreeFileCache.fileContent === routeTreeContent\n ) {\n // existing route tree file is already up-to-date, don't write it\n // we should only get here in the initial run when the route cache is not filled yet\n } else {\n const newRouteTreeFileStat = await this.safeFileWrite({\n filePath: this.generatedRouteTreePath,\n newContent: routeTreeContent,\n strategy: {\n type: 'mtime',\n expectedMtimeMs: this.routeTreeFileCache.mtimeMs,\n },\n })\n newMtimeMs = newRouteTreeFileStat.mtimeMs\n }\n } else {\n const newRouteTreeFileStat = await this.safeFileWrite({\n filePath: this.generatedRouteTreePath,\n newContent: routeTreeContent,\n strategy: {\n type: 'new-file',\n },\n })\n newMtimeMs = newRouteTreeFileStat.mtimeMs\n }\n\n if (newMtimeMs !== undefined) {\n this.routeTreeFileCache = {\n fileContent: routeTreeContent,\n mtimeMs: newMtimeMs,\n }\n }\n\n this.swapCaches()\n }\n\n private swapCaches() {\n this.routeNodeCache = this.routeNodeShadowCache\n this.routeNodeShadowCache = new Map()\n }\n\n private buildRouteTreeFileContent(\n rootRouteNode: RouteNode,\n preRouteNodes: Array<RouteNode>,\n routeFileResult: Array<{\n cacheEntry: RouteNodeCacheEntry\n node: RouteNode\n }>,\n ) {\n const getImportForRouteNode = (node: RouteNode, exportName: string) => {\n if (node.exports?.includes(exportName)) {\n return {\n source: `./${this.getImportPath(node)}`,\n specifiers: [\n {\n imported: exportName,\n local: `${node.variableName}${exportName}Import`,\n },\n ],\n } satisfies ImportDeclaration\n }\n return undefined\n }\n\n const buildRouteTreeForExport = (plugin: GeneratorPluginWithTransform) => {\n const exportName = plugin.transformPlugin.exportName\n const acc: HandleNodeAccumulator = {\n routeTree: [],\n routeNodes: [],\n routePiecesByPath: {},\n }\n for (const node of preRouteNodes) {\n if (node.exports?.includes(plugin.transformPlugin.exportName)) {\n this.handleNode(node, acc)\n }\n }\n\n const sortedRouteNodes = multiSortBy(acc.routeNodes, [\n (d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1),\n (d) => d.routePath?.split('/').length,\n (d) => (d.routePath?.endsWith(this.config.indexToken) ? -1 : 1),\n (d) => d,\n ])\n\n const pluginConfig = plugin.config({\n generator: this,\n rootRouteNode,\n sortedRouteNodes,\n })\n\n const routeImports = sortedRouteNodes\n .filter((d) => !d.isVirtual)\n .flatMap((node) => getImportForRouteNode(node, exportName) ?? [])\n\n const hasMatchingRouteFiles =\n acc.routeNodes.length > 0 || rootRouteNode.exports?.includes(exportName)\n\n const virtualRouteNodes = sortedRouteNodes\n .filter((d) => d.isVirtual)\n .map((node) => {\n return `const ${\n node.variableName\n }${exportName}Import = ${plugin.createVirtualRouteCode({ node })}`\n })\n if (\n !rootRouteNode.exports?.includes(exportName) &&\n pluginConfig.virtualRootRoute\n ) {\n virtualRouteNodes.unshift(\n `const ${rootRouteNode.variableName}${exportName}Import = ${plugin.createRootRouteCode()}`,\n )\n }\n\n const imports = plugin.imports({\n sortedRouteNodes,\n acc,\n generator: this,\n rootRouteNode,\n })\n\n const routeTreeConfig = buildRouteTreeConfig(\n acc.routeTree,\n exportName,\n this.config.disableTypes,\n )\n\n const createUpdateRoutes = sortedRouteNodes.map((node) => {\n const loaderNode = acc.routePiecesByPath[node.routePath!]?.loader\n const componentNode = acc.routePiecesByPath[node.routePath!]?.component\n const errorComponentNode =\n acc.routePiecesByPath[node.routePath!]?.errorComponent\n const pendingComponentNode =\n acc.routePiecesByPath[node.routePath!]?.pendingComponent\n const lazyComponentNode = acc.routePiecesByPath[node.routePath!]?.lazy\n\n return [\n [\n `const ${node.variableName}${exportName} = ${node.variableName}${exportName}Import.update({\n ${[\n `id: '${node.path}'`,\n !node.isNonPath ? `path: '${node.cleanedPath}'` : undefined,\n `getParentRoute: () => ${findParent(node, exportName)}`,\n ]\n .filter(Boolean)\n .join(',')}\n }${this.config.disableTypes ? '' : 'as any'})`,\n loaderNode\n ? `.updateLoader({ loader: lazyFn(() => import('./${replaceBackslash(\n removeExt(\n path.relative(\n path.dirname(this.config.generatedRouteTree),\n path.resolve(\n this.config.routesDirectory,\n loaderNode.filePath,\n ),\n ),\n this.config.addExtensions,\n ),\n )}'), 'loader') })`\n : '',\n componentNode || errorComponentNode || pendingComponentNode\n ? `.update({\n ${(\n [\n ['component', componentNode],\n ['errorComponent', errorComponentNode],\n ['pendingComponent', pendingComponentNode],\n ] as const\n )\n .filter((d) => d[1])\n .map((d) => {\n return `${\n d[0]\n }: lazyRouteComponent(() => import('./${replaceBackslash(\n removeExt(\n path.relative(\n path.dirname(this.config.generatedRouteTree),\n path.resolve(\n this.config.routesDirectory,\n d[1]!.filePath,\n ),\n ),\n this.config.addExtensions,\n ),\n )}'), '${d[0]}')`\n })\n .join('\\n,')}\n })`\n : '',\n lazyComponentNode\n ? `.lazy(() => import('./${replaceBackslash(\n removeExt(\n path.relative(\n path.dirname(this.config.generatedRouteTree),\n path.resolve(\n this.config.routesDirectory,\n lazyComponentNode.filePath,\n ),\n ),\n this.config.addExtensions,\n ),\n )}').then((d) => d.${exportName}))`\n : '',\n ].join(''),\n ].join('\\n\\n')\n })\n\n let fileRoutesByPathInterfacePerPlugin = ''\n let fileRoutesByFullPathPerPlugin = ''\n\n if (!this.config.disableTypes && hasMatchingRouteFiles) {\n fileRoutesByFullPathPerPlugin = [\n `export interface File${exportName}sByFullPath {\n${[...createRouteNodesByFullPath(acc.routeNodes).entries()]\n .filter(([fullPath]) => fullPath)\n .map(([fullPath, routeNode]) => {\n return `'${fullPath}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`\n })}\n}`,\n `export interface File${exportName}sByTo {\n${[...createRouteNodesByTo(acc.routeNodes).entries()]\n .filter(([to]) => to)\n .map(([to, routeNode]) => {\n return `'${to}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`\n })}\n}`,\n `export interface File${exportName}sById {\n'${rootRouteId}': typeof root${exportName}Import,\n${[...createRouteNodesById(acc.routeNodes).entries()].map(([id, routeNode]) => {\n return `'${id}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`\n})}\n}`,\n `export interface File${exportName}Types {\nfile${exportName}sByFullPath: File${exportName}sByFullPath\nfullPaths: ${\n acc.routeNodes.length > 0\n ? [...createRouteNodesByFullPath(acc.routeNodes).keys()]\n .filter((fullPath) => fullPath)\n .map((fullPath) => `'${fullPath}'`)\n .join('|')\n : 'never'\n }\nfile${exportName}sByTo: File${exportName}sByTo\nto: ${\n acc.routeNodes.length > 0\n ? [...createRouteNodesByTo(acc.routeNodes).keys()]\n .filter((to) => to)\n .map((to) => `'${to}'`)\n .join('|')\n : 'never'\n }\nid: ${[`'${rootRouteId}'`, ...[...createRouteNodesById(acc.routeNodes).keys()].map((id) => `'${id}'`)].join('|')}\nfile${exportName}sById: File${exportName}sById\n}`,\n `export interface Root${exportName}Children {\n${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${getResolvedRouteNodeVariableName(child, exportName)}`).join(',')}\n}`,\n ].join('\\n')\n\n fileRoutesByPathInterfacePerPlugin = buildFileRoutesByPathInterface({\n ...plugin.moduleAugmentation({ generator: this }),\n routeNodes:\n this.config.verboseFileRoutes !== false\n ? sortedRouteNodes\n : [\n ...routeFileResult.map(({ node }) => node),\n ...sortedRouteNodes.filter((d) => d.isVirtual),\n ],\n exportName,\n })\n }\n\n let routeTree = ''\n if (hasMatchingRouteFiles) {\n routeTree = [\n `const root${exportName}Children${this.config.disableTypes ? '' : `: Root${exportName}Children`} = {\n ${acc.routeTree\n .map(\n (child) =>\n `${child.variableName}${exportName}: ${getResolvedRouteNodeVariableName(child, exportName)}`,\n )\n .join(',')}\n}`,\n `export const ${lowerCaseFirstChar(exportName)}Tree = root${exportName}Import._addFileChildren(root${exportName}Children)${this.config.disableTypes ? '' : `._addFileTypes<File${exportName}Types>()`}`,\n ].join('\\n')\n }\n\n return {\n routeImports,\n sortedRouteNodes,\n acc,\n virtualRouteNodes,\n routeTreeConfig,\n routeTree,\n imports,\n createUpdateRoutes,\n fileRoutesByFullPathPerPlugin,\n fileRoutesByPathInterfacePerPlugin,\n }\n }\n\n const routeTrees = this.pluginsWithTransform.map((plugin) => ({\n exportName: plugin.transformPlugin.exportName,\n ...buildRouteTreeForExport(plugin),\n }))\n\n this.plugins.map((plugin) => {\n return plugin.onRouteTreesChanged?.({\n routeTrees,\n rootRouteNode,\n generator: this,\n })\n })\n\n let mergedImports = mergeImportDeclarations(\n routeTrees.flatMap((d) => d.imports),\n )\n if (this.config.disableTypes) {\n mergedImports = mergedImports.filter((d) => d.importKind !== 'type')\n }\n\n const importStatements = mergedImports.map(buildImportString)\n\n let moduleAugmentation = ''\n if (this.config.verboseFileRoutes === false && !this.config.disableTypes) {\n moduleAugmentation = routeFileResult\n .map(({ node }) => {\n const getModuleDeclaration = (routeNode?: RouteNode) => {\n if (!isRouteNodeValidForAugmentation(routeNode)) {\n return ''\n }\n const moduleAugmentation = this.pluginsWithTransform\n .map((plugin) => {\n return plugin.routeModuleAugmentation({\n routeNode,\n })\n })\n .filter(Boolean)\n .join('\\n')\n\n return `declare module './${this.getImportPath(routeNode)}' {\n ${moduleAugmentation}\n }`\n }\n return getModuleDeclaration(node)\n })\n .join('\\n')\n }\n\n const routeImports = routeTrees.flatMap((t) => t.routeImports)\n const rootRouteImports = this.pluginsWithTransform.flatMap(\n (p) =>\n getImportForRouteNode(rootRouteNode, p.transformPlugin.exportName) ??\n [],\n )\n if (rootRouteImports.length > 0) {\n routeImports.unshift(...rootRouteImports)\n }\n const routeTreeContent = [\n ...this.config.routeTreeFileHeader,\n `// This file was automatically generated by TanStack Router.\n// You should NOT make any changes in this file as it will be overwritten.\n// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.`,\n [...importStatements].join('\\n'),\n mergeImportDeclarations(routeImports).map(buildImportString).join('\\n'),\n routeTrees.flatMap((t) => t.virtualRouteNodes).join('\\n'),\n routeTrees.flatMap((t) => t.createUpdateRoutes).join('\\n'),\n\n routeTrees.map((t) => t.fileRoutesByFullPathPerPlugin).join('\\n'),\n routeTrees.map((t) => t.fileRoutesByPathInterfacePerPlugin).join('\\n'),\n moduleAugmentation,\n routeTrees.flatMap((t) => t.routeTreeConfig).join('\\n'),\n routeTrees.map((t) => t.routeTree).join('\\n'),\n ...this.config.routeTreeFileFooter,\n ]\n .filter(Boolean)\n .join('\\n\\n')\n return routeTreeContent\n }\n\n private getImportPath(node: RouteNode) {\n return replaceBackslash(\n removeExt(\n path.relative(\n path.dirname(this.config.generatedRouteTree),\n path.resolve(this.config.routesDirectory, node.filePath),\n ),\n this.config.addExtensions,\n ),\n )\n }\n\n private async processRouteNodeFile(node: RouteNode): Promise<{\n shouldWriteTree: boolean\n cacheEntry: RouteNodeCacheEntry\n node: RouteNode\n } | null> {\n const result = await this.isRouteFileCacheFresh(node)\n\n if (result.status === 'fresh') {\n node.exports = result.cacheEntry.exports\n return {\n node,\n shouldWriteTree: result.exportsChanged,\n cacheEntry: result.cacheEntry,\n }\n }\n\n const existingRouteFile = await this.fs.readFile(node.fullPath)\n if (existingRouteFile === 'file-not-existing') {\n throw new Error(`⚠️ File ${node.fullPath} does not exist`)\n }\n\n const updatedCacheEntry: RouteNodeCacheEntry = {\n fileContent: existingRouteFile.fileContent,\n mtimeMs: existingRouteFile.stat.mtimeMs,\n exports: [],\n routeId: node.routePath ?? '$$TSR_NO_ROUTE_PATH_ASSIGNED$$',\n }\n\n const escapedRoutePath = node.routePath?.replaceAll('$', '$$') ?? ''\n\n let shouldWriteRouteFile = false\n // now we need to either scaffold the file or transform it\n if (!existingRouteFile.fileContent) {\n shouldWriteRouteFile = true\n // Creating a new lazy route file\n if (node._fsRouteType === 'lazy') {\n const tLazyRouteTemplate = this.targetTemplate.lazyRoute\n // Check by default check if the user has a specific lazy route template\n // If not, check if the user has a route template and use that instead\n updatedCacheEntry.fileContent = await fillTemplate(\n this.config,\n (this.config.customScaffolding?.lazyRouteTemplate ||\n this.config.customScaffolding?.routeTemplate) ??\n tLazyRouteTemplate.template(),\n {\n tsrImports: tLazyRouteTemplate.imports.tsrImports(),\n tsrPath: escapedRoutePath.replaceAll(/\\{(.+)\\}/gm, '$1'),\n tsrExportStart:\n tLazyRouteTemplate.imports.tsrExportStart(escapedRoutePath),\n tsrExportEnd: tLazyRouteTemplate.imports.tsrExportEnd(),\n },\n )\n updatedCacheEntry.exports = ['Route']\n } else if (\n // Creating a new normal route file\n (['layout', 'static'] satisfies Array<FsRouteType>).some(\n (d) => d === node._fsRouteType,\n ) ||\n (\n [\n 'component',\n 'pendingComponent',\n 'errorComponent',\n 'loader',\n ] satisfies Array<FsRouteType>\n ).every((d) => d !== node._fsRouteType)\n ) {\n const tRouteTemplate = this.targetTemplate.route\n updatedCacheEntry.fileContent = await fillTemplate(\n this.config,\n this.config.customScaffolding?.routeTemplate ??\n tRouteTemplate.template(),\n {\n tsrImports: tRouteTemplate.imports.tsrImports(),\n tsrPath: escapedRoutePath.replaceAll(/\\{(.+)\\}/gm, '$1'),\n tsrExportStart:\n tRouteTemplate.imports.tsrExportStart(escapedRoutePath),\n tsrExportEnd: tRouteTemplate.imports.tsrExportEnd(),\n },\n )\n updatedCacheEntry.exports = ['Route']\n } else {\n return null\n }\n } else {\n // transform the file\n const transformResult = await transform({\n source: updatedCacheEntry.fileContent,\n ctx: {\n target: this.config.target,\n routeId: escapedRoutePath,\n lazy: node._fsRouteType === 'lazy',\n verboseFileRoutes: !(this.config.verboseFileRoutes === false),\n },\n plugins: this.transformPlugins,\n })\n\n if (transformResult.result === 'error') {\n throw new Error(\n `Error transforming route file ${node.fullPath}: ${transformResult.error}`,\n )\n }\n updatedCacheEntry.exports = transformResult.exports\n if (transformResult.result === 'modified') {\n updatedCacheEntry.fileContent = transformResult.output\n shouldWriteRouteFile = true\n }\n }\n\n // file was changed\n if (shouldWriteRouteFile) {\n const stats = await this.safeFileWrite({\n filePath: node.fullPath,\n newContent: updatedCacheEntry.fileContent,\n strategy: {\n type: 'mtime',\n expectedMtimeMs: updatedCacheEntry.mtimeMs,\n },\n })\n updatedCacheEntry.mtimeMs = stats.mtimeMs\n }\n\n this.routeNodeShadowCache.set(node.fullPath, updatedCacheEntry)\n node.exports = updatedCacheEntry.exports\n const shouldWriteTree = !deepEqual(\n result.cacheEntry?.exports,\n updatedCacheEntry.exports,\n )\n return {\n node,\n shouldWriteTree,\n cacheEntry: updatedCacheEntry,\n }\n }\n\n private async didRouteFileChangeComparedToCache(\n file: {\n path: string\n mtimeMs?: bigint\n },\n cache: 'routeNodeCache' | 'routeNodeShadowCache',\n ): Promise<FileCacheChange<RouteNodeCacheEntry>> {\n const cacheEntry = this[cache].get(file.path)\n return this.didFileChangeComparedToCache(file, cacheEntry)\n }\n\n private async didFileChangeComparedToCache<\n TCacheEntry extends GeneratorCacheEntry,\n >(\n file: {\n path: string\n mtimeMs?: bigint\n },\n cacheEntry: TCacheEntry | undefined,\n ): Promise<FileCacheChange<TCacheEntry>> {\n // for now we rely on the modification time of the file\n // to determine if the file has changed\n // we could also compare the file content but this would be slower as we would have to read the file\n\n if (!cacheEntry) {\n return { result: 'file-not-in-cache' }\n }\n let mtimeMs = file.mtimeMs\n\n if (mtimeMs === undefined) {\n try {\n const currentStat = await this.fs.stat(file.path)\n mtimeMs = currentStat.mtimeMs\n } catch {\n return { result: 'cannot-stat-file' }\n }\n }\n return { result: mtimeMs !== cacheEntry.mtimeMs, mtimeMs, cacheEntry }\n }\n\n private async safeFileWrite(opts: {\n filePath: string\n newContent: string\n strategy:\n | {\n type: 'mtime'\n expectedMtimeMs: bigint\n }\n | {\n type: 'new-file'\n }\n }) {\n const tmpPath = this.getTempFileName(opts.filePath)\n await this.fs.writeFile(tmpPath, opts.newContent)\n\n if (opts.strategy.type === 'mtime') {\n const beforeStat = await this.fs.stat(opts.filePath)\n if (beforeStat.mtimeMs !== opts.strategy.expectedMtimeMs) {\n throw rerun({\n msg: `File ${opts.filePath} was modified by another process during processing.`,\n event: { type: 'update', path: opts.filePath },\n })\n }\n } else {\n if (await checkFileExists(opts.filePath)) {\n throw rerun({\n msg: `File ${opts.filePath} already exists. Cannot overwrite.`,\n event: { type: 'update', path: opts.filePath },\n })\n }\n }\n\n const stat = await this.fs.stat(tmpPath)\n\n await this.fs.rename(tmpPath, opts.filePath)\n\n return stat\n }\n\n private getTempFileName(filePath: string) {\n const absPath = path.resolve(filePath)\n const hash = crypto.createHash('md5').update(absPath).digest('hex')\n return path.join(this.tmpDir, hash)\n }\n\n private async isRouteFileCacheFresh(node: RouteNode): Promise<\n | {\n status: 'fresh'\n cacheEntry: RouteNodeCacheEntry\n exportsChanged: boolean\n }\n | { status: 'stale'; cacheEntry?: RouteNodeCacheEntry }\n > {\n const fileChangedCache = await this.didRouteFileChangeComparedToCache(\n { path: node.fullPath },\n 'routeNodeCache',\n )\n if (fileChangedCache.result === false) {\n this.routeNodeShadowCache.set(node.fullPath, fileChangedCache.cacheEntry)\n return {\n status: 'fresh',\n exportsChanged: false,\n cacheEntry: fileChangedCache.cacheEntry,\n }\n }\n if (fileChangedCache.result === 'cannot-stat-file') {\n throw new Error(`⚠️ expected route file to exist at ${node.fullPath}`)\n }\n const mtimeMs =\n fileChangedCache.result === true ? fileChangedCache.mtimeMs : undefined\n\n const shadowCacheFileChange = await this.didRouteFileChangeComparedToCache(\n { path: node.fullPath, mtimeMs },\n 'routeNodeShadowCache',\n )\n\n if (shadowCacheFileChange.result === 'cannot-stat-file') {\n throw new Error(`⚠️ expected route file to exist at ${node.fullPath}`)\n }\n\n if (shadowCacheFileChange.result === false) {\n // shadow cache has latest file state already\n // compare shadowCache against cache to determine whether exports changed\n // if they didn't, cache is fresh\n if (fileChangedCache.result === true) {\n if (\n deepEqual(\n fileChangedCache.cacheEntry.exports,\n shadowCacheFileChange.cacheEntry.exports,\n )\n ) {\n return {\n status: 'fresh',\n exportsChanged: false,\n cacheEntry: shadowCacheFileChange.cacheEntry,\n }\n }\n return {\n status: 'fresh',\n exportsChanged: true,\n cacheEntry: shadowCacheFileChange.cacheEntry,\n }\n }\n }\n\n if (fileChangedCache.result === 'file-not-in-cache') {\n return {\n status: 'stale',\n }\n }\n return { status: 'stale', cacheEntry: fileChangedCache.cacheEntry }\n }\n\n private async handleRootNode(node: RouteNode) {\n const result = await this.isRouteFileCacheFresh(node)\n\n if (result.status === 'fresh') {\n node.exports = result.cacheEntry.exports\n this.routeNodeShadowCache.set(node.fullPath, result.cacheEntry)\n return result.exportsChanged\n }\n const rootNodeFile = await this.fs.readFile(node.fullPath)\n if (rootNodeFile === 'file-not-existing') {\n throw new Error(`⚠️ expected root route to exist at ${node.fullPath}`)\n }\n\n const updatedCacheEntry: RouteNodeCacheEntry = {\n fileContent: rootNodeFile.fileContent,\n mtimeMs: rootNodeFile.stat.mtimeMs,\n exports: [],\n routeId: node.routePath ?? '$$TSR_NO_ROOT_ROUTE_PATH_ASSIGNED$$',\n }\n\n // scaffold the root route\n if (!rootNodeFile.fileContent) {\n const rootTemplate = this.targetTemplate.rootRoute\n const rootRouteContent = await fillTemplate(\n this.config,\n rootTemplate.template(),\n {\n tsrImports: rootTemplate.imports.tsrImports(),\n tsrPath: rootPathId,\n tsrExportStart: rootTemplate.imports.tsrExportStart(),\n tsrExportEnd: rootTemplate.imports.tsrExportEnd(),\n },\n )\n\n this.logger.log(`🟡 Creating ${node.fullPath}`)\n const stats = await this.safeFileWrite({\n filePath: node.fullPath,\n newContent: rootRouteContent,\n strategy: {\n type: 'mtime',\n expectedMtimeMs: rootNodeFile.stat.mtimeMs,\n },\n })\n updatedCacheEntry.fileContent = rootRouteContent\n updatedCacheEntry.mtimeMs = stats.mtimeMs\n }\n\n const rootRouteExports: Array<string> = []\n for (const plugin of this.pluginsWithTransform) {\n const exportName = plugin.transformPlugin.exportName\n // TODO we need to parse instead of just string match\n // otherwise a commented out export will still be detected\n if (rootNodeFile.fileContent.includes(`export const ${exportName}`)) {\n rootRouteExports.push(exportName)\n }\n }\n\n updatedCacheEntry.exports = rootRouteExports\n node.exports = rootRouteExports\n this.routeNodeShadowCache.set(node.fullPath, updatedCacheEntry)\n\n const shouldWriteTree = !deepEqual(\n result.cacheEntry?.exports,\n rootRouteExports,\n )\n return shouldWriteTree\n }\n\n private handleNode(node: RouteNode, acc: HandleNodeAccumulator) {\n // Do not remove this as we need to set the lastIndex to 0 as it\n // is necessary to reset the regex's index when using the global flag\n // otherwise it might not match the next time it's used\n resetRegex(this.routeGroupPatternRegex)\n\n let parentRoute = hasParentRoute(acc.routeNodes, node, node.routePath)\n\n // if the parent route is a virtual parent route, we need to find the real parent route\n if (parentRoute?.isVirtualParentRoute && parentRoute.children?.length) {\n // only if this sub-parent route returns a valid parent route, we use it, if not leave it as it\n const possibleParentRoute = hasParentRoute(\n parentRoute.children,\n node,\n node.routePath,\n )\n if (possibleParentRoute) {\n parentRoute = possibleParentRoute\n }\n }\n\n if (parentRoute) node.parent = parentRoute\n\n node.path = determineNodePath(node)\n\n const trimmedPath = trimPathLeft(node.path ?? '')\n\n const split = trimmedPath.split('/')\n const lastRouteSegment = split[split.length - 1] ?? trimmedPath\n\n node.isNonPath =\n lastRouteSegment.startsWith('_') ||\n this.routeGroupPatternRegex.test(lastRouteSegment)\n\n node.cleanedPath = removeGroups(\n removeUnderscores(removeLayoutSegments(node.path)) ?? '',\n )\n\n if (\n !node.isVirtual &&\n (\n [\n 'lazy',\n 'loader',\n 'component',\n 'pendingComponent',\n 'errorComponent',\n ] satisfies Array<FsRouteType>\n ).some((d) => d === node._fsRouteType)\n ) {\n acc.routePiecesByPath[node.routePath!] =\n acc.routePiecesByPath[node.routePath!] || {}\n\n acc.routePiecesByPath[node.routePath!]![\n node._fsRouteType === 'lazy'\n ? 'lazy'\n : node._fsRouteType === 'loader'\n ? 'loader'\n : node._fsRouteType === 'errorComponent'\n ? 'errorComponent'\n : node._fsRouteType === 'pendingComponent'\n ? 'pendingComponent'\n : 'component'\n ] = node\n\n const anchorRoute = acc.routeNodes.find(\n (d) => d.routePath === node.routePath,\n )\n\n if (!anchorRoute) {\n this.handleNode(\n {\n ...node,\n isVirtual: true,\n _fsRouteType: 'static',\n },\n acc,\n )\n }\n return\n }\n\n const cleanedPathIsEmpty = (node.cleanedPath || '').length === 0\n const nonPathRoute =\n node._fsRouteType === 'pathless_layout' && node.isNonPath\n\n node.isVirtualParentRequired =\n node._fsRouteType === 'pathless_layout' || nonPathRoute\n ? !cleanedPathIsEmpty\n : false\n\n if (!node.isVirtual && node.isVirtualParentRequired) {\n const parentRoutePath = removeLastSegmentFromPath(node.routePath) || '/'\n const parentVariableName = routePathToVariable(parentRoutePath)\n\n const anchorRoute = acc.routeNodes.find(\n (d) => d.routePath === parentRoutePath,\n )\n\n if (!anchorRoute) {\n const parentNode: RouteNode = {\n ...node,\n path: removeLastSegmentFromPath(node.path) || '/',\n filePath: removeLastSegmentFromPath(node.filePath) || '/',\n fullPath: removeLastSegmentFromPath(node.fullPath) || '/',\n routePath: parentRoutePath,\n variableName: parentVariableName,\n isVirtual: true,\n _fsRouteType: 'layout', // layout since this route will wrap other routes\n isVirtualParentRoute: true,\n isVirtualParentRequired: false,\n }\n\n parentNode.children = parentNode.children ?? []\n parentNode.children.push(node)\n\n node.parent = parentNode\n\n if (node._fsRouteType === 'pathless_layout') {\n // since `node.path` is used as the `id` on the route definition, we need to update it\n node.path = determineNodePath(node)\n }\n\n this.handleNode(parentNode, acc)\n } else {\n anchorRoute.children = anchorRoute.children ?? []\n anchorRoute.children.push(node)\n\n node.parent = anchorRoute\n }\n }\n\n if (node.parent) {\n if (!node.isVirtualParentRequired) {\n node.parent.children = node.parent.children ?? []\n node.parent.children.push(node)\n }\n } else {\n acc.routeTree.push(node)\n }\n\n acc.routeNodes.push(node)\n }\n\n // only process files that are relevant for the route tree generation\n private isFileRelevantForRouteTreeGeneration(filePath: string): boolean {\n // the generated route tree file\n if (filePath === this.generatedRouteTreePath) {\n return true\n }\n\n // files inside the routes folder\n if (filePath.startsWith(this.routesDirectoryPath)) {\n return true\n }\n\n // the virtual route config file passed into `virtualRouteConfig`\n if (\n typeof this.config.virtualRouteConfig === 'string' &&\n filePath === this.config.virtualRouteConfig\n ) {\n return true\n }\n\n // this covers all files that are mounted via `virtualRouteConfig` or any `__virtual.ts` files\n if (this.routeNodeCache.has(filePath)) {\n return true\n }\n\n // virtual config files such as`__virtual.ts`\n if (isVirtualConfigFile(path.basename(filePath))) {\n return true\n }\n\n // route files inside directories mounted via `physical()` inside a virtual route config\n if (this.physicalDirectories.some((dir) => filePath.startsWith(dir))) {\n return true\n }\n\n return false\n }\n}\n"],"names":["fsp","mkdtempSync","defaultGeneratorPlugin","logging","getTargetTemplate","virtualGetRouteNodes","physicalGetRouteNodes","rootPathId","multiSortBy","format","_a","routeImports","buildRouteTreeConfig","_b","_c","findParent","replaceBackslash","removeExt","createRouteNodesByFullPath","getResolvedRouteNodeVariableName","createRouteNodesByTo","rootRouteId","createRouteNodesById","buildFileRoutesByPathInterface","lowerCaseFirstChar","mergeImportDeclarations","buildImportString","isRouteNodeValidForAugmentation","moduleAugmentation","fillTemplate","transform","deepEqual","checkFileExists","resetRegex","hasParentRoute","determineNodePath","trimPathLeft","removeGroups","removeUnderscores","removeLayoutSegments","removeLastSegmentFromPath","routePathToVariable","isVirtualConfigFile"],"mappings":"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