-
-
Notifications
You must be signed in to change notification settings - Fork 780
feat(types): speed up typed routes #3941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -108,26 +108,81 @@ export async function writeTypes(nitro: Nitro) { | |
| ]; | ||
| } | ||
|
|
||
| const generateRoutes = () => [ | ||
| "// Generated by nitro", | ||
| 'import type { Serialize, Simplify } from "nitro/types";', | ||
| 'declare module "nitro/types" {', | ||
| " type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T", | ||
| " interface InternalApi {", | ||
| ...Object.entries(types.routes).map(([path, methods]) => | ||
| [ | ||
| ` '${path}': {`, | ||
| ...Object.entries(methods).map( | ||
| ([method, types]) => ` '${method}': ${types.join(" | ")}` | ||
| ), | ||
| " }", | ||
| ].join("\n") | ||
| ), | ||
| " }", | ||
| "}", | ||
| // Makes this a module for augmentation purposes | ||
| "export {}", | ||
| ]; | ||
| const buildRouteTree = () => { | ||
| type RouteNode = { | ||
| $?: Record<string, string[]>; | ||
| [segment: string]: RouteNode | Record<string, string[]> | undefined; | ||
| }; | ||
|
|
||
| const tree: RouteNode = {}; | ||
|
|
||
| for (const [path, methods] of Object.entries(types.routes)) { | ||
| const segments = path === "/" ? [] : path.slice(1).split("/"); | ||
| let node = tree; | ||
|
|
||
| for (const segment of segments) { | ||
| node[segment] ??= {}; | ||
| node = node[segment] as RouteNode; | ||
| } | ||
|
|
||
| node.$ ??= {}; | ||
|
|
||
| for (const [method, typeEntries] of Object.entries(methods)) { | ||
| node.$[method] = [...(node.$[method] ?? []), ...typeEntries]; | ||
| } | ||
| } | ||
|
|
||
| return tree; | ||
| }; | ||
|
|
||
| const renderRouteTree = ( | ||
| node: ReturnType<typeof buildRouteTree>, | ||
| level: number | ||
| ): string[] => { | ||
| const lines: string[] = []; | ||
| const indent = " ".repeat(level); | ||
| const childIndent = " ".repeat(level + 1); | ||
|
|
||
| if (node.$) { | ||
| lines.push(`${indent}'$': {`); | ||
| for (const [method, types] of Object.entries(node.$)) { | ||
| lines.push(`${childIndent}'${method}': ${types.join(" | ")}`); | ||
| } | ||
| lines.push(`${indent}}`); | ||
| } | ||
|
|
||
| for (const [segment, child] of Object.entries(node)) { | ||
| if (segment === "$" || !child || typeof child !== "object") { | ||
| continue; | ||
| } | ||
| lines.push(`${indent}'${segment}': {`); | ||
| lines.push( | ||
| ...renderRouteTree( | ||
| child as ReturnType<typeof buildRouteTree>, | ||
| level + 1 | ||
| ) | ||
| ); | ||
| lines.push(`${indent}}`); | ||
| } | ||
|
|
||
| return lines; | ||
| }; | ||
|
|
||
| const generateRoutes = () => { | ||
| const routeTree = buildRouteTree(); | ||
| return [ | ||
| "// Generated by nitro", | ||
| 'import type { Serialize, Simplify } from "nitro/types";', | ||
| 'declare module "nitro/types" {', | ||
| " type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T", | ||
| " interface InternalApi {", | ||
| ...renderRouteTree(routeTree, 2), | ||
| " }", | ||
| "}", | ||
| // Makes this a module for augmentation purposes | ||
| "export {}", | ||
| ]; | ||
| }; | ||
|
Comment on lines
+171
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's read the file to see the full context
head -200 src/build/types.ts | tail -100Repository: nitrojs/nitro Length of output: 2816 🏁 Script executed: # Check TypeScript version requirements in package.json
cat package.json | grep -A 5 '"typescript"'Repository: nitrojs/nitro Length of output: 207 🏁 Script executed: # Check tsconfig.json for TypeScript version target
cat tsconfig.json | grep -E '(compilerOptions|target)' -A 5Repository: nitrojs/nitro Length of output: 254 🏁 Script executed: # Search for any comments or documentation about the Awaited type
rg "Awaited" --context 5Repository: nitrojs/nitro Length of output: 2411 Remove redundant The local 🤖 Prompt for AI Agents |
||
|
|
||
| const config = [ | ||
| "// Generated by nitro", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,104 +1,139 @@ | ||
| import type { InternalApi } from "./fetch.ts"; | ||
|
|
||
| type MatchResult< | ||
| Key extends string, | ||
| Exact extends boolean = false, | ||
| Score extends any[] = [], | ||
| catchAll extends boolean = false, | ||
| > = { | ||
| [k in Key]: { key: k; exact: Exact; score: Score; catchAll: catchAll }; | ||
| }[Key]; | ||
|
|
||
| type Subtract< | ||
| Minuend extends any[] = [], | ||
| Subtrahend extends any[] = [], | ||
| > = Minuend extends [...Subtrahend, ...infer Remainder] ? Remainder : never; | ||
|
|
||
| type TupleIfDiff< | ||
| First extends string, | ||
| Second extends string, | ||
| Tuple extends any[] = [], | ||
| > = First extends `${Second}${infer Diff}` | ||
| ? Diff extends "" | ||
| ? [] | ||
| : Tuple | ||
| : []; | ||
|
|
||
| type MaxTuple<N extends any[] = [], T extends any[] = []> = { | ||
| current: T; | ||
| result: MaxTuple<N, ["", ...T]>; | ||
| }[[N["length"]] extends [Partial<T>["length"]] ? "current" : "result"]; | ||
|
|
||
| type CalcMatchScore< | ||
| Key extends string, | ||
| Route extends string, | ||
| Score extends any[] = [], | ||
| Init extends boolean = false, | ||
| FirstKeySegMatcher extends string = Init extends true ? ":Invalid:" : "", | ||
| > = `${Key}/` extends `${infer KeySeg}/${infer KeyRest}` | ||
| ? KeySeg extends FirstKeySegMatcher // return score if `KeySeg` is empty string (except first pass) | ||
| ? Subtract< | ||
| [...Score, ...TupleIfDiff<Route, Key, ["", ""]>], | ||
| TupleIfDiff<Key, Route, ["", ""]> | ||
| > | ||
| : `${Route}/` extends `${infer RouteSeg}/${infer RouteRest}` | ||
| ? `${RouteSeg}?` extends `${infer RouteSegWithoutQuery}?${string}` | ||
| ? RouteSegWithoutQuery extends KeySeg | ||
| ? CalcMatchScore<KeyRest, RouteRest, [...Score, "", ""]> // exact match | ||
| : KeySeg extends `:${string}` | ||
| ? RouteSegWithoutQuery extends "" | ||
| ? never | ||
| : CalcMatchScore<KeyRest, RouteRest, [...Score, ""]> // param match | ||
| : KeySeg extends RouteSegWithoutQuery | ||
| ? CalcMatchScore<KeyRest, RouteRest, [...Score, ""]> // match by ${string} | ||
| : never | ||
| : never | ||
| : never | ||
| type MatchResult<Node, Path extends string, Exact extends boolean> = { | ||
| node: Node; | ||
| path: Path; | ||
| exact: Exact; | ||
| }; | ||
|
|
||
| type RouteMethods<Node> = Node extends { $: infer Methods } ? Methods : never; | ||
|
|
||
| type StripQuery<Route extends string> = Route extends `${infer Clean}?${string}` | ||
| ? Clean | ||
| : Route; | ||
|
|
||
| type StripHash<Route extends string> = Route extends `${infer Clean}#${string}` | ||
| ? Clean | ||
| : Route; | ||
|
|
||
| type StripLeadingSlash<Route extends string> = Route extends `/${infer Rest}` | ||
| ? Rest | ||
| : Route; | ||
|
|
||
| type StripTrailingSlash<Route extends string> = Route extends `${infer Rest}/` | ||
| ? Rest | ||
| : Route; | ||
|
|
||
| type NormalizePath<Route extends string> = StripTrailingSlash< | ||
| StripLeadingSlash<StripHash<StripQuery<Route>>> | ||
| >; | ||
|
|
||
| type SplitPath<Route extends string> = | ||
| Route extends `${infer Head}/${infer Tail}` | ||
| ? [Head, ...SplitPath<Tail>] | ||
| : Route extends "" | ||
| ? [] | ||
| : [Route]; | ||
|
|
||
| type JoinPath<Prefix extends string, Segment extends string> = Prefix extends "" | ||
| ? `/${Segment}` | ||
| : `${Prefix}/${Segment}`; | ||
|
|
||
| type RootPath<Prefix extends string> = Prefix extends "" ? "/" : Prefix; | ||
|
|
||
| type ParamKey<Routes> = Extract<keyof Routes, `:${string}`>; | ||
| type CatchAllKey<Routes> = Extract<keyof Routes, `**${string}`>; | ||
|
|
||
| type ExactMatch< | ||
| Routes, | ||
| Segments extends string[], | ||
| Prefix extends string = "", | ||
| > = Segments extends [infer Head extends string, ...infer Tail extends string[]] | ||
| ? Head extends keyof Routes | ||
| ? ExactMatch<Routes[Head], Tail, JoinPath<Prefix, Head>> | ||
| : never | ||
| : Routes extends { $: any } | ||
| ? MatchResult<Routes, RootPath<Prefix>, true> | ||
| : never; | ||
|
|
||
| type LooseMatch< | ||
| Routes, | ||
| Segments extends string[], | ||
| Prefix extends string = "", | ||
| Fallback = never, | ||
| CatchAll = never, | ||
| > = Routes extends object | ||
| ? Segments extends [infer Head extends string, ...infer Tail extends string[]] | ||
| ? LooseMatchStep<Routes, Head, Tail, Prefix, Fallback, CatchAll> | ||
| : | ||
| | (Routes extends { $: any } | ||
| ? MatchResult<Routes, RootPath<Prefix>, false> | ||
| : Fallback) | ||
| | CatchAll | ||
| : never; | ||
|
|
||
| type _MatchedRoutes< | ||
| Route extends string, | ||
| MatchedResultUnion extends MatchResult<string> = MatchResult< | ||
| keyof InternalApi | ||
| >, | ||
| > = MatchedResultUnion["key"] extends infer MatchedKeys // spread union type | ||
| ? MatchedKeys extends string | ||
| ? Route extends MatchedKeys | ||
| ? MatchResult<MatchedKeys, true> // exact match | ||
| : MatchedKeys extends `${infer Root}/**${string}` | ||
| ? MatchedKeys extends `${string}/**` | ||
| ? Route extends `${Root}/${string}` | ||
| ? MatchResult<MatchedKeys, false, [], true> | ||
| : never // catchAll match | ||
| : MatchResult< | ||
| MatchedKeys, | ||
| false, | ||
| CalcMatchScore<Root, Route, [], true> | ||
| > // glob match | ||
| : MatchResult< | ||
| MatchedKeys, | ||
| false, | ||
| CalcMatchScore<MatchedKeys, Route, [], true> | ||
| > // partial match | ||
| type LooseMatchStep< | ||
| Routes extends object, | ||
| Head extends string, | ||
| Tail extends string[], | ||
| Prefix extends string, | ||
| Fallback, | ||
| CatchAll, | ||
| > = ( | ||
| Routes extends { $: any } | ||
| ? MatchResult<Routes, RootPath<Prefix>, false> | ||
| : Fallback | ||
| ) extends infer NextFallback | ||
| ? ( | ||
| CatchAllKey<Routes> extends never | ||
| ? CatchAll | ||
| : | ||
| | CatchAll | ||
| | MatchResult< | ||
| Routes[CatchAllKey<Routes>], | ||
| JoinPath<Prefix, CatchAllKey<Routes> & string>, | ||
| false | ||
| > | ||
| ) extends infer NextCatchAll | ||
| ? Head extends keyof Routes | ||
| ? LooseMatch< | ||
| Routes[Head], | ||
| Tail, | ||
| JoinPath<Prefix, Head>, | ||
| NextFallback, | ||
| NextCatchAll | ||
| > | ||
| : ParamKey<Routes> extends never | ||
| ? CatchAllKey<Routes> extends never | ||
| ? NextFallback | NextCatchAll | ||
| : | ||
| | MatchResult< | ||
| Routes[CatchAllKey<Routes>], | ||
| JoinPath<Prefix, CatchAllKey<Routes> & string>, | ||
| false | ||
| > | ||
| | NextCatchAll | ||
| | NextFallback | ||
| : LooseMatch< | ||
| Routes[ParamKey<Routes>], | ||
| Tail, | ||
| JoinPath<Prefix, ParamKey<Routes> & string>, | ||
| NextFallback, | ||
| NextCatchAll | ||
| > | ||
| : never | ||
| : never; | ||
|
|
||
| export type MatchedRoutes< | ||
| Route extends string, | ||
| MatchedKeysResult extends MatchResult<string> = MatchResult< | ||
| keyof InternalApi | ||
| >, | ||
| Matches extends MatchResult<string> = _MatchedRoutes< | ||
| Route, | ||
| MatchedKeysResult | ||
| >, | ||
| > = Route extends "/" | ||
| ? keyof InternalApi // root middleware | ||
| : Extract<Matches, { exact: true }> extends never | ||
| ? // @ts-ignore | ||
| | Extract< | ||
| Exclude<Matches, { score: never }>, | ||
| { score: MaxTuple<Matches["score"]> } | ||
| >["key"] | ||
| | Extract<Matches, { catchAll: true }>["key"] // partial, glob and catchAll matches | ||
| : Extract<Matches, { exact: true }>["key"]; // exact matches | ||
| type MatchRoute<Route extends string> = | ||
| ExactMatch<InternalApi, SplitPath<NormalizePath<Route>>> extends infer Exact | ||
| ? [Exact] extends [never] | ||
| ? LooseMatch<InternalApi, SplitPath<NormalizePath<Route>>> | ||
| : Exact | ||
| : never; | ||
|
|
||
| export type MatchedRouteMethods<Route extends string> = | ||
| MatchRoute<Route> extends MatchResult<infer Node, any, any> | ||
| ? RouteMethods<Node> | ||
| : never; | ||
|
|
||
| export type MatchedRoutes<Route extends string> = MatchedRouteMethods<Route>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing semicolon in generated TypeScript code.
The rendered method entries are missing trailing semicolons, which will produce invalid TypeScript syntax in the generated
nitro-routes.d.tsfile.🐛 Proposed fix
if (node.$) { lines.push(`${indent}'$': {`); for (const [method, types] of Object.entries(node.$)) { - lines.push(`${childIndent}'${method}': ${types.join(" | ")}`); + lines.push(`${childIndent}'${method}': ${types.join(" | ")};`); } lines.push(`${indent}}`); }📝 Committable suggestion
🤖 Prompt for AI Agents