@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/plugins/routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/plugins/routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAiBlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AA0E1D,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAmLxD"}
|
package/dist/routing/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { t as classifyUrlSegment } from "../_chunks/segment-classify-BDNn6EzD.js";
|
|
2
|
-
import { a as DEFAULT_PAGE_EXTENSIONS, i as scanRoutes, n as generateRouteMap, o as INTERCEPTION_MARKERS, r as classifySegment, t as collectInterceptionRewrites } from "../_chunks/interception-
|
|
2
|
+
import { a as DEFAULT_PAGE_EXTENSIONS, i as scanRoutes, n as generateRouteMap, o as INTERCEPTION_MARKERS, r as classifySegment, t as collectInterceptionRewrites } from "../_chunks/interception-DRlhJWbu.js";
|
|
3
3
|
export { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS, classifySegment, classifyUrlSegment, collectInterceptionRewrites, generateRouteMap, scanRoutes };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AAuBA,OAAO,uCAAuC,CAAC;AA0C/C,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAoCtB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,mBAAmB,EAAE,KAAK,IAAI,GACtF,IAAI,CAEN;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAE5E;AAqjBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAInE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;8BA5SrC,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA8ShD,wBAAiE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,uCAAuC,CAAC;AA2E/C;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE;QACZ,gFAAgF;QAChF,QAAQ,EAAE,MAAM,CAAC;QACjB,uDAAuD;QACvD,OAAO,EAAE,MAAM,CAAC;QAChB,gEAAgE;QAChE,MAAM,EAAE,MAAM,CAAC;QACf,wEAAwE;QACxE,UAAU,EAAE,MAAM,CAAC;QACnB,+CAA+C;QAC/C,OAAO,EAAE,MAAM,CAAC;QAChB,sDAAsD;QACtD,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAuLnB;AAED,eAAe,SAAS,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.90",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -110,6 +110,11 @@
|
|
|
110
110
|
"publishConfig": {
|
|
111
111
|
"access": "public"
|
|
112
112
|
},
|
|
113
|
+
"scripts": {
|
|
114
|
+
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
115
|
+
"typecheck": "tsgo --noEmit",
|
|
116
|
+
"prepublishOnly": "pnpm run build"
|
|
117
|
+
},
|
|
113
118
|
"dependencies": {
|
|
114
119
|
"@opentelemetry/api": "^1.9.1",
|
|
115
120
|
"@opentelemetry/context-async-hooks": "^2.6.1",
|
|
@@ -152,9 +157,5 @@
|
|
|
152
157
|
},
|
|
153
158
|
"engines": {
|
|
154
159
|
"node": ">=22.12.0"
|
|
155
|
-
},
|
|
156
|
-
"scripts": {
|
|
157
|
-
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
158
|
-
"typecheck": "tsgo --noEmit"
|
|
159
160
|
}
|
|
160
|
-
}
|
|
161
|
+
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
|
|
30
30
|
// @ts-expect-error — virtual module provided by timber-entries plugin
|
|
31
31
|
import config from 'virtual:timber-config';
|
|
32
|
+
// TIM-830: Populate the search-params registry eagerly so <Link> on the
|
|
33
|
+
// client can serialize flat `Partial<T>` values synchronously during
|
|
34
|
+
// render and click handlers. Side-effect-only import.
|
|
35
|
+
// @ts-expect-error — virtual module provided by timber-routing plugin
|
|
36
|
+
import 'virtual:timber-search-params-registry';
|
|
32
37
|
|
|
33
38
|
import { setClientDeploymentId } from '../rsc-fetch.js';
|
|
34
39
|
import {
|
package/src/client/index.ts
CHANGED
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
export type { RenderErrorDigest } from './types';
|
|
11
11
|
|
|
12
12
|
// Navigation
|
|
13
|
-
import type { JSX } from 'react';
|
|
14
|
-
import type { SearchParamsDefinition } from '../search-params/define.js';
|
|
15
|
-
import type { LinkBaseProps } from './link';
|
|
16
13
|
export { Link, interpolateParams, resolveHref, validateLinkHref, buildLinkProps } from './link';
|
|
17
14
|
export { mergePreservedSearchParams } from '../shared/merge-search-params.js';
|
|
18
15
|
export type { LinkProps, LinkPropsWithHref, LinkPropsWithParams, LinkBaseProps } from './link';
|
|
@@ -20,75 +17,48 @@ export type { LinkSegmentParams, OnNavigateHandler, OnNavigateEvent } from './li
|
|
|
20
17
|
|
|
21
18
|
// ─── LinkFunction (originally declared here for module augmentation) ───
|
|
22
19
|
//
|
|
23
|
-
// The codegen emits
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* match a known route from the codegen.
|
|
63
|
-
*/
|
|
20
|
+
// The codegen emits TWO augmentations of this interface:
|
|
21
|
+
//
|
|
22
|
+
// 1. Per-route call signatures — `href: '/products/[id]'` with direct
|
|
23
|
+
// `segmentParams`/`searchParams` types.
|
|
24
|
+
// 2. Catch-all call signatures — `ExternalHref` + computed `string`
|
|
25
|
+
// variable hrefs.
|
|
26
|
+
//
|
|
27
|
+
// Both live in the codegen output (NOT here). Overload ORDER matters
|
|
28
|
+
// for error UX, and TypeScript's declaration-merging rule "later overload
|
|
29
|
+
// sets ordered first" means that whichever augmentation block is
|
|
30
|
+
// declared LATER in the codegen file ends up FIRST in the merged call-
|
|
31
|
+
// signature list — and therefore the EARLIER block ends up LAST, which
|
|
32
|
+
// is the overload whose error message TypeScript reports when no
|
|
33
|
+
// overload matches.
|
|
34
|
+
//
|
|
35
|
+
// We want the PER-ROUTE overloads to be the error-reporting ones when
|
|
36
|
+
// a user passes a known literal `href` with a mistyped segmentParam.
|
|
37
|
+
// So the codegen emits them FIRST and the catch-all SECOND. That way
|
|
38
|
+
// a call like:
|
|
39
|
+
//
|
|
40
|
+
// <Link href="/products/[id]" segmentParams={{ id: maybeUndef }} />
|
|
41
|
+
//
|
|
42
|
+
// reports the actual mismatch (`'string | undefined' is not assignable
|
|
43
|
+
// to 'string | number'` on `id`) instead of the notorious "Type 'string'
|
|
44
|
+
// is not assignable to type 'never'" cascade that the old fixed
|
|
45
|
+
// catch-all produced (TIM-832, sibling of TIM-830 / TIM-624).
|
|
46
|
+
//
|
|
47
|
+
// The interface is kept as an empty placeholder here so:
|
|
48
|
+
// - module augmentation has a base interface to merge into
|
|
49
|
+
// - `link.tsx` can `import type { LinkFunction }` and use it as the
|
|
50
|
+
// cast target for the `Link` const (an empty interface accepts any
|
|
51
|
+
// non-nullable value, so the cast is valid even without codegen run)
|
|
52
|
+
// - NO call signatures live here — in a fresh checkout where codegen
|
|
53
|
+
// hasn't run yet, invoking `<Link>` is deliberately a type error,
|
|
54
|
+
// which surfaces "codegen hasn't run" rather than a misleading
|
|
55
|
+
// structural type mismatch.
|
|
56
|
+
//
|
|
57
|
+
// See TIM-624 (original typed Link), TIM-830 (flat searchParams), and
|
|
58
|
+
// TIM-832 (this restructure for overload error UX).
|
|
64
59
|
export interface LinkFunction {
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
props: LinkBaseProps & {
|
|
68
|
-
href: ExternalHref;
|
|
69
|
-
segmentParams?: never;
|
|
70
|
-
searchParams?: {
|
|
71
|
-
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
72
|
-
values: Record<string, unknown>;
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
): JSX.Element;
|
|
76
|
-
// Computed/variable href (non-literal string) — e.g. href={myVar}
|
|
77
|
-
// `string extends H` is true only when H is the wide `string` type,
|
|
78
|
-
// not a specific literal. Template literal hrefs like `/blog/${slug}`
|
|
79
|
-
// are handled by resolved-pattern signatures in the codegen.
|
|
80
|
-
<H extends string>(
|
|
81
|
-
props: string extends H
|
|
82
|
-
? LinkBaseProps & {
|
|
83
|
-
href: H;
|
|
84
|
-
segmentParams?: Record<string, string | number | string[]>;
|
|
85
|
-
searchParams?: {
|
|
86
|
-
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
87
|
-
values: Record<string, unknown>;
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
: never
|
|
91
|
-
): JSX.Element;
|
|
60
|
+
// intentionally empty — call signatures are provided via codegen
|
|
61
|
+
// augmentation in `.timber/timber-routes.d.ts`.
|
|
92
62
|
}
|
|
93
63
|
export { usePendingNavigation } from './use-pending-navigation';
|
|
94
64
|
export { useLinkStatus, LinkStatusContext } from './use-link-status';
|
package/src/client/link.tsx
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type MouseEvent as ReactMouseEvent,
|
|
28
28
|
} from 'react';
|
|
29
29
|
import type { SearchParamsDefinition } from '../search-params/define.js';
|
|
30
|
+
import { getSearchParamsDefinition } from '../search-params/registry.js';
|
|
30
31
|
import type { LinkFunction } from './index.js';
|
|
31
32
|
import { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.js';
|
|
32
33
|
import { LinkStatusContext } from './use-link-status.js';
|
|
@@ -128,6 +129,26 @@ export type LinkSegmentParams<T> = {
|
|
|
128
129
|
// augmentation merging — only originally-declared interfaces do.
|
|
129
130
|
// See TIM-624.
|
|
130
131
|
|
|
132
|
+
// ─── searchParams prop shapes ────────────────────────────────────
|
|
133
|
+
//
|
|
134
|
+
// Per-route Link overloads (emitted by codegen) pass the flat values shape:
|
|
135
|
+
// searchParams={{ page: 2, q: 'boots' }}
|
|
136
|
+
// The framework looks up the route's SearchParamsDefinition from the
|
|
137
|
+
// search-params registry at runtime (see TIM-830).
|
|
138
|
+
//
|
|
139
|
+
// The catch-all overload in client/index.ts (external/computed hrefs)
|
|
140
|
+
// additionally accepts the legacy wrapped shape:
|
|
141
|
+
// searchParams={{ definition: def, values: { page: 2 } }}
|
|
142
|
+
// because there is no way to look up a definition from a computed string.
|
|
143
|
+
//
|
|
144
|
+
// `resolveHref` discriminates at runtime by presence of a `definition` key.
|
|
145
|
+
type WrappedSearchParamsProp = {
|
|
146
|
+
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
147
|
+
values: Record<string, unknown>;
|
|
148
|
+
};
|
|
149
|
+
type FlatSearchParamsProp = Record<string, unknown>;
|
|
150
|
+
type LinkSearchParamsProp = WrappedSearchParamsProp | FlatSearchParamsProp;
|
|
151
|
+
|
|
131
152
|
/**
|
|
132
153
|
* Runtime-only loose props used internally by the Link implementation.
|
|
133
154
|
* Not exposed to callers — the public API uses LinkFunction.
|
|
@@ -135,20 +156,14 @@ export type LinkSegmentParams<T> = {
|
|
|
135
156
|
interface LinkRuntimeProps extends LinkBaseProps {
|
|
136
157
|
href: string;
|
|
137
158
|
segmentParams?: Record<string, string | number | string[]>;
|
|
138
|
-
searchParams?:
|
|
139
|
-
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
140
|
-
values: Record<string, unknown>;
|
|
141
|
-
};
|
|
159
|
+
searchParams?: LinkSearchParamsProp;
|
|
142
160
|
}
|
|
143
161
|
|
|
144
162
|
// Legacy exports for backward compat (used by buildLinkProps, tests, etc.)
|
|
145
163
|
export type LinkPropsWithHref = LinkBaseProps & {
|
|
146
164
|
href: string;
|
|
147
165
|
segmentParams?: never;
|
|
148
|
-
searchParams?:
|
|
149
|
-
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
150
|
-
values: Record<string, unknown>;
|
|
151
|
-
};
|
|
166
|
+
searchParams?: LinkSearchParamsProp;
|
|
152
167
|
};
|
|
153
168
|
export type LinkPropsWithParams = LinkRuntimeProps & {
|
|
154
169
|
segmentParams: Record<string, string | number | string[]>;
|
|
@@ -296,13 +311,19 @@ export function interpolateParams(
|
|
|
296
311
|
* - searchParams serialization via SearchParamsDefinition
|
|
297
312
|
* - Validation that searchParams and inline query strings are exclusive
|
|
298
313
|
*/
|
|
314
|
+
/**
|
|
315
|
+
* Runtime discriminator: treat `searchParams` as the legacy wrapped shape
|
|
316
|
+
* only when it literally has a `definition` key. Everything else is the
|
|
317
|
+
* flat `Partial<T>` values shape (TIM-830).
|
|
318
|
+
*/
|
|
319
|
+
function isWrappedSearchParamsProp(sp: LinkSearchParamsProp): sp is WrappedSearchParamsProp {
|
|
320
|
+
return 'definition' in sp;
|
|
321
|
+
}
|
|
322
|
+
|
|
299
323
|
export function resolveHref(
|
|
300
324
|
href: string,
|
|
301
325
|
params?: Record<string, string | number | string[]>,
|
|
302
|
-
searchParams?:
|
|
303
|
-
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
304
|
-
values: Record<string, unknown>;
|
|
305
|
-
}
|
|
326
|
+
searchParams?: LinkSearchParamsProp
|
|
306
327
|
): string {
|
|
307
328
|
let resolvedPath = href;
|
|
308
329
|
|
|
@@ -321,7 +342,36 @@ export function resolveHref(
|
|
|
321
342
|
);
|
|
322
343
|
}
|
|
323
344
|
|
|
324
|
-
|
|
345
|
+
let definition: SearchParamsDefinition<Record<string, unknown>> | undefined;
|
|
346
|
+
let values: Record<string, unknown>;
|
|
347
|
+
|
|
348
|
+
if (isWrappedSearchParamsProp(searchParams)) {
|
|
349
|
+
// Legacy wrapped shape — used by the catch-all overload for
|
|
350
|
+
// computed/external hrefs where no route lookup is possible.
|
|
351
|
+
definition = searchParams.definition;
|
|
352
|
+
values = searchParams.values;
|
|
353
|
+
} else {
|
|
354
|
+
// Flat shape (TIM-830): look up the definition from the runtime
|
|
355
|
+
// registry using the un-interpolated href pattern (e.g. '/products/[id]').
|
|
356
|
+
// The search-params registry is populated eagerly at startup by the
|
|
357
|
+
// virtual:timber-search-params-registry module generated by the
|
|
358
|
+
// timber-routing Vite plugin.
|
|
359
|
+
definition = getSearchParamsDefinition(href) as
|
|
360
|
+
| SearchParamsDefinition<Record<string, unknown>>
|
|
361
|
+
| undefined;
|
|
362
|
+
values = searchParams;
|
|
363
|
+
if (!definition) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`<Link> received a flat searchParams object for href "${href}", but no ` +
|
|
366
|
+
`SearchParamsDefinition is registered for that route. ` +
|
|
367
|
+
`Either the route does not export \`searchParams\` from its \`params.ts\`/\`page.tsx\`, ` +
|
|
368
|
+
`or the search-params registry module was not loaded. ` +
|
|
369
|
+
`For external or computed hrefs, pass the legacy \`{ definition, values }\` shape instead.`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const qs = definition.serialize(values);
|
|
325
375
|
if (qs) {
|
|
326
376
|
resolvedPath = `${resolvedPath}?${qs}`;
|
|
327
377
|
}
|
|
@@ -343,10 +393,7 @@ interface LinkOutputProps {
|
|
|
343
393
|
export function buildLinkProps(
|
|
344
394
|
props: Pick<LinkPropsWithHref, 'href'> & {
|
|
345
395
|
params?: Record<string, string | number | string[]>;
|
|
346
|
-
searchParams?:
|
|
347
|
-
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
348
|
-
values: Record<string, unknown>;
|
|
349
|
-
};
|
|
396
|
+
searchParams?: LinkSearchParamsProp;
|
|
350
397
|
}
|
|
351
398
|
): LinkOutputProps {
|
|
352
399
|
const resolvedHref = resolveHref(props.href, props.params, props.searchParams);
|
package/src/plugins/routing.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Plugin, ViteDevServer } from 'vite';
|
|
12
12
|
import { writeFile, mkdir } from 'node:fs/promises';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
14
|
import { join } from 'node:path';
|
|
14
15
|
import { scanRoutes } from '../routing/scanner.js';
|
|
15
16
|
import { generateRouteMap } from '../routing/codegen.js';
|
|
@@ -29,6 +30,21 @@ import type { PluginContext } from '../plugin-context.js';
|
|
|
29
30
|
const VIRTUAL_MODULE_ID = 'virtual:timber-route-manifest';
|
|
30
31
|
const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_MODULE_ID}`;
|
|
31
32
|
|
|
33
|
+
// TIM-830: Search-params registry virtual module.
|
|
34
|
+
//
|
|
35
|
+
// Statically imports the `searchParams` named export from every page
|
|
36
|
+
// that exports one, and registers each definition into the shared
|
|
37
|
+
// search-params registry keyed by the un-interpolated route pattern
|
|
38
|
+
// (e.g. '/products/[id]'). This lets `<Link>` serialize flat
|
|
39
|
+
// `Partial<T>` values at runtime without callers importing the
|
|
40
|
+
// definition at the call site.
|
|
41
|
+
//
|
|
42
|
+
// The imports are STATIC (`import { searchParams as r0 } from '...'`)
|
|
43
|
+
// so Vite/Rolldown tree-shakes each page's component body out of the
|
|
44
|
+
// registry chunk — only the `searchParams` named export is pulled in.
|
|
45
|
+
const SEARCH_PARAMS_REGISTRY_ID = 'virtual:timber-search-params-registry';
|
|
46
|
+
const RESOLVED_SEARCH_PARAMS_REGISTRY_ID = `\0${SEARCH_PARAMS_REGISTRY_ID}`;
|
|
47
|
+
|
|
32
48
|
/**
|
|
33
49
|
* File convention names we track for changes that require manifest regeneration.
|
|
34
50
|
*/
|
|
@@ -155,6 +171,14 @@ export function timberRouting(ctx: PluginContext): Plugin {
|
|
|
155
171
|
return RESOLVED_VIRTUAL_ID;
|
|
156
172
|
}
|
|
157
173
|
|
|
174
|
+
// TIM-830: Search-params registry virtual module
|
|
175
|
+
if (
|
|
176
|
+
cleanId === SEARCH_PARAMS_REGISTRY_ID ||
|
|
177
|
+
cleanId.endsWith(`/${SEARCH_PARAMS_REGISTRY_ID}`)
|
|
178
|
+
) {
|
|
179
|
+
return RESOLVED_SEARCH_PARAMS_REGISTRY_ID;
|
|
180
|
+
}
|
|
181
|
+
|
|
158
182
|
return null;
|
|
159
183
|
},
|
|
160
184
|
|
|
@@ -165,14 +189,22 @@ export function timberRouting(ctx: PluginContext): Plugin {
|
|
|
165
189
|
* with absolute import paths for all file references.
|
|
166
190
|
*/
|
|
167
191
|
load(id: string) {
|
|
168
|
-
if (id
|
|
192
|
+
if (id === RESOLVED_VIRTUAL_ID) {
|
|
193
|
+
// If routeTree hasn't been built yet (shouldn't happen), scan now
|
|
194
|
+
if (!ctx.routeTree) {
|
|
195
|
+
rescan();
|
|
196
|
+
}
|
|
197
|
+
return generateManifestModule(ctx.routeTree!, ctx.root);
|
|
198
|
+
}
|
|
169
199
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
200
|
+
if (id === RESOLVED_SEARCH_PARAMS_REGISTRY_ID) {
|
|
201
|
+
if (!ctx.routeTree) {
|
|
202
|
+
rescan();
|
|
203
|
+
}
|
|
204
|
+
return generateSearchParamsRegistryModule(ctx.routeTree!);
|
|
173
205
|
}
|
|
174
206
|
|
|
175
|
-
return
|
|
207
|
+
return null;
|
|
176
208
|
},
|
|
177
209
|
|
|
178
210
|
/**
|
|
@@ -259,9 +291,11 @@ function invalidateManifest(server: ViteDevServer): void {
|
|
|
259
291
|
const env = server.environments[envName];
|
|
260
292
|
if (!env?.moduleGraph) continue;
|
|
261
293
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
294
|
+
for (const virtualId of [RESOLVED_VIRTUAL_ID, RESOLVED_SEARCH_PARAMS_REGISTRY_ID]) {
|
|
295
|
+
const mod = env.moduleGraph.getModuleById(virtualId);
|
|
296
|
+
if (mod) {
|
|
297
|
+
env.moduleGraph.invalidateModule(mod);
|
|
298
|
+
}
|
|
265
299
|
}
|
|
266
300
|
}
|
|
267
301
|
|
|
@@ -269,6 +303,81 @@ function invalidateManifest(server: ViteDevServer): void {
|
|
|
269
303
|
server.hot.send({ type: 'full-reload' });
|
|
270
304
|
}
|
|
271
305
|
|
|
306
|
+
/**
|
|
307
|
+
* TIM-830: Walk the route tree and collect every page that exports a
|
|
308
|
+
* `searchParams` definition, then emit a module that statically imports
|
|
309
|
+
* each export and registers it into the shared search-params registry.
|
|
310
|
+
*
|
|
311
|
+
* Keys use the un-interpolated route pattern (e.g. '/products/[id]') so
|
|
312
|
+
* <Link> can look up the definition from the original `href` prop before
|
|
313
|
+
* segmentParams are interpolated.
|
|
314
|
+
*/
|
|
315
|
+
function generateSearchParamsRegistryModule(tree: RouteTree): string {
|
|
316
|
+
const entries: Array<{ urlPath: string; filePath: string }> = [];
|
|
317
|
+
|
|
318
|
+
function walk(node: SegmentNode): void {
|
|
319
|
+
// params.ts is the canonical location for `export const searchParams`;
|
|
320
|
+
// page.tsx is the legacy fallback (see codegen.ts `collectRoutes`).
|
|
321
|
+
if (node.page) {
|
|
322
|
+
if (node.params && fileHasSearchParamsExport(node.params.filePath)) {
|
|
323
|
+
entries.push({ urlPath: node.urlPath, filePath: node.params.filePath });
|
|
324
|
+
} else if (fileHasSearchParamsExport(node.page.filePath)) {
|
|
325
|
+
entries.push({ urlPath: node.urlPath, filePath: node.page.filePath });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (const child of node.children) walk(child);
|
|
329
|
+
for (const [, slot] of node.slots) walk(slot);
|
|
330
|
+
}
|
|
331
|
+
walk(tree.root);
|
|
332
|
+
|
|
333
|
+
// Deterministic order for stable output / snapshots
|
|
334
|
+
entries.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
335
|
+
|
|
336
|
+
const lines: string[] = [];
|
|
337
|
+
lines.push('// Auto-generated search-params registry — do not edit.');
|
|
338
|
+
lines.push('// Generated by timber-routing plugin (TIM-830).');
|
|
339
|
+
lines.push('');
|
|
340
|
+
lines.push("import { registerSearchParams } from '@timber-js/app/search-params';");
|
|
341
|
+
lines.push('');
|
|
342
|
+
|
|
343
|
+
// Static imports — Vite/Rolldown tree-shakes the page component body out
|
|
344
|
+
// of this chunk, leaving only the `searchParams` named export.
|
|
345
|
+
entries.forEach((entry, i) => {
|
|
346
|
+
lines.push(`import { searchParams as r${i} } from ${JSON.stringify(entry.filePath)};`);
|
|
347
|
+
});
|
|
348
|
+
lines.push('');
|
|
349
|
+
|
|
350
|
+
entries.forEach((entry, i) => {
|
|
351
|
+
lines.push(`registerSearchParams(${JSON.stringify(entry.urlPath)}, r${i});`);
|
|
352
|
+
});
|
|
353
|
+
lines.push('');
|
|
354
|
+
|
|
355
|
+
// Exporting an empty object keeps the module a real ESM module even
|
|
356
|
+
// when there are zero entries (no top-level imports in that case).
|
|
357
|
+
lines.push('export {};');
|
|
358
|
+
lines.push('');
|
|
359
|
+
|
|
360
|
+
return lines.join('\n');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Lightweight check for a `searchParams` named export. Mirrors
|
|
365
|
+
* `fileHasExport('searchParams')` in routing/codegen.ts so we stay
|
|
366
|
+
* in sync with what the type codegen detects.
|
|
367
|
+
*/
|
|
368
|
+
function fileHasSearchParamsExport(filePath: string): boolean {
|
|
369
|
+
try {
|
|
370
|
+
if (!existsSync(filePath)) return false;
|
|
371
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
372
|
+
return (
|
|
373
|
+
/export\s+(const|let|var)\s+searchParams\b/.test(content) ||
|
|
374
|
+
/export\s*\{[^}]*\bsearchParams\b[^}]*\}/.test(content)
|
|
375
|
+
);
|
|
376
|
+
} catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
272
381
|
/**
|
|
273
382
|
* Generate the virtual module source code for the route manifest.
|
|
274
383
|
*
|