Skip to content

Commit 8ed1541

Browse files
committed
wip
1 parent 1923f4b commit 8ed1541

File tree

4 files changed

+105
-0
lines changed

4 files changed

+105
-0
lines changed

packages/react-router-dev/typegen/index.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import fs from "node:fs";
22

3+
import ts from "dedent";
34
import * as Path from "pathe";
45
import pc from "picocolors";
56
import type vite from "vite";
67

78
import { createConfigLoader } from "../config/config";
9+
import * as Babel from "../vite/babel";
810

911
import { generate } from "./generate";
1012
import type { Context } from "./context";
1113
import { getTypesDir, getTypesPath } from "./paths";
14+
import type { RouteManifest, RouteManifestEntry } from "../config/routes";
1215

1316
export async function run(rootDirectory: string) {
1417
const ctx = await createContext({ rootDirectory, watch: false });
@@ -81,4 +84,80 @@ async function writeAll(ctx: Context): Promise<void> {
8184
fs.mkdirSync(Path.dirname(typesPath), { recursive: true });
8285
fs.writeFileSync(typesPath, content);
8386
});
87+
88+
const registerPath = Path.join(typegenDir, "+register.ts");
89+
fs.writeFileSync(registerPath, register(ctx));
90+
}
91+
92+
function register(ctx: Context) {
93+
const register = ts`
94+
import "react-router";
95+
96+
declare module "react-router" {
97+
interface Register {
98+
params: Params;
99+
}
100+
}
101+
`;
102+
103+
const { t } = Babel;
104+
const typeParams = t.tsTypeAliasDeclaration(
105+
t.identifier("Params"),
106+
null,
107+
t.tsTypeLiteral(
108+
Object.values(ctx.config.routes).map((route) => {
109+
// TODO: filter out layout (pathless) routes?
110+
const lineage = getRouteLineage(ctx.config.routes, route);
111+
const fullpath = lineage.map((route) => route.path).join("/");
112+
const params = parseParams(fullpath);
113+
return t.tsPropertySignature(
114+
t.stringLiteral(fullpath),
115+
t.tsTypeAnnotation(
116+
t.tsTypeLiteral(
117+
Object.entries(params).map(([param, isRequired]) => {
118+
const property = t.tsPropertySignature(
119+
t.stringLiteral(param),
120+
t.tsTypeAnnotation(t.tsStringKeyword())
121+
);
122+
property.optional = !isRequired;
123+
return property;
124+
})
125+
)
126+
)
127+
);
128+
})
129+
)
130+
);
131+
132+
return [register, Babel.generate(typeParams).code].join("\n\n");
133+
}
134+
135+
function parseParams(fullpath: string) {
136+
const result: Record<string, boolean> = {};
137+
138+
let segments = fullpath.split("/");
139+
segments.forEach((segment) => {
140+
const match = segment.match(/^:([\w-]+)(\?)?/);
141+
if (!match) return;
142+
const param = match[1];
143+
const isRequired = match[2] === undefined;
144+
145+
result[param] ||= isRequired;
146+
return;
147+
});
148+
149+
const hasSplat = segments.at(-1) === "*";
150+
if (hasSplat) result["*"] = true;
151+
return result;
152+
}
153+
154+
function getRouteLineage(routes: RouteManifest, route: RouteManifestEntry) {
155+
const result: RouteManifestEntry[] = [];
156+
while (route) {
157+
result.push(route);
158+
if (!route.parentId) break;
159+
route = routes[route.parentId];
160+
}
161+
result.reverse();
162+
return result;
84163
}

packages/react-router/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export {
5959
export {
6060
data,
6161
generatePath,
62+
href,
6263
isRouteErrorResponse,
6364
matchPath,
6465
matchRoutes,
@@ -263,6 +264,8 @@ export type {
263264
FlashSessionData,
264265
} from "./lib/server-runtime/sessions";
265266

267+
export type { Register } from "./lib/types/register";
268+
266269
///////////////////////////////////////////////////////////////////////////////
267270
// DANGER! PLEASE READ ME!
268271
// We provide these exports as an escape hatch in the event that you need any

packages/react-router/lib/router/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Register } from "../types/register";
12
import type { Equal, Expect } from "../types/utils";
23
import type { Location, Path, To } from "./history";
34
import { invariant, parsePath, warning } from "./history";
@@ -1471,3 +1472,24 @@ export function isRouteErrorResponse(error: any): error is ErrorResponse {
14711472
"data" in error
14721473
);
14731474
}
1475+
1476+
type AnyParams = Record<string, Record<string, string | undefined>>;
1477+
type HrefParams = Register extends {
1478+
params: infer TParams extends AnyParams;
1479+
}
1480+
? TParams
1481+
: AnyParams;
1482+
1483+
type HrefArgs = {
1484+
[K in keyof HrefParams]:
1485+
| [HrefParams[K]]
1486+
| (Partial<HrefParams[K]> extends HrefParams[K] ? [] : never);
1487+
};
1488+
1489+
export function href<Path extends keyof HrefArgs>(
1490+
path: Path,
1491+
...args: HrefArgs[Path]
1492+
): string {
1493+
// TODO: implement this
1494+
return "";
1495+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export interface Register {}

0 commit comments

Comments
 (0)