UNPKG

@tanstack/router-plugin

Version:

Modern and scalable routing for React applications

1 lines 95.5 kB
{"version":3,"file":"compilers.cjs","names":[],"sources":["../../../../src/core/code-splitter/compilers.ts"],"sourcesContent":["import * as t from '@babel/types'\nimport * as babel from '@babel/core'\nimport * as template from '@babel/template'\nimport {\n deadCodeElimination,\n findReferencedIdentifiers,\n generateFromAst,\n parseAst,\n} from '@tanstack/router-utils'\nimport { tsrShared, tsrSplit } from '../constants'\nimport { routeHmrStatement } from '../route-hmr-statement'\nimport { createIdentifier } from './path-ids'\nimport { getFrameworkOptions } from './framework-options'\nimport type {\n CompileCodeSplitReferenceRouteOptions,\n ReferenceRouteCompilerPlugin,\n} from './plugins'\nimport type { GeneratorResult, ParseAstOptions } from '@tanstack/router-utils'\nimport type { CodeSplitGroupings, SplitRouteIdentNodes } from '../constants'\nimport type { Config, DeletableNodes } from '../config'\n\ntype SplitNodeMeta = {\n routeIdent: SplitRouteIdentNodes\n splitStrategy: 'lazyFn' | 'lazyRouteComponent'\n localImporterIdent: string\n exporterIdent: string\n localExporterIdent: string\n}\nconst SPLIT_NODES_CONFIG = new Map<SplitRouteIdentNodes, SplitNodeMeta>([\n [\n 'loader',\n {\n routeIdent: 'loader',\n localImporterIdent: '$$splitLoaderImporter', // const $$splitLoaderImporter = () => import('...')\n splitStrategy: 'lazyFn',\n localExporterIdent: 'SplitLoader', // const SplitLoader = ...\n exporterIdent: 'loader', // export { SplitLoader as loader }\n },\n ],\n [\n 'component',\n {\n routeIdent: 'component',\n localImporterIdent: '$$splitComponentImporter', // const $$splitComponentImporter = () => import('...')\n splitStrategy: 'lazyRouteComponent',\n localExporterIdent: 'SplitComponent', // const SplitComponent = ...\n exporterIdent: 'component', // export { SplitComponent as component }\n },\n ],\n [\n 'pendingComponent',\n {\n routeIdent: 'pendingComponent',\n localImporterIdent: '$$splitPendingComponentImporter', // const $$splitPendingComponentImporter = () => import('...')\n splitStrategy: 'lazyRouteComponent',\n localExporterIdent: 'SplitPendingComponent', // const SplitPendingComponent = ...\n exporterIdent: 'pendingComponent', // export { SplitPendingComponent as pendingComponent }\n },\n ],\n [\n 'errorComponent',\n {\n routeIdent: 'errorComponent',\n localImporterIdent: '$$splitErrorComponentImporter', // const $$splitErrorComponentImporter = () => import('...')\n splitStrategy: 'lazyRouteComponent',\n localExporterIdent: 'SplitErrorComponent', // const SplitErrorComponent = ...\n exporterIdent: 'errorComponent', // export { SplitErrorComponent as errorComponent }\n },\n ],\n [\n 'notFoundComponent',\n {\n routeIdent: 'notFoundComponent',\n localImporterIdent: '$$splitNotFoundComponentImporter', // const $$splitNotFoundComponentImporter = () => import('...')\n splitStrategy: 'lazyRouteComponent',\n localExporterIdent: 'SplitNotFoundComponent', // const SplitNotFoundComponent = ...\n exporterIdent: 'notFoundComponent', // export { SplitNotFoundComponent as notFoundComponent }\n },\n ],\n])\nconst KNOWN_SPLIT_ROUTE_IDENTS = [...SPLIT_NODES_CONFIG.keys()] as const\n\nfunction addSplitSearchParamToFilename(\n filename: string,\n grouping: Array<string>,\n) {\n const [bareFilename] = filename.split('?')\n\n const params = new URLSearchParams()\n params.append(tsrSplit, createIdentifier(grouping))\n\n const result = `${bareFilename}?${params.toString()}`\n return result\n}\n\nfunction removeSplitSearchParamFromFilename(filename: string) {\n const [bareFilename] = filename.split('?')\n return bareFilename!\n}\n\nexport function addSharedSearchParamToFilename(filename: string) {\n const [bareFilename] = filename.split('?')\n return `${bareFilename}?${tsrShared}=1`\n}\n\nconst splittableCreateRouteFns = ['createFileRoute']\nconst unsplittableCreateRouteFns = [\n 'createRootRoute',\n 'createRootRouteWithContext',\n]\nconst allCreateRouteFns = [\n ...splittableCreateRouteFns,\n ...unsplittableCreateRouteFns,\n]\n\n/**\n * Recursively walk an AST node and collect referenced identifier-like names.\n * Much cheaper than babel.traverse — no path/scope overhead.\n *\n * Notes:\n * - Uses @babel/types `isReferenced` to avoid collecting non-references like\n * object keys, member expression properties, or binding identifiers.\n * - Also handles JSX identifiers for component references.\n */\nexport function collectIdentifiersFromNode(node: t.Node): Set<string> {\n const ids = new Set<string>()\n\n ;(function walk(\n n: t.Node | null | undefined,\n parent?: t.Node,\n grandparent?: t.Node,\n parentKey?: string,\n ) {\n if (!n) return\n\n if (t.isIdentifier(n)) {\n // When we don't have parent info (node passed in isolation), treat as referenced.\n if (!parent || t.isReferenced(n, parent, grandparent)) {\n ids.add(n.name)\n }\n return\n }\n\n if (t.isJSXIdentifier(n)) {\n // Skip attribute names: <div data-testid=\"x\" />\n if (parent && t.isJSXAttribute(parent) && parentKey === 'name') {\n return\n }\n\n // Skip member properties: <Foo.Bar /> should count Foo, not Bar\n if (\n parent &&\n t.isJSXMemberExpression(parent) &&\n parentKey === 'property'\n ) {\n return\n }\n\n // Intrinsic elements (lowercase) are not identifiers\n const first = n.name[0]\n if (first && first === first.toLowerCase()) {\n return\n }\n\n ids.add(n.name)\n return\n }\n\n for (const key of t.VISITOR_KEYS[n.type] || []) {\n const child = (n as any)[key]\n if (Array.isArray(child)) {\n for (const c of child) {\n if (c && typeof c.type === 'string') {\n walk(c, n, parent, key)\n }\n }\n } else if (child && typeof child.type === 'string') {\n walk(child, n, parent, key)\n }\n }\n })(node)\n\n return ids\n}\n\n/**\n * Build a map from binding name → declaration AST node for all\n * locally-declared module-level bindings. Built once, O(1) lookup.\n */\nexport function buildDeclarationMap(ast: t.File): Map<string, t.Node> {\n const map = new Map<string, t.Node>()\n for (const stmt of ast.program.body) {\n const decl =\n t.isExportNamedDeclaration(stmt) && stmt.declaration\n ? stmt.declaration\n : stmt\n\n if (t.isVariableDeclaration(decl)) {\n for (const declarator of decl.declarations) {\n for (const name of collectIdentifiersFromPattern(declarator.id)) {\n map.set(name, declarator)\n }\n }\n } else if (t.isFunctionDeclaration(decl) && decl.id) {\n map.set(decl.id.name, decl)\n } else if (t.isClassDeclaration(decl) && decl.id) {\n map.set(decl.id.name, decl)\n }\n }\n return map\n}\n\n/**\n * Build a dependency graph: for each local binding, the set of other local\n * bindings its declaration references. Built once via simple node walking.\n */\nexport function buildDependencyGraph(\n declMap: Map<string, t.Node>,\n localBindings: Set<string>,\n): Map<string, Set<string>> {\n const graph = new Map<string, Set<string>>()\n for (const [name, declNode] of declMap) {\n if (!localBindings.has(name)) continue\n const allIds = collectIdentifiersFromNode(declNode)\n const deps = new Set<string>()\n for (const id of allIds) {\n if (id !== name && localBindings.has(id)) deps.add(id)\n }\n graph.set(name, deps)\n }\n return graph\n}\n\n/**\n * Computes module-level bindings that are shared between split and non-split\n * route properties. These bindings need to be extracted into a shared virtual\n * module to avoid double-initialization.\n *\n * A binding is \"shared\" if it is referenced by at least one split property\n * AND at least one non-split property. Only locally-declared module-level\n * bindings are candidates (not imports — bundlers dedupe those).\n */\nexport function computeSharedBindings(opts: {\n code: string\n codeSplitGroupings: CodeSplitGroupings\n}): Set<string> {\n const ast = parseAst(opts)\n\n // Early bailout: collect all module-level locally-declared binding names.\n // This is a cheap loop over program.body (no traversal). If the file has\n // no local bindings (aside from `Route`), nothing can be shared — skip\n // the expensive babel.traverse entirely.\n const localModuleLevelBindings = new Set<string>()\n for (const node of ast.program.body) {\n collectLocalBindingsFromStatement(node, localModuleLevelBindings)\n }\n\n // File-based routes always export a route config binding (usually `Route`).\n // This must never be extracted into the shared module.\n localModuleLevelBindings.delete('Route')\n\n if (localModuleLevelBindings.size === 0) {\n return new Set()\n }\n\n function findIndexForSplitNode(str: string) {\n return opts.codeSplitGroupings.findIndex((group) =>\n group.includes(str as any),\n )\n }\n\n // Find the route options object — needs babel.traverse for scope resolution\n let routeOptions: t.ObjectExpression | undefined\n\n babel.traverse(ast, {\n CallExpression(path) {\n if (!t.isIdentifier(path.node.callee)) return\n if (!splittableCreateRouteFns.includes(path.node.callee.name)) return\n\n if (t.isCallExpression(path.parentPath.node)) {\n const opts = resolveIdentifier(path, path.parentPath.node.arguments[0])\n if (t.isObjectExpression(opts)) routeOptions = opts\n } else if (t.isVariableDeclarator(path.parentPath.node)) {\n const caller = resolveIdentifier(path, path.parentPath.node.init)\n if (t.isCallExpression(caller)) {\n const opts = resolveIdentifier(path, caller.arguments[0])\n if (t.isObjectExpression(opts)) routeOptions = opts\n }\n }\n },\n })\n\n if (!routeOptions) return new Set()\n\n // Fast path: if fewer than 2 distinct groups are referenced by route options,\n // nothing can be shared and we can skip the rest of the work.\n const splitGroupsPresent = new Set<number>()\n let hasNonSplit = false\n for (const prop of routeOptions.properties) {\n if (!t.isObjectProperty(prop) || !t.isIdentifier(prop.key)) continue\n if (prop.key.name === 'codeSplitGroupings') continue\n if (t.isIdentifier(prop.value) && prop.value.name === 'undefined') continue\n const groupIndex = findIndexForSplitNode(prop.key.name) // -1 if non-split\n if (groupIndex === -1) {\n hasNonSplit = true\n } else {\n splitGroupsPresent.add(groupIndex)\n }\n }\n\n if (!hasNonSplit && splitGroupsPresent.size < 2) return new Set()\n\n // Build dependency graph up front — needed for transitive expansion per-property.\n // This graph excludes `Route` (deleted above) so group attribution works correctly.\n const declMap = buildDeclarationMap(ast)\n const depGraph = buildDependencyGraph(declMap, localModuleLevelBindings)\n\n // Build a second dependency graph that includes `Route` so we can detect\n // bindings that transitively depend on it. Such bindings must NOT be\n // extracted into the shared module because they would drag the Route\n // singleton with them, duplicating it across modules.\n const allLocalBindings = new Set(localModuleLevelBindings)\n allLocalBindings.add('Route')\n const fullDepGraph = buildDependencyGraph(declMap, allLocalBindings)\n\n // For each route property, track which \"group\" it belongs to.\n // Non-split properties get group index -1.\n // Split properties get their codeSplitGroupings index (0, 1, ...).\n // A binding is \"shared\" if it appears in 2+ distinct groups.\n // We expand each property's refs transitively BEFORE comparing groups,\n // so indirect refs (e.g., component: MyComp where MyComp uses `shared`)\n // are correctly attributed.\n const refsByGroup = new Map<string, Set<number>>()\n\n for (const prop of routeOptions.properties) {\n if (!t.isObjectProperty(prop) || !t.isIdentifier(prop.key)) continue\n const key = prop.key.name\n\n if (key === 'codeSplitGroupings') continue\n\n const groupIndex = findIndexForSplitNode(key) // -1 if non-split\n\n const directRefs = collectModuleLevelRefsFromNode(\n prop.value,\n localModuleLevelBindings,\n )\n\n // Expand transitively: if component references SharedComp which references\n // `shared`, then `shared` is also attributed to component's group.\n const allRefs = new Set(directRefs)\n expandTransitively(allRefs, depGraph)\n\n for (const ref of allRefs) {\n let groups = refsByGroup.get(ref)\n if (!groups) {\n groups = new Set()\n refsByGroup.set(ref, groups)\n }\n groups.add(groupIndex)\n }\n }\n\n // Shared = bindings appearing in 2+ distinct groups\n const shared = new Set<string>()\n for (const [name, groups] of refsByGroup) {\n if (groups.size >= 2) shared.add(name)\n }\n\n // Destructured declarators (e.g. `const { a, b } = fn()`) must be treated\n // as a single initialization unit. Even if each binding is referenced by\n // only one group, if *different* bindings from the same declarator are\n // referenced by different groups, the declarator must be extracted to the\n // shared module to avoid double initialization.\n expandSharedDestructuredDeclarators(ast, refsByGroup, shared)\n\n if (shared.size === 0) return shared\n\n // If any binding from a destructured declaration is shared,\n // all bindings from that declaration must be shared\n expandDestructuredDeclarations(ast, shared)\n\n // Remove shared bindings that transitively depend on `Route`.\n // The Route singleton must stay in the reference file; extracting a\n // binding that references it would duplicate Route in the shared module.\n removeBindingsDependingOnRoute(shared, fullDepGraph)\n\n return shared\n}\n\n/**\n * If bindings from the same destructured declarator are referenced by\n * different groups, mark all bindings from that declarator as shared.\n */\nexport function expandSharedDestructuredDeclarators(\n ast: t.File,\n refsByGroup: Map<string, Set<number>>,\n shared: Set<string>,\n) {\n for (const stmt of ast.program.body) {\n const decl =\n t.isExportNamedDeclaration(stmt) && stmt.declaration\n ? stmt.declaration\n : stmt\n\n if (!t.isVariableDeclaration(decl)) continue\n\n for (const declarator of decl.declarations) {\n if (!t.isObjectPattern(declarator.id) && !t.isArrayPattern(declarator.id))\n continue\n\n const names = collectIdentifiersFromPattern(declarator.id)\n\n const usedGroups = new Set<number>()\n for (const name of names) {\n const groups = refsByGroup.get(name)\n if (!groups) continue\n for (const g of groups) usedGroups.add(g)\n }\n\n if (usedGroups.size >= 2) {\n for (const name of names) {\n shared.add(name)\n }\n }\n }\n }\n}\n\n/**\n * Collect locally-declared module-level binding names from a statement.\n * Pure node inspection, no traversal.\n */\nexport function collectLocalBindingsFromStatement(\n node: t.Statement | t.ModuleDeclaration,\n bindings: Set<string>,\n) {\n const decl =\n t.isExportNamedDeclaration(node) && node.declaration\n ? node.declaration\n : node\n\n if (t.isVariableDeclaration(decl)) {\n for (const declarator of decl.declarations) {\n for (const name of collectIdentifiersFromPattern(declarator.id)) {\n bindings.add(name)\n }\n }\n } else if (t.isFunctionDeclaration(decl) && decl.id) {\n bindings.add(decl.id.name)\n } else if (t.isClassDeclaration(decl) && decl.id) {\n bindings.add(decl.id.name)\n }\n}\n\n/**\n * Collect direct module-level binding names referenced from a given AST node.\n * Uses a simple recursive walk instead of babel.traverse.\n */\nexport function collectModuleLevelRefsFromNode(\n node: t.Node,\n localModuleLevelBindings: Set<string>,\n): Set<string> {\n const allIds = collectIdentifiersFromNode(node)\n const refs = new Set<string>()\n for (const name of allIds) {\n if (localModuleLevelBindings.has(name)) refs.add(name)\n }\n return refs\n}\n\n/**\n * Expand the shared set transitively using a prebuilt dependency graph.\n * No AST traversals — pure graph BFS.\n */\nexport function expandTransitively(\n shared: Set<string>,\n depGraph: Map<string, Set<string>>,\n) {\n const queue = [...shared]\n const visited = new Set<string>()\n\n while (queue.length > 0) {\n const name = queue.pop()!\n if (visited.has(name)) continue\n visited.add(name)\n\n const deps = depGraph.get(name)\n if (!deps) continue\n\n for (const dep of deps) {\n if (!shared.has(dep)) {\n shared.add(dep)\n queue.push(dep)\n }\n }\n }\n}\n\n/**\n * Remove any bindings from `shared` that transitively depend on `Route`.\n * The Route singleton must remain in the reference file; if a shared binding\n * references it (directly or transitively), extracting that binding would\n * duplicate Route in the shared module.\n *\n * Uses `depGraph` which must include `Route` as a node so the dependency\n * chain is visible.\n */\nexport function removeBindingsDependingOnRoute(\n shared: Set<string>,\n depGraph: Map<string, Set<string>>,\n) {\n const reverseGraph = new Map<string, Set<string>>()\n for (const [name, deps] of depGraph) {\n for (const dep of deps) {\n let parents = reverseGraph.get(dep)\n if (!parents) {\n parents = new Set<string>()\n reverseGraph.set(dep, parents)\n }\n parents.add(name)\n }\n }\n\n // Walk backwards from Route to find all bindings that can reach it.\n const visited = new Set<string>()\n const queue = ['Route']\n while (queue.length > 0) {\n const cur = queue.pop()!\n if (visited.has(cur)) continue\n visited.add(cur)\n\n const parents = reverseGraph.get(cur)\n if (!parents) continue\n for (const parent of parents) {\n if (!visited.has(parent)) queue.push(parent)\n }\n }\n\n for (const name of [...shared]) {\n if (visited.has(name)) {\n shared.delete(name)\n }\n }\n}\n\n/**\n * If any binding from a destructured declaration is shared,\n * ensure all bindings from that same declaration are also shared.\n * Pure node inspection of program.body, no traversal.\n */\nexport function expandDestructuredDeclarations(\n ast: t.File,\n shared: Set<string>,\n) {\n for (const stmt of ast.program.body) {\n const decl =\n t.isExportNamedDeclaration(stmt) && stmt.declaration\n ? stmt.declaration\n : stmt\n\n if (!t.isVariableDeclaration(decl)) continue\n\n for (const declarator of decl.declarations) {\n if (!t.isObjectPattern(declarator.id) && !t.isArrayPattern(declarator.id))\n continue\n\n const names = collectIdentifiersFromPattern(declarator.id)\n const hasShared = names.some((n) => shared.has(n))\n if (hasShared) {\n for (const n of names) {\n shared.add(n)\n }\n }\n }\n }\n}\n\n/**\n * Find which shared bindings are user-exported in the original source.\n * These need to be re-exported from the shared module.\n */\nfunction findExportedSharedBindings(\n ast: t.File,\n sharedBindings: Set<string>,\n): Set<string> {\n const exported = new Set<string>()\n for (const stmt of ast.program.body) {\n if (!t.isExportNamedDeclaration(stmt) || !stmt.declaration) continue\n\n if (t.isVariableDeclaration(stmt.declaration)) {\n for (const decl of stmt.declaration.declarations) {\n for (const name of collectIdentifiersFromPattern(decl.id)) {\n if (sharedBindings.has(name)) exported.add(name)\n }\n }\n } else if (\n t.isFunctionDeclaration(stmt.declaration) &&\n stmt.declaration.id\n ) {\n if (sharedBindings.has(stmt.declaration.id.name))\n exported.add(stmt.declaration.id.name)\n } else if (t.isClassDeclaration(stmt.declaration) && stmt.declaration.id) {\n if (sharedBindings.has(stmt.declaration.id.name))\n exported.add(stmt.declaration.id.name)\n }\n }\n return exported\n}\n\n/**\n * Remove declarations of shared bindings from the AST.\n * Handles both plain and exported declarations, including destructured patterns.\n * Removes the entire statement if all bindings in it are shared.\n */\nfunction removeSharedDeclarations(ast: t.File, sharedBindings: Set<string>) {\n ast.program.body = ast.program.body.filter((stmt) => {\n const decl =\n t.isExportNamedDeclaration(stmt) && stmt.declaration\n ? stmt.declaration\n : stmt\n\n if (t.isVariableDeclaration(decl)) {\n // Filter out declarators where all bound names are shared\n decl.declarations = decl.declarations.filter((declarator) => {\n const names = collectIdentifiersFromPattern(declarator.id)\n return !names.every((n) => sharedBindings.has(n))\n })\n // If no declarators remain, remove the entire statement\n if (decl.declarations.length === 0) return false\n } else if (t.isFunctionDeclaration(decl) && decl.id) {\n if (sharedBindings.has(decl.id.name)) return false\n } else if (t.isClassDeclaration(decl) && decl.id) {\n if (sharedBindings.has(decl.id.name)) return false\n }\n\n return true\n })\n}\n\nexport function compileCodeSplitReferenceRoute(\n opts: ParseAstOptions & {\n codeSplitGroupings: CodeSplitGroupings\n deleteNodes?: Set<DeletableNodes>\n targetFramework: Config['target']\n filename: string\n id: string\n addHmr?: boolean\n sharedBindings?: Set<string>\n compilerPlugins?: Array<ReferenceRouteCompilerPlugin>\n },\n): GeneratorResult | null {\n const ast = parseAst(opts)\n\n const refIdents = findReferencedIdentifiers(ast)\n\n const knownExportedIdents = new Set<string>()\n\n function findIndexForSplitNode(str: string) {\n return opts.codeSplitGroupings.findIndex((group) =>\n group.includes(str as any),\n )\n }\n\n const frameworkOptions = getFrameworkOptions(opts.targetFramework)\n const PACKAGE = frameworkOptions.package\n const LAZY_ROUTE_COMPONENT_IDENT = frameworkOptions.idents.lazyRouteComponent\n const LAZY_FN_IDENT = frameworkOptions.idents.lazyFn\n\n let createRouteFn: string\n\n let modified = false as boolean\n let hmrAdded = false as boolean\n let sharedExportedNames: Set<string> | undefined\n babel.traverse(ast, {\n Program: {\n enter(programPath) {\n /**\n * If the component for the route is being imported from\n * another file, this is to track the path to that file\n * the path itself doesn't matter, we just need to keep\n * track of it so that we can remove it from the imports\n * list if it's not being used like:\n *\n * `import '../shared/imported'`\n */\n const removableImportPaths = new Set<string>([])\n\n programPath.traverse({\n CallExpression: (path) => {\n if (!t.isIdentifier(path.node.callee)) {\n return\n }\n\n if (!allCreateRouteFns.includes(path.node.callee.name)) {\n return\n }\n\n createRouteFn = path.node.callee.name\n\n function babelHandleReference(routeOptions: t.Node | undefined) {\n const hasImportedOrDefinedIdentifier = (name: string) => {\n return programPath.scope.hasBinding(name)\n }\n\n if (t.isObjectExpression(routeOptions)) {\n if (opts.deleteNodes && opts.deleteNodes.size > 0) {\n routeOptions.properties = routeOptions.properties.filter(\n (prop) => {\n if (t.isObjectProperty(prop)) {\n if (t.isIdentifier(prop.key)) {\n if (opts.deleteNodes!.has(prop.key.name as any)) {\n modified = true\n return false\n }\n }\n }\n return true\n },\n )\n }\n if (!splittableCreateRouteFns.includes(createRouteFn)) {\n const insertionPath = path.getStatementParent() ?? path\n\n opts.compilerPlugins?.forEach((plugin) => {\n const pluginResult = plugin.onUnsplittableRoute?.({\n programPath,\n callExpressionPath: path,\n insertionPath,\n routeOptions,\n createRouteFn,\n opts: opts as CompileCodeSplitReferenceRouteOptions,\n })\n\n if (pluginResult?.modified) {\n modified = true\n }\n })\n\n // we can't split this route but we still add HMR handling if enabled\n if (opts.addHmr && !hmrAdded) {\n programPath.pushContainer('body', routeHmrStatement)\n modified = true\n hmrAdded = true\n }\n // exit traversal so this route is not split\n return programPath.stop()\n }\n routeOptions.properties.forEach((prop) => {\n if (t.isObjectProperty(prop)) {\n if (t.isIdentifier(prop.key)) {\n const key = prop.key.name\n\n // If the user has not specified a split grouping for this key\n // then we should not split it\n const codeSplitGroupingByKey = findIndexForSplitNode(key)\n if (codeSplitGroupingByKey === -1) {\n return\n }\n const codeSplitGroup = [\n ...new Set(\n opts.codeSplitGroupings[codeSplitGroupingByKey],\n ),\n ]\n\n // find key in nodeSplitConfig\n const isNodeConfigAvailable = SPLIT_NODES_CONFIG.has(\n key as any,\n )\n\n if (!isNodeConfigAvailable) {\n return\n }\n\n // Exit early if the value is a boolean, null, or undefined.\n // These values mean \"don't use this component, fallback to parent\"\n // No code splitting needed to preserve fallback behavior\n if (\n t.isBooleanLiteral(prop.value) ||\n t.isNullLiteral(prop.value) ||\n (t.isIdentifier(prop.value) &&\n prop.value.name === 'undefined')\n ) {\n return\n }\n\n const splitNodeMeta = SPLIT_NODES_CONFIG.get(key as any)!\n\n // We need to extract the existing search params from the filename, if any\n // and add the relevant codesplitPrefix to them, then write them back to the filename\n const splitUrl = addSplitSearchParamToFilename(\n opts.filename,\n codeSplitGroup,\n )\n\n if (\n splitNodeMeta.splitStrategy === 'lazyRouteComponent'\n ) {\n const value = prop.value\n\n let shouldSplit = true\n\n if (t.isIdentifier(value)) {\n const existingImportPath =\n getImportSpecifierAndPathFromLocalName(\n programPath,\n value.name,\n ).path\n if (existingImportPath) {\n removableImportPaths.add(existingImportPath)\n }\n\n // exported identifiers should not be split\n // since they are already being imported\n // and need to be retained in the compiled file\n const isExported = hasExport(ast, value)\n if (isExported) {\n knownExportedIdents.add(value.name)\n }\n shouldSplit = !isExported\n\n if (shouldSplit) {\n removeIdentifierLiteral(path, value)\n }\n }\n\n if (!shouldSplit) {\n return\n }\n\n modified = true\n\n // Prepend the import statement to the program along with the importer function\n // Check to see if lazyRouteComponent is already imported before attempting\n // to import it again\n if (\n !hasImportedOrDefinedIdentifier(\n LAZY_ROUTE_COMPONENT_IDENT,\n )\n ) {\n programPath.unshiftContainer('body', [\n template.statement(\n `import { ${LAZY_ROUTE_COMPONENT_IDENT} } from '${PACKAGE}'`,\n )(),\n ])\n }\n\n // Check to see if the importer function is already defined\n // If not, define it with the dynamic import statement\n if (\n !hasImportedOrDefinedIdentifier(\n splitNodeMeta.localImporterIdent,\n )\n ) {\n programPath.unshiftContainer('body', [\n template.statement(\n `const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`,\n )(),\n ])\n }\n\n prop.value = template.expression(\n `${LAZY_ROUTE_COMPONENT_IDENT}(${splitNodeMeta.localImporterIdent}, '${splitNodeMeta.exporterIdent}')`,\n )()\n\n // add HMR handling\n if (opts.addHmr && !hmrAdded) {\n programPath.pushContainer('body', routeHmrStatement)\n modified = true\n hmrAdded = true\n }\n } else {\n // if (splitNodeMeta.splitStrategy === 'lazyFn') {\n const value = prop.value\n\n let shouldSplit = true\n\n if (t.isIdentifier(value)) {\n const existingImportPath =\n getImportSpecifierAndPathFromLocalName(\n programPath,\n value.name,\n ).path\n if (existingImportPath) {\n removableImportPaths.add(existingImportPath)\n }\n\n // exported identifiers should not be split\n // since they are already being imported\n // and need to be retained in the compiled file\n const isExported = hasExport(ast, value)\n if (isExported) {\n knownExportedIdents.add(value.name)\n }\n shouldSplit = !isExported\n\n if (shouldSplit) {\n removeIdentifierLiteral(path, value)\n }\n }\n\n if (!shouldSplit) {\n return\n }\n modified = true\n\n // Prepend the import statement to the program along with the importer function\n if (!hasImportedOrDefinedIdentifier(LAZY_FN_IDENT)) {\n programPath.unshiftContainer(\n 'body',\n template.smart(\n `import { ${LAZY_FN_IDENT} } from '${PACKAGE}'`,\n )(),\n )\n }\n\n // Check to see if the importer function is already defined\n // If not, define it with the dynamic import statement\n if (\n !hasImportedOrDefinedIdentifier(\n splitNodeMeta.localImporterIdent,\n )\n ) {\n programPath.unshiftContainer('body', [\n template.statement(\n `const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`,\n )(),\n ])\n }\n\n // Add the lazyFn call with the dynamic import to the prop value\n prop.value = template.expression(\n `${LAZY_FN_IDENT}(${splitNodeMeta.localImporterIdent}, '${splitNodeMeta.exporterIdent}')`,\n )()\n }\n }\n }\n\n programPath.scope.crawl()\n })\n }\n }\n\n if (t.isCallExpression(path.parentPath.node)) {\n // createFileRoute('/')({ ... })\n const options = resolveIdentifier(\n path,\n path.parentPath.node.arguments[0],\n )\n\n babelHandleReference(options)\n } else if (t.isVariableDeclarator(path.parentPath.node)) {\n // createFileRoute({ ... })\n const caller = resolveIdentifier(path, path.parentPath.node.init)\n\n if (t.isCallExpression(caller)) {\n const options = resolveIdentifier(path, caller.arguments[0])\n babelHandleReference(options)\n }\n }\n },\n })\n\n /**\n * If the component for the route is being imported,\n * and it's not being used, remove the import statement\n * from the program, by checking that the import has no\n * specifiers\n */\n if (removableImportPaths.size > 0) {\n modified = true\n programPath.traverse({\n ImportDeclaration(path) {\n if (path.node.specifiers.length > 0) return\n if (removableImportPaths.has(path.node.source.value)) {\n path.remove()\n }\n },\n })\n }\n\n // Handle shared bindings inside the Program visitor so we have\n // access to programPath for cheap refIdents registration.\n if (opts.sharedBindings && opts.sharedBindings.size > 0) {\n sharedExportedNames = findExportedSharedBindings(\n ast,\n opts.sharedBindings,\n )\n removeSharedDeclarations(ast, opts.sharedBindings)\n\n const sharedModuleUrl = addSharedSearchParamToFilename(opts.filename)\n\n const sharedImportSpecifiers = [...opts.sharedBindings].map((name) =>\n t.importSpecifier(t.identifier(name), t.identifier(name)),\n )\n const [sharedImportPath] = programPath.unshiftContainer(\n 'body',\n t.importDeclaration(\n sharedImportSpecifiers,\n t.stringLiteral(sharedModuleUrl),\n ),\n )\n\n // Register import specifier locals in refIdents so DCE can remove unused ones\n sharedImportPath.traverse({\n Identifier(identPath) {\n if (\n identPath.parentPath.isImportSpecifier() &&\n identPath.key === 'local'\n ) {\n refIdents.add(identPath)\n }\n },\n })\n\n // Re-export user-exported shared bindings from the shared module\n if (sharedExportedNames.size > 0) {\n const reExportSpecifiers = [...sharedExportedNames].map((name) =>\n t.exportSpecifier(t.identifier(name), t.identifier(name)),\n )\n programPath.pushContainer(\n 'body',\n t.exportNamedDeclaration(\n null,\n reExportSpecifiers,\n t.stringLiteral(sharedModuleUrl),\n ),\n )\n }\n }\n },\n },\n })\n\n if (!modified) {\n return null\n }\n\n deadCodeElimination(ast, refIdents)\n\n // if there are exported identifiers, then we need to add a warning\n // to the file to let the user know that the exported identifiers\n // will not in the split file but in the original file, therefore\n // increasing the bundle size\n if (knownExportedIdents.size > 0) {\n const warningMessage = createNotExportableMessage(\n opts.filename,\n knownExportedIdents,\n )\n console.warn(warningMessage)\n\n // append this warning to the file using a template\n if (process.env.NODE_ENV !== 'production') {\n const warningTemplate = template.statement(\n `console.warn(${JSON.stringify(warningMessage)})`,\n )()\n ast.program.body.unshift(warningTemplate)\n }\n }\n\n const result = generateFromAst(ast, {\n sourceMaps: true,\n sourceFileName: opts.filename,\n filename: opts.filename,\n })\n\n // @babel/generator does not populate sourcesContent because it only has\n // the AST, not the original text. Without this, Vite's composed\n // sourcemap omits the original source, causing downstream consumers\n // (e.g. import-protection snippet display) to fall back to the shorter\n // compiled output and fail to resolve original line numbers.\n if (result.map) {\n result.map.sourcesContent = [opts.code]\n }\n\n return result\n}\n\nexport function compileCodeSplitVirtualRoute(\n opts: ParseAstOptions & {\n splitTargets: Array<SplitRouteIdentNodes>\n filename: string\n sharedBindings?: Set<string>\n },\n): GeneratorResult {\n const ast = parseAst(opts)\n const refIdents = findReferencedIdentifiers(ast)\n\n // Remove shared declarations BEFORE babel.traverse so the scope never sees\n // conflicting bindings (avoids checkBlockScopedCollisions crash in DCE)\n if (opts.sharedBindings && opts.sharedBindings.size > 0) {\n removeSharedDeclarations(ast, opts.sharedBindings)\n }\n\n const intendedSplitNodes = new Set(opts.splitTargets)\n\n const knownExportedIdents = new Set<string>()\n\n babel.traverse(ast, {\n Program: {\n enter(programPath) {\n const trackedNodesToSplitByType: Record<\n SplitRouteIdentNodes,\n { node: t.Node | undefined; meta: SplitNodeMeta } | undefined\n > = {\n component: undefined,\n loader: undefined,\n pendingComponent: undefined,\n errorComponent: undefined,\n notFoundComponent: undefined,\n }\n\n // Find and track all the known split-able nodes\n programPath.traverse({\n CallExpression: (path) => {\n if (!t.isIdentifier(path.node.callee)) {\n return\n }\n\n if (!splittableCreateRouteFns.includes(path.node.callee.name)) {\n return\n }\n\n function babelHandleVirtual(options: t.Node | undefined) {\n if (t.isObjectExpression(options)) {\n options.properties.forEach((prop) => {\n if (t.isObjectProperty(prop)) {\n // do not use `intendedSplitNodes` here\n // since we have special considerations that need\n // to be accounted for like (not splitting exported identifiers)\n KNOWN_SPLIT_ROUTE_IDENTS.forEach((splitType) => {\n if (\n !t.isIdentifier(prop.key) ||\n prop.key.name !== splitType\n ) {\n return\n }\n\n const value = prop.value\n\n // If the value for the `key` is `undefined`, then we don't need to include it\n // in the split file, so we can just return, since it will kept in-place in the\n // reference file\n // This is useful for cases like: `createFileRoute('/')({ component: undefined })`\n if (t.isIdentifier(value) && value.name === 'undefined') {\n return\n }\n\n let isExported = false\n if (t.isIdentifier(value)) {\n isExported = hasExport(ast, value)\n if (isExported) {\n knownExportedIdents.add(value.name)\n }\n }\n\n // If the node is exported, we need to remove\n // the export from the split file\n if (isExported && t.isIdentifier(value)) {\n removeExports(ast, value)\n } else {\n const meta = SPLIT_NODES_CONFIG.get(splitType)!\n trackedNodesToSplitByType[splitType] = {\n node: prop.value,\n meta,\n }\n }\n })\n }\n })\n\n // Remove all of the options\n options.properties = []\n }\n }\n\n if (t.isCallExpression(path.parentPath.node)) {\n // createFileRoute('/')({ ... })\n const options = resolveIdentifier(\n path,\n path.parentPath.node.arguments[0],\n )\n\n babelHandleVirtual(options)\n } else if (t.isVariableDeclarator(path.parentPath.node)) {\n // createFileRoute({ ... })\n const caller = resolveIdentifier(path, path.parentPath.node.init)\n\n if (t.isCallExpression(caller)) {\n const options = resolveIdentifier(path, caller.arguments[0])\n babelHandleVirtual(options)\n }\n }\n },\n })\n\n // Start the transformation to only exported the intended split nodes\n intendedSplitNodes.forEach((SPLIT_TYPE) => {\n const splitKey = trackedNodesToSplitByType[SPLIT_TYPE]\n\n if (!splitKey) {\n return\n }\n\n let splitNode = splitKey.node\n const splitMeta = { ...splitKey.meta, shouldRemoveNode: true }\n\n // Track the original identifier name before resolving through bindings,\n // needed for destructured patterns where the binding resolves to the\n // entire VariableDeclarator (ObjectPattern) rather than the specific binding\n let originalIdentName: string | undefined\n if (t.isIdentifier(splitNode)) {\n originalIdentName = splitNode.name\n }\n\n while (t.isIdentifier(splitNode)) {\n const binding = programPath.scope.getBinding(splitNode.name)\n splitNode = binding?.path.node\n }\n\n // Add the node to the program\n if (splitNode) {\n if (t.isFunctionDeclaration(splitNode)) {\n // an anonymous function declaration should only happen for `export default function() {...}`\n // so we should never get here\n if (!splitNode.id) {\n throw new Error(\n `Function declaration for \"${SPLIT_TYPE}\" must have an identifier.`,\n )\n }\n splitMeta.shouldRemoveNode = false\n splitMeta.localExporterIdent = splitNode.id.name\n } else if (\n t.isFunctionExpression(splitNode) ||\n t.isArrowFunctionExpression(splitNode)\n ) {\n programPath.pushContainer(\n 'body',\n t.variableDeclaration('const', [\n t.variableDeclarator(\n t.identifier(splitMeta.localExporterIdent),\n splitNode as any,\n ),\n ]),\n )\n } else if (\n t.isImportSpecifier(splitNode) ||\n t.isImportDefaultSpecifier(splitNode)\n ) {\n programPath.pushContainer(\n 'body',\n t.variableDeclaration('const', [\n t.variableDeclarator(\n t.identifier(splitMeta.localExporterIdent),\n splitNode.local,\n ),\n ]),\n )\n } else if (t.isVariableDeclarator(splitNode)) {\n if (t.isIdentifier(splitNode.id)) {\n splitMeta.localExporterIdent = splitNode.id.name\n splitMeta.shouldRemoveNode = false\n } else if (t.isObjectPattern(splitNode.id)) {\n // Destructured binding like `const { component: MyComp } = createBits()`\n // Use the original identifier name that was tracked before resolving\n if (originalIdentName) {\n splitMeta.localExporterIdent = originalIdentName\n }\n splitMeta.shouldRemoveNode = false\n } else {\n throw new Error(\n `Unexpected splitNode type ☝️: ${splitNode.type}`,\n )\n }\n } else if (t.isCallExpression(splitNode)) {\n const outputSplitNodeCode = generateFromAst(splitNode).code\n const splitNodeAst = babel.parse(outputSplitNodeCode)\n\n if (!splitNodeAst) {\n throw new Error(\n `Failed to parse the generated code for \"${SPLIT_TYPE}\" in the node type \"${splitNode.type}\"`,\n )\n }\n\n const statement = splitNodeAst.program.body[0]\n\n if (!statement) {\n throw new Error(\n `Failed to parse the generated code for \"${SPLIT_TYPE}\" in the node type \"${splitNode.type}\" as no statement was found in the program body`,\n )\n }\n\n if (t.isExpressionStatement(statement)) {\n const expression = statement.expression\n programPath.pushContainer(\n 'body',\n t.variableDeclaration('const', [\n t.variableDeclarator(\n t.identifier(splitMeta.localExporterIdent),\n expression,\n ),\n ]),\n )\n } else {\n throw new Error(\n `Unexpected expression type encounter for \"${SPLIT_TYPE}\" in the node type \"${splitNode.type}\"`,\n )\n }\n } else if (t.isConditionalExpression(splitNode)) {\n programPath.pushContainer(\n 'body',\n t.variableDeclaration('const', [\n t.variableDeclarator(\n t.identifier(splitMeta.localExporterIdent),\n splitNode,\n ),\n ]),\n )\n } else if (t.isTSAsExpression(splitNode)) {\n // remove the type assertion\n splitNode = splitNode.expression\n programPath.pushContainer(\n 'body',\n t.variableDeclaration('const', [\n t.variableDeclarator(\n t.identifier(splitMeta.localExporterIdent),\n splitNode,\n ),\n ]),\n )\n } else if (t.isBooleanLiteral(splitNode)) {\n // Handle boolean literals\n // This exits early here, since this value will be kept in the reference file\n return\n } else if (t.isNullLiteral(splitNode)) {\n // Handle null literals\n // This exits early here, since this value will be kept in the reference file\n return\n } else {\n console.info('Unexpected splitNode type:', splitNode)\n throw new Error(`Unexpected splitNode type ☝️: ${splitNode.type}`)\n }\n }\n\n if (splitMeta.shouldRemoveNode) {\n // If the splitNode exists at the top of the program\n // then we need to remove that copy\n programPath.node.body = programPath.node.body.filter((node) => {\n return node !== splitNode\n })\n }\n\n // Export the node\n programPath.pushContainer('body', [\n t.export