diff --git a/packages/router-generator/src/config.ts b/packages/router-generator/src/config.ts index e7ba47217b1..2252f794a9c 100644 --- a/packages/router-generator/src/config.ts +++ b/packages/router-generator/src/config.ts @@ -3,22 +3,8 @@ import { existsSync, readFileSync } from 'node:fs' import { z } from 'zod' import { virtualRootRouteSchema } from './filesystem/virtual/config' -const defaultTemplate = { - routeTemplate: [ - '%%tsrImports%%', - '\n\n', - '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n', - 'function RouteComponent() { return
Hello "%%tsrPath%%"!
};\n', - ].join(''), - apiTemplate: [ - 'import { json } from "@tanstack/start";\n', - '%%tsrImports%%', - '\n\n', - '%%tsrExportStart%%{ GET: ({ request, params }) => { return json({ message:\'Hello "%%tsrPath%%"!\' }) }}%%tsrExportEnd%%\n', - ].join(''), -} - export const configSchema = z.object({ + target: z.enum(['react']).optional().default('react'), virtualRouteConfig: virtualRootRouteSchema.or(z.string()).optional(), routeFilePrefix: z.string().optional(), routeFileIgnorePrefix: z.string().optional().default('-'), @@ -49,14 +35,11 @@ export const configSchema = z.object({ .optional(), customScaffolding: z .object({ - routeTemplate: z - .string() - .optional() - .default(defaultTemplate.routeTemplate), - apiTemplate: z.string().optional().default(defaultTemplate.apiTemplate), + routeTemplate: z.string().optional(), + lazyRouteTemplate: z.string().optional(), + apiTemplate: z.string().optional(), }) - .optional() - .default(defaultTemplate), + .optional(), experimental: z .object({ // TODO: Remove this option in the next major release (v2). diff --git a/packages/router-generator/src/generator.ts b/packages/router-generator/src/generator.ts index b0de40905e1..7dd7d62d4f5 100644 --- a/packages/router-generator/src/generator.ts +++ b/packages/router-generator/src/generator.ts @@ -18,6 +18,11 @@ import { import { getRouteNodes as physicalGetRouteNodes } from './filesystem/physical/getRouteNodes' import { getRouteNodes as virtualGetRouteNodes } from './filesystem/virtual/getRouteNodes' import { rootPathId } from './filesystem/physical/rootPathId' +import { + defaultAPIRouteTemplate, + fillTemplate, + getTargetTemplate, +} from './template' import type { GetRouteNodesResult, RouteNode } from './types' import type { Config } from './config' @@ -42,6 +47,7 @@ type RouteSubNode = { } export async function generator(config: Config, root: string) { + const ROUTE_TEMPLATE = getTargetTemplate(config.target) const logger = logging({ disabled: config.disableLogging }) logger.log('') @@ -131,22 +137,13 @@ export async function generator(config: Config, root: string) { const routeCode = fs.readFileSync(node.fullPath, 'utf-8') if (!routeCode) { - const replaced = fillTemplate( - [ - 'import * as React from "react"\n', - '%%tsrImports%%', - '\n\n', - '%%tsrExportStart%%{\n component: RootComponent\n }%%tsrExportEnd%%\n\n', - 'function RootComponent() { return (
Hello "%%tsrPath%%"!
) };\n', - ].join(''), - { - tsrImports: - "import { Outlet, createRootRoute } from '@tanstack/react-router';", - tsrPath: rootPathId, - tsrExportStart: `export const Route = createRootRoute(`, - tsrExportEnd: ');', - }, - ) + const _rootTemplate = ROUTE_TEMPLATE.rootRoute + const replaced = fillTemplate(_rootTemplate.template(), { + tsrImports: _rootTemplate.imports.tsrImports(), + tsrPath: rootPathId, + tsrExportStart: _rootTemplate.imports.tsrExportStart(), + tsrExportEnd: _rootTemplate.imports.tsrExportEnd(), + }) logger.log(`🟡 Creating ${node.fullPath}`) fs.writeFileSync( @@ -204,15 +201,25 @@ export async function generator(config: Config, root: string) { let replaced = routeCode + const tRouteTemplate = ROUTE_TEMPLATE.route + const tLazyRouteTemplate = ROUTE_TEMPLATE.lazyRoute + if (!routeCode) { if (node.isLazy) { - replaced = fillTemplate(config.customScaffolding.routeTemplate, { - tsrImports: - "import { createLazyFileRoute } from '@tanstack/react-router';", - tsrPath: escapedRoutePath, - tsrExportStart: `export const Route = createLazyFileRoute('${escapedRoutePath}')(`, - tsrExportEnd: ');', - }) + // Check by default check if the user has a specific lazy route template + // If not, check if the user has a route template and use that instead + replaced = fillTemplate( + (config.customScaffolding?.lazyRouteTemplate || + config.customScaffolding?.routeTemplate) ?? + tLazyRouteTemplate.template(), + { + tsrImports: tLazyRouteTemplate.imports.tsrImports(), + tsrPath: escapedRoutePath, + tsrExportStart: + tLazyRouteTemplate.imports.tsrExportStart(escapedRoutePath), + tsrExportEnd: tLazyRouteTemplate.imports.tsrExportEnd(), + }, + ) } else if ( node.isRoute || (!node.isComponent && @@ -220,13 +227,17 @@ export async function generator(config: Config, root: string) { !node.isPendingComponent && !node.isLoader) ) { - replaced = fillTemplate(config.customScaffolding.routeTemplate, { - tsrImports: - "import { createFileRoute } from '@tanstack/react-router';", - tsrPath: escapedRoutePath, - tsrExportStart: `export const Route = createFileRoute('${escapedRoutePath}')(`, - tsrExportEnd: ');', - }) + replaced = fillTemplate( + config.customScaffolding?.routeTemplate ?? + tRouteTemplate.template(), + { + tsrImports: tRouteTemplate.imports.tsrImports(), + tsrPath: escapedRoutePath, + tsrExportStart: + tRouteTemplate.imports.tsrExportStart(escapedRoutePath), + tsrExportEnd: tRouteTemplate.imports.tsrExportEnd(), + }, + ) } } else { replaced = routeCode @@ -235,7 +246,10 @@ export async function generator(config: Config, root: string) { (_, p1, __, p3) => `${p1}${escapedRoutePath}${p3}`, ) .replace( - /(import\s*\{.*)(create(Lazy)?FileRoute)(.*\}\s*from\s*['"]@tanstack\/react-router['"])/gs, + new RegExp( + `(import\\s*\\{.*)(create(Lazy)?FileRoute)(.*\\}\\s*from\\s*['"]@tanstack\\/${ROUTE_TEMPLATE.subPkg}['"])`, + 'gs', + ), (_, p1, __, ___, p4) => `${p1}${node.isLazy ? 'createLazyFileRoute' : 'createFileRoute'}${p4}`, ) @@ -376,12 +390,16 @@ export async function generator(config: Config, root: string) { const escapedRoutePath = node.routePath?.replaceAll('$', '$$') ?? '' if (!routeCode) { - const replaced = fillTemplate(config.customScaffolding.apiTemplate, { - tsrImports: "import { createAPIFileRoute } from '@tanstack/start/api';", - tsrPath: escapedRoutePath, - tsrExportStart: `export const ${CONSTANTS.APIRouteExportVariable} = createAPIFileRoute('${escapedRoutePath}')(`, - tsrExportEnd: ');', - }) + const replaced = fillTemplate( + config.customScaffolding?.apiTemplate ?? defaultAPIRouteTemplate, + { + tsrImports: + "import { createAPIFileRoute } from '@tanstack/start/api';", + tsrPath: escapedRoutePath, + tsrExportStart: `export const ${CONSTANTS.APIRouteExportVariable} = createAPIFileRoute('${escapedRoutePath}')(`, + tsrExportEnd: ');', + }, + ) logger.log(`🟡 Creating ${node.fullPath}`) fs.writeFileSync( @@ -494,7 +512,7 @@ export async function generator(config: Config, root: string) { // 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.`, imports.length - ? `import { ${imports.join(', ')} } from '@tanstack/react-router'\n` + ? `import { ${imports.join(', ')} } from '${ROUTE_TEMPLATE.fullPkg}'\n` : '', '// Import Routes', [ @@ -594,7 +612,7 @@ export async function generator(config: Config, root: string) { ? [] : [ '// Populate the FileRoutesByPath interface', - `declare module '@tanstack/react-router' { + `declare module '${ROUTE_TEMPLATE.fullPkg}' { interface FileRoutesByPath { ${routeNodes .map((routeNode) => { @@ -693,15 +711,18 @@ export async function generator(config: Config, root: string) { ) } - const routeConfigFileContent = config.disableManifestGeneration - ? routeImports - : [ - routeImports, - '\n', - '/* ROUTE_MANIFEST_START', - createRouteManifest(), - 'ROUTE_MANIFEST_END */', - ].join('\n') + const routeConfigFileContent = + // TODO: Remove this disabled eslint rule when more target types are added. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + config.disableManifestGeneration || config.target !== 'react' + ? routeImports + : [ + routeImports, + '\n', + '/* ROUTE_MANIFEST_START', + createRouteManifest(), + 'ROUTE_MANIFEST_END */', + ].join('\n') if (!checkLatest()) return @@ -1022,12 +1043,3 @@ export function startAPIRouteSegmentsFromTSRFilePath( return segments } - -type TemplateTag = 'tsrImports' | 'tsrPath' | 'tsrExportStart' | 'tsrExportEnd' - -function fillTemplate(template: string, values: Record) { - return template.replace( - /%%(\w+)%%/g, - (_, key) => values[key as TemplateTag] || '', - ) -} diff --git a/packages/router-generator/src/template.ts b/packages/router-generator/src/template.ts new file mode 100644 index 00000000000..2870f2492e4 --- /dev/null +++ b/packages/router-generator/src/template.ts @@ -0,0 +1,111 @@ +import type { Config } from './config' + +type TemplateTag = 'tsrImports' | 'tsrPath' | 'tsrExportStart' | 'tsrExportEnd' + +export function fillTemplate( + template: string, + values: Record, +) { + return template.replace( + /%%(\w+)%%/g, + (_, key) => values[key as TemplateTag] || '', + ) +} + +type TargetTemplate = { + fullPkg: string + subPkg: string + rootRoute: { + template: () => string + imports: { + tsrImports: () => string + tsrExportStart: () => string + tsrExportEnd: () => string + } + } + route: { + template: () => string + imports: { + tsrImports: () => string + tsrExportStart: (routePath: string) => string + tsrExportEnd: () => string + } + } + lazyRoute: { + template: () => string + imports: { + tsrImports: () => string + tsrExportStart: (routePath: string) => string + tsrExportEnd: () => string + } + } +} + +export function getTargetTemplate(target: Config['target']): TargetTemplate { + switch (target) { + // TODO: Remove this disabled eslint rule when more target types are added. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + case 'react': + return { + fullPkg: '@tanstack/react-router', + subPkg: 'react-router', + rootRoute: { + template: () => + [ + 'import * as React from "react"\n', + '%%tsrImports%%', + '\n\n', + '%%tsrExportStart%%{\n component: RootComponent\n }%%tsrExportEnd%%\n\n', + 'function RootComponent() { return (
Hello "%%tsrPath%%"!
) };\n', + ].join(''), + imports: { + tsrImports: () => + "import { Outlet, createRootRoute } from '@tanstack/react-router';", + tsrExportStart: () => 'export const Route = createRootRoute(', + tsrExportEnd: () => ');', + }, + }, + route: { + template: () => + [ + '%%tsrImports%%', + '\n\n', + '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n', + 'function RouteComponent() { return
Hello "%%tsrPath%%"!
};\n', + ].join(''), + imports: { + tsrImports: () => + "import { createFileRoute } from '@tanstack/react-router';", + tsrExportStart: (routePath) => + `export const Route = createFileRoute('${routePath}')(`, + tsrExportEnd: () => ');', + }, + }, + lazyRoute: { + template: () => + [ + '%%tsrImports%%', + '\n\n', + '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n', + 'function RouteComponent() { return
Hello "%%tsrPath%%"!
};\n', + ].join(''), + imports: { + tsrImports: () => + "import { createLazyFileRoute } from '@tanstack/react-router';", + tsrExportStart: (routePath) => + `export const Route = createLazyFileRoute('${routePath}')(`, + tsrExportEnd: () => ');', + }, + }, + } + default: + throw new Error(`router-generator: Unknown target type: ${target}`) + } +} + +export const defaultAPIRouteTemplate = [ + 'import { json } from "@tanstack/start";\n', + '%%tsrImports%%', + '\n\n', + '%%tsrExportStart%%{ GET: ({ request, params }) => { return json({ message:\'Hello "%%tsrPath%%"!\' }) }}%%tsrExportEnd%%\n', +].join('') diff --git a/packages/router-generator/tests/generator.test.ts b/packages/router-generator/tests/generator.test.ts index 71061696f9c..86c70f9e5d7 100644 --- a/packages/router-generator/tests/generator.test.ts +++ b/packages/router-generator/tests/generator.test.ts @@ -118,6 +118,12 @@ function rewriteConfigByFolderName(folderName: string, config: Config) { '%%tsrImports%%\n\n', '%%tsrExportStart%%{ GET: ({ request, params }) => { return json({ message: "Hello /api/test" }) }}%%tsrExportEnd%%\n', ].join(''), + lazyRouteTemplate: [ + 'import React, { useState } from "react";\n', + '%%tsrImports%%\n\n', + '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n', + 'function RouteComponent() { return "Hello %%tsrPath%%!" };\n', + ].join(''), } break default: diff --git a/packages/router-generator/tests/generator/custom-scaffolding/routes/foo.lazy.tsx b/packages/router-generator/tests/generator/custom-scaffolding/routes/foo.lazy.tsx index 515193584eb..1d5655f8935 100644 --- a/packages/router-generator/tests/generator/custom-scaffolding/routes/foo.lazy.tsx +++ b/packages/router-generator/tests/generator/custom-scaffolding/routes/foo.lazy.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React, { useState } from 'react' import { createLazyFileRoute } from '@tanstack/react-router' export const Route = createLazyFileRoute('/foo')({ diff --git a/packages/router-generator/tests/generator/custom-scaffolding/snapshot/foo.lazy.tsx b/packages/router-generator/tests/generator/custom-scaffolding/snapshot/foo.lazy.tsx index 515193584eb..1d5655f8935 100644 --- a/packages/router-generator/tests/generator/custom-scaffolding/snapshot/foo.lazy.tsx +++ b/packages/router-generator/tests/generator/custom-scaffolding/snapshot/foo.lazy.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React, { useState } from 'react' import { createLazyFileRoute } from '@tanstack/react-router' export const Route = createLazyFileRoute('/foo')({