Skip to content
Open
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
95 changes: 75 additions & 20 deletions src/build/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}}`);
}
Comment on lines +146 to +152
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.ts file.

🐛 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (node.$) {
lines.push(`${indent}'$': {`);
for (const [method, types] of Object.entries(node.$)) {
lines.push(`${childIndent}'${method}': ${types.join(" | ")}`);
}
lines.push(`${indent}}`);
}
if (node.$) {
lines.push(`${indent}'$': {`);
for (const [method, types] of Object.entries(node.$)) {
lines.push(`${childIndent}'${method}': ${types.join(" | ")};`);
}
lines.push(`${indent}}`);
}
🤖 Prompt for AI Agents
In @src/build/types.ts around lines 146 - 152, The generated method entries for
node.$ are missing trailing semicolons; update the code that pushes method lines
(the loop using lines.push(`${childIndent}'${method}': ${types.join(" | ")}`))
to append a semicolon so each entry becomes `${childIndent}'${method}':
${types.join(" | ")};`, ensuring valid TypeScript in the emitted declarations.
Ensure the change is applied where indent/childIndent and node.$ are handled so
all rendered method lines include the semicolon.


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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to see the full context
head -200 src/build/types.ts | tail -100

Repository: 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 5

Repository: nitrojs/nitro

Length of output: 254


🏁 Script executed:

# Search for any comments or documentation about the Awaited type
rg "Awaited" --context 5

Repository: nitrojs/nitro

Length of output: 2411


Remove redundant Awaited type declaration.

The local Awaited type at line 177 shadows TypeScript's built-in utility type. Since the project requires TypeScript ^5.9.3 (which includes the built-in Awaited), this local redeclaration is unnecessary and should be removed. The type will remain available within the module augmentation scope without the redundant definition.

🤖 Prompt for AI Agents
In @src/build/types.ts around lines 171 - 185, The generateRoutes function
currently injects a local Awaited type declaration that shadows TypeScript's
built-in Awaited; remove the redundant "type Awaited..." line from the generated
output in generateRoutes so the module augmentation relies on the built-in
utility instead—update generateRoutes (which builds routeTree via buildRouteTree
and renders with renderRouteTree) to omit the manual Awaited declaration while
keeping the rest of the declared module and exported empty module.


const config = [
"// Generated by nitro",
Expand Down
229 changes: 132 additions & 97 deletions src/types/fetch/_match.ts
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>;
Loading