diff --git a/packages/router-generator/src/generator.ts b/packages/router-generator/src/generator.ts index 6d868af1ab..9b1f8f2524 100644 --- a/packages/router-generator/src/generator.ts +++ b/packages/router-generator/src/generator.ts @@ -18,6 +18,7 @@ import { createRouteNodesByFullPath, createRouteNodesById, createRouteNodesByTo, + detectExportsFromSource, determineNodePath, findParent, format, @@ -1207,15 +1208,13 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get updatedCacheEntry.mtimeMs = stats.mtimeMs } - const rootRouteExports: Array = [] - for (const plugin of this.pluginsWithTransform) { - const exportName = plugin.transformPlugin.exportName - // TODO we need to parse instead of just string match - // otherwise a commented out export will still be detected - if (rootNodeFile.fileContent.includes(`export const ${exportName}`)) { - rootRouteExports.push(exportName) - } - } + const exportNames = this.pluginsWithTransform.map( + (plugin) => plugin.transformPlugin.exportName, + ) + const rootRouteExports = detectExportsFromSource( + rootNodeFile.fileContent, + exportNames, + ) updatedCacheEntry.exports = rootRouteExports node.exports = rootRouteExports diff --git a/packages/router-generator/src/transform/transform.ts b/packages/router-generator/src/transform/transform.ts index 162acfb281..574c798c5e 100644 --- a/packages/router-generator/src/transform/transform.ts +++ b/packages/router-generator/src/transform/transform.ts @@ -1,7 +1,7 @@ import { parseAst } from '@tanstack/router-utils' import { parse, print, types, visit } from 'recast' import { SourceMapConsumer } from 'source-map' -import { mergeImportDeclarations } from '../utils' +import { matchExportInNode, mergeImportDeclarations } from '../utils' import type { ImportDeclaration } from '../types' import type { RawSourceMap } from 'source-map' import type { @@ -76,49 +76,15 @@ export async function transform({ } const program: types.namedTypes.Program = ast.program - // first pass: find registered exports + + const exportNames = Array.from(registeredExports.keys()) for (const n of program.body) { - if (registeredExports.size > 0 && n.type === 'ExportNamedDeclaration') { - // direct export of a variable declaration, e.g. `export const Route = createFileRoute('/path')` - if (n.declaration?.type === 'VariableDeclaration') { - const decl = n.declaration.declarations[0] - if ( - decl && - decl.type === 'VariableDeclarator' && - decl.id.type === 'Identifier' - ) { - const plugin = registeredExports.get(decl.id.name) - if (plugin) { - onExportFound(decl, decl.id.name, plugin) - } - } - } - // this is an export without a declaration, e.g. `export { Route }` - else if (n.declaration === null && n.specifiers) { - for (const spec of n.specifiers) { - if (typeof spec.exported.name === 'string') { - const plugin = registeredExports.get(spec.exported.name) - if (plugin) { - const variableName = spec.local?.name || spec.exported.name - // find the matching variable declaration by iterating over the top-level declarations - for (const decl of program.body) { - if ( - decl.type === 'VariableDeclaration' && - decl.declarations[0] - ) { - const variable = decl.declarations[0] - if ( - variable.type === 'VariableDeclarator' && - variable.id.type === 'Identifier' && - variable.id.name === variableName - ) { - onExportFound(variable, spec.exported.name, plugin) - break - } - } - } - } - } + if (registeredExports.size > 0) { + const match = matchExportInNode(n, exportNames, program) + if (match) { + const plugin = registeredExports.get(match.exportName) + if (plugin) { + onExportFound(match.decl, match.exportName, plugin) } } } diff --git a/packages/router-generator/src/utils.ts b/packages/router-generator/src/utils.ts index b8c45d61f0..ea408ec908 100644 --- a/packages/router-generator/src/utils.ts +++ b/packages/router-generator/src/utils.ts @@ -1,7 +1,10 @@ import * as fsp from 'node:fs/promises' import path from 'node:path' import * as prettier from 'prettier' +import { parseAst } from '@tanstack/router-utils' +import { parse } from 'recast' import { rootPathId } from './filesystem/physical/rootPathId' +import type { types } from 'recast' import type { Config } from './config' import type { ImportDeclaration, RouteNode } from './types' @@ -631,3 +634,91 @@ export function buildFileRoutesByPathInterface(opts: { } }` } + +function findVariableDeclaration( + program: types.namedTypes.Program, + variableName: string, +): types.namedTypes.VariableDeclarator | null { + for (const decl of program.body) { + if (decl.type === 'VariableDeclaration' && decl.declarations[0]) { + const variable = decl.declarations[0] + if ( + variable.type === 'VariableDeclarator' && + variable.id.type === 'Identifier' && + variable.id.name === variableName + ) { + return variable + } + } + } + return null +} + +export function matchExportInNode( + node: any, + exportNames: Array, + program: types.namedTypes.Program, +): { exportName: string; decl: types.namedTypes.VariableDeclarator } | null { + if (node.type === 'ExportNamedDeclaration') { + // direct export of a variable declaration, e.g. `export const Route = createFileRoute('/path')` + if (node.declaration?.type === 'VariableDeclaration') { + const decl = node.declaration.declarations[0] + if ( + decl && + decl.type === 'VariableDeclarator' && + decl.id.type === 'Identifier' && + exportNames.includes(decl.id.name) + ) { + return { exportName: decl.id.name, decl } + } + } + // this is an export without a declaration, e.g. `export { Route }` + else if (node.declaration === null && node.specifiers) { + for (const spec of node.specifiers) { + if ( + typeof spec.exported.name === 'string' && + exportNames.includes(spec.exported.name) + ) { + const variableName = spec.local?.name || spec.exported.name + const decl = findVariableDeclaration(program, variableName) + if (decl) { + return { exportName: spec.exported.name, decl } + } + } + } + } + } + return null +} + +export function detectExportsFromSource( + source: string, + exportNames: Array, +): Array { + try { + const ast = parse(source, { + sourceFileName: 'detect-exports.ts', + parser: { + parse(code: string) { + return parseAst({ + code, + tokens: true, + }) + }, + }, + }) + + const foundExports: Array = [] + for (const node of ast.program.body) { + const match = matchExportInNode(node, exportNames, ast.program) + if (match) { + foundExports.push(match.exportName) + } + } + return foundExports + } catch (error) { + return exportNames.filter((exportName) => + source.includes(`export const ${exportName}`), + ) + } +} diff --git a/packages/router-generator/tests/generator/root-export-separate/routeTree.snapshot.ts b/packages/router-generator/tests/generator/root-export-separate/routeTree.snapshot.ts new file mode 100644 index 0000000000..d204c269b3 --- /dev/null +++ b/packages/router-generator/tests/generator/root-export-separate/routeTree.snapshot.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// 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. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/packages/router-generator/tests/generator/root-export-separate/routes/__root.tsx b/packages/router-generator/tests/generator/root-export-separate/routes/__root.tsx new file mode 100644 index 0000000000..014506f7e7 --- /dev/null +++ b/packages/router-generator/tests/generator/root-export-separate/routes/__root.tsx @@ -0,0 +1,7 @@ +import { createRootRoute } from '@tanstack/react-router' + +const Route = createRootRoute({ + component: () =>
Root Layout
, +}) + +export { Route } diff --git a/packages/router-generator/tests/generator/root-export-separate/routes/index.tsx b/packages/router-generator/tests/generator/root-export-separate/routes/index.tsx new file mode 100644 index 0000000000..727d16639b --- /dev/null +++ b/packages/router-generator/tests/generator/root-export-separate/routes/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: () =>
Hello World
, +})