@timber-js/app 0.2.0-alpha.34 → 0.2.0-alpha.36
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/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
- package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
- package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
- package/dist/_chunks/{debug-B3Gypr3D.js → debug-ECi_61pb.js} +1 -1
- package/dist/_chunks/{debug-B3Gypr3D.js.map → debug-ECi_61pb.js.map} +1 -1
- package/dist/_chunks/define-cookie-w5GWm_bL.js +93 -0
- package/dist/_chunks/define-cookie-w5GWm_bL.js.map +1 -0
- package/dist/_chunks/error-boundary-TYEQJZ1-.js +211 -0
- package/dist/_chunks/error-boundary-TYEQJZ1-.js.map +1 -0
- package/dist/_chunks/{format-RyoGQL74.js → format-cX7wzEp2.js} +2 -2
- package/dist/_chunks/{format-RyoGQL74.js.map → format-cX7wzEp2.js.map} +1 -1
- package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
- package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/{request-context-BQUC8PHn.js → request-context-CZz_T0Bc.js} +40 -71
- package/dist/_chunks/request-context-CZz_T0Bc.js.map +1 -0
- package/dist/_chunks/segment-context-Dpq2XOKg.js +34 -0
- package/dist/_chunks/segment-context-Dpq2XOKg.js.map +1 -0
- package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
- package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
- package/dist/_chunks/{tracing-CemImE6h.js → tracing-BPyIzIdu.js} +2 -2
- package/dist/_chunks/{tracing-CemImE6h.js.map → tracing-BPyIzIdu.js.map} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
- package/dist/_chunks/wrappers-C1SN725w.js +331 -0
- package/dist/_chunks/wrappers-C1SN725w.js.map +1 -0
- package/dist/cache/index.js +1 -1
- package/dist/client/error-boundary.d.ts +10 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -125
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +193 -90
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +8 -8
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +2 -2
- package/dist/client/router.d.ts +25 -3
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +23 -2
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/stale-reload.d.ts +15 -0
- package/dist/client/stale-reload.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +1 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +2 -2
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/codec.d.ts +21 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/cookies/define-cookie.d.ts +33 -12
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -81
- package/dist/index.d.ts +87 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +346 -210
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts +76 -0
- package/dist/params/define.d.ts.map +1 -0
- package/dist/params/index.d.ts +8 -0
- package/dist/params/index.d.ts.map +1 -0
- package/dist/params/index.js +104 -0
- package/dist/params/index.js.map +1 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/client-chunks.d.ts +32 -0
- package/dist/plugins/client-chunks.d.ts.map +1 -0
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts +2 -2
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts +2 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +6 -4
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/search-params/codecs.d.ts +1 -1
- package/dist/search-params/define.d.ts +153 -0
- package/dist/search-params/define.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +4 -5
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +3 -474
- package/dist/search-params/registry.d.ts +1 -1
- package/dist/search-params/wrappers.d.ts +53 -0
- package/dist/search-params/wrappers.d.ts.map +1 -0
- package/dist/server/access-gate.d.ts +4 -0
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-encryption.d.ts +76 -0
- package/dist/server/action-encryption.d.ts.map +1 -0
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +4 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +2 -2
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints.d.ts +13 -5
- package/dist/server/early-hints.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +4 -0
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/flight-injection-state.d.ts +78 -0
- package/dist/server/flight-injection-state.d.ts.map +1 -0
- package/dist/server/flight-scripts.d.ts +39 -0
- package/dist/server/flight-scripts.d.ts.map +1 -0
- package/dist/server/form-data.d.ts +29 -0
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +3 -9
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1819 -1629
- package/dist/server/index.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +28 -40
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +7 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +2 -2
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +3 -0
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +12 -8
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -3
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/version-skew.d.ts +61 -0
- package/dist/server/version-skew.d.ts.map +1 -0
- package/dist/shims/navigation-client.d.ts +1 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +1 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/state-machine.d.ts +80 -0
- package/dist/utils/state-machine.d.ts.map +1 -0
- package/package.json +12 -8
- package/src/client/browser-entry.ts +58 -25
- package/src/client/error-boundary.tsx +18 -1
- package/src/client/index.ts +9 -1
- package/src/client/link.tsx +9 -9
- package/src/client/navigation-context.ts +2 -2
- package/src/client/router.ts +102 -55
- package/src/client/rsc-fetch.ts +63 -2
- package/src/client/segment-cache.ts +1 -1
- package/src/client/stale-reload.ts +28 -0
- package/src/client/top-loader.tsx +2 -2
- package/src/client/use-params.ts +3 -3
- package/src/client/use-query-states.ts +1 -1
- package/src/codec.ts +21 -0
- package/src/cookies/define-cookie.ts +69 -18
- package/src/index.ts +255 -65
- package/src/params/define.ts +260 -0
- package/src/params/index.ts +28 -0
- package/src/plugins/adapter-build.ts +6 -0
- package/src/plugins/build-manifest.ts +11 -0
- package/src/plugins/client-chunks.ts +65 -0
- package/src/plugins/entries.ts +3 -6
- package/src/plugins/routing.ts +40 -14
- package/src/plugins/server-bundle.ts +32 -1
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +8 -4
- package/src/routing/codegen.ts +109 -88
- package/src/routing/scanner.ts +55 -6
- package/src/routing/status-file-lint.ts +2 -1
- package/src/routing/types.ts +7 -4
- package/src/rsc-runtime/rsc.ts +2 -0
- package/src/search-params/codecs.ts +1 -1
- package/src/search-params/define.ts +504 -0
- package/src/search-params/index.ts +12 -18
- package/src/search-params/registry.ts +1 -1
- package/src/search-params/wrappers.ts +85 -0
- package/src/server/access-gate.tsx +38 -8
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +16 -0
- package/src/server/als-registry.ts +4 -4
- package/src/server/build-manifest.ts +4 -4
- package/src/server/deny-renderer.ts +2 -1
- package/src/server/early-hints.ts +36 -15
- package/src/server/error-boundary-wrapper.ts +57 -14
- package/src/server/flight-injection-state.ts +152 -0
- package/src/server/flight-scripts.ts +59 -0
- package/src/server/form-data.ts +76 -0
- package/src/server/html-injectors.ts +50 -58
- package/src/server/index.ts +2 -4
- package/src/server/node-stream-transforms.ts +65 -54
- package/src/server/pipeline.ts +98 -26
- package/src/server/request-context.ts +49 -124
- package/src/server/route-element-builder.ts +102 -99
- package/src/server/route-matcher.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +5 -3
- package/src/server/rsc-entry/index.ts +26 -11
- package/src/server/rsc-entry/rsc-payload.ts +2 -2
- package/src/server/rsc-entry/ssr-renderer.ts +13 -5
- package/src/server/slot-resolver.ts +204 -206
- package/src/server/ssr-entry.ts +3 -1
- package/src/server/ssr-render.ts +3 -0
- package/src/server/tree-builder.ts +84 -48
- package/src/server/types.ts +1 -3
- package/src/server/version-skew.ts +104 -0
- package/src/shims/navigation-client.ts +1 -1
- package/src/shims/navigation.ts +1 -1
- package/src/utils/state-machine.ts +111 -0
- package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
- package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-BQUC8PHn.js.map +0 -1
- package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
- package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
- package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
- package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/cookies/index.js.map +0 -1
- package/dist/plugins/dynamic-transform.d.ts +0 -72
- package/dist/plugins/dynamic-transform.d.ts.map +0 -1
- package/dist/search-params/analyze.d.ts +0 -54
- package/dist/search-params/analyze.d.ts.map +0 -1
- package/dist/search-params/builtin-codecs.d.ts +0 -105
- package/dist/search-params/builtin-codecs.d.ts.map +0 -1
- package/dist/search-params/create.d.ts +0 -106
- package/dist/search-params/create.d.ts.map +0 -1
- package/dist/search-params/index.js.map +0 -1
- package/dist/server/prerender.d.ts +0 -77
- package/dist/server/prerender.d.ts.map +0 -1
- package/src/plugins/dynamic-transform.ts +0 -161
- package/src/search-params/analyze.ts +0 -192
- package/src/search-params/builtin-codecs.ts +0 -228
- package/src/search-params/create.ts +0 -321
- package/src/server/prerender.ts +0 -139
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineParams — factory for typed route param coercion.
|
|
3
|
+
*
|
|
4
|
+
* Creates a ParamsDefinition that coerces raw string params from the
|
|
5
|
+
* URL into typed values. Used by exporting from layout.tsx (segment-level)
|
|
6
|
+
* or page.tsx (fallback).
|
|
7
|
+
*
|
|
8
|
+
* Reuses the shared Codec<T> protocol with Standard Schema auto-detection,
|
|
9
|
+
* same pattern as defineSearchParams. Runtime constraints are stricter:
|
|
10
|
+
* - serialize must return string (not null — path segments can't be omitted)
|
|
11
|
+
* - parse throwing → 404 (invalid param value)
|
|
12
|
+
*
|
|
13
|
+
* Design doc: design/07a-route-params-triage.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Codec } from '#/codec.js';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** Infer the output type from a Codec or StandardSchemaV1. */
|
|
23
|
+
export type InferParamField<V> =
|
|
24
|
+
V extends Codec<infer T> ? T : V extends StandardSchemaV1<infer T> ? T : never;
|
|
25
|
+
|
|
26
|
+
/** Acceptable field value for defineParams: a Codec or a Standard Schema. */
|
|
27
|
+
export type ParamField<T = unknown> = Codec<T> | StandardSchemaV1<T>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A typed route params definition.
|
|
31
|
+
*
|
|
32
|
+
* Returned by defineParams(). Provides parse (string → typed) and
|
|
33
|
+
* serialize (typed → string) for each declared param.
|
|
34
|
+
*/
|
|
35
|
+
export interface ParamsDefinition<T extends Record<string, unknown>> {
|
|
36
|
+
/** Parse raw string params into typed values. Throws on invalid values. */
|
|
37
|
+
parse(raw: Record<string, string | string[]>): T;
|
|
38
|
+
|
|
39
|
+
/** Serialize typed values back to strings for URL construction. */
|
|
40
|
+
serialize(values: T): Record<string, string>;
|
|
41
|
+
|
|
42
|
+
/** Read-only codec map. */
|
|
43
|
+
codecs: { [K in keyof T]: Codec<T[K]> };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Standard Schema interface (subset — same as in search-params/define.ts)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
interface StandardSchemaV1<Output = unknown> {
|
|
51
|
+
'~standard': {
|
|
52
|
+
validate(
|
|
53
|
+
value: unknown
|
|
54
|
+
):
|
|
55
|
+
| { value: Output; issues?: undefined }
|
|
56
|
+
| { value?: undefined; issues: ReadonlyArray<{ message: string }> }
|
|
57
|
+
| Promise<
|
|
58
|
+
| { value: Output; issues?: undefined }
|
|
59
|
+
| { value?: undefined; issues: ReadonlyArray<{ message: string }> }
|
|
60
|
+
>;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Internal helpers
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
function isStandardSchema(value: unknown): value is StandardSchemaV1 {
|
|
69
|
+
return (
|
|
70
|
+
typeof value === 'object' &&
|
|
71
|
+
value !== null &&
|
|
72
|
+
'~standard' in value &&
|
|
73
|
+
typeof (value as StandardSchemaV1)['~standard']?.validate === 'function'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isCodec(value: unknown): value is Codec<unknown> {
|
|
78
|
+
return (
|
|
79
|
+
typeof value === 'object' &&
|
|
80
|
+
value !== null &&
|
|
81
|
+
typeof (value as Codec<unknown>).parse === 'function' &&
|
|
82
|
+
typeof (value as Codec<unknown>).serialize === 'function'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate sync for Standard Schema (same helper as search-params/codecs.ts).
|
|
88
|
+
*/
|
|
89
|
+
function validateSync<Output>(
|
|
90
|
+
schema: StandardSchemaV1<Output>,
|
|
91
|
+
value: unknown
|
|
92
|
+
):
|
|
93
|
+
| { value: Output; issues?: undefined }
|
|
94
|
+
| { value?: undefined; issues: ReadonlyArray<{ message: string }> } {
|
|
95
|
+
const result = schema['~standard'].validate(value);
|
|
96
|
+
if (result instanceof Promise) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'[timber] defineParams: schema returned a Promise — only sync schemas are supported.'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Wrap a Standard Schema into a Codec for route params.
|
|
106
|
+
*
|
|
107
|
+
* Unlike fromSchema for search params:
|
|
108
|
+
* - Parse throws on failure (no fallback to default)
|
|
109
|
+
* - Serialize returns string (not null)
|
|
110
|
+
*/
|
|
111
|
+
function fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {
|
|
112
|
+
return {
|
|
113
|
+
parse(value: string | string[] | undefined): T {
|
|
114
|
+
// Route params are always strings (single segment) or string[] (catch-all)
|
|
115
|
+
const input = Array.isArray(value) ? value : value;
|
|
116
|
+
|
|
117
|
+
const result = validateSync(schema, input);
|
|
118
|
+
if (!result.issues) {
|
|
119
|
+
return result.value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// For route params, parse failure means the param is invalid → throw
|
|
123
|
+
const messages = result.issues.map((i) => i.message).join(', ');
|
|
124
|
+
throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
serialize(value: T): string | null {
|
|
128
|
+
if (value === null || value === undefined) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
// Catch-all segments produce arrays — join with '/' for path reconstruction
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return value.join('/');
|
|
134
|
+
}
|
|
135
|
+
return String(value);
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a field value to a Codec. Auto-detects Standard Schema objects.
|
|
142
|
+
*/
|
|
143
|
+
function resolveField(fieldName: string, value: ParamField): Codec<unknown> {
|
|
144
|
+
if (isCodec(value)) {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isStandardSchema(value)) {
|
|
149
|
+
return fromParamSchema(fieldName, value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
throw new Error(
|
|
153
|
+
`[timber] defineParams: field '${fieldName}' is not a valid codec or Standard Schema. ` +
|
|
154
|
+
`Expected an object with { parse, serialize } methods, or a Standard Schema object ` +
|
|
155
|
+
`(Zod, Valibot, ArkType).`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate that no codec's serialize returns null.
|
|
161
|
+
* Route params are structural — they must produce a valid path segment.
|
|
162
|
+
*/
|
|
163
|
+
function validateSerialize(codecMap: Record<string, Codec<unknown>>): void {
|
|
164
|
+
for (const [key, codec] of Object.entries(codecMap)) {
|
|
165
|
+
// Test serialize with a sample parsed value to check for null
|
|
166
|
+
// We can't exhaustively test, but we can check that serialize(parse("test"))
|
|
167
|
+
// doesn't return null for a basic input.
|
|
168
|
+
try {
|
|
169
|
+
const testValue = codec.parse('test');
|
|
170
|
+
const serialized = codec.serialize(testValue);
|
|
171
|
+
if (serialized === null) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`[timber] defineParams: field '${key}' codec.serialize() returned null.\n` +
|
|
174
|
+
` Route params are path segments — they cannot be omitted.\n` +
|
|
175
|
+
` Ensure serialize() always returns a string.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
// parse('test') may throw for strict codecs (e.g., number-only).
|
|
180
|
+
// That's fine — it means the codec validates. We only care about
|
|
181
|
+
// serialize returning null, which we can't test without a valid value.
|
|
182
|
+
if (e instanceof Error && e.message.includes('returned null')) {
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Factory
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create a ParamsDefinition from a map of codecs and/or Standard Schema objects.
|
|
195
|
+
*
|
|
196
|
+
* ```ts
|
|
197
|
+
* // app/products/[id]/layout.tsx
|
|
198
|
+
* import { defineParams } from '@timber-js/app/params'
|
|
199
|
+
* import { z } from 'zod/v4'
|
|
200
|
+
*
|
|
201
|
+
* export const params = defineParams({
|
|
202
|
+
* id: z.coerce.number().int().positive(),
|
|
203
|
+
* })
|
|
204
|
+
*
|
|
205
|
+
* export default function Layout({ children }) { return children }
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
export function defineSegmentParams<C extends Record<string, ParamField>>(
|
|
209
|
+
codecs: C
|
|
210
|
+
): ParamsDefinition<{ [K in keyof C]: InferParamField<C[K]> }> {
|
|
211
|
+
type T = { [K in keyof C]: InferParamField<C[K]> };
|
|
212
|
+
|
|
213
|
+
const resolvedCodecs: Record<string, Codec<unknown>> = {};
|
|
214
|
+
|
|
215
|
+
for (const [key, value] of Object.entries(codecs)) {
|
|
216
|
+
resolvedCodecs[key] = resolveField(key, value as ParamField);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Validate that serialize doesn't return null
|
|
220
|
+
validateSerialize(resolvedCodecs);
|
|
221
|
+
|
|
222
|
+
// ---- parse ----
|
|
223
|
+
function parse(raw: Record<string, string | string[]>): T {
|
|
224
|
+
const result: Record<string, unknown> = {};
|
|
225
|
+
|
|
226
|
+
for (const [key, codec] of Object.entries(resolvedCodecs)) {
|
|
227
|
+
const rawValue = raw[key];
|
|
228
|
+
// Route params are always present (the route matched)
|
|
229
|
+
result[key] = codec.parse(rawValue);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return result as T;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---- serialize ----
|
|
236
|
+
function serialize(values: T): Record<string, string> {
|
|
237
|
+
const result: Record<string, string> = {};
|
|
238
|
+
|
|
239
|
+
for (const [key, codec] of Object.entries(resolvedCodecs)) {
|
|
240
|
+
const serialized = codec.serialize(values[key as keyof T] as unknown);
|
|
241
|
+
if (serialized === null) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`[timber] params.serialize: field '${key}' serialized to null. ` +
|
|
244
|
+
`Route params must produce a valid path segment.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
result[key] = serialized;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const definition: ParamsDefinition<T> = {
|
|
254
|
+
parse,
|
|
255
|
+
serialize,
|
|
256
|
+
codecs: resolvedCodecs as { [K in keyof T]: Codec<T[K]> },
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return definition;
|
|
260
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @timber-js/app/params — Typed route param coercion and search param definitions
|
|
2
|
+
//
|
|
3
|
+
// This is the primary import path for both segmentParams and searchParams.
|
|
4
|
+
// params.ts convention files import from here.
|
|
5
|
+
//
|
|
6
|
+
// See design/07-routing.md §"params.ts Convention File"
|
|
7
|
+
|
|
8
|
+
// --- Segment params (route path param coercion) ---
|
|
9
|
+
export type { ParamsDefinition, InferParamField, ParamField } from './define.js';
|
|
10
|
+
export { defineSegmentParams } from './define.js';
|
|
11
|
+
|
|
12
|
+
// --- Search params (re-exported from search-params for convenience) ---
|
|
13
|
+
// This lets params.ts import both from a single path:
|
|
14
|
+
// import { defineSegmentParams, defineSearchParams } from '@timber-js/app/params'
|
|
15
|
+
export { defineSearchParams } from '#/search-params/define.js';
|
|
16
|
+
|
|
17
|
+
// --- Codec utilities (re-exported for convenience) ---
|
|
18
|
+
export { fromSchema, fromArraySchema } from '#/search-params/codecs.js';
|
|
19
|
+
export { withDefault, withUrlKey } from '#/search-params/wrappers.js';
|
|
20
|
+
export type { Codec } from '#/codec.js';
|
|
21
|
+
export type {
|
|
22
|
+
SearchParamCodec,
|
|
23
|
+
SearchParamsDefinition,
|
|
24
|
+
SetParams,
|
|
25
|
+
SetParamsOptions,
|
|
26
|
+
QueryStatesOptions,
|
|
27
|
+
CodecMap,
|
|
28
|
+
} from '#/search-params/define.js';
|
|
@@ -50,6 +50,12 @@ export function timberAdapterBuild(ctx: PluginContext): Plugin {
|
|
|
50
50
|
: ctx.buildManifest;
|
|
51
51
|
const json = JSON.stringify(manifest);
|
|
52
52
|
manifestInit = `globalThis.__TIMBER_BUILD_MANIFEST__ = ${json};\n`;
|
|
53
|
+
|
|
54
|
+
// Embed the deployment ID for version skew detection (TIM-446).
|
|
55
|
+
// The server reads this at startup via setDeploymentId().
|
|
56
|
+
if (ctx.deploymentId) {
|
|
57
|
+
manifestInit += `globalThis.__TIMBER_DEPLOYMENT_ID__ = ${JSON.stringify(ctx.deploymentId)};\n`;
|
|
58
|
+
}
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
// Strip JS from the RSC plugin's assets manifest when client JS
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* Design docs: 18-build-system.md §"Build Manifest", 02-rendering-pipeline.md §"Early Hints"
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
19
20
|
import type { Plugin, ResolvedConfig } from 'vite';
|
|
20
21
|
import type { PluginContext } from '#/index.js';
|
|
21
22
|
import type { BuildManifest } from '#/server/build-manifest.js';
|
|
@@ -242,6 +243,16 @@ export function timberBuildManifest(ctx: PluginContext): Plugin {
|
|
|
242
243
|
configResolved(config: ResolvedConfig) {
|
|
243
244
|
resolvedBase = config.base;
|
|
244
245
|
isDev = config.command === 'serve';
|
|
246
|
+
|
|
247
|
+
// Generate a per-build deployment ID early so virtual:timber-config
|
|
248
|
+
// can serialize it during the load hook (which runs before generateBundle).
|
|
249
|
+
// A random UUID is used instead of a content hash — determinism provides
|
|
250
|
+
// no real benefit here since you almost never redeploy identical code,
|
|
251
|
+
// and a random ID avoids the timing problem where the content hash was
|
|
252
|
+
// only available after generateBundle (TIM-452).
|
|
253
|
+
if (!isDev && !ctx.deploymentId) {
|
|
254
|
+
ctx.deploymentId = randomUUID().replace(/-/g, '').slice(0, 16);
|
|
255
|
+
}
|
|
245
256
|
},
|
|
246
257
|
|
|
247
258
|
resolveId(id: string) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client chunk grouping strategy for @vitejs/plugin-rsc.
|
|
3
|
+
*
|
|
4
|
+
* Groups client reference modules by layout boundary to balance route-scoped
|
|
5
|
+
* code splitting with HTTP request count. A constant group name would collapse
|
|
6
|
+
* all routes into one chunk (every page downloads every client component).
|
|
7
|
+
* Per-serverChunk grouping creates many sub-500B files. Layout-boundary
|
|
8
|
+
* grouping is the middle ground.
|
|
9
|
+
*
|
|
10
|
+
* See design/27-chunking-strategy.md, TIM-440, TIM-499.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Derive a chunk group name for a client reference module.
|
|
15
|
+
*
|
|
16
|
+
* Groups by the first non-group route segment under appDir so that all
|
|
17
|
+
* client components belonging to the same layout boundary land in one
|
|
18
|
+
* chunk. For example:
|
|
19
|
+
* - `facade:app/dashboard/settings/page.tsx` → `"client-dashboard"`
|
|
20
|
+
* - `facade:app/(group-a)/group-page-a/page.tsx` → `"client-group-page-a"`
|
|
21
|
+
* - `facade:app/layout.tsx` (root layout) → `"client-shared"`
|
|
22
|
+
* - `shared:...` (shared across chunks) → `"client-shared"`
|
|
23
|
+
*
|
|
24
|
+
* This balances route-scoped code splitting with HTTP request count:
|
|
25
|
+
* fewer chunks than per-serverChunk, but still avoids downloading every
|
|
26
|
+
* client component on every page.
|
|
27
|
+
*/
|
|
28
|
+
export function clientChunkGroup(
|
|
29
|
+
meta: { id: string; normalizedId: string; serverChunk: string },
|
|
30
|
+
appDir: string
|
|
31
|
+
): string {
|
|
32
|
+
const { normalizedId, serverChunk } = meta;
|
|
33
|
+
|
|
34
|
+
// Shared chunks (not associated with a single route entry) get one group.
|
|
35
|
+
if (serverChunk.startsWith('shared:')) return 'client-shared';
|
|
36
|
+
|
|
37
|
+
// Derive the layout boundary from the file's location relative to the
|
|
38
|
+
// app directory. normalizedId is root-relative (e.g. "app/dashboard/shell.tsx"
|
|
39
|
+
// or "src/app/dashboard/shell.tsx"). We find the "app/" prefix and walk
|
|
40
|
+
// segments after it to find the first non-group directory.
|
|
41
|
+
const relPath = normalizedId.replace(/\\/g, '/');
|
|
42
|
+
|
|
43
|
+
// Find the app directory boundary in the path. The last segment of appDir
|
|
44
|
+
// is the app folder name (usually "app").
|
|
45
|
+
const appDirName = appDir.replace(/\\/g, '/').split('/').pop() || 'app';
|
|
46
|
+
const appIdx = relPath.indexOf(appDirName + '/');
|
|
47
|
+
if (appIdx === -1) return 'client-shared';
|
|
48
|
+
|
|
49
|
+
const withinApp = relPath.slice(appIdx + appDirName.length + 1);
|
|
50
|
+
const segments = withinApp.split('/');
|
|
51
|
+
|
|
52
|
+
// Find first directory segment that isn't a route group like (group-a)
|
|
53
|
+
for (const seg of segments) {
|
|
54
|
+
// Skip the filename itself
|
|
55
|
+
if (seg.includes('.')) break;
|
|
56
|
+
// Skip route groups — parenthesized segments like (group-a)
|
|
57
|
+
if (seg.startsWith('(') && seg.endsWith(')')) continue;
|
|
58
|
+
// Found a real route segment
|
|
59
|
+
return `client-${seg}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Root-level files (layout.tsx, page.tsx directly in app/) go into the
|
|
63
|
+
// shared group since the root layout is loaded on every page.
|
|
64
|
+
return 'client-shared';
|
|
65
|
+
}
|
package/src/plugins/entries.ts
CHANGED
|
@@ -95,11 +95,6 @@ function stripRootPrefix(id: string, root: string): string {
|
|
|
95
95
|
* Serializes output mode and feature flags for runtime consumption.
|
|
96
96
|
*/
|
|
97
97
|
function generateConfigModule(ctx: PluginContext): string {
|
|
98
|
-
// Resolve cookie secrets: `secret` shorthand expands to `secrets: [secret]`
|
|
99
|
-
const cookieSecrets =
|
|
100
|
-
ctx.config.cookies?.secrets ??
|
|
101
|
-
(ctx.config.cookies?.secret ? [ctx.config.cookies.secret] : undefined);
|
|
102
|
-
|
|
103
98
|
const runtimeConfig = {
|
|
104
99
|
output: ctx.config.output ?? 'server',
|
|
105
100
|
csrf: ctx.config.csrf ?? true,
|
|
@@ -108,10 +103,12 @@ function generateConfigModule(ctx: PluginContext): string {
|
|
|
108
103
|
dev: ctx.dev ?? false,
|
|
109
104
|
slowPhaseMs: ctx.config.dev?.slowPhaseMs ?? 200,
|
|
110
105
|
slowRequestMs: ctx.config.slowRequestMs ?? 3000,
|
|
111
|
-
cookieSecrets,
|
|
112
106
|
topLoader: ctx.config.topLoader,
|
|
113
107
|
debug: ctx.config.debug ?? false,
|
|
114
108
|
serverTiming: ctx.config.serverTiming,
|
|
109
|
+
// Per-build deployment ID for version skew detection (TIM-446).
|
|
110
|
+
// Null in dev mode — HMR handles code updates without full reloads.
|
|
111
|
+
deploymentId: ctx.deploymentId ?? null,
|
|
115
112
|
};
|
|
116
113
|
|
|
117
114
|
return [
|
package/src/plugins/routing.ts
CHANGED
|
@@ -28,7 +28,7 @@ const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_MODULE_ID}`;
|
|
|
28
28
|
* File convention names we track for changes that require manifest regeneration.
|
|
29
29
|
*/
|
|
30
30
|
const ROUTE_FILE_PATTERNS =
|
|
31
|
-
/\/(page|layout|middleware|access|route|error|default|denied
|
|
31
|
+
/\/(page|layout|middleware|access|route|error|default|denied|\d{3}|[45]xx|not-found|forbidden|unauthorized|sitemap|robots|manifest|favicon|icon|opengraph-image|twitter-image|apple-icon)\./;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Create the timber-routing Vite plugin.
|
|
@@ -168,20 +168,49 @@ export function timberRouting(ctx: PluginContext): Plugin {
|
|
|
168
168
|
// Watch the app directory
|
|
169
169
|
devServer.watcher.add(ctx.appDir);
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
/** Snapshot of the last generated manifest, used to detect structural changes. */
|
|
172
|
+
let lastManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree) : '';
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Handle a route-significant file being added or removed.
|
|
176
|
+
* Always triggers a full-reload since the route tree structure changed.
|
|
177
|
+
*/
|
|
178
|
+
const handleStructuralChange = (filePath: string) => {
|
|
173
179
|
if (!filePath.startsWith(ctx.appDir)) return;
|
|
174
180
|
if (!ROUTE_FILE_PATTERNS.test(filePath)) return;
|
|
175
181
|
|
|
176
|
-
// Rescan the route tree
|
|
177
182
|
rescan();
|
|
178
|
-
|
|
179
|
-
// Invalidate the virtual module in all environments
|
|
183
|
+
lastManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree) : '';
|
|
180
184
|
invalidateManifest(devServer);
|
|
181
185
|
};
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Handle a route file's content changing.
|
|
189
|
+
*
|
|
190
|
+
* Most content edits (JSX changes, fixing typos) don't affect route
|
|
191
|
+
* metadata — Vite's React Fast Refresh handles those via normal HMR.
|
|
192
|
+
* Only rescan and full-reload when route metadata actually changed
|
|
193
|
+
* (e.g., searchParams export added/removed, metadata export changed).
|
|
194
|
+
*/
|
|
195
|
+
const handleContentChange = (filePath: string) => {
|
|
196
|
+
if (!filePath.startsWith(ctx.appDir)) return;
|
|
197
|
+
if (!ROUTE_FILE_PATTERNS.test(filePath)) return;
|
|
198
|
+
|
|
199
|
+
rescan();
|
|
200
|
+
const newManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree) : '';
|
|
201
|
+
if (newManifest !== lastManifest) {
|
|
202
|
+
lastManifest = newManifest;
|
|
203
|
+
invalidateManifest(devServer);
|
|
204
|
+
}
|
|
205
|
+
// Otherwise: content edit didn't change route metadata — let Vite HMR handle it
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
devServer.watcher.on('add', handleStructuralChange);
|
|
209
|
+
devServer.watcher.on('unlink', handleStructuralChange);
|
|
210
|
+
// Watch content changes to page files — searchParams detection depends
|
|
211
|
+
// on file contents (export const searchParams), not just file presence.
|
|
212
|
+
// But only full-reload when route metadata actually changes.
|
|
213
|
+
devServer.watcher.on('change', handleContentChange);
|
|
185
214
|
// Also watch renames (which are add+unlink) — handled by the above
|
|
186
215
|
},
|
|
187
216
|
};
|
|
@@ -301,12 +330,9 @@ function generateManifestModule(tree: RouteTree): string {
|
|
|
301
330
|
`${nextIndent}denied: { load: ${v}, filePath: ${JSON.stringify(node.denied.filePath)} },`
|
|
302
331
|
);
|
|
303
332
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
`${nextIndent}searchParams: { load: ${v}, filePath: ${JSON.stringify(node.searchParams.filePath)} },`
|
|
308
|
-
);
|
|
309
|
-
}
|
|
333
|
+
// searchParams is now a named export from page.tsx, not a separate file.
|
|
334
|
+
// The page module's searchParams export is loaded via the page's lazy import.
|
|
335
|
+
// Runtime registration happens in the route loader using the page module.
|
|
310
336
|
|
|
311
337
|
// Status-code files
|
|
312
338
|
if (node.statusFiles && node.statusFiles.size > 0) {
|
|
@@ -137,5 +137,36 @@ export function timberServerBundle(): Plugin[] {
|
|
|
137
137
|
},
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
// Fix Rolldown's `createRequire(import.meta.url)` CJS interop shim for
|
|
141
|
+
// Cloudflare Workers. Rolldown emits this for CJS dependencies (e.g.
|
|
142
|
+
// @opentelemetry/context-async-hooks) that use `require()`. On Workers,
|
|
143
|
+
// `import.meta.url` is `undefined` for non-entry modules, causing:
|
|
144
|
+
// TypeError: The argument 'path' must be a file URL object, a file URL
|
|
145
|
+
// string, or an absolute path string. Received 'undefined'
|
|
146
|
+
//
|
|
147
|
+
// The fix: provide a fallback URL when `import.meta.url` is undefined.
|
|
148
|
+
// The actual URL doesn't matter — `createRequire` only needs it for
|
|
149
|
+
// resolving relative paths, but the only `__require()` calls are for
|
|
150
|
+
// Node built-ins (events, async_hooks) which resolve from any base.
|
|
151
|
+
//
|
|
152
|
+
// The top-level `ssr: { target: 'webworker' }` was supposed to prevent
|
|
153
|
+
// this, but it doesn't propagate to custom environments (rsc) in Vite's
|
|
154
|
+
// Environment API. See LOCAL-405.
|
|
155
|
+
const createRequireFixPlugin: Plugin = {
|
|
156
|
+
name: 'timber-create-require-fix',
|
|
157
|
+
applyToEnvironment(environment) {
|
|
158
|
+
return environment.name === 'rsc' || environment.name === 'ssr';
|
|
159
|
+
},
|
|
160
|
+
renderChunk(code) {
|
|
161
|
+
const pattern = 'createRequire(import.meta.url)';
|
|
162
|
+
if (!code.includes(pattern)) return null;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
code: code.replace(pattern, 'createRequire(import.meta.url || "file:///app")'),
|
|
166
|
+
map: null,
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return [bundlePlugin, esmInitFixPlugin, createRequireFixPlugin];
|
|
141
172
|
}
|
package/src/plugins/shims.ts
CHANGED
|
@@ -137,7 +137,7 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
137
137
|
// package.json exports), creating a module instance split: ssr-entry.ts
|
|
138
138
|
// registers the ALS-backed SSR data provider on the src/ instance of
|
|
139
139
|
// ssr-data.ts, but client component hooks read getSsrData() from the
|
|
140
|
-
// dist/ instance — which has no provider. Result: hooks like
|
|
140
|
+
// dist/ instance — which has no provider. Result: hooks like useSegmentParams()
|
|
141
141
|
// return empty defaults during SSR.
|
|
142
142
|
//
|
|
143
143
|
// This remap is SSR-only. The RSC environment still resolves to dist/
|
|
@@ -163,9 +163,6 @@ export function validateStaticMode(
|
|
|
163
163
|
* - transform: Validates source files for static mode violations
|
|
164
164
|
*/
|
|
165
165
|
export function timberStaticBuild(ctx: PluginContext): Plugin {
|
|
166
|
-
const isStatic = ctx.config.output === 'static';
|
|
167
|
-
const clientJavascriptDisabled = ctx.clientJavascript.disabled;
|
|
168
|
-
|
|
169
166
|
return {
|
|
170
167
|
name: 'timber-static-build',
|
|
171
168
|
|
|
@@ -177,6 +174,11 @@ export function timberStaticBuild(ctx: PluginContext): Plugin {
|
|
|
177
174
|
* - When client JS disabled: 'use client' / 'use server' directives → build error
|
|
178
175
|
*/
|
|
179
176
|
transform(code: string, id: string) {
|
|
177
|
+
// Read ctx.config lazily inside the hook — not at plugin construction
|
|
178
|
+
// time — so file-based config from timber.config.ts is respected.
|
|
179
|
+
// See TIM-451.
|
|
180
|
+
const isStatic = ctx.config.output === 'static';
|
|
181
|
+
|
|
180
182
|
// Only active in static mode
|
|
181
183
|
if (!isStatic) return null;
|
|
182
184
|
|
|
@@ -189,7 +191,9 @@ export function timberStaticBuild(ctx: PluginContext): Plugin {
|
|
|
189
191
|
// Only check JS/TS files
|
|
190
192
|
if (!/\.[jt]sx?$/.test(id)) return null;
|
|
191
193
|
|
|
192
|
-
const errors = validateStaticMode(code, id, {
|
|
194
|
+
const errors = validateStaticMode(code, id, {
|
|
195
|
+
clientJavascriptDisabled: ctx.clientJavascript.disabled,
|
|
196
|
+
});
|
|
193
197
|
|
|
194
198
|
if (errors.length > 0) {
|
|
195
199
|
// Format all errors into a single build error message
|