Last active
August 21, 2023 22:54
-
-
Save codebutler/c0b8e03219d96dd5a5f3f54f1211f79a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Generates typesafe-routes code for all routes in the app. | |
See the typesafe-routes docs for information on how to use it: | |
https://github.com/kruschid/typesafe-routes | |
Usage: | |
pnpm vite-node buildSrc/typesafe-routes-gen.ts | |
Authors: | |
Eric Butler <eric@codebutler.com> | |
Any copyright is dedicated to the Public Domain. | |
https://creativecommons.org/publicdomain/zero/1.0/ | |
*/ | |
import { GlobalRegistrator } from "@happy-dom/global-registrator"; | |
import type { AgnosticDataRouteObject } from "@remix-run/router/dist/utils"; | |
import { ESLint } from "eslint"; | |
import fs from "fs/promises"; | |
import { camelCase } from "lodash"; | |
import path from "path"; | |
import prettier from "prettier"; | |
import * as ts from "typescript"; | |
// Keep this if your code expects a global window object. | |
GlobalRegistrator.register(); | |
// Update to your root route. | |
// This must be imported after the global window object is registered. | |
const { appRouter } = await import("app/AppRouter"); | |
const cleanId = (id: string) => camelCase(id.replace(/\W/g, " ")); | |
const stripParentId = (id: string, parentId: string) => | |
id.startsWith(parentId) ? id.slice(parentId.length) : id; | |
export const cleanPath = (path: string) => | |
path === "/" ? path : path.replace(/\/*\*$/, "").replace(/(\/$)/, ""); | |
export const extractPathParams = (path: string) => { | |
const pathParams = path.match(/:(\w+)/g); | |
return pathParams ? pathParams.map((p) => p.slice(1)) : []; | |
}; | |
const visitRoutes = ( | |
routes: AgnosticDataRouteObject[], | |
parentId = "", | |
): ts.PropertyAssignment[] => | |
routes.flatMap((route) => | |
// Flatten non-leaf routes without path | |
(route.path && route.path !== "*") || route.index || !route.children?.length | |
? [ | |
ts.factory.createPropertyAssignment( | |
ts.factory.createStringLiteral( | |
cleanId(stripParentId(cleanId(route.id), parentId)), | |
), | |
visitRoute(route, cleanId(route.id)), | |
), | |
] | |
: visitRoutes(route.children ?? [], parentId), | |
); | |
const visitRoute = ( | |
route: AgnosticDataRouteObject, | |
parentId = "", | |
): ts.CallExpression => | |
ts.factory.createCallExpression( | |
ts.factory.createIdentifier("route"), | |
undefined, | |
[ | |
ts.factory.createStringLiteral(cleanPath(route.path ?? "")), | |
ts.factory.createObjectLiteralExpression( | |
extractPathParams(route.path ?? "").map((param) => | |
ts.factory.createPropertyAssignment( | |
ts.factory.createStringLiteral(param), | |
ts.factory.createIdentifier("stringParser"), | |
), | |
), | |
true, | |
), | |
ts.factory.createObjectLiteralExpression( | |
visitRoutes( | |
route.children ?? [], | |
parentId, | |
), | |
true, | |
), | |
], | |
); | |
const generateImports = () => | |
ts.factory.createImportDeclaration( | |
undefined, | |
ts.factory.createImportClause( | |
false, | |
undefined, | |
ts.factory.createNamedImports([ | |
ts.factory.createImportSpecifier( | |
false, | |
undefined, | |
ts.factory.createIdentifier("route"), | |
), | |
ts.factory.createImportSpecifier( | |
false, | |
undefined, | |
ts.factory.createIdentifier("stringParser"), | |
), | |
]), | |
), | |
ts.factory.createStringLiteral("typesafe-routes"), | |
undefined, | |
); | |
const generateRoutes = () => | |
ts.factory.createVariableStatement( | |
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], | |
ts.factory.createVariableDeclarationList( | |
[ | |
ts.factory.createVariableDeclaration( | |
ts.factory.createIdentifier("rootRoute"), | |
undefined, | |
undefined, | |
visitRoute({ | |
id: "root", | |
path: "/", | |
children: appRouter.routes, | |
}), | |
), | |
], | |
ts.NodeFlags.Const, | |
), | |
); | |
const getAllRouteIds = (routes: AgnosticDataRouteObject[]): string[] => | |
routes.flatMap((route) => [ | |
route.id, | |
...(route.children?.length ? getAllRouteIds(route.children) : []), | |
]); | |
const generateRouteIds = () => | |
ts.factory.createVariableStatement( | |
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], | |
ts.factory.createVariableDeclarationList( | |
[ | |
ts.factory.createVariableDeclaration( | |
ts.factory.createIdentifier("RouteIds"), | |
undefined, | |
undefined, | |
ts.factory.createAsExpression( | |
ts.factory.createArrayLiteralExpression( | |
getAllRouteIds(appRouter.routes).map((routeId) => | |
ts.factory.createStringLiteral(routeId), | |
), | |
true, | |
), | |
ts.factory.createTypeReferenceNode( | |
ts.factory.createIdentifier("const"), | |
undefined, | |
), | |
), | |
), | |
], | |
ts.NodeFlags.Const, | |
), | |
); | |
const generateRouteIdAlias = () => | |
ts.factory.createTypeAliasDeclaration( | |
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], | |
ts.factory.createIdentifier("RouteId"), | |
undefined, | |
ts.factory.createIndexedAccessTypeNode( | |
ts.factory.createTypeQueryNode( | |
ts.factory.createIdentifier("RouteIds"), | |
undefined, | |
), | |
ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), | |
), | |
); | |
const generateCode = () => | |
ts | |
.createPrinter() | |
.printFile( | |
ts.factory.createSourceFile( | |
[ | |
generateImports(), | |
generateRouteIds(), | |
generateRouteIdAlias(), | |
generateRoutes(), | |
], | |
ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), | |
ts.NodeFlags.None, | |
), | |
); | |
const filePath = path.resolve(__dirname, "../src/gen/routes.ts"); | |
const code = ` | |
/** | |
* This file was auto-generated, do not edit manually. | |
* | |
* Run \`pnpm gen:routes\` to re-generate this file. | |
*/ | |
${generateCode()} | |
`; | |
let formattedCode; | |
formattedCode = await prettier.format(code, { parser: "typescript" }); | |
const eslint = new ESLint({ fix: true }); | |
const result = await eslint.lintText(formattedCode, { | |
filePath, | |
warnIgnored: true, | |
}); | |
if (result[0].messages.length) { | |
// eslint-disable-next-line no-console | |
console.error(result[0].messages); | |
process.exit(1); | |
} | |
formattedCode = result[0].output!; | |
await fs.writeFile(filePath, formattedCode, "utf-8"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment