@timber-js/app 0.2.0-alpha.90 → 0.2.0-alpha.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{interception-DRlhJWbu.js → interception-DSv3A2Zn.js} +235 -160
- package/dist/_chunks/interception-DSv3A2Zn.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/routing/codegen-shared.d.ts +55 -0
- package/dist/routing/codegen-shared.d.ts.map +1 -0
- package/dist/routing/codegen-types.d.ts +52 -0
- package/dist/routing/codegen-types.d.ts.map +1 -0
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/link-codegen.d.ts +90 -0
- package/dist/routing/link-codegen.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/routing/codegen-shared.ts +98 -0
- package/src/routing/codegen-types.ts +53 -0
- package/src/routing/codegen.ts +78 -279
- package/src/routing/link-codegen.ts +262 -0
- package/dist/_chunks/interception-DRlhJWbu.js.map +0 -1
package/src/routing/codegen.ts
CHANGED
|
@@ -9,29 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
-
import { relative, posix } from 'node:path';
|
|
13
12
|
import type { RouteTree, SegmentNode } from './types.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/** Whether the page.tsx exports searchParams */
|
|
22
|
-
hasSearchParams: boolean;
|
|
23
|
-
/** Absolute path to the page file that exports searchParams (for import paths) */
|
|
24
|
-
searchParamsPagePath?: string;
|
|
25
|
-
/** Whether this is an API route (route.ts) vs page route */
|
|
26
|
-
isApiRoute: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface ParamEntry {
|
|
30
|
-
name: string;
|
|
31
|
-
type: 'string' | 'string[]' | 'string[] | undefined';
|
|
32
|
-
/** When a layout/page exports defineParams, the absolute path to that file. */
|
|
33
|
-
codecFilePath?: string;
|
|
34
|
-
}
|
|
13
|
+
import type { ParamEntry, RouteEntry } from './codegen-types.js';
|
|
14
|
+
import {
|
|
15
|
+
buildCodecChainType,
|
|
16
|
+
emitResolveSegmentFieldHelper,
|
|
17
|
+
formatSearchParamsType,
|
|
18
|
+
} from './codegen-shared.js';
|
|
19
|
+
import { formatLinkCatchAllOverloads, formatTypedLinkOverloads } from './link-codegen.js';
|
|
35
20
|
|
|
36
21
|
/** Options for route map generation. */
|
|
37
22
|
export interface CodegenOptions {
|
|
@@ -53,7 +38,7 @@ export interface CodegenOptions {
|
|
|
53
38
|
*/
|
|
54
39
|
export function generateRouteMap(tree: RouteTree, options: CodegenOptions = {}): string {
|
|
55
40
|
const routes: RouteEntry[] = [];
|
|
56
|
-
collectRoutes(tree.root, [], routes);
|
|
41
|
+
collectRoutes(tree.root, [], [], routes);
|
|
57
42
|
|
|
58
43
|
// Sort routes alphabetically for deterministic output
|
|
59
44
|
routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
@@ -73,28 +58,73 @@ export function generateRouteMap(tree: RouteTree, options: CodegenOptions = {}):
|
|
|
73
58
|
function collectRoutes(
|
|
74
59
|
node: SegmentNode,
|
|
75
60
|
ancestorParams: ParamEntry[],
|
|
61
|
+
ancestorParamsFiles: string[],
|
|
76
62
|
routes: RouteEntry[]
|
|
77
63
|
): void {
|
|
78
|
-
//
|
|
64
|
+
// TIM-834: Identify this segment's own params.ts (if it has a
|
|
65
|
+
// segmentParams export). The full chain of params.ts files in the
|
|
66
|
+
// route ancestry is threaded down via `ancestorParamsFiles`; codec
|
|
67
|
+
// resolution for each ParamEntry is deferred until leaf time so that
|
|
68
|
+
// descendant params.ts files can override ancestor codecs (closest-
|
|
69
|
+
// to-leaf wins, matching the runtime semantics of
|
|
70
|
+
// coerceSegmentParams which walks segments top-down and overwrites
|
|
71
|
+
// earlier coercions).
|
|
72
|
+
const ownParamsFile =
|
|
73
|
+
node.params && fileHasExport(node.params.filePath, 'segmentParams')
|
|
74
|
+
? node.params.filePath
|
|
75
|
+
: undefined;
|
|
76
|
+
|
|
77
|
+
// Accumulate params from this segment. We attach `codecFilePaths`
|
|
78
|
+
// later (at leaf time) using the FULL chain so descendant overrides
|
|
79
|
+
// are visible. The legacy layout/page fallback is recorded now
|
|
80
|
+
// because it is per-segment (and does not participate in inheritance).
|
|
79
81
|
const params = [...ancestorParams];
|
|
80
82
|
if (node.paramName) {
|
|
81
|
-
|
|
82
|
-
const codecFile = findParamsExport(node);
|
|
83
|
+
const legacyFallback = ownParamsFile ? undefined : findLegacyParamsExport(node);
|
|
83
84
|
params.push({
|
|
84
85
|
name: node.paramName,
|
|
85
86
|
type: paramTypeForSegment(node.segmentType),
|
|
86
|
-
|
|
87
|
+
// Codec chain populated at leaf time. We carry the per-segment
|
|
88
|
+
// legacy fallback (if any) so leaf-time resolution can fall back
|
|
89
|
+
// to it when no params.ts in the chain declares this key.
|
|
90
|
+
legacyCodecFilePath: legacyFallback,
|
|
87
91
|
});
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
// Extend the chain for descendants of this segment.
|
|
95
|
+
const nextAncestorFiles = ownParamsFile
|
|
96
|
+
? [...ancestorParamsFiles, ownParamsFile]
|
|
97
|
+
: ancestorParamsFiles;
|
|
98
|
+
|
|
90
99
|
// Check if this segment is a leaf route (has page or route file)
|
|
91
100
|
const isPage = !!node.page;
|
|
92
101
|
const isApiRoute = !!node.route;
|
|
93
102
|
|
|
94
103
|
if (isPage || isApiRoute) {
|
|
104
|
+
// TIM-834 P1 fix: at LEAF time, the full chain of params.ts files
|
|
105
|
+
// (root-to-leaf) is known. Resolve every ParamEntry's
|
|
106
|
+
// `codecFilePaths` to the chain in LEAF-FIRST order so the
|
|
107
|
+
// closest-to-leaf entry is checked first — matching runtime
|
|
108
|
+
// closest-wins semantics. The chain is shared by all params in the
|
|
109
|
+
// route, so we compute it once.
|
|
110
|
+
const leafFirstChain = nextAncestorFiles.length > 0 ? [...nextAncestorFiles].reverse() : [];
|
|
111
|
+
const resolvedParams: ParamEntry[] = params.map((p) => {
|
|
112
|
+
const codecFilePaths =
|
|
113
|
+
leafFirstChain.length > 0
|
|
114
|
+
? leafFirstChain
|
|
115
|
+
: p.legacyCodecFilePath
|
|
116
|
+
? [p.legacyCodecFilePath]
|
|
117
|
+
: undefined;
|
|
118
|
+
return {
|
|
119
|
+
name: p.name,
|
|
120
|
+
type: p.type,
|
|
121
|
+
codecFilePaths,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
95
125
|
const entry: RouteEntry = {
|
|
96
126
|
urlPath: node.urlPath,
|
|
97
|
-
params:
|
|
127
|
+
params: resolvedParams,
|
|
98
128
|
hasSearchParams: false,
|
|
99
129
|
isApiRoute,
|
|
100
130
|
};
|
|
@@ -115,12 +145,12 @@ function collectRoutes(
|
|
|
115
145
|
|
|
116
146
|
// Recurse into children
|
|
117
147
|
for (const child of node.children) {
|
|
118
|
-
collectRoutes(child, params, routes);
|
|
148
|
+
collectRoutes(child, params, nextAncestorFiles, routes);
|
|
119
149
|
}
|
|
120
150
|
|
|
121
151
|
// Recurse into slots (they share the parent's URL path, but may have their own pages)
|
|
122
152
|
for (const [, slot] of node.slots) {
|
|
123
|
-
collectRoutes(slot, params, routes);
|
|
153
|
+
collectRoutes(slot, params, nextAncestorFiles, routes);
|
|
124
154
|
}
|
|
125
155
|
}
|
|
126
156
|
|
|
@@ -159,18 +189,13 @@ function fileHasExport(filePath: string, exportName: string): boolean {
|
|
|
159
189
|
}
|
|
160
190
|
|
|
161
191
|
/**
|
|
162
|
-
* Find
|
|
192
|
+
* Find a legacy `segmentParams` export on layout.tsx or page.tsx.
|
|
163
193
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
194
|
+
* Backward-compat shim: TIM-508 made params.ts the canonical location
|
|
195
|
+
* for `segmentParams`. Layout/page exports are still accepted for the
|
|
196
|
+
* OWN segment only (not inherited by descendants — see TIM-834).
|
|
167
197
|
*/
|
|
168
|
-
function
|
|
169
|
-
// params.ts convention file — primary location
|
|
170
|
-
if (node.params && fileHasExport(node.params.filePath, 'segmentParams')) {
|
|
171
|
-
return node.params.filePath;
|
|
172
|
-
}
|
|
173
|
-
// Fallback: layout or page export (legacy, will be removed)
|
|
198
|
+
function findLegacyParamsExport(node: SegmentNode): string | undefined {
|
|
174
199
|
if (node.layout && fileHasExport(node.layout.filePath, 'segmentParams')) {
|
|
175
200
|
return node.layout.filePath;
|
|
176
201
|
}
|
|
@@ -195,6 +220,12 @@ function formatDeclarationFile(routes: RouteEntry[], importBase?: string): strin
|
|
|
195
220
|
// (removing exports like bindUseQueryStates that aren't listed here).
|
|
196
221
|
lines.push('export {};');
|
|
197
222
|
lines.push('');
|
|
223
|
+
|
|
224
|
+
// TIM-834 P2: emit the shared codec-resolution helper type ONCE so the
|
|
225
|
+
// per-param chain conditionals reference it instead of inlining the
|
|
226
|
+
// fallback in both branches (which grows O(2^N) in chain depth).
|
|
227
|
+
lines.push(emitResolveSegmentFieldHelper());
|
|
228
|
+
lines.push('');
|
|
198
229
|
lines.push("declare module '@timber-js/app' {");
|
|
199
230
|
lines.push(' interface Routes {');
|
|
200
231
|
|
|
@@ -290,79 +321,12 @@ function formatParamsType(params: ParamEntry[], importBase?: string): string {
|
|
|
290
321
|
}
|
|
291
322
|
|
|
292
323
|
const fields = params.map((p) => {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const absPath = p.codecFilePath.replace(/\.(ts|tsx)$/, '');
|
|
296
|
-
let importPath: string;
|
|
297
|
-
if (importBase) {
|
|
298
|
-
importPath = './' + relative(importBase, absPath).replace(/\\/g, '/');
|
|
299
|
-
} else {
|
|
300
|
-
importPath = './' + posix.basename(absPath);
|
|
301
|
-
}
|
|
302
|
-
// Extract T from the 'segmentParams' named export's ParamsDefinition<T>
|
|
303
|
-
const codecType = `(typeof import('${importPath}'))['segmentParams'] extends import('@timber-js/app/segment-params').ParamsDefinition<infer T> ? T[${JSON.stringify(p.name)}] : ${p.type}`;
|
|
304
|
-
return `${p.name}: ${codecType}`;
|
|
305
|
-
}
|
|
306
|
-
return `${p.name}: ${p.type}`;
|
|
307
|
-
});
|
|
308
|
-
return `{ ${fields.join('; ')} }`;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Format the params type for Link overloads.
|
|
313
|
-
*
|
|
314
|
-
* Link params accept `string | number` for single dynamic segments
|
|
315
|
-
* (convenience — values are stringified at runtime). Catch-all and
|
|
316
|
-
* optional catch-all remain `string[]` / `string[] | undefined`.
|
|
317
|
-
*
|
|
318
|
-
* Uses DIRECT types (not conditional) to preserve excess property checking.
|
|
319
|
-
*/
|
|
320
|
-
function formatLinkParamsType(params: ParamEntry[], importBase?: string): string {
|
|
321
|
-
if (params.length === 0) {
|
|
322
|
-
return '{}';
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const fields = params.map((p) => {
|
|
326
|
-
if (p.codecFilePath) {
|
|
327
|
-
const absPath = p.codecFilePath.replace(/\.(ts|tsx)$/, '');
|
|
328
|
-
let importPath: string;
|
|
329
|
-
if (importBase) {
|
|
330
|
-
importPath = './' + relative(importBase, absPath).replace(/\\/g, '/');
|
|
331
|
-
} else {
|
|
332
|
-
importPath = './' + posix.basename(absPath);
|
|
333
|
-
}
|
|
334
|
-
const codecType = `(typeof import('${importPath}'))['segmentParams'] extends import('@timber-js/app/segment-params').ParamsDefinition<infer T> ? T[${JSON.stringify(p.name)}] : ${p.type}`;
|
|
335
|
-
return `${p.name}: ${codecType}`;
|
|
336
|
-
}
|
|
337
|
-
const type = p.type === 'string' ? 'string | number' : p.type;
|
|
338
|
-
return `${p.name}: ${type}`;
|
|
324
|
+
const codecType = buildCodecChainType(p, importBase, p.type);
|
|
325
|
+
return `${p.name}: ${codecType}`;
|
|
339
326
|
});
|
|
340
327
|
return `{ ${fields.join('; ')} }`;
|
|
341
328
|
}
|
|
342
329
|
|
|
343
|
-
/**
|
|
344
|
-
* Format the searchParams type for a route entry.
|
|
345
|
-
*
|
|
346
|
-
* When a page.tsx exports searchParams, we reference its inferred type via an import type.
|
|
347
|
-
* The import path is relative to `importBase` (the directory where the .d.ts will be
|
|
348
|
-
* written). When importBase is undefined, falls back to a bare relative path.
|
|
349
|
-
*/
|
|
350
|
-
function formatSearchParamsType(route: RouteEntry, importBase?: string): string {
|
|
351
|
-
if (route.hasSearchParams && route.searchParamsPagePath) {
|
|
352
|
-
const absPath = route.searchParamsPagePath.replace(/\.(ts|tsx)$/, '');
|
|
353
|
-
let importPath: string;
|
|
354
|
-
if (importBase) {
|
|
355
|
-
// Make the path relative to the output directory, converted to posix separators
|
|
356
|
-
importPath = './' + relative(importBase, absPath).replace(/\\/g, '/');
|
|
357
|
-
} else {
|
|
358
|
-
importPath = './' + posix.basename(absPath);
|
|
359
|
-
}
|
|
360
|
-
// Extract the type from the named 'searchParams' export of the page module.
|
|
361
|
-
return `(typeof import('${importPath}'))['searchParams'] extends import('@timber-js/app/search-params').SearchParamsDefinition<infer T> ? T : never`;
|
|
362
|
-
}
|
|
363
|
-
return '{}';
|
|
364
|
-
}
|
|
365
|
-
|
|
366
330
|
/**
|
|
367
331
|
* Generate useQueryStates overloads.
|
|
368
332
|
*
|
|
@@ -392,173 +356,8 @@ function formatUseQueryStatesOverloads(routes: RouteEntry[], importBase?: string
|
|
|
392
356
|
return lines;
|
|
393
357
|
}
|
|
394
358
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
* '/[org]/[repo]' → '/${string}/${string}'
|
|
401
|
-
*/
|
|
402
|
-
function buildResolvedPattern(route: RouteEntry): string | null {
|
|
403
|
-
const parts = route.urlPath.split('/');
|
|
404
|
-
const templateParts = parts.map((part) => {
|
|
405
|
-
if (part.startsWith('[[...') && part.endsWith(']]')) {
|
|
406
|
-
// Optional catch-all — matches any trailing path
|
|
407
|
-
return '${string}';
|
|
408
|
-
}
|
|
409
|
-
if (part.startsWith('[...') && part.endsWith(']')) {
|
|
410
|
-
// Catch-all — matches one or more segments
|
|
411
|
-
return '${string}';
|
|
412
|
-
}
|
|
413
|
-
if (part.startsWith('[') && part.endsWith(']')) {
|
|
414
|
-
// Dynamic segment
|
|
415
|
-
return '${string}';
|
|
416
|
-
}
|
|
417
|
-
return part;
|
|
418
|
-
});
|
|
419
|
-
return templateParts.join('/');
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/** Shared Link base-props type literal used in every emitted call signature. */
|
|
423
|
-
const LINK_BASE_PROPS_TYPE =
|
|
424
|
-
"Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & { prefetch?: boolean; scroll?: boolean; preserveSearchParams?: true | string[]; onNavigate?: import('./client/link.js').OnNavigateHandler; children?: import('react').ReactNode }";
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* TIM-832: Catch-all call signatures for `<Link>` — external hrefs and
|
|
428
|
-
* computed `string` variables. Emitted from codegen in a SEPARATE
|
|
429
|
-
* `declare module` block (declared AFTER the per-route block) so the TS
|
|
430
|
-
* "later overload set ordered first" rule places these catch-all
|
|
431
|
-
* signatures ahead of per-route in resolution order, leaving per-route
|
|
432
|
-
* as the final overload whose error message is reported on failure.
|
|
433
|
-
*
|
|
434
|
-
* The conditional `string extends H ? ... : never` protection preserves
|
|
435
|
-
* TIM-624's guarantee that unknown internal path literals don't match
|
|
436
|
-
* the catch-all — typos like `<Link href="/typo" />` still error.
|
|
437
|
-
*/
|
|
438
|
-
function formatLinkCatchAllOverloads(): string[] {
|
|
439
|
-
const lines: string[] = [];
|
|
440
|
-
const baseProps = LINK_BASE_PROPS_TYPE;
|
|
441
|
-
|
|
442
|
-
// TIM-830: the catch-all signatures accept EITHER the legacy
|
|
443
|
-
// `{ definition, values }` wrapper OR a flat `Record<string, unknown>`
|
|
444
|
-
// values object. External/computed hrefs can't be looked up in the
|
|
445
|
-
// runtime registry, so the wrapped form is still the reliable path;
|
|
446
|
-
// the flat form is kept permissive for callers migrating from typed-
|
|
447
|
-
// route hrefs to computed strings. `resolveHref` discriminates at
|
|
448
|
-
// runtime via the presence of a `definition` key.
|
|
449
|
-
const catchAllSearchParams =
|
|
450
|
-
'{ definition: SearchParamsDefinition<Record<string, unknown>>; values: Record<string, unknown> } | Record<string, unknown>';
|
|
451
|
-
|
|
452
|
-
// ExternalHref inlined here rather than referenced as an exported
|
|
453
|
-
// alias so the generated .d.ts stands alone without source imports.
|
|
454
|
-
const externalHref =
|
|
455
|
-
'`http://${string}` | `https://${string}` | `mailto:${string}` | `tel:${string}` | `ftp://${string}` | `//${string}` | `#${string}` | `?${string}`';
|
|
456
|
-
|
|
457
|
-
lines.push(' // Typed Link overloads — catch-all (block 2 / emitted second)');
|
|
458
|
-
lines.push(' interface LinkFunction {');
|
|
459
|
-
|
|
460
|
-
// (1) External/literal-protocol hrefs.
|
|
461
|
-
lines.push(` (props: ${baseProps} & {`);
|
|
462
|
-
lines.push(` href: ${externalHref}`);
|
|
463
|
-
lines.push(` segmentParams?: never`);
|
|
464
|
-
lines.push(` searchParams?: ${catchAllSearchParams}`);
|
|
465
|
-
lines.push(` }): import('react').JSX.Element`);
|
|
466
|
-
|
|
467
|
-
// (2) Computed/variable href — non-literal `string` only.
|
|
468
|
-
// `string extends H` is true only when H is the wide `string` type,
|
|
469
|
-
// not a specific literal. Literal internal paths (typos) that don't
|
|
470
|
-
// match any per-route signature collapse to `never` here, but since
|
|
471
|
-
// this block is NOT the "last overload" at resolution time, TS
|
|
472
|
-
// instead reports the error from the per-route block — which now
|
|
473
|
-
// says `Type '"/typo"' is not assignable to type '"/products/[id]"'`
|
|
474
|
-
// or similar per-route-specific guidance.
|
|
475
|
-
lines.push(` <H extends string>(`);
|
|
476
|
-
lines.push(` props: string extends H`);
|
|
477
|
-
lines.push(` ? ${baseProps} & {`);
|
|
478
|
-
lines.push(` href: H`);
|
|
479
|
-
lines.push(` segmentParams?: Record<string, string | number | string[]>`);
|
|
480
|
-
lines.push(` searchParams?: ${catchAllSearchParams}`);
|
|
481
|
-
lines.push(` }`);
|
|
482
|
-
lines.push(` : never`);
|
|
483
|
-
lines.push(` ): import('react').JSX.Element`);
|
|
484
|
-
|
|
485
|
-
lines.push(' }');
|
|
486
|
-
return lines;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Generate typed per-route Link call signatures via LinkFunction
|
|
491
|
-
* interface merging.
|
|
492
|
-
*
|
|
493
|
-
* Each call signature uses DIRECT types (not conditional types) for
|
|
494
|
-
* segmentParams, preserving TypeScript's excess property checking.
|
|
495
|
-
* Interface merging is the only reliable way to add "overloads" via
|
|
496
|
-
* module augmentation — function overloads can't be augmented.
|
|
497
|
-
*
|
|
498
|
-
* TIM-832: this function emits ONLY the per-route block. The catch-all
|
|
499
|
-
* block is emitted separately by `formatLinkCatchAllOverloads` inside a
|
|
500
|
-
* second `declare module` so TS overload ordering reports per-route
|
|
501
|
-
* errors (see `formatDeclarationFile`).
|
|
502
|
-
*/
|
|
503
|
-
function formatTypedLinkOverloads(routes: RouteEntry[], importBase?: string): string[] {
|
|
504
|
-
const lines: string[] = [];
|
|
505
|
-
const baseProps = LINK_BASE_PROPS_TYPE;
|
|
506
|
-
|
|
507
|
-
lines.push(' interface LinkFunction {');
|
|
508
|
-
for (const route of routes) {
|
|
509
|
-
const hasDynamicParams = route.params.length > 0;
|
|
510
|
-
const paramsProp = hasDynamicParams
|
|
511
|
-
? `segmentParams: ${formatLinkParamsType(route.params, importBase)}`
|
|
512
|
-
: 'segmentParams?: never';
|
|
513
|
-
|
|
514
|
-
const searchParamsType = route.hasSearchParams
|
|
515
|
-
? formatSearchParamsType(route, importBase)
|
|
516
|
-
: null;
|
|
517
|
-
// TIM-830: the per-route pattern signature (href: '/products/[id]')
|
|
518
|
-
// uses a FLAT `Partial<T>` shape — the registry is keyed by the
|
|
519
|
-
// un-interpolated pattern, which matches `href` here exactly.
|
|
520
|
-
const patternSearchParamsProp = searchParamsType
|
|
521
|
-
? `searchParams?: Partial<${searchParamsType}>`
|
|
522
|
-
: 'searchParams?: never';
|
|
523
|
-
|
|
524
|
-
// TIM-832: For dynamic routes, emit the resolved-template signature
|
|
525
|
-
// FIRST and the pattern signature SECOND. The TS overload resolution
|
|
526
|
-
// order is top-to-bottom, and TS reports the error from the LAST
|
|
527
|
-
// overload tried on a failed call. For a user who passes the literal
|
|
528
|
-
// pattern href (`href="/products/[id]"`) and mistyped segmentParams,
|
|
529
|
-
// we want the PATTERN signature's error (which names the specific
|
|
530
|
-
// `segmentParams` shape like `{ id: string | number }`) to be the one
|
|
531
|
-
// TS reports — so the pattern must come LAST.
|
|
532
|
-
//
|
|
533
|
-
// TIM-830: the resolved-template signature keeps the legacy WRAPPED
|
|
534
|
-
// `{ definition, values }` shape. The runtime registry is keyed by
|
|
535
|
-
// the un-interpolated pattern (e.g. '/products/[id]'), so a flat
|
|
536
|
-
// lookup for an already-interpolated href like '/products/42' would
|
|
537
|
-
// fail. Flat values only work on the pattern signature below where
|
|
538
|
-
// `href` is the pattern itself. Callers using a resolved-template
|
|
539
|
-
// href must pass the definition inline, same as the external/
|
|
540
|
-
// computed catch-all signature.
|
|
541
|
-
if (hasDynamicParams) {
|
|
542
|
-
const templatePattern = buildResolvedPattern(route);
|
|
543
|
-
if (templatePattern) {
|
|
544
|
-
const resolvedSearchParamsProp = searchParamsType
|
|
545
|
-
? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }`
|
|
546
|
-
: 'searchParams?: never';
|
|
547
|
-
lines.push(` (props: ${baseProps} & {`);
|
|
548
|
-
lines.push(` href: \`${templatePattern}\``);
|
|
549
|
-
lines.push(` segmentParams?: never`);
|
|
550
|
-
lines.push(` ${resolvedSearchParamsProp}`);
|
|
551
|
-
lines.push(` }): import('react').JSX.Element`);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
lines.push(` (props: ${baseProps} & {`);
|
|
556
|
-
lines.push(` href: '${route.urlPath}'`);
|
|
557
|
-
lines.push(` ${paramsProp}`);
|
|
558
|
-
lines.push(` ${patternSearchParamsProp}`);
|
|
559
|
-
lines.push(` }): import('react').JSX.Element`);
|
|
560
|
-
}
|
|
561
|
-
lines.push(' }');
|
|
562
|
-
|
|
563
|
-
return lines;
|
|
564
|
-
}
|
|
359
|
+
// Link overload formatters and helpers (`formatTypedLinkOverloads`,
|
|
360
|
+
// `formatLinkCatchAllOverloads`, `formatLinkParamsType`,
|
|
361
|
+
// `buildResolvedPattern`, `LINK_BASE_PROPS_TYPE`) were extracted to
|
|
362
|
+
// `./link-codegen.ts` (TIM-835) to keep this file under the 500-line
|
|
363
|
+
// cap. They are imported at the top of this file.
|