@tanstack/router-generator
Advanced tools
Comparing version 1.49.3 to 1.51.0
@@ -15,2 +15,3 @@ import { z } from 'zod'; | ||
disableManifestGeneration: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>; | ||
apiBase: z.ZodDefault<z.ZodOptional<z.ZodString>>; | ||
routeTreeFileHeader: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>; | ||
@@ -36,2 +37,3 @@ routeTreeFileFooter: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>; | ||
disableManifestGeneration: boolean; | ||
apiBase: string; | ||
routeTreeFileHeader: string[]; | ||
@@ -57,2 +59,3 @@ routeTreeFileFooter: string[]; | ||
disableManifestGeneration?: boolean | undefined; | ||
apiBase?: string | undefined; | ||
routeTreeFileHeader?: string[] | undefined; | ||
@@ -59,0 +62,0 @@ routeTreeFileFooter?: string[] | undefined; |
@@ -16,2 +16,3 @@ import path from "node:path"; | ||
disableManifestGeneration: z.boolean().optional().default(false), | ||
apiBase: z.string().optional().default("/api"), | ||
routeTreeFileHeader: z.array(z.string()).optional().default([ | ||
@@ -18,0 +19,0 @@ "/* prettier-ignore-start */", |
@@ -17,2 +17,3 @@ import { Config } from './config.js'; | ||
isRoute?: boolean; | ||
isAPIRoute?: boolean; | ||
isLoader?: boolean; | ||
@@ -49,1 +50,14 @@ isComponent?: boolean; | ||
export declare const inferPath: (routeNode: RouteNode) => string; | ||
export type StartAPIRoutePathSegment = { | ||
value: string; | ||
type: 'path' | 'param' | 'splat'; | ||
}; | ||
/** | ||
* This function takes in a path in the format accepted by TanStack Router | ||
* and returns an array of path segments that can be used to generate | ||
* the pathname of the TanStack Start API route. | ||
* | ||
* @param src | ||
* @returns | ||
*/ | ||
export declare function startAPIRouteSegmentsFromTSRFilePath(src: string): Array<StartAPIRoutePathSegment>; |
@@ -41,3 +41,3 @@ import path from "node:path"; | ||
const filePathNoExt = removeExt(filePath); | ||
let routePath = cleanPath(`/${filePathNoExt.split(".").join("/")}`) || ""; | ||
let routePath = determineInitialRoutePath(filePathNoExt); | ||
if (routeFilePrefix) { | ||
@@ -61,2 +61,5 @@ routePath = routePath.replaceAll(routeFilePrefix, ""); | ||
const isLoader = routePath.endsWith("/loader"); | ||
const isAPIRoute = routePath.startsWith( | ||
`${removeTrailingSlash(config.apiBase)}/` | ||
); | ||
const segments = routePath.split("/"); | ||
@@ -95,3 +98,4 @@ const isLayout = ((_a = segments[segments.length - 1]) == null ? void 0 : _a.startsWith("_")) || false; | ||
isLazy, | ||
isLayout | ||
isLayout, | ||
isAPIRoute | ||
}); | ||
@@ -311,6 +315,48 @@ } | ||
}; | ||
for (const node of preRouteNodes) { | ||
for (const node of preRouteNodes.filter((d) => !d.isAPIRoute)) { | ||
await handleNode(node); | ||
} | ||
function buildRouteConfig(nodes, depth = 1) { | ||
const startAPIRouteNodes = checkStartAPIRoutes( | ||
preRouteNodes.filter((d) => d.isAPIRoute) | ||
); | ||
const handleAPINode = async (node) => { | ||
var _a; | ||
const routeCode = fs.readFileSync(node.fullPath, "utf-8"); | ||
const escapedRoutePath = removeTrailingUnderscores( | ||
((_a = node.routePath) == null ? void 0 : _a.replaceAll("$", "$$")) ?? "" | ||
); | ||
if (!routeCode) { | ||
const replaced = `import { json } from '@tanstack/start' | ||
import { createAPIFileRoute } from '@tanstack/start/api' | ||
export const Route = createAPIFileRoute('${escapedRoutePath}')({ | ||
GET: ({ request, params }) => { | ||
return json({ message: 'Hello ${escapedRoutePath}' }) | ||
}, | ||
}) | ||
`; | ||
logger.log(`🟡 Creating ${node.fullPath}`); | ||
fs.writeFileSync( | ||
node.fullPath, | ||
await prettier.format(replaced, prettierOptions) | ||
); | ||
} else { | ||
const copied = routeCode.replace( | ||
/(createAPIFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g, | ||
(_, p1, __, p3) => `${p1}${escapedRoutePath}${p3}` | ||
); | ||
if (copied !== routeCode) { | ||
logger.log(`🟡 Updating ${node.fullPath}`); | ||
await fsp.writeFile( | ||
node.fullPath, | ||
await prettier.format(copied, prettierOptions) | ||
); | ||
} | ||
} | ||
}; | ||
for (const node of startAPIRouteNodes) { | ||
await handleAPINode(node); | ||
} | ||
function buildRouteTreeConfig(nodes, depth = 1) { | ||
const children = nodes.map((node) => { | ||
@@ -326,3 +372,3 @@ var _a, _b; | ||
if ((_b = node.children) == null ? void 0 : _b.length) { | ||
const childConfigs = buildRouteConfig(node.children, depth + 1); | ||
const childConfigs = buildRouteTreeConfig(node.children, depth + 1); | ||
return `${route}: ${route}.addChildren({${spaces(depth * 4)}${childConfigs}})`; | ||
@@ -334,3 +380,3 @@ } | ||
} | ||
const routeConfigChildrenText = buildRouteConfig(routeTree); | ||
const routeConfigChildrenText = buildRouteTreeConfig(routeTree); | ||
const sortedRouteNodes = multiSortBy(routeNodes, [ | ||
@@ -481,34 +527,35 @@ (d) => { | ||
].filter(Boolean).join("\n\n"); | ||
const createRouteManifest = () => JSON.stringify( | ||
{ | ||
routes: { | ||
__root__: { | ||
filePath: rootRouteNode == null ? void 0 : rootRouteNode.filePath, | ||
children: routeTree.map( | ||
(d) => getFilePathIdAndRouteIdFromPath(d.routePath)[1] | ||
) | ||
}, | ||
...Object.fromEntries( | ||
routeNodes.map((d) => { | ||
var _a, _b; | ||
const [filePathId, routeId] = getFilePathIdAndRouteIdFromPath( | ||
d.routePath | ||
); | ||
return [ | ||
routeId, | ||
{ | ||
filePath: d.filePath, | ||
parent: ((_a = d.parent) == null ? void 0 : _a.routePath) ? getFilePathIdAndRouteIdFromPath(d.parent.routePath)[1] : void 0, | ||
children: (_b = d.children) == null ? void 0 : _b.map( | ||
(childRoute) => getFilePathIdAndRouteIdFromPath(childRoute.routePath)[1] | ||
) | ||
} | ||
]; | ||
}) | ||
const createRouteManifest = () => { | ||
const routesManifest = { | ||
__root__: { | ||
filePath: rootRouteNode == null ? void 0 : rootRouteNode.filePath, | ||
children: routeTree.map( | ||
(d) => getFilePathIdAndRouteIdFromPath(d.routePath)[1] | ||
) | ||
} | ||
}, | ||
null, | ||
2 | ||
); | ||
}, | ||
...Object.fromEntries( | ||
routeNodes.map((d) => { | ||
var _a, _b; | ||
const [_, routeId] = getFilePathIdAndRouteIdFromPath(d.routePath); | ||
return [ | ||
routeId, | ||
{ | ||
filePath: d.filePath, | ||
parent: ((_a = d.parent) == null ? void 0 : _a.routePath) ? getFilePathIdAndRouteIdFromPath(d.parent.routePath)[1] : void 0, | ||
children: (_b = d.children) == null ? void 0 : _b.map( | ||
(childRoute) => getFilePathIdAndRouteIdFromPath(childRoute.routePath)[1] | ||
) | ||
} | ||
]; | ||
}) | ||
) | ||
}; | ||
return JSON.stringify( | ||
{ | ||
routes: routesManifest | ||
}, | ||
null, | ||
2 | ||
); | ||
}; | ||
const routeConfigFileContent = await prettier.format( | ||
@@ -592,2 +639,8 @@ config.disableManifestGeneration ? routeImports : [ | ||
} | ||
function removeTrailingSlash(s) { | ||
return s.replace(/\/$/, ""); | ||
} | ||
function determineInitialRoutePath(routePath) { | ||
return cleanPath(`/${routePath.split(".").join("/")}`) || ""; | ||
} | ||
function determineNodePath(node) { | ||
@@ -641,2 +694,41 @@ var _a; | ||
} | ||
function checkStartAPIRoutes(_routes) { | ||
if (_routes.length === 0) { | ||
return []; | ||
} | ||
const routes = _routes.map((d) => { | ||
const routePath = removeTrailingSlash(d.routePath ?? ""); | ||
return { ...d, routePath }; | ||
}); | ||
const routePaths = routes.map((d) => d.routePath); | ||
const uniqueRoutePaths = new Set(routePaths); | ||
if (routePaths.length !== uniqueRoutePaths.size) { | ||
const duplicateRoutePaths = routePaths.filter( | ||
(d, i) => routePaths.indexOf(d) !== i | ||
); | ||
const conflictingFiles = routes.filter((d) => duplicateRoutePaths.includes(d.routePath)).map((d) => `${d.fullPath}`); | ||
const errorMessage = `Conflicting configuration paths was for found for the following API route${duplicateRoutePaths.length > 1 ? "s" : ""}: ${duplicateRoutePaths.map((p) => `"${p}"`).join(", ")}. | ||
Please ensure each API route has a unique route path. | ||
Conflicting files: | ||
${conflictingFiles.join("\n ")} | ||
`; | ||
throw new Error(errorMessage); | ||
} | ||
return routes; | ||
} | ||
function startAPIRouteSegmentsFromTSRFilePath(src) { | ||
const routePath = determineInitialRoutePath(src); | ||
const parts = routePath.replaceAll(".", "/").split("/").filter((p) => !!p && p !== "index"); | ||
const segments = parts.map((part) => { | ||
if (part.startsWith("$")) { | ||
if (part === "$") { | ||
return { value: part, type: "splat" }; | ||
} | ||
part.replaceAll("$", ""); | ||
return { value: part, type: "param" }; | ||
} | ||
return { value: part, type: "path" }; | ||
}); | ||
return segments; | ||
} | ||
export { | ||
@@ -650,4 +742,5 @@ generator, | ||
removeLastSegmentFromPath, | ||
rootPathId | ||
rootPathId, | ||
startAPIRouteSegmentsFromTSRFilePath | ||
}; | ||
//# sourceMappingURL=generator.js.map |
@@ -1,2 +0,4 @@ | ||
export { type Config, configSchema, getConfig } from './config.js'; | ||
export { generator } from './generator.js'; | ||
export { configSchema, getConfig } from './config.js'; | ||
export type { Config } from './config.js'; | ||
export { generator, startAPIRouteSegmentsFromTSRFilePath } from './generator.js'; | ||
export type { StartAPIRoutePathSegment } from './generator.js'; |
import { configSchema, getConfig } from "./config.js"; | ||
import { generator } from "./generator.js"; | ||
import { generator, startAPIRouteSegmentsFromTSRFilePath } from "./generator.js"; | ||
export { | ||
configSchema, | ||
generator, | ||
getConfig | ||
getConfig, | ||
startAPIRouteSegmentsFromTSRFilePath | ||
}; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@tanstack/router-generator", | ||
"version": "1.49.3", | ||
"version": "1.51.0", | ||
"description": "Modern and scalable routing for React applications", | ||
@@ -5,0 +5,0 @@ "author": "Tanner Linsley", |
@@ -17,2 +17,3 @@ import path from 'node:path' | ||
disableManifestGeneration: z.boolean().optional().default(false), | ||
apiBase: z.string().optional().default('/api'), | ||
routeTreeFileHeader: z | ||
@@ -19,0 +20,0 @@ .array(z.string()) |
@@ -27,2 +27,3 @@ import path from 'node:path' | ||
isRoute?: boolean | ||
isAPIRoute?: boolean | ||
isLoader?: boolean | ||
@@ -80,4 +81,3 @@ isComponent?: boolean | ||
const filePathNoExt = removeExt(filePath) | ||
let routePath = | ||
cleanPath(`/${filePathNoExt.split('.').join('/')}`) || '' | ||
let routePath = determineInitialRoutePath(filePathNoExt) | ||
@@ -110,2 +110,5 @@ if (routeFilePrefix) { | ||
const isLoader = routePath.endsWith('/loader') | ||
const isAPIRoute = routePath.startsWith( | ||
`${removeTrailingSlash(config.apiBase)}/`, | ||
) | ||
@@ -154,2 +157,3 @@ const segments = routePath.split('/') | ||
isLayout, | ||
isAPIRoute, | ||
}) | ||
@@ -465,7 +469,55 @@ } | ||
for (const node of preRouteNodes) { | ||
for (const node of preRouteNodes.filter((d) => !d.isAPIRoute)) { | ||
await handleNode(node) | ||
} | ||
function buildRouteConfig(nodes: Array<RouteNode>, depth = 1): string { | ||
const startAPIRouteNodes: Array<RouteNode> = checkStartAPIRoutes( | ||
preRouteNodes.filter((d) => d.isAPIRoute), | ||
) | ||
const handleAPINode = async (node: RouteNode) => { | ||
const routeCode = fs.readFileSync(node.fullPath, 'utf-8') | ||
const escapedRoutePath = removeTrailingUnderscores( | ||
node.routePath?.replaceAll('$', '$$') ?? '', | ||
) | ||
if (!routeCode) { | ||
const replaced = `import { json } from '@tanstack/start' | ||
import { createAPIFileRoute } from '@tanstack/start/api' | ||
export const Route = createAPIFileRoute('${escapedRoutePath}')({ | ||
GET: ({ request, params }) => { | ||
return json({ message: 'Hello ${escapedRoutePath}' }) | ||
}, | ||
}) | ||
` | ||
logger.log(`🟡 Creating ${node.fullPath}`) | ||
fs.writeFileSync( | ||
node.fullPath, | ||
await prettier.format(replaced, prettierOptions), | ||
) | ||
} else { | ||
const copied = routeCode.replace( | ||
/(createAPIFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g, | ||
(_, p1, __, p3) => `${p1}${escapedRoutePath}${p3}`, | ||
) | ||
if (copied !== routeCode) { | ||
logger.log(`🟡 Updating ${node.fullPath}`) | ||
await fsp.writeFile( | ||
node.fullPath, | ||
await prettier.format(copied, prettierOptions), | ||
) | ||
} | ||
} | ||
} | ||
for (const node of startAPIRouteNodes) { | ||
await handleAPINode(node) | ||
} | ||
function buildRouteTreeConfig(nodes: Array<RouteNode>, depth = 1): string { | ||
const children = nodes.map((node) => { | ||
@@ -483,3 +535,3 @@ if (node.isRoot) { | ||
if (node.children?.length) { | ||
const childConfigs = buildRouteConfig(node.children, depth + 1) | ||
const childConfigs = buildRouteTreeConfig(node.children, depth + 1) | ||
return `${route}: ${route}.addChildren({${spaces(depth * 4)}${childConfigs}})` | ||
@@ -494,3 +546,3 @@ } | ||
const routeConfigChildrenText = buildRouteConfig(routeTree) | ||
const routeConfigChildrenText = buildRouteTreeConfig(routeTree) | ||
@@ -681,34 +733,34 @@ const sortedRouteNodes = multiSortBy(routeNodes, [ | ||
const createRouteManifest = () => | ||
JSON.stringify( | ||
const createRouteManifest = () => { | ||
const routesManifest = { | ||
__root__: { | ||
filePath: rootRouteNode?.filePath, | ||
children: routeTree.map( | ||
(d) => getFilePathIdAndRouteIdFromPath(d.routePath!)[1], | ||
), | ||
}, | ||
...Object.fromEntries( | ||
routeNodes.map((d) => { | ||
const [_, routeId] = getFilePathIdAndRouteIdFromPath(d.routePath!) | ||
return [ | ||
routeId, | ||
{ | ||
filePath: d.filePath, | ||
parent: d.parent?.routePath | ||
? getFilePathIdAndRouteIdFromPath(d.parent.routePath)[1] | ||
: undefined, | ||
children: d.children?.map( | ||
(childRoute) => | ||
getFilePathIdAndRouteIdFromPath(childRoute.routePath!)[1], | ||
), | ||
}, | ||
] | ||
}), | ||
), | ||
} | ||
return JSON.stringify( | ||
{ | ||
routes: { | ||
__root__: { | ||
filePath: rootRouteNode?.filePath, | ||
children: routeTree.map( | ||
(d) => getFilePathIdAndRouteIdFromPath(d.routePath!)[1], | ||
), | ||
}, | ||
...Object.fromEntries( | ||
routeNodes.map((d) => { | ||
const [filePathId, routeId] = getFilePathIdAndRouteIdFromPath( | ||
d.routePath!, | ||
) | ||
return [ | ||
routeId, | ||
{ | ||
filePath: d.filePath, | ||
parent: d.parent?.routePath | ||
? getFilePathIdAndRouteIdFromPath(d.parent.routePath)[1] | ||
: undefined, | ||
children: d.children?.map( | ||
(childRoute) => | ||
getFilePathIdAndRouteIdFromPath(childRoute.routePath!)[1], | ||
), | ||
}, | ||
] | ||
}), | ||
), | ||
}, | ||
routes: routesManifest, | ||
}, | ||
@@ -718,2 +770,3 @@ null, | ||
) | ||
} | ||
@@ -845,2 +898,10 @@ const routeConfigFileContent = await prettier.format( | ||
function removeTrailingSlash(s: string) { | ||
return s.replace(/\/$/, '') | ||
} | ||
function determineInitialRoutePath(routePath: string) { | ||
return cleanPath(`/${routePath.split('.').join('/')}`) || '' | ||
} | ||
/** | ||
@@ -944,1 +1005,75 @@ * The `node.path` is used as the `id` in the route definition. | ||
} | ||
function checkStartAPIRoutes(_routes: Array<RouteNode>) { | ||
if (_routes.length === 0) { | ||
return [] | ||
} | ||
// Make sure these are valid URLs | ||
// Route Groups and Layout Routes aren't being removed since | ||
// you may want to have an API route that starts with an underscore | ||
// or be wrapped in parentheses | ||
const routes = _routes.map((d) => { | ||
const routePath = removeTrailingSlash(d.routePath ?? '') | ||
return { ...d, routePath } | ||
}) | ||
// Check no two API routes have the same routePath | ||
// if they do, throw an error with the conflicting filePaths | ||
const routePaths = routes.map((d) => d.routePath) | ||
const uniqueRoutePaths = new Set(routePaths) | ||
if (routePaths.length !== uniqueRoutePaths.size) { | ||
const duplicateRoutePaths = routePaths.filter( | ||
(d, i) => routePaths.indexOf(d) !== i, | ||
) | ||
const conflictingFiles = routes | ||
.filter((d) => duplicateRoutePaths.includes(d.routePath)) | ||
.map((d) => `${d.fullPath}`) | ||
const errorMessage = `Conflicting configuration paths was for found for the following API route${duplicateRoutePaths.length > 1 ? 's' : ''}: ${duplicateRoutePaths | ||
.map((p) => `"${p}"`) | ||
.join(', ')}. | ||
Please ensure each API route has a unique route path. | ||
Conflicting files: \n ${conflictingFiles.join('\n ')}\n` | ||
throw new Error(errorMessage) | ||
} | ||
return routes | ||
} | ||
export type StartAPIRoutePathSegment = { | ||
value: string | ||
type: 'path' | 'param' | 'splat' | ||
} | ||
/** | ||
* This function takes in a path in the format accepted by TanStack Router | ||
* and returns an array of path segments that can be used to generate | ||
* the pathname of the TanStack Start API route. | ||
* | ||
* @param src | ||
* @returns | ||
*/ | ||
export function startAPIRouteSegmentsFromTSRFilePath( | ||
src: string, | ||
): Array<StartAPIRoutePathSegment> { | ||
const routePath = determineInitialRoutePath(src) | ||
const parts = routePath | ||
.replaceAll('.', '/') | ||
.split('/') | ||
.filter((p) => !!p && p !== 'index') | ||
const segments: Array<StartAPIRoutePathSegment> = parts.map((part) => { | ||
if (part.startsWith('$')) { | ||
if (part === '$') { | ||
return { value: part, type: 'splat' } | ||
} | ||
part.replaceAll('$', '') | ||
return { value: part, type: 'param' } | ||
} | ||
return { value: part, type: 'path' } | ||
}) | ||
return segments | ||
} |
@@ -1,2 +0,5 @@ | ||
export { type Config, configSchema, getConfig } from './config' | ||
export { generator } from './generator' | ||
export { configSchema, getConfig } from './config' | ||
export type { Config } from './config' | ||
export { generator, startAPIRouteSegmentsFromTSRFilePath } from './generator' | ||
export type { StartAPIRoutePathSegment } from './generator' |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
225765
2903