@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
|
@@ -388,6 +388,213 @@ function findPageExtFile(dirPath, name, extSet) {
|
|
|
388
388
|
}
|
|
389
389
|
}
|
|
390
390
|
//#endregion
|
|
391
|
+
//#region src/routing/codegen-shared.ts
|
|
392
|
+
/**
|
|
393
|
+
* Shared codegen helpers — import-path computation, codec chain type
|
|
394
|
+
* builder, and searchParams type formatter.
|
|
395
|
+
*
|
|
396
|
+
* Extracted from `codegen.ts` so `link-codegen.ts` can use the same
|
|
397
|
+
* helpers without a cyclic import.
|
|
398
|
+
*/
|
|
399
|
+
/**
|
|
400
|
+
* Compute a relative import specifier for a codec/page file, stripping
|
|
401
|
+
* the .ts/.tsx extension and resolving against the codegen output dir.
|
|
402
|
+
*/
|
|
403
|
+
function codecImportPath(codecFilePath, importBase) {
|
|
404
|
+
const absPath = codecFilePath.replace(/\.(ts|tsx)$/, "");
|
|
405
|
+
if (importBase) return "./" + relative(importBase, absPath).replace(/\\/g, "/");
|
|
406
|
+
return "./" + posix.basename(absPath);
|
|
407
|
+
}
|
|
408
|
+
/** Name of the shared helper type emitted at the top of the .d.ts. */
|
|
409
|
+
var RESOLVE_SEGMENT_FIELD_TYPE_NAME = "_TimberResolveSegmentField";
|
|
410
|
+
/**
|
|
411
|
+
* Helper type emitted once at the top of the generated `.d.ts` and
|
|
412
|
+
* referenced by every codec-chain conditional. Without this shared
|
|
413
|
+
* helper, the inline expansion would duplicate the fallback branch on
|
|
414
|
+
* every step and grow O(2^N) in chain depth (a single deep nested route
|
|
415
|
+
* could blow up the file size and TS performance). With the helper,
|
|
416
|
+
* each step reuses the named type and growth is O(N).
|
|
417
|
+
*
|
|
418
|
+
* The helper is a 2-arg conditional: given a typeof import expression
|
|
419
|
+
* (`Def`), a key (`K`), and a fallback (`F`), it returns `T[K]` if
|
|
420
|
+
* `Def extends ParamsDefinition<T>` and `K extends keyof T`, otherwise
|
|
421
|
+
* `F`. The codec chain composes calls to this helper.
|
|
422
|
+
*/
|
|
423
|
+
function emitResolveSegmentFieldHelper() {
|
|
424
|
+
return [
|
|
425
|
+
`type ${RESOLVE_SEGMENT_FIELD_TYPE_NAME}<Def, K extends string, F> =`,
|
|
426
|
+
` Def extends import('@timber-js/app/segment-params').ParamsDefinition<infer T>`,
|
|
427
|
+
` ? K extends keyof T ? T[K] : F`,
|
|
428
|
+
` : F;`
|
|
429
|
+
].join("\n");
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Build a TypeScript type expression that resolves a single param's
|
|
433
|
+
* codec by walking a chain of params.ts files in priority order.
|
|
434
|
+
*
|
|
435
|
+
* Each entry in the chain emits one application of the shared
|
|
436
|
+
* `_TimberResolveSegmentField` helper type. Composing N applications
|
|
437
|
+
* grows linearly with chain depth (O(N) characters), unlike an inline
|
|
438
|
+
* conditional that would duplicate the fallback in each branch and
|
|
439
|
+
* grow O(2^N).
|
|
440
|
+
*
|
|
441
|
+
* The closest match (position 0 in the chain) is checked first; if its
|
|
442
|
+
* `segmentParams` definition declares the key, its inferred type wins.
|
|
443
|
+
* Otherwise we fall through to the next entry, and finally to the
|
|
444
|
+
* provided fallback. See TIM-834.
|
|
445
|
+
*/
|
|
446
|
+
function buildCodecChainType(p, importBase, fallback) {
|
|
447
|
+
const files = p.codecFilePaths;
|
|
448
|
+
if (!files || files.length === 0) return fallback;
|
|
449
|
+
const key = JSON.stringify(p.name);
|
|
450
|
+
let inner = fallback;
|
|
451
|
+
for (let i = files.length - 1; i >= 0; i--) inner = `${RESOLVE_SEGMENT_FIELD_TYPE_NAME}<(typeof import('${codecImportPath(files[i], importBase)}'))['segmentParams'], ${key}, ${inner}>`;
|
|
452
|
+
return inner;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Format the searchParams type for a route entry.
|
|
456
|
+
*
|
|
457
|
+
* When a page.tsx (or params.ts) exports searchParams, we reference its
|
|
458
|
+
* inferred type via an import type. The import path is relative to
|
|
459
|
+
* `importBase` (the directory where the .d.ts will be written). When
|
|
460
|
+
* importBase is undefined, falls back to a bare relative path.
|
|
461
|
+
*/
|
|
462
|
+
function formatSearchParamsType(route, importBase) {
|
|
463
|
+
if (route.hasSearchParams && route.searchParamsPagePath) return `(typeof import('${codecImportPath(route.searchParamsPagePath, importBase)}'))['searchParams'] extends import('@timber-js/app/search-params').SearchParamsDefinition<infer T> ? T : never`;
|
|
464
|
+
return "{}";
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/routing/link-codegen.ts
|
|
468
|
+
/** Shared Link base-props type literal used in every emitted call signature. */
|
|
469
|
+
var LINK_BASE_PROPS_TYPE = "Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & { prefetch?: boolean; scroll?: boolean; preserveSearchParams?: true | string[]; onNavigate?: import('./client/link.js').OnNavigateHandler; children?: import('react').ReactNode }";
|
|
470
|
+
/**
|
|
471
|
+
* Build a TypeScript template literal pattern for a dynamic route.
|
|
472
|
+
* e.g. '/products/[id]' → '/products/${string}'
|
|
473
|
+
* '/blog/[...slug]' → '/blog/${string}'
|
|
474
|
+
* '/docs/[[...path]]' → '/docs/${string}' (also matches /docs)
|
|
475
|
+
* '/[org]/[repo]' → '/${string}/${string}'
|
|
476
|
+
*/
|
|
477
|
+
function buildResolvedPattern(route) {
|
|
478
|
+
return route.urlPath.split("/").map((part) => {
|
|
479
|
+
if (part.startsWith("[[...") && part.endsWith("]]")) return "${string}";
|
|
480
|
+
if (part.startsWith("[...") && part.endsWith("]")) return "${string}";
|
|
481
|
+
if (part.startsWith("[") && part.endsWith("]")) return "${string}";
|
|
482
|
+
return part;
|
|
483
|
+
}).join("/");
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Format the segmentParams type for Link overloads.
|
|
487
|
+
*
|
|
488
|
+
* Link params accept `string | number` for single dynamic segments
|
|
489
|
+
* (convenience — values are stringified at runtime). Catch-all and
|
|
490
|
+
* optional catch-all remain `string[]` / `string[] | undefined`.
|
|
491
|
+
*
|
|
492
|
+
* When the segment's params chain (TIM-834) declares a typed codec, the
|
|
493
|
+
* inferred type from the codec wins via a nested conditional.
|
|
494
|
+
*/
|
|
495
|
+
function formatLinkParamsType(params, importBase) {
|
|
496
|
+
if (params.length === 0) return "{}";
|
|
497
|
+
return `{ ${params.map((p) => {
|
|
498
|
+
const codecType = buildCodecChainType(p, importBase, p.type === "string" ? "string | number" : p.type);
|
|
499
|
+
return `${p.name}: ${codecType}`;
|
|
500
|
+
}).join("; ")} }`;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Catch-all call signatures for `<Link>` — external hrefs and computed
|
|
504
|
+
* `string` variables. Emitted from codegen in a SEPARATE
|
|
505
|
+
* `declare module` block (declared AFTER the per-route block) so the TS
|
|
506
|
+
* "later overload set ordered first" rule places these catch-all
|
|
507
|
+
* signatures ahead of per-route in resolution order, leaving per-route
|
|
508
|
+
* as the final overload whose error message is reported on failure.
|
|
509
|
+
*
|
|
510
|
+
* The conditional `string extends H ? ... : never` protection preserves
|
|
511
|
+
* TIM-624's guarantee that unknown internal path literals don't match
|
|
512
|
+
* the catch-all — typos like `<Link href="/typo" />` still error.
|
|
513
|
+
*/
|
|
514
|
+
function formatLinkCatchAllOverloads() {
|
|
515
|
+
const lines = [];
|
|
516
|
+
const baseProps = LINK_BASE_PROPS_TYPE;
|
|
517
|
+
const catchAllSearchParams = "{ definition: SearchParamsDefinition<Record<string, unknown>>; values: Record<string, unknown> } | Record<string, unknown>";
|
|
518
|
+
const externalHref = "`http://${string}` | `https://${string}` | `mailto:${string}` | `tel:${string}` | `ftp://${string}` | `//${string}` | `#${string}` | `?${string}`";
|
|
519
|
+
lines.push(" // Typed Link overloads — catch-all (block 2 / emitted second)");
|
|
520
|
+
lines.push(" interface LinkFunction {");
|
|
521
|
+
lines.push(` (props: ${baseProps} & {`);
|
|
522
|
+
lines.push(` href: ${externalHref}`);
|
|
523
|
+
lines.push(` segmentParams?: never`);
|
|
524
|
+
lines.push(` searchParams?: ${catchAllSearchParams}`);
|
|
525
|
+
lines.push(` }): import('react').JSX.Element`);
|
|
526
|
+
lines.push(` <H extends string>(`);
|
|
527
|
+
lines.push(` props: string extends H`);
|
|
528
|
+
lines.push(` ? ${baseProps} & {`);
|
|
529
|
+
lines.push(` href: H`);
|
|
530
|
+
lines.push(` segmentParams?: Record<string, string | number | string[]>`);
|
|
531
|
+
lines.push(` searchParams?: ${catchAllSearchParams}`);
|
|
532
|
+
lines.push(` }`);
|
|
533
|
+
lines.push(` : never`);
|
|
534
|
+
lines.push(` ): import('react').JSX.Element`);
|
|
535
|
+
lines.push(" }");
|
|
536
|
+
return lines;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Generate typed per-route Link call signatures via LinkFunction
|
|
540
|
+
* interface merging.
|
|
541
|
+
*
|
|
542
|
+
* TIM-835: This emits a SINGLE call signature whose props is a
|
|
543
|
+
* discriminated union keyed on `href`. TypeScript narrows the union by
|
|
544
|
+
* the literal `href` at the call site, then checks the rest of the
|
|
545
|
+
* props against the matched variant. When `segmentParams` or
|
|
546
|
+
* `searchParams` is wrong, TS reports the error against the matched
|
|
547
|
+
* variant — naming the user's actual `href` and the offending field.
|
|
548
|
+
*
|
|
549
|
+
* Before TIM-835, this function emitted N separate per-route overloads
|
|
550
|
+
* (one per route, sometimes two for dynamic routes). When ALL overloads
|
|
551
|
+
* failed, TS would pick an arbitrary failed overload (heuristically the
|
|
552
|
+
* "last tried") to render the diagnostic, often pointing at a route
|
|
553
|
+
* completely unrelated to the one the user wrote. The discriminated
|
|
554
|
+
* union sidesteps overload-resolution heuristics entirely.
|
|
555
|
+
*
|
|
556
|
+
* Open property shapes (`Record<string, unknown>` instead of `never`)
|
|
557
|
+
* for `segmentParams` on routes that don't declare them keep generic
|
|
558
|
+
* `LinkProps`-spreading wrappers compiling. (Aligned with TIM-833.)
|
|
559
|
+
*
|
|
560
|
+
* TIM-832: this function still emits the per-route block FIRST and the
|
|
561
|
+
* catch-all block follows, so per-route remains the LAST overload set in
|
|
562
|
+
* resolution order — the one TS reports against on failure. With a
|
|
563
|
+
* discriminated union there is only one call signature in this block,
|
|
564
|
+
* so the resolved-template-vs-pattern ordering reduces to placing the
|
|
565
|
+
* pattern variant before the resolved-template variant inside the union.
|
|
566
|
+
*/
|
|
567
|
+
function formatTypedLinkOverloads(routes, importBase) {
|
|
568
|
+
const lines = [];
|
|
569
|
+
const baseProps = LINK_BASE_PROPS_TYPE;
|
|
570
|
+
const variants = [];
|
|
571
|
+
for (const route of routes) {
|
|
572
|
+
const hasDynamicParams = route.params.length > 0;
|
|
573
|
+
const paramsProp = hasDynamicParams ? `segmentParams: ${formatLinkParamsType(route.params, importBase)}` : "segmentParams?: Record<string, unknown>";
|
|
574
|
+
const searchParamsType = route.hasSearchParams ? formatSearchParamsType(route, importBase) : null;
|
|
575
|
+
const patternSearchParamsProp = searchParamsType ? `searchParams?: Partial<${searchParamsType}>` : "searchParams?: Record<string, never>";
|
|
576
|
+
variants.push(`${baseProps} & { href: '${route.urlPath}'; ${paramsProp}; ${patternSearchParamsProp} }`);
|
|
577
|
+
if (hasDynamicParams) {
|
|
578
|
+
const templatePattern = buildResolvedPattern(route);
|
|
579
|
+
if (templatePattern) {
|
|
580
|
+
const resolvedSearchParamsProp = searchParamsType ? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }` : "searchParams?: Record<string, never>";
|
|
581
|
+
variants.push(`${baseProps} & { href: \`${templatePattern}\`; segmentParams?: Record<string, never>; ${resolvedSearchParamsProp} }`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
lines.push(" interface LinkFunction {");
|
|
586
|
+
if (variants.length === 0) {
|
|
587
|
+
lines.push(" }");
|
|
588
|
+
return lines;
|
|
589
|
+
}
|
|
590
|
+
lines.push(" (");
|
|
591
|
+
lines.push(" props:");
|
|
592
|
+
for (const variant of variants) lines.push(` | (${variant})`);
|
|
593
|
+
lines.push(` ): import('react').JSX.Element`);
|
|
594
|
+
lines.push(" }");
|
|
595
|
+
return lines;
|
|
596
|
+
}
|
|
597
|
+
//#endregion
|
|
391
598
|
//#region src/routing/codegen.ts
|
|
392
599
|
/**
|
|
393
600
|
* Route map codegen.
|
|
@@ -406,7 +613,7 @@ function findPageExtFile(dirPath, name, extSet) {
|
|
|
406
613
|
*/
|
|
407
614
|
function generateRouteMap(tree, options = {}) {
|
|
408
615
|
const routes = [];
|
|
409
|
-
collectRoutes(tree.root, [], routes);
|
|
616
|
+
collectRoutes(tree.root, [], [], routes);
|
|
410
617
|
routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
411
618
|
return formatDeclarationFile(routes, options.outputDir ?? options.appDir);
|
|
412
619
|
}
|
|
@@ -416,22 +623,33 @@ function generateRouteMap(tree, options = {}) {
|
|
|
416
623
|
* A route entry is created for any segment that has a `page` or `route` file.
|
|
417
624
|
* Params accumulate from ancestor dynamic segments.
|
|
418
625
|
*/
|
|
419
|
-
function collectRoutes(node, ancestorParams, routes) {
|
|
626
|
+
function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes) {
|
|
627
|
+
const ownParamsFile = node.params && fileHasExport(node.params.filePath, "segmentParams") ? node.params.filePath : void 0;
|
|
420
628
|
const params = [...ancestorParams];
|
|
421
629
|
if (node.paramName) {
|
|
422
|
-
const
|
|
630
|
+
const legacyFallback = ownParamsFile ? void 0 : findLegacyParamsExport(node);
|
|
423
631
|
params.push({
|
|
424
632
|
name: node.paramName,
|
|
425
633
|
type: paramTypeForSegment(node.segmentType),
|
|
426
|
-
|
|
634
|
+
legacyCodecFilePath: legacyFallback
|
|
427
635
|
});
|
|
428
636
|
}
|
|
637
|
+
const nextAncestorFiles = ownParamsFile ? [...ancestorParamsFiles, ownParamsFile] : ancestorParamsFiles;
|
|
429
638
|
const isPage = !!node.page;
|
|
430
639
|
const isApiRoute = !!node.route;
|
|
431
640
|
if (isPage || isApiRoute) {
|
|
641
|
+
const leafFirstChain = nextAncestorFiles.length > 0 ? [...nextAncestorFiles].reverse() : [];
|
|
642
|
+
const resolvedParams = params.map((p) => {
|
|
643
|
+
const codecFilePaths = leafFirstChain.length > 0 ? leafFirstChain : p.legacyCodecFilePath ? [p.legacyCodecFilePath] : void 0;
|
|
644
|
+
return {
|
|
645
|
+
name: p.name,
|
|
646
|
+
type: p.type,
|
|
647
|
+
codecFilePaths
|
|
648
|
+
};
|
|
649
|
+
});
|
|
432
650
|
const entry = {
|
|
433
651
|
urlPath: node.urlPath,
|
|
434
|
-
params:
|
|
652
|
+
params: resolvedParams,
|
|
435
653
|
hasSearchParams: false,
|
|
436
654
|
isApiRoute
|
|
437
655
|
};
|
|
@@ -446,8 +664,8 @@ function collectRoutes(node, ancestorParams, routes) {
|
|
|
446
664
|
}
|
|
447
665
|
routes.push(entry);
|
|
448
666
|
}
|
|
449
|
-
for (const child of node.children) collectRoutes(child, params, routes);
|
|
450
|
-
for (const [, slot] of node.slots) collectRoutes(slot, params, routes);
|
|
667
|
+
for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, routes);
|
|
668
|
+
for (const [, slot] of node.slots) collectRoutes(slot, params, nextAncestorFiles, routes);
|
|
451
669
|
}
|
|
452
670
|
/**
|
|
453
671
|
* Determine the TypeScript type for a segment's param.
|
|
@@ -476,14 +694,13 @@ function fileHasExport(filePath, exportName) {
|
|
|
476
694
|
}
|
|
477
695
|
}
|
|
478
696
|
/**
|
|
479
|
-
* Find
|
|
697
|
+
* Find a legacy `segmentParams` export on layout.tsx or page.tsx.
|
|
480
698
|
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
699
|
+
* Backward-compat shim: TIM-508 made params.ts the canonical location
|
|
700
|
+
* for `segmentParams`. Layout/page exports are still accepted for the
|
|
701
|
+
* OWN segment only (not inherited by descendants — see TIM-834).
|
|
484
702
|
*/
|
|
485
|
-
function
|
|
486
|
-
if (node.params && fileHasExport(node.params.filePath, "segmentParams")) return node.params.filePath;
|
|
703
|
+
function findLegacyParamsExport(node) {
|
|
487
704
|
if (node.layout && fileHasExport(node.layout.filePath, "segmentParams")) return node.layout.filePath;
|
|
488
705
|
if (node.page && fileHasExport(node.page.filePath, "segmentParams")) return node.page.filePath;
|
|
489
706
|
}
|
|
@@ -497,6 +714,8 @@ function formatDeclarationFile(routes, importBase) {
|
|
|
497
714
|
lines.push("");
|
|
498
715
|
lines.push("export {};");
|
|
499
716
|
lines.push("");
|
|
717
|
+
lines.push(emitResolveSegmentFieldHelper());
|
|
718
|
+
lines.push("");
|
|
500
719
|
lines.push("declare module '@timber-js/app' {");
|
|
501
720
|
lines.push(" interface Routes {");
|
|
502
721
|
for (const route of routes) {
|
|
@@ -550,59 +769,11 @@ function formatDeclarationFile(routes, importBase) {
|
|
|
550
769
|
function formatParamsType(params, importBase) {
|
|
551
770
|
if (params.length === 0) return "{}";
|
|
552
771
|
return `{ ${params.map((p) => {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
let importPath;
|
|
556
|
-
if (importBase) importPath = "./" + relative(importBase, absPath).replace(/\\/g, "/");
|
|
557
|
-
else importPath = "./" + posix.basename(absPath);
|
|
558
|
-
const codecType = `(typeof import('${importPath}'))['segmentParams'] extends import('@timber-js/app/segment-params').ParamsDefinition<infer T> ? T[${JSON.stringify(p.name)}] : ${p.type}`;
|
|
559
|
-
return `${p.name}: ${codecType}`;
|
|
560
|
-
}
|
|
561
|
-
return `${p.name}: ${p.type}`;
|
|
562
|
-
}).join("; ")} }`;
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Format the params type for Link overloads.
|
|
566
|
-
*
|
|
567
|
-
* Link params accept `string | number` for single dynamic segments
|
|
568
|
-
* (convenience — values are stringified at runtime). Catch-all and
|
|
569
|
-
* optional catch-all remain `string[]` / `string[] | undefined`.
|
|
570
|
-
*
|
|
571
|
-
* Uses DIRECT types (not conditional) to preserve excess property checking.
|
|
572
|
-
*/
|
|
573
|
-
function formatLinkParamsType(params, importBase) {
|
|
574
|
-
if (params.length === 0) return "{}";
|
|
575
|
-
return `{ ${params.map((p) => {
|
|
576
|
-
if (p.codecFilePath) {
|
|
577
|
-
const absPath = p.codecFilePath.replace(/\.(ts|tsx)$/, "");
|
|
578
|
-
let importPath;
|
|
579
|
-
if (importBase) importPath = "./" + relative(importBase, absPath).replace(/\\/g, "/");
|
|
580
|
-
else importPath = "./" + posix.basename(absPath);
|
|
581
|
-
const codecType = `(typeof import('${importPath}'))['segmentParams'] extends import('@timber-js/app/segment-params').ParamsDefinition<infer T> ? T[${JSON.stringify(p.name)}] : ${p.type}`;
|
|
582
|
-
return `${p.name}: ${codecType}`;
|
|
583
|
-
}
|
|
584
|
-
const type = p.type === "string" ? "string | number" : p.type;
|
|
585
|
-
return `${p.name}: ${type}`;
|
|
772
|
+
const codecType = buildCodecChainType(p, importBase, p.type);
|
|
773
|
+
return `${p.name}: ${codecType}`;
|
|
586
774
|
}).join("; ")} }`;
|
|
587
775
|
}
|
|
588
776
|
/**
|
|
589
|
-
* Format the searchParams type for a route entry.
|
|
590
|
-
*
|
|
591
|
-
* When a page.tsx exports searchParams, we reference its inferred type via an import type.
|
|
592
|
-
* The import path is relative to `importBase` (the directory where the .d.ts will be
|
|
593
|
-
* written). When importBase is undefined, falls back to a bare relative path.
|
|
594
|
-
*/
|
|
595
|
-
function formatSearchParamsType(route, importBase) {
|
|
596
|
-
if (route.hasSearchParams && route.searchParamsPagePath) {
|
|
597
|
-
const absPath = route.searchParamsPagePath.replace(/\.(ts|tsx)$/, "");
|
|
598
|
-
let importPath;
|
|
599
|
-
if (importBase) importPath = "./" + relative(importBase, absPath).replace(/\\/g, "/");
|
|
600
|
-
else importPath = "./" + posix.basename(absPath);
|
|
601
|
-
return `(typeof import('${importPath}'))['searchParams'] extends import('@timber-js/app/search-params').SearchParamsDefinition<infer T> ? T : never`;
|
|
602
|
-
}
|
|
603
|
-
return "{}";
|
|
604
|
-
}
|
|
605
|
-
/**
|
|
606
777
|
* Generate useQueryStates overloads.
|
|
607
778
|
*
|
|
608
779
|
* For each page route:
|
|
@@ -620,102 +791,6 @@ function formatUseQueryStatesOverloads(routes, importBase) {
|
|
|
620
791
|
lines.push(" export function useQueryStates<T extends Record<string, unknown>>(codecs: { [K in keyof T]: SearchParamCodec<T[K]> }, options?: QueryStatesOptions): [T, SetParams<T>]");
|
|
621
792
|
return lines;
|
|
622
793
|
}
|
|
623
|
-
/**
|
|
624
|
-
* Build a TypeScript template literal pattern for a dynamic route.
|
|
625
|
-
* e.g. '/products/[id]' → '/products/${string}'
|
|
626
|
-
* '/blog/[...slug]' → '/blog/${string}'
|
|
627
|
-
* '/docs/[[...path]]' → '/docs/${string}' (also matches /docs)
|
|
628
|
-
* '/[org]/[repo]' → '/${string}/${string}'
|
|
629
|
-
*/
|
|
630
|
-
function buildResolvedPattern(route) {
|
|
631
|
-
return route.urlPath.split("/").map((part) => {
|
|
632
|
-
if (part.startsWith("[[...") && part.endsWith("]]")) return "${string}";
|
|
633
|
-
if (part.startsWith("[...") && part.endsWith("]")) return "${string}";
|
|
634
|
-
if (part.startsWith("[") && part.endsWith("]")) return "${string}";
|
|
635
|
-
return part;
|
|
636
|
-
}).join("/");
|
|
637
|
-
}
|
|
638
|
-
/** Shared Link base-props type literal used in every emitted call signature. */
|
|
639
|
-
var LINK_BASE_PROPS_TYPE = "Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & { prefetch?: boolean; scroll?: boolean; preserveSearchParams?: true | string[]; onNavigate?: import('./client/link.js').OnNavigateHandler; children?: import('react').ReactNode }";
|
|
640
|
-
/**
|
|
641
|
-
* TIM-832: Catch-all call signatures for `<Link>` — external hrefs and
|
|
642
|
-
* computed `string` variables. Emitted from codegen in a SEPARATE
|
|
643
|
-
* `declare module` block (declared AFTER the per-route block) so the TS
|
|
644
|
-
* "later overload set ordered first" rule places these catch-all
|
|
645
|
-
* signatures ahead of per-route in resolution order, leaving per-route
|
|
646
|
-
* as the final overload whose error message is reported on failure.
|
|
647
|
-
*
|
|
648
|
-
* The conditional `string extends H ? ... : never` protection preserves
|
|
649
|
-
* TIM-624's guarantee that unknown internal path literals don't match
|
|
650
|
-
* the catch-all — typos like `<Link href="/typo" />` still error.
|
|
651
|
-
*/
|
|
652
|
-
function formatLinkCatchAllOverloads() {
|
|
653
|
-
const lines = [];
|
|
654
|
-
const baseProps = LINK_BASE_PROPS_TYPE;
|
|
655
|
-
const catchAllSearchParams = "{ definition: SearchParamsDefinition<Record<string, unknown>>; values: Record<string, unknown> } | Record<string, unknown>";
|
|
656
|
-
const externalHref = "`http://${string}` | `https://${string}` | `mailto:${string}` | `tel:${string}` | `ftp://${string}` | `//${string}` | `#${string}` | `?${string}`";
|
|
657
|
-
lines.push(" // Typed Link overloads — catch-all (block 2 / emitted second)");
|
|
658
|
-
lines.push(" interface LinkFunction {");
|
|
659
|
-
lines.push(` (props: ${baseProps} & {`);
|
|
660
|
-
lines.push(` href: ${externalHref}`);
|
|
661
|
-
lines.push(` segmentParams?: never`);
|
|
662
|
-
lines.push(` searchParams?: ${catchAllSearchParams}`);
|
|
663
|
-
lines.push(` }): import('react').JSX.Element`);
|
|
664
|
-
lines.push(` <H extends string>(`);
|
|
665
|
-
lines.push(` props: string extends H`);
|
|
666
|
-
lines.push(` ? ${baseProps} & {`);
|
|
667
|
-
lines.push(` href: H`);
|
|
668
|
-
lines.push(` segmentParams?: Record<string, string | number | string[]>`);
|
|
669
|
-
lines.push(` searchParams?: ${catchAllSearchParams}`);
|
|
670
|
-
lines.push(` }`);
|
|
671
|
-
lines.push(` : never`);
|
|
672
|
-
lines.push(` ): import('react').JSX.Element`);
|
|
673
|
-
lines.push(" }");
|
|
674
|
-
return lines;
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Generate typed per-route Link call signatures via LinkFunction
|
|
678
|
-
* interface merging.
|
|
679
|
-
*
|
|
680
|
-
* Each call signature uses DIRECT types (not conditional types) for
|
|
681
|
-
* segmentParams, preserving TypeScript's excess property checking.
|
|
682
|
-
* Interface merging is the only reliable way to add "overloads" via
|
|
683
|
-
* module augmentation — function overloads can't be augmented.
|
|
684
|
-
*
|
|
685
|
-
* TIM-832: this function emits ONLY the per-route block. The catch-all
|
|
686
|
-
* block is emitted separately by `formatLinkCatchAllOverloads` inside a
|
|
687
|
-
* second `declare module` so TS overload ordering reports per-route
|
|
688
|
-
* errors (see `formatDeclarationFile`).
|
|
689
|
-
*/
|
|
690
|
-
function formatTypedLinkOverloads(routes, importBase) {
|
|
691
|
-
const lines = [];
|
|
692
|
-
const baseProps = LINK_BASE_PROPS_TYPE;
|
|
693
|
-
lines.push(" interface LinkFunction {");
|
|
694
|
-
for (const route of routes) {
|
|
695
|
-
const hasDynamicParams = route.params.length > 0;
|
|
696
|
-
const paramsProp = hasDynamicParams ? `segmentParams: ${formatLinkParamsType(route.params, importBase)}` : "segmentParams?: never";
|
|
697
|
-
const searchParamsType = route.hasSearchParams ? formatSearchParamsType(route, importBase) : null;
|
|
698
|
-
const patternSearchParamsProp = searchParamsType ? `searchParams?: Partial<${searchParamsType}>` : "searchParams?: never";
|
|
699
|
-
if (hasDynamicParams) {
|
|
700
|
-
const templatePattern = buildResolvedPattern(route);
|
|
701
|
-
if (templatePattern) {
|
|
702
|
-
const resolvedSearchParamsProp = searchParamsType ? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }` : "searchParams?: never";
|
|
703
|
-
lines.push(` (props: ${baseProps} & {`);
|
|
704
|
-
lines.push(` href: \`${templatePattern}\``);
|
|
705
|
-
lines.push(` segmentParams?: never`);
|
|
706
|
-
lines.push(` ${resolvedSearchParamsProp}`);
|
|
707
|
-
lines.push(` }): import('react').JSX.Element`);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
lines.push(` (props: ${baseProps} & {`);
|
|
711
|
-
lines.push(` href: '${route.urlPath}'`);
|
|
712
|
-
lines.push(` ${paramsProp}`);
|
|
713
|
-
lines.push(` ${patternSearchParamsProp}`);
|
|
714
|
-
lines.push(` }): import('react').JSX.Element`);
|
|
715
|
-
}
|
|
716
|
-
lines.push(" }");
|
|
717
|
-
return lines;
|
|
718
|
-
}
|
|
719
794
|
//#endregion
|
|
720
795
|
//#region src/routing/interception.ts
|
|
721
796
|
/**
|
|
@@ -795,4 +870,4 @@ function computeInterceptedBase(parentUrlPath, marker) {
|
|
|
795
870
|
//#endregion
|
|
796
871
|
export { DEFAULT_PAGE_EXTENSIONS as a, scanRoutes as i, generateRouteMap as n, INTERCEPTION_MARKERS as o, classifySegment as r, collectInterceptionRewrites as t };
|
|
797
872
|
|
|
798
|
-
//# sourceMappingURL=interception-
|
|
873
|
+
//# sourceMappingURL=interception-DSv3A2Zn.js.map
|