Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 5 additions & 22 deletions packages/router-generator/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div>Hello "%%tsrPath%%"!</div> };\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('-'),
Expand Down Expand Up @@ -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).
Expand Down
126 changes: 69 additions & 57 deletions packages/router-generator/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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('')

Expand Down Expand Up @@ -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 (<React.Fragment><div>Hello "%%tsrPath%%"!</div><Outlet /></React.Fragment>) };\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(
Expand Down Expand Up @@ -204,29 +201,43 @@ 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 &&
!node.isErrorComponent &&
!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
Expand All @@ -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}`,
)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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',
[
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -1022,12 +1043,3 @@ export function startAPIRouteSegmentsFromTSRFilePath(

return segments
}

type TemplateTag = 'tsrImports' | 'tsrPath' | 'tsrExportStart' | 'tsrExportEnd'

function fillTemplate(template: string, values: Record<TemplateTag, string>) {
return template.replace(
/%%(\w+)%%/g,
(_, key) => values[key as TemplateTag] || '',
)
}
111 changes: 111 additions & 0 deletions packages/router-generator/src/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { Config } from './config'

type TemplateTag = 'tsrImports' | 'tsrPath' | 'tsrExportStart' | 'tsrExportEnd'

export function fillTemplate(
template: string,
values: Record<TemplateTag, string>,
) {
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 (<React.Fragment><div>Hello "%%tsrPath%%"!</div><Outlet /></React.Fragment>) };\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 <div>Hello "%%tsrPath%%"!</div> };\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 <div>Hello "%%tsrPath%%"!</div> };\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('')
6 changes: 6 additions & 0 deletions packages/router-generator/tests/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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')({
Expand Down
Original file line number Diff line number Diff line change
@@ -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')({
Expand Down