@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.
- package/dist/_chunks/{interception-Dpn_UfAD.js → interception-DRlhJWbu.js} +61 -13
- package/dist/_chunks/{interception-Dpn_UfAD.js.map → interception-DRlhJWbu.js.map} +1 -1
- package/dist/client/browser-entry/index.d.ts +1 -1
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/index.d.ts +0 -41
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +20 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +10 -24
- package/dist/client/link.d.ts.map +1 -1
- package/dist/index.js +76 -6
- package/dist/index.js.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/server/rsc-entry/index.d.ts +1 -0
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +1 -0
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/cli.ts +0 -0
- package/src/client/browser-entry/index.ts +5 -0
- package/src/client/index.ts +41 -71
- package/src/client/link.tsx +64 -17
- package/src/plugins/routing.ts +117 -8
- package/src/routing/codegen.ts +128 -20
- package/src/server/rsc-entry/index.ts +5 -0
- package/src/server/ssr-entry.ts +4 -0
- package/LICENSE +0 -8
package/src/routing/codegen.ts
CHANGED
|
@@ -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
|
-
//
|
|
251
|
-
//
|
|
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
|
|
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
|
|
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
|
-
*
|
|
412
|
-
*
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
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(` ${
|
|
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
|
package/src/server/ssr-entry.ts
CHANGED
|
@@ -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.
|