@timber-js/app 0.2.0-alpha.89 → 0.2.0-alpha.90

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.
@@ -247,10 +247,18 @@ function formatDeclarationFile(routes: RouteEntry[], importBase?: string): strin
247
247
 
248
248
  // Typed Link overloads — per-route with DIRECT types (no conditionals).
249
249
  // Direct types preserve TypeScript's excess property checking.
250
- // The catch-all overload (segmentParams?: never) lives in link.tsx.
251
- // See TIM-624.
250
+ //
251
+ // TIM-832: per-route and catch-all are emitted as TWO separate
252
+ // augmentation blocks. Per TS's merging rule "later overload sets
253
+ // ordered first", the catch-all block (declared SECOND in this file)
254
+ // ends up FIRST in the merged call-signature list at resolution time,
255
+ // which puts the per-route block LAST — so its error message is the
256
+ // one TypeScript reports when no overload matches. This gives users a
257
+ // clear prop-mismatch error (e.g. "'string | undefined' is not
258
+ // assignable to 'string | number' on id") instead of the old
259
+ // confusing "Type 'string' is not assignable to type 'never'" cascade.
252
260
  if (pageRoutes.length > 0) {
253
- lines.push(' // Typed Link overloads per route');
261
+ lines.push(' // Typed Link overloads per-route (block 1 / emitted first)');
254
262
  lines.push(...formatTypedLinkOverloads(pageRoutes, importBase));
255
263
  lines.push('');
256
264
  }
@@ -259,6 +267,17 @@ function formatDeclarationFile(routes: RouteEntry[], importBase?: string): strin
259
267
  lines.push('');
260
268
  }
261
269
 
270
+ // TIM-832: catch-all block — emitted as a SEPARATE `declare module`
271
+ // augmentation so TS's "later overload set first" rule orders it ahead
272
+ // of the per-route block above at resolution time, leaving per-route as
273
+ // the "last overload" whose error TypeScript reports.
274
+ lines.push("declare module '@timber-js/app/client' {");
275
+ lines.push(" import type { SearchParamsDefinition } from '@timber-js/app/search-params'");
276
+ lines.push('');
277
+ lines.push(...formatLinkCatchAllOverloads());
278
+ lines.push('}');
279
+ lines.push('');
280
+
262
281
  return lines.join('\n');
263
282
  }
264
283
 
@@ -400,21 +419,90 @@ function buildResolvedPattern(route: RouteEntry): string | null {
400
419
  return templateParts.join('/');
401
420
  }
402
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
+
403
489
  /**
404
- * Generate typed Link call signatures via LinkFunction interface merging.
490
+ * Generate typed per-route Link call signatures via LinkFunction
491
+ * interface merging.
405
492
  *
406
493
  * Each call signature uses DIRECT types (not conditional types) for
407
494
  * segmentParams, preserving TypeScript's excess property checking.
408
495
  * Interface merging is the only reliable way to add "overloads" via
409
496
  * module augmentation — function overloads can't be augmented.
410
497
  *
411
- * The catch-all call signature (external protocols + computed strings)
412
- * lives in link.tsx. See TIM-624.
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`).
413
502
  */
414
503
  function formatTypedLinkOverloads(routes: RouteEntry[], importBase?: string): string[] {
415
504
  const lines: string[] = [];
416
- const baseProps =
417
- "Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & { prefetch?: boolean; scroll?: boolean; preserveSearchParams?: true | string[]; onNavigate?: import('./client/link.js').OnNavigateHandler; children?: import('react').ReactNode }";
505
+ const baseProps = LINK_BASE_PROPS_TYPE;
418
506
 
419
507
  lines.push(' interface LinkFunction {');
420
508
  for (const route of routes) {
@@ -426,29 +514,49 @@ function formatTypedLinkOverloads(routes: RouteEntry[], importBase?: string): st
426
514
  const searchParamsType = route.hasSearchParams
427
515
  ? formatSearchParamsType(route, importBase)
428
516
  : null;
429
- const searchParamsProp = searchParamsType
430
- ? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }`
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}>`
431
522
  : 'searchParams?: never';
432
523
 
433
- lines.push(` (props: ${baseProps} & {`);
434
- lines.push(` href: '${route.urlPath}'`);
435
- lines.push(` ${paramsProp}`);
436
- lines.push(` ${searchParamsProp}`);
437
- lines.push(` }): import('react').JSX.Element`);
438
-
439
- // For dynamic routes, also emit a resolved-pattern signature.
440
- // This accepts template literal hrefs like `/products/${id}` without
441
- // segmentParams — the params are already interpolated into the URL.
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.
442
541
  if (hasDynamicParams) {
443
542
  const templatePattern = buildResolvedPattern(route);
444
543
  if (templatePattern) {
544
+ const resolvedSearchParamsProp = searchParamsType
545
+ ? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }`
546
+ : 'searchParams?: never';
445
547
  lines.push(` (props: ${baseProps} & {`);
446
548
  lines.push(` href: \`${templatePattern}\``);
447
549
  lines.push(` segmentParams?: never`);
448
- lines.push(` ${searchParamsProp}`);
550
+ lines.push(` ${resolvedSearchParamsProp}`);
449
551
  lines.push(` }): import('react').JSX.Element`);
450
552
  }
451
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`);
452
560
  }
453
561
  lines.push(' }');
454
562
 
@@ -17,6 +17,11 @@
17
17
 
18
18
  // @ts-expect-error — virtual module provided by timber-routing plugin
19
19
  import routeManifest from 'virtual:timber-route-manifest';
20
+ // TIM-830: Populate the search-params registry eagerly so <Link> can
21
+ // serialize flat `Partial<T>` values synchronously in RSC renders.
22
+ // The module has side effects only — no exports consumed here.
23
+ // @ts-expect-error — virtual module provided by timber-routing plugin
24
+ import 'virtual:timber-search-params-registry';
20
25
  // @ts-expect-error — virtual module provided by timber-entries plugin
21
26
  import config from 'virtual:timber-config';
22
27
  // @ts-expect-error — virtual module provided by timber-build-manifest plugin
@@ -14,6 +14,10 @@
14
14
 
15
15
  // @ts-expect-error — virtual module provided by timber-entries plugin
16
16
  import config from 'virtual:timber-config';
17
+ // TIM-830: Populate the search-params registry eagerly so <Link> rendered
18
+ // during SSR can serialize flat `Partial<T>` values via registry lookup.
19
+ // @ts-expect-error — virtual module provided by timber-routing plugin
20
+ import 'virtual:timber-search-params-registry';
17
21
  import {
18
22
  createFromReadableStream,
19
23
  createFromNodeStream,
package/LICENSE DELETED
@@ -1,8 +0,0 @@
1
- DONTFUCKINGUSE LICENSE
2
-
3
- Copyright (c) 2025 Daniel Saewitz
4
-
5
- This software may not be used, copied, modified, merged, published,
6
- distributed, sublicensed, or sold by anyone other than the copyright holder.
7
-
8
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.