@timber-js/app 0.2.0-alpha.186 → 0.2.0-alpha.187
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/{actions-C-Rw9vPc.js → actions-35jnMdeJ.js} +4 -3
- package/dist/_chunks/{actions-C-Rw9vPc.js.map → actions-35jnMdeJ.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
- package/dist/_chunks/{als-slots-mFweg276.js → als-slots-BEEIPKYm.js} +3 -4
- package/dist/_chunks/{als-slots-mFweg276.js.map → als-slots-BEEIPKYm.js.map} +1 -1
- package/dist/_chunks/{cache-api-Cd0VZ_Pd.js → cache-api-DjNrIWRR.js} +7 -13
- package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
- package/dist/_chunks/{cloudflare-Cs0uZXea.js → cloudflare-CGP6BZKO.js} +4 -3
- package/dist/_chunks/{cloudflare-Cs0uZXea.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
- package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
- package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
- package/dist/_chunks/{error-boundary-DpYRI_I1.js → error-boundary-D-ODYX41.js} +25 -2
- package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
- package/dist/_chunks/{logger-N7e5auP0.js → logger-D8xJZXIN.js} +27 -40
- package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
- package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
- package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
- package/dist/_chunks/{plugin-context-DEGLSJs3.js → plugin-context-rCinWLiE.js} +7 -2
- package/dist/_chunks/{plugin-context-DEGLSJs3.js.map → plugin-context-rCinWLiE.js.map} +1 -1
- package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
- package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-CKgHC2MB.js → scanner-C8b0Gcw3.js} +5 -298
- package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
- package/dist/_chunks/{param-value-C8TNYchQ.js → segment-context-CjOlyB8Y.js} +33 -2
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
- package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
- package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
- package/dist/_chunks/{navigation-root-B29qg0_T.js → use-segment-params-C4r4BD9T.js} +129 -5
- package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
- package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
- package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/compress-module.d.ts +12 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.js +54 -2
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cdn/cloudflare-purge.js +30 -0
- package/dist/cdn/cloudflare-purge.js.map +1 -0
- package/dist/cdn/fastly-purge.js +33 -0
- package/dist/cdn/fastly-purge.js.map +1 -0
- package/dist/cdn/index.js +94 -0
- package/dist/cdn/index.js.map +1 -0
- package/dist/cdn/workers-cache-purge.js +35 -0
- package/dist/cdn/workers-cache-purge.js.map +1 -0
- package/dist/cli-check.d.ts +153 -0
- package/dist/cli-check.d.ts.map +1 -0
- package/dist/cli.d.ts +34 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +46 -21
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.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/error-boundary.d.ts +6 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +27 -33
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +21 -14
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +1 -7
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +16 -0
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +9 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +8 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +9 -3
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -2
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +142 -477
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/plugin-context.d.ts +15 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts +0 -9
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +24 -0
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-shared.d.ts +3 -44
- package/dist/routing/codegen-shared.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +10 -31
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts +51 -0
- package/dist/routing/codegen-write.d.ts.map +1 -0
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts +18 -4
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/export-detect.d.ts +16 -0
- package/dist/routing/export-detect.d.ts.map +1 -1
- package/dist/routing/index.js +3 -2
- package/dist/routing/link-codegen.d.ts +19 -4
- package/dist/routing/link-codegen.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +1 -7
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/types.d.ts +0 -6
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +60 -9
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +62 -8
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.d.ts +0 -1
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +66 -29
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/parse-total.d.ts +70 -0
- package/dist/search-params/parse-total.d.ts.map +1 -0
- package/dist/search-params/wrappers.d.ts +26 -3
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts +19 -8
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +16 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/compress.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +148 -15
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +2 -2
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +85 -15
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +3 -1
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +343 -230
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +52 -0
- package/dist/server/metadata-collector.d.ts.map +1 -0
- package/dist/server/param-coercion.d.ts +12 -5
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +23 -0
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +1 -12
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +0 -7
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +0 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +1 -3
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +12 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -2
- 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-bridge-types.d.ts +11 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +0 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +8 -1
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +28 -36
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +12 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +40 -0
- package/dist/server/utils/element-type.d.ts.map +1 -0
- package/dist/shared/rsc-media-type.d.ts +40 -0
- package/dist/shared/rsc-media-type.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +1 -1
- package/docs/api/33-api-search-params.mdx +38 -16
- package/docs/api/35-api-typescript.mdx +3 -3
- package/docs/api/36-cli.mdx +34 -7
- package/docs/learn/00-introduction.mdx +1 -1
- package/docs/learn/02-pages-and-layouts.mdx +1 -1
- package/docs/learn/05-typed-params.mdx +8 -8
- package/docs/learn/07-typed-routes.mdx +12 -7
- package/docs/learn/11-error-handling.mdx +16 -0
- package/docs/more/01-advanced-routing.mdx +1 -1
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/50-ai-agent-instructions.mdx +6 -4
- package/package.json +8 -5
- package/src/adapters/cloudflare.ts +4 -1
- package/src/adapters/compress-module.ts +79 -1
- package/src/cli-check.ts +458 -0
- package/src/cli.ts +59 -24
- package/src/client/browser-entry/action-dispatch.ts +2 -1
- package/src/client/browser-entry/index.ts +0 -5
- package/src/client/error-boundary.tsx +65 -1
- package/src/client/index.ts +14 -3
- package/src/client/link.tsx +65 -64
- package/src/client/navigation-commit.ts +27 -4
- package/src/client/params-context.ts +4 -4
- package/src/client/router-pipeline.ts +4 -0
- package/src/client/router.ts +1 -0
- package/src/client/rsc-fetch.ts +14 -4
- package/src/client/segment-cache.ts +8 -0
- package/src/client/use-query-states.ts +102 -39
- package/src/cookies/define-cookie.ts +6 -1
- package/src/index.ts +20 -3
- package/src/plugin-context.ts +26 -0
- package/src/plugins/routing.ts +84 -146
- package/src/plugins/shims.ts +0 -1
- package/src/plugins/static-build.ts +78 -24
- package/src/routing/codegen-shared.ts +3 -79
- package/src/routing/codegen-types.ts +10 -31
- package/src/routing/codegen-write.ts +139 -0
- package/src/routing/codegen.ts +56 -182
- package/src/routing/convention-lint.ts +139 -40
- package/src/routing/export-detect.ts +151 -7
- package/src/routing/link-codegen.ts +32 -65
- package/src/routing/manifest-codegen.ts +1 -59
- package/src/routing/scanner.ts +3 -3
- package/src/routing/types.ts +0 -6
- package/src/schema-bridge.ts +180 -58
- package/src/search-params/define.ts +102 -37
- package/src/search-params/index.ts +0 -1
- package/src/search-params/parse-total.ts +78 -0
- package/src/search-params/wrappers.ts +60 -11
- package/src/server/access-gate.tsx +60 -40
- package/src/server/action-handler.ts +1 -4
- package/src/server/als-registry.ts +16 -0
- package/src/server/compress.ts +9 -1
- package/src/server/deny-boundary.ts +269 -41
- package/src/server/deny-renderer.ts +32 -21
- package/src/server/error-boundary-wrapper.ts +166 -79
- package/src/server/index.ts +1 -3
- package/src/server/internal.ts +2 -2
- package/src/server/metadata-collector.ts +115 -0
- package/src/server/param-coercion.ts +13 -61
- package/src/server/pipeline-helpers.ts +2 -2
- package/src/server/pipeline-outcome.ts +2 -1
- package/src/server/pipeline-phases.ts +9 -9
- package/src/server/primitives.ts +25 -0
- package/src/server/route-element-builder.ts +167 -170
- package/src/server/rsc-cache-key-guard.ts +2 -8
- package/src/server/rsc-entry/deny-fallback.ts +3 -2
- package/src/server/rsc-entry/error-renderer.ts +31 -11
- package/src/server/rsc-entry/helpers.ts +2 -12
- package/src/server/rsc-entry/index.ts +0 -5
- package/src/server/rsc-entry/render-route.ts +14 -11
- package/src/server/rsc-entry/revalidate-renderer.ts +2 -1
- package/src/server/rsc-entry/rsc-payload.ts +104 -20
- package/src/server/rsc-entry/rsc-stream.ts +25 -2
- package/src/server/rsc-entry/ssr-renderer.ts +28 -16
- package/src/server/slot-resolver.ts +10 -2
- package/src/server/ssr-bridge-types.ts +12 -0
- package/src/server/ssr-entry.ts +8 -6
- package/src/server/static-generator.ts +3 -2
- package/src/server/status-code-resolver.ts +28 -11
- package/src/server/tree-builder.ts +35 -218
- package/src/server/types.ts +12 -7
- package/src/server/utils/element-type.ts +72 -0
- package/src/shared/rsc-media-type.ts +43 -0
- package/dist/_chunks/cache-api-Cd0VZ_Pd.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +0 -1
- package/dist/_chunks/error-boundary-DpYRI_I1.js.map +0 -1
- package/dist/_chunks/logger-N7e5auP0.js.map +0 -1
- package/dist/_chunks/navigation-root-B29qg0_T.js.map +0 -1
- package/dist/_chunks/param-value-C8TNYchQ.js.map +0 -1
- package/dist/_chunks/registry-DbJPKoBp.js +0 -20
- package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
- package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
- package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
- package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
- package/dist/_chunks/use-segment-params-ClyUNq4d.js +0 -128
- package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +0 -1
- package/dist/_chunks/walkers-BhhwI9TD.js +0 -936
- package/dist/_chunks/walkers-BhhwI9TD.js.map +0 -1
- package/dist/search-params/registry.d.ts +0 -20
- package/dist/search-params/registry.d.ts.map +0 -1
- package/dist/segment-params/define.d.ts +0 -83
- package/dist/segment-params/define.d.ts.map +0 -1
- package/dist/segment-params/index.d.ts +0 -3
- package/dist/segment-params/index.d.ts.map +0 -1
- package/dist/segment-params/index.js +0 -70
- package/dist/segment-params/index.js.map +0 -1
- package/src/search-params/registry.ts +0 -31
- package/src/segment-params/define.ts +0 -226
- package/src/segment-params/index.ts +0 -9
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
//#region src/schema-bridge.ts
|
|
2
|
-
/**
|
|
3
|
-
* Run a Standard Schema's `~standard.validate()` synchronously.
|
|
4
|
-
*
|
|
5
|
-
* Zod v4's signature includes `Promise` in the return union to satisfy the
|
|
6
|
-
* Standard Schema spec, but in practice Zod always validates synchronously
|
|
7
|
-
* for the schema types we use. We assert the result is sync and throw if
|
|
8
|
-
* it isn't — codec parsing must be synchronous.
|
|
9
|
-
*/
|
|
10
|
-
function validateSync(schema, value) {
|
|
11
|
-
const result = schema["~standard"].validate(value);
|
|
12
|
-
if (result instanceof Promise) throw new Error("[timber] fromSchema: schema returned a Promise — only sync schemas are supported.");
|
|
13
|
-
return result;
|
|
14
|
-
}
|
|
15
|
-
/** Check if a value is a Standard Schema object. */
|
|
16
|
-
function isStandardSchema(value) {
|
|
17
|
-
return typeof value === "object" && value !== null && "~standard" in value && typeof value["~standard"]?.validate === "function";
|
|
18
|
-
}
|
|
19
|
-
/** Check if a value is a Codec (has parse + serialize methods). */
|
|
20
|
-
function isCodec(value) {
|
|
21
|
-
return typeof value === "object" && value !== null && typeof value.parse === "function" && typeof value.serialize === "function";
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Bridge a Standard Schema to a Codec for route params.
|
|
25
|
-
* Parse throws on failure (invalid param → 404). Serialize returns string.
|
|
26
|
-
*/
|
|
27
|
-
function fromParamSchema(fieldName, schema) {
|
|
28
|
-
return {
|
|
29
|
-
parse(value) {
|
|
30
|
-
const result = validateSync(schema, value);
|
|
31
|
-
if (!result.issues) return result.value;
|
|
32
|
-
const messages = result.issues.map((i) => i.message).join(", ");
|
|
33
|
-
throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);
|
|
34
|
-
},
|
|
35
|
-
serialize(value) {
|
|
36
|
-
if (value === null || value === void 0) return null;
|
|
37
|
-
if (Array.isArray(value)) return value.join("/");
|
|
38
|
-
return String(value);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Resolve a field value to a Codec. Accepts Codec<T>, StandardSchemaV1<T>,
|
|
44
|
-
* or (for route params) auto-wraps via fromParamSchema.
|
|
45
|
-
*
|
|
46
|
-
* @param fieldName - used in error messages
|
|
47
|
-
* @param value - the codec or schema to resolve
|
|
48
|
-
* @param mode - 'param' uses fromParamSchema (throws on parse failure),
|
|
49
|
-
* 'search' uses fromSchema (falls back to default on failure)
|
|
50
|
-
*/
|
|
51
|
-
function resolveCodecOrSchema(fieldName, value, mode = "search") {
|
|
52
|
-
if (isCodec(value)) return value;
|
|
53
|
-
if (isStandardSchema(value)) return mode === "param" ? fromParamSchema(fieldName, value) : fromSchema(value);
|
|
54
|
-
throw new Error(`[timber] Field '${fieldName}' is not a valid codec or Standard Schema. Expected an object with { parse, serialize } methods, or a Standard Schema object (Zod, Valibot, ArkType).`);
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Bridge a Standard Schema-compatible schema (Zod, Valibot, ArkType) to a
|
|
58
|
-
* Codec<T>.
|
|
59
|
-
*
|
|
60
|
-
* Parse: coerces the raw string through the schema. On validation failure,
|
|
61
|
-
* parses `undefined` to get the schema's default value (the schema should have
|
|
62
|
-
* a `.default()` call). If that also fails, returns `undefined`.
|
|
63
|
-
*
|
|
64
|
-
* Serialize: uses `String()` for primitives, `null` for null/undefined.
|
|
65
|
-
*
|
|
66
|
-
* ```ts
|
|
67
|
-
* import { fromSchema } from '@timber-js/app/codec'
|
|
68
|
-
* import { z } from 'zod/v4'
|
|
69
|
-
*
|
|
70
|
-
* const pageCodec = fromSchema(z.coerce.number().int().min(1).default(1))
|
|
71
|
-
* ```
|
|
72
|
-
*/
|
|
73
|
-
function fromSchema(schema) {
|
|
74
|
-
return {
|
|
75
|
-
parse(value) {
|
|
76
|
-
const result = validateSync(schema, Array.isArray(value) ? value[0] : value);
|
|
77
|
-
if (!result.issues) return result.value;
|
|
78
|
-
const defaultResult = validateSync(schema, void 0);
|
|
79
|
-
if (!defaultResult.issues) return defaultResult.value;
|
|
80
|
-
},
|
|
81
|
-
serialize(value) {
|
|
82
|
-
if (value === null || value === void 0) return null;
|
|
83
|
-
return String(value);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Bridge a Standard Schema for array values. Handles both single strings
|
|
89
|
-
* and repeated query keys (`?tag=a&tag=b`).
|
|
90
|
-
*
|
|
91
|
-
* ```ts
|
|
92
|
-
* import { fromArraySchema } from '@timber-js/app/codec'
|
|
93
|
-
* import { z } from 'zod/v4'
|
|
94
|
-
*
|
|
95
|
-
* const tagsCodec = fromArraySchema(z.array(z.string()).default([]))
|
|
96
|
-
* ```
|
|
97
|
-
*/
|
|
98
|
-
function fromArraySchema(schema) {
|
|
99
|
-
return {
|
|
100
|
-
parse(value) {
|
|
101
|
-
let input = value;
|
|
102
|
-
if (typeof value === "string") input = [value];
|
|
103
|
-
else if (value === void 0) input = void 0;
|
|
104
|
-
const result = validateSync(schema, input);
|
|
105
|
-
if (!result.issues) return result.value;
|
|
106
|
-
const defaultResult = validateSync(schema, void 0);
|
|
107
|
-
if (!defaultResult.issues) return defaultResult.value;
|
|
108
|
-
},
|
|
109
|
-
serialize(value) {
|
|
110
|
-
if (value === null || value === void 0) return null;
|
|
111
|
-
if (Array.isArray(value)) return value.length === 0 ? null : value.join(",");
|
|
112
|
-
return String(value);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
//#endregion
|
|
117
|
-
export { resolveCodecOrSchema as a, isStandardSchema as i, fromSchema as n, isCodec as r, fromArraySchema as t };
|
|
118
|
-
|
|
119
|
-
//# sourceMappingURL=schema-bridge-DT_Tn0Xf.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"schema-bridge-DT_Tn0Xf.js","names":[],"sources":["../../src/schema-bridge.ts"],"sourcesContent":["/**\n * Standard Schema bridge — shared helpers for bridging Standard Schema-compatible\n * validation libraries (Zod, Valibot, ArkType) to the Codec<T> protocol.\n *\n * This module is the single source of truth for:\n * - StandardSchemaV1 interface (subset of the Standard Schema spec)\n * - validateSync() helper\n * - fromSchema() — bridge from Standard Schema to Codec<T>\n * - fromArraySchema() — bridge for array-valued codecs\n *\n * These are re-exported from @timber-js/app/search-params, @timber-js/app/segment-params,\n * and @timber-js/app/cookies for convenience. The canonical import is\n * @timber-js/app/codec.\n *\n * Design doc: design/23a-search-params-triage.md §\"Unify Codec<T> type\"\n */\n\nimport type { Codec } from './codec.js';\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset)\n//\n// Standard Schema (https://github.com/standard-schema/standard-schema) defines\n// a minimal interface that Zod ≥3.24, Valibot ≥1.0, and ArkType all implement.\n// We depend only on `~standard.validate` to avoid coupling to any specific lib.\n// ---------------------------------------------------------------------------\n\n/** Minimal Standard Schema interface for auto-detection. */\nexport interface StandardSchemaV1<Output = unknown> {\n '~standard': {\n validate(value: unknown): StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;\n };\n}\n\nexport type StandardSchemaResult<Output> =\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> };\n\n// ---------------------------------------------------------------------------\n// Sync validate helper\n// ---------------------------------------------------------------------------\n\n/**\n * Run a Standard Schema's `~standard.validate()` synchronously.\n *\n * Zod v4's signature includes `Promise` in the return union to satisfy the\n * Standard Schema spec, but in practice Zod always validates synchronously\n * for the schema types we use. We assert the result is sync and throw if\n * it isn't — codec parsing must be synchronous.\n */\nexport function validateSync<Output>(\n schema: StandardSchemaV1<Output>,\n value: unknown\n): StandardSchemaResult<Output> {\n const result = schema['~standard'].validate(value);\n if (result instanceof Promise) {\n throw new Error(\n '[timber] fromSchema: schema returned a Promise — only sync schemas are supported.'\n );\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Type guards\n// ---------------------------------------------------------------------------\n\n/** Check if a value is a Standard Schema object. */\nexport function isStandardSchema(value: unknown): value is StandardSchemaV1 {\n return (\n typeof value === 'object' &&\n value !== null &&\n '~standard' in value &&\n typeof (value as StandardSchemaV1)['~standard']?.validate === 'function'\n );\n}\n\n/** Check if a value is a Codec (has parse + serialize methods). */\nexport function isCodec(value: unknown): value is Codec<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as Codec<unknown>).parse === 'function' &&\n typeof (value as Codec<unknown>).serialize === 'function'\n );\n}\n\n// ---------------------------------------------------------------------------\n// fromParamSchema — bridge from Standard Schema to Codec<T> for route params\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema to a Codec for route params.\n * Parse throws on failure (invalid param → 404). Serialize returns string.\n */\nexport function fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n const result = validateSync(schema, value);\n if (!result.issues) {\n return result.value;\n }\n const messages = result.issues.map((i) => i.message).join(', ');\n throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);\n },\n serialize(value: T): string | null {\n if (value === null || value === undefined) return null;\n if (Array.isArray(value)) return value.join('/');\n return String(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// resolveCodecOrSchema — generic resolver for any codec-or-schema field\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a field value to a Codec. Accepts Codec<T>, StandardSchemaV1<T>,\n * or (for route params) auto-wraps via fromParamSchema.\n *\n * @param fieldName - used in error messages\n * @param value - the codec or schema to resolve\n * @param mode - 'param' uses fromParamSchema (throws on parse failure),\n * 'search' uses fromSchema (falls back to default on failure)\n */\nexport function resolveCodecOrSchema(\n fieldName: string,\n value: unknown,\n mode: 'param' | 'search' = 'search'\n): Codec<unknown> {\n if (isCodec(value)) return value;\n if (isStandardSchema(value)) {\n return mode === 'param'\n ? fromParamSchema(fieldName, value)\n : (fromSchema(value) as Codec<unknown>);\n }\n throw new Error(\n `[timber] Field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n// ---------------------------------------------------------------------------\n// fromSchema — bridge from Standard Schema to Codec<T>\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema-compatible schema (Zod, Valibot, ArkType) to a\n * Codec<T>.\n *\n * Parse: coerces the raw string through the schema. On validation failure,\n * parses `undefined` to get the schema's default value (the schema should have\n * a `.default()` call). If that also fails, returns `undefined`.\n *\n * Serialize: uses `String()` for primitives, `null` for null/undefined.\n *\n * ```ts\n * import { fromSchema } from '@timber-js/app/codec'\n * import { z } from 'zod/v4'\n *\n * const pageCodec = fromSchema(z.coerce.number().int().min(1).default(1))\n * ```\n */\nexport function fromSchema<T>(schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // For array inputs (duplicate query keys), use the first value.\n // Browsers and URLSearchParams.get() return the first occurrence.\n const input = Array.isArray(value) ? value[0] : value;\n\n // Try parsing the raw value\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // On failure, try parsing undefined to get the default.\n // Re-validate each time so factory defaults (e.g. .default(() => []))\n // produce fresh values.\n const defaultResult = validateSync(schema, undefined);\n if (!defaultResult.issues) {\n return defaultResult.value;\n }\n\n // No default available — the field is implicitly optional. Return\n // undefined; defineSearchParams widens the field's inferred type to\n // T | undefined via InferField so this doesn't lie.\n // design/23-search-params.md §\"Implicit Optionality\"\n return undefined as T;\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n return String(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// fromArraySchema — bridge for array-valued codecs\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema for array values. Handles both single strings\n * and repeated query keys (`?tag=a&tag=b`).\n *\n * ```ts\n * import { fromArraySchema } from '@timber-js/app/codec'\n * import { z } from 'zod/v4'\n *\n * const tagsCodec = fromArraySchema(z.array(z.string()).default([]))\n * ```\n */\nexport function fromArraySchema<T>(schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // Coerce single string to array for array schemas\n let input: unknown = value;\n if (typeof value === 'string') {\n input = [value];\n } else if (value === undefined) {\n input = undefined;\n }\n\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // On failure, try undefined for default\n const defaultResult = validateSync(schema, undefined);\n if (!defaultResult.issues) {\n return defaultResult.value;\n }\n\n return undefined as T;\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (Array.isArray(value)) {\n return value.length === 0 ? null : value.join(',');\n }\n return String(value);\n },\n };\n}\n"],"mappings":";;;;;;;;;AAkDA,SAAgB,aACd,QACA,OAC8B;CAC9B,MAAM,SAAS,OAAO,YAAY,CAAC,SAAS,KAAK;CACjD,IAAI,kBAAkB,SACpB,MAAM,IAAI,MACR,mFACF;CAEF,OAAO;AACT;;AAOA,SAAgB,iBAAiB,OAA2C;CAC1E,OACE,OAAO,UAAU,YACjB,UAAU,QACV,eAAe,SACf,OAAQ,MAA2B,YAAY,EAAE,aAAa;AAElE;;AAGA,SAAgB,QAAQ,OAAyC;CAC/D,OACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAAyB,UAAU,cAC3C,OAAQ,MAAyB,cAAc;AAEnD;;;;;AAUA,SAAgB,gBAAmB,WAAmB,QAAuC;CAC3F,OAAO;EACL,MAAM,OAAyC;GAC7C,MAAM,SAAS,aAAa,QAAQ,KAAK;GACzC,IAAI,CAAC,OAAO,QACV,OAAO,OAAO;GAEhB,MAAM,WAAW,OAAO,OAAO,KAAK,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI;GAC9D,MAAM,IAAI,MAAM,mBAAmB,UAAU,qBAAqB,UAAU;EAC9E;EACA,UAAU,OAAyB;GACjC,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;GAClD,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,KAAK,GAAG;GAC/C,OAAO,OAAO,KAAK;EACrB;CACF;AACF;;;;;;;;;;AAeA,SAAgB,qBACd,WACA,OACA,OAA2B,UACX;CAChB,IAAI,QAAQ,KAAK,GAAG,OAAO;CAC3B,IAAI,iBAAiB,KAAK,GACxB,OAAO,SAAS,UACZ,gBAAgB,WAAW,KAAK,IAC/B,WAAW,KAAK;CAEvB,MAAM,IAAI,MACR,mBAAmB,UAAU,sJAG/B;AACF;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,WAAc,QAAuC;CACnE,OAAO;EACL,MAAM,OAAyC;GAM7C,MAAM,SAAS,aAAa,QAHd,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,KAGP;GACzC,IAAI,CAAC,OAAO,QACV,OAAO,OAAO;GAMhB,MAAM,gBAAgB,aAAa,QAAQ,KAAA,CAAS;GACpD,IAAI,CAAC,cAAc,QACjB,OAAO,cAAc;EAQzB;EAEA,UAAU,OAAyB;GACjC,IAAI,UAAU,QAAQ,UAAU,KAAA,GAC9B,OAAO;GAET,OAAO,OAAO,KAAK;EACrB;CACF;AACF;;;;;;;;;;;;AAiBA,SAAgB,gBAAmB,QAAuC;CACxE,OAAO;EACL,MAAM,OAAyC;GAE7C,IAAI,QAAiB;GACrB,IAAI,OAAO,UAAU,UACnB,QAAQ,CAAC,KAAK;QACT,IAAI,UAAU,KAAA,GACnB,QAAQ,KAAA;GAGV,MAAM,SAAS,aAAa,QAAQ,KAAK;GACzC,IAAI,CAAC,OAAO,QACV,OAAO,OAAO;GAIhB,MAAM,gBAAgB,aAAa,QAAQ,KAAA,CAAS;GACpD,IAAI,CAAC,cAAc,QACjB,OAAO,cAAc;EAIzB;EAEA,UAAU,OAAyB;GACjC,IAAI,UAAU,QAAQ,UAAU,KAAA,GAC9B,OAAO;GAET,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,WAAW,IAAI,OAAO,MAAM,KAAK,GAAG;GAEnD,OAAO,OAAO,KAAK;EACrB;CACF;AACF"}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { createContext, createElement, useContext, useMemo } from "react";
|
|
2
|
-
//#region src/client/segment-context.ts
|
|
3
|
-
/**
|
|
4
|
-
* Segment Context — provides layout segment position for useSelectedLayoutSegment hooks.
|
|
5
|
-
*
|
|
6
|
-
* Each layout in the segment tree is wrapped with a SegmentProvider that stores
|
|
7
|
-
* the URL segments from root to the current layout level. The hooks read this
|
|
8
|
-
* context to determine which child segments are active below the calling layout.
|
|
9
|
-
*
|
|
10
|
-
* The context value is intentionally minimal: just the segment path array and
|
|
11
|
-
* parallel route keys. No internal cache details are exposed.
|
|
12
|
-
*
|
|
13
|
-
* Design docs: design/19-client-navigation.md, design/14-ecosystem.md
|
|
14
|
-
*/
|
|
15
|
-
var SegmentContext = createContext(null);
|
|
16
|
-
/** Read the segment context. Returns null if no provider is above this component. */
|
|
17
|
-
function useSegmentContext() {
|
|
18
|
-
return useContext(SegmentContext);
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Wraps each layout to provide segment position context.
|
|
22
|
-
* Injected by rsc-entry.ts during element tree construction.
|
|
23
|
-
*/
|
|
24
|
-
function SegmentProvider({ segments, segmentId: _segmentId, parallelRouteKeys, children }) {
|
|
25
|
-
const value = useMemo(() => ({
|
|
26
|
-
segments,
|
|
27
|
-
parallelRouteKeys
|
|
28
|
-
}), [segments.join("/"), parallelRouteKeys.join(",")]);
|
|
29
|
-
return createElement(SegmentContext.Provider, { value }, children);
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
|
-
export { useSegmentContext as n, SegmentProvider as t };
|
|
33
|
-
|
|
34
|
-
//# sourceMappingURL=segment-context-ZDnXDkbz.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"segment-context-ZDnXDkbz.js","names":[],"sources":["../../src/client/segment-context.ts"],"sourcesContent":["/**\n * Segment Context — provides layout segment position for useSelectedLayoutSegment hooks.\n *\n * Each layout in the segment tree is wrapped with a SegmentProvider that stores\n * the URL segments from root to the current layout level. The hooks read this\n * context to determine which child segments are active below the calling layout.\n *\n * The context value is intentionally minimal: just the segment path array and\n * parallel route keys. No internal cache details are exposed.\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { createContext, useContext, createElement, useMemo } from 'react';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface SegmentContextValue {\n /** URL segments from root to this layout (e.g. ['', 'dashboard', 'settings']) */\n segments: string[];\n /** Parallel route slot keys available at this layout level (e.g. ['sidebar', 'modal']) */\n parallelRouteKeys: string[];\n}\n\n// ─── Context ─────────────────────────────────────────────────────\n\nconst SegmentContext = createContext<SegmentContextValue | null>(null);\n\n/** Read the segment context. Returns null if no provider is above this component. */\nexport function useSegmentContext(): SegmentContextValue | null {\n return useContext(SegmentContext);\n}\n\n// ─── Provider ────────────────────────────────────────────────────\n\ninterface SegmentProviderProps {\n segments: string[];\n /**\n * Unique identifier for this segment, used by the client-side segment\n * merger for element caching. For route groups this includes the group\n * name (e.g., \"/(marketing)\") since groups share their parent's urlPath.\n * Falls back to the reconstructed path from `segments` if not provided.\n */\n segmentId?: string;\n parallelRouteKeys: string[];\n children: React.ReactNode;\n}\n\n/**\n * Wraps each layout to provide segment position context.\n * Injected by rsc-entry.ts during element tree construction.\n */\nexport function SegmentProvider({\n segments,\n segmentId: _segmentId,\n parallelRouteKeys,\n children,\n}: SegmentProviderProps) {\n const value = useMemo(\n () => ({ segments, parallelRouteKeys }),\n // segments and parallelRouteKeys are static per layout — they don't change\n // across navigations. The layout's position in the tree is fixed.\n // Intentionally using derived keys — segments/parallelRouteKeys are static per layout\n [segments.join('/'), parallelRouteKeys.join(',')]\n );\n return createElement(SegmentContext.Provider, { value }, children);\n}\n"],"mappings":";;;;;;;;;;;;;;AA4BA,IAAM,iBAAiB,cAA0C,IAAI;;AAGrE,SAAgB,oBAAgD;CAC9D,OAAO,WAAW,cAAc;AAClC;;;;;AAqBA,SAAgB,gBAAgB,EAC9B,UACA,WAAW,YACX,mBACA,YACuB;CACvB,MAAM,QAAQ,eACL;EAAE;EAAU;CAAkB,IAIrC,CAAC,SAAS,KAAK,GAAG,GAAG,kBAAkB,KAAK,GAAG,CAAC,CAClD;CACA,OAAO,cAAc,eAAe,UAAU,EAAE,MAAM,GAAG,QAAQ;AACnE"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-query-states-DFvWd-EA.js","names":[],"sources":["../../src/client/use-query-states.ts"],"sourcesContent":["/**\n * useQueryStates — client-side hook for URL-synced search params.\n *\n * Delegates to nuqs for URL synchronization, batching, React 19 transitions,\n * and throttled URL writes. Bridges timber's SearchParamCodec protocol to\n * nuqs-compatible parsers.\n *\n * Design doc: design/23-search-params.md §\"Codec Bridge\"\n */\n\n'use client';\n\nimport { useQueryStates as nuqsUseQueryStates } from 'nuqs';\nimport type { SingleParser } from 'nuqs';\nimport type {\n SearchParamCodec,\n SearchParamsDefinition,\n SetParams,\n QueryStatesOptions,\n} from '../search-params/define.js';\nimport { getSearchParamsDefinition } from '../search-params/registry.js';\n\n// ─── Codec Bridge ─────────────────────────────────────────────────\n\n// nuqs's parser contract conflates values timber codecs distinguish:\n// parse() returning null means \"unparseable, substitute defaultValue\",\n// and undefined entries are skipped entirely. Timber codecs can\n// legitimately produce both — bare z.string() yields undefined for absent\n// params (implicit optionality), and a codec may map a present value to\n// null. Wrap those two values in sentinels across the nuqs boundary and\n// unwrap them before handing values back to the caller, so the client\n// hook returns exactly what server-side parse() returns.\n// Unique object references compared by identity — a codec can never\n// produce these from URL input, so user-controlled strings cannot collide\n// with them (unlike string sentinels), and unlike Symbols they survive\n// nuqs's internal string coercion without throwing.\nconst NULL_SENTINEL: object = { timberSentinel: 'null' };\nconst UNDEFINED_SENTINEL: object = { timberSentinel: 'undefined' };\n\nfunction wrapNuqsValue(value: unknown): unknown {\n if (value === null) return NULL_SENTINEL;\n if (value === undefined) return UNDEFINED_SENTINEL;\n return value;\n}\n\nfunction unwrapNuqsValue(value: unknown): unknown {\n if (value === NULL_SENTINEL) return null;\n if (value === UNDEFINED_SENTINEL) return undefined;\n return value;\n}\n\n/**\n * Bridge a timber SearchParamCodec to a nuqs-compatible SingleParser.\n *\n * nuqs parsers: { parse(string) → T|null, serialize?(T) → string, eq?, defaultValue? }\n * timber codecs: { parse(string|string[]|undefined) → T, serialize(T) → string|null }\n *\n * The defaultValue is computed eagerly. Codecs are documented to return a\n * default rather than throw, but a throwing codec must not crash every\n * component that mounts the hook — treat its default as undefined and let\n * its error surface from server-side parse() instead.\n */\nfunction bridgeCodec<T>(codec: SearchParamCodec<T>): SingleParser<T> & { defaultValue: T } {\n let defaultValue: unknown;\n try {\n defaultValue = codec.parse(undefined);\n } catch {\n defaultValue = undefined;\n }\n return {\n parse: (v: string) => wrapNuqsValue(codec.parse(v)),\n serialize: (v: unknown) => {\n const value = unwrapNuqsValue(v);\n // Delegate null to the codec — some codecs encode null as a real\n // query value. undefined has no encoding; nuqs requires a string.\n return value === undefined ? '' : (codec.serialize(value as T) ?? '');\n },\n defaultValue: wrapNuqsValue(defaultValue),\n eq: (a: unknown, b: unknown) => {\n if (a === b) return true;\n try {\n return (\n codec.serialize(unwrapNuqsValue(a) as T) === codec.serialize(unwrapNuqsValue(b) as T)\n );\n } catch {\n return false;\n }\n },\n } as SingleParser<T> & { defaultValue: T };\n}\n\n/**\n * Bridge an entire codec map to nuqs-compatible parsers.\n */\nfunction bridgeCodecs<T extends Record<string, unknown>>(codecs: {\n [K in keyof T]: SearchParamCodec<T[K]>;\n}) {\n const result: Record<string, SingleParser<unknown> & { defaultValue: unknown }> = {};\n for (const key of Object.keys(codecs)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n result[key] = bridgeCodec(codecs[key as keyof T]) as any;\n }\n return result as { [K in keyof T]: SingleParser<T[K]> & { defaultValue: T[K] } };\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────\n\n/**\n * Read and write typed search params from/to the URL.\n *\n * Delegates to nuqs internally. The timber nuqs adapter (auto-injected in\n * browser-entry.ts) handles RSC navigation on non-shallow updates.\n *\n * Usage:\n * ```ts\n * // Via a SearchParamsDefinition\n * const [params, setParams] = definition.useQueryStates()\n *\n * // Standalone with inline codecs\n * const [params, setParams] = useQueryStates({\n * page: fromSchema(z.coerce.number().int().min(1).default(1)),\n * })\n * ```\n */\nexport function useQueryStates<T extends Record<string, unknown>>(\n codecsOrRoute: { [K in keyof T]: SearchParamCodec<T[K]> } | string,\n _options?: QueryStatesOptions,\n urlKeys?: Readonly<Record<string, string>>\n): [T, SetParams<T>] {\n // Route-string overload: resolve codecs from the registry\n let codecs: { [K in keyof T]: SearchParamCodec<T[K]> };\n let resolvedUrlKeys = urlKeys;\n if (typeof codecsOrRoute === 'string') {\n const definition = getSearchParamsDefinition(codecsOrRoute);\n if (!definition) {\n throw new Error(\n `useQueryStates('${codecsOrRoute}'): no search params registered for this route. ` +\n `Either the route has no search-params.ts file, or it hasn't been loaded yet. ` +\n `For cross-route usage, import the definition explicitly.`\n );\n }\n codecs = definition.codecs as { [K in keyof T]: SearchParamCodec<T[K]> };\n resolvedUrlKeys = definition.urlKeys;\n } else {\n codecs = codecsOrRoute;\n }\n\n const bridged = bridgeCodecs(codecs);\n\n // Forward hook-level options (shallow, scroll, history) to nuqs.\n // These become the default for all setter calls from this hook instance.\n // Per-call options in setParams(values, opts) override these defaults.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const nuqsOptions: any = {};\n if (_options?.shallow !== undefined) nuqsOptions.shallow = _options.shallow;\n if (_options?.scroll !== undefined) nuqsOptions.scroll = _options.scroll;\n if (_options?.history !== undefined) nuqsOptions.history = _options.history;\n if (resolvedUrlKeys && Object.keys(resolvedUrlKeys).length > 0) {\n nuqsOptions.urlKeys = resolvedUrlKeys;\n }\n\n let values: Record<string, unknown>;\n let setValues: Function;\n try {\n [values, setValues] = nuqsUseQueryStates(bridged, nuqsOptions);\n } catch (err) {\n if (\n err instanceof Error &&\n /Invalid hook call|cannot be called|Cannot read properties of null/i.test(err.message)\n ) {\n throw new Error(\n 'useQueryStates is a client component hook and cannot be called outside a React component. ' +\n 'Use definition.parse(searchParams) in server components instead.'\n );\n }\n throw err;\n }\n\n // Unwrap the null/undefined sentinels the bridge injected (see Codec\n // Bridge above) so callers see exactly what server-side parse() returns.\n // Copy-on-write preserves the identity of nuqs's memoized values object\n // when nothing needs unwrapping.\n let normalized = values;\n for (const key of Object.keys(bridged)) {\n const value = normalized[key];\n if (value === NULL_SENTINEL || value === UNDEFINED_SENTINEL) {\n if (normalized === values) normalized = { ...values };\n normalized[key] = unwrapNuqsValue(value);\n }\n }\n\n // Wrap the nuqs setter to match timber's SetParams<T> signature.\n // nuqs's setter accepts Partial<Nullable<Values>> | UpdaterFn | null.\n // timber's setter accepts Partial<T> with optional SetParamsOptions.\n const setParams: SetParams<T> = (partial, setOptions?) => {\n const nuqsSetOptions: Record<string, unknown> = {};\n if (setOptions?.shallow !== undefined) nuqsSetOptions.shallow = setOptions.shallow;\n if (setOptions?.scroll !== undefined) nuqsSetOptions.scroll = setOptions.scroll;\n if (setOptions?.history !== undefined) nuqsSetOptions.history = setOptions.history;\n // nuqs's update loop skips undefined entries and treats null as a\n // key deletion before serialize runs. Timber semantics:\n // - setParams({ q: undefined }) must clear ?q= (absent = undefined),\n // so explicit undefined maps to a null deletion.\n // - setParams({ q: null }) clears the key only when the codec encodes\n // null as \"omit\" (serialize(null) === null). If the codec encodes\n // null as a real query value, forward the sentinel so the bridged\n // serialize writes it — matching definition.serialize({ q: null }).\n let forwarded: Record<string, unknown> = partial;\n for (const key of Object.keys(partial)) {\n const value = partial[key as keyof T];\n if (value === undefined) {\n if (forwarded === partial) forwarded = { ...partial };\n forwarded[key] = null;\n } else if (value === null) {\n let encoded: string | null = null;\n try {\n encoded = codecs[key as keyof T]?.serialize(null as T[keyof T]) ?? null;\n } catch {\n // Codec can't serialize null — treat as a deletion.\n }\n if (encoded !== null) {\n if (forwarded === partial) forwarded = { ...partial };\n forwarded[key] = NULL_SENTINEL;\n }\n }\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n void setValues(forwarded as any, nuqsSetOptions);\n };\n\n return [normalized as T, setParams];\n}\n\n// ─── Definition binding ───────────────────────────────────────────\n\n/**\n * Create a useQueryStates binding for a SearchParamsDefinition.\n * This is used internally by SearchParamsDefinition.useQueryStates().\n */\nexport function bindUseQueryStates<T extends Record<string, unknown>>(\n definition: SearchParamsDefinition<T>\n): (options?: QueryStatesOptions) => [T, SetParams<T>] {\n return (options?: QueryStatesOptions) => {\n return useQueryStates<T>(definition.codecs, options, definition.urlKeys);\n };\n}\n"],"mappings":";;;;;;;;;;;;AAoCA,IAAM,gBAAwB,EAAE,gBAAgB,OAAO;AACvD,IAAM,qBAA6B,EAAE,gBAAgB,YAAY;AAEjE,SAAS,cAAc,OAAyB;CAC9C,IAAI,UAAU,MAAM,OAAO;CAC3B,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,OAAO;AACT;AAEA,SAAS,gBAAgB,OAAyB;CAChD,IAAI,UAAU,eAAe,OAAO;CACpC,IAAI,UAAU,oBAAoB,OAAO,KAAA;CACzC,OAAO;AACT;;;;;;;;;;;;AAaA,SAAS,YAAe,OAAmE;CACzF,IAAI;CACJ,IAAI;EACF,eAAe,MAAM,MAAM,KAAA,CAAS;CACtC,QAAQ;EACN,eAAe,KAAA;CACjB;CACA,OAAO;EACL,QAAQ,MAAc,cAAc,MAAM,MAAM,CAAC,CAAC;EAClD,YAAY,MAAe;GACzB,MAAM,QAAQ,gBAAgB,CAAC;GAG/B,OAAO,UAAU,KAAA,IAAY,KAAM,MAAM,UAAU,KAAU,KAAK;EACpE;EACA,cAAc,cAAc,YAAY;EACxC,KAAK,GAAY,MAAe;GAC9B,IAAI,MAAM,GAAG,OAAO;GACpB,IAAI;IACF,OACE,MAAM,UAAU,gBAAgB,CAAC,CAAM,MAAM,MAAM,UAAU,gBAAgB,CAAC,CAAM;GAExF,QAAQ;IACN,OAAO;GACT;EACF;CACF;AACF;;;;AAKA,SAAS,aAAgD,QAEtD;CACD,MAAM,SAA4E,CAAC;CACnF,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAElC,OAAO,OAAO,YAAY,OAAO,IAAe;CAElD,OAAO;AACT;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,iBACd,eACA,UACA,SACmB;CAEnB,IAAI;CACJ,IAAI,kBAAkB;CACtB,IAAI,OAAO,kBAAkB,UAAU;EACrC,MAAM,aAAa,0BAA0B,aAAa;EAC1D,IAAI,CAAC,YACH,MAAM,IAAI,MACR,mBAAmB,cAAc,sLAGnC;EAEF,SAAS,WAAW;EACpB,kBAAkB,WAAW;CAC/B,OACE,SAAS;CAGX,MAAM,UAAU,aAAa,MAAM;CAMnC,MAAM,cAAmB,CAAC;CAC1B,IAAI,UAAU,YAAY,KAAA,GAAW,YAAY,UAAU,SAAS;CACpE,IAAI,UAAU,WAAW,KAAA,GAAW,YAAY,SAAS,SAAS;CAClE,IAAI,UAAU,YAAY,KAAA,GAAW,YAAY,UAAU,SAAS;CACpE,IAAI,mBAAmB,OAAO,KAAK,eAAe,CAAC,CAAC,SAAS,GAC3D,YAAY,UAAU;CAGxB,IAAI;CACJ,IAAI;CACJ,IAAI;EACF,CAAC,QAAQ,aAAa,eAAmB,SAAS,WAAW;CAC/D,SAAS,KAAK;EACZ,IACE,eAAe,SACf,qEAAqE,KAAK,IAAI,OAAO,GAErF,MAAM,IAAI,MACR,4JAEF;EAEF,MAAM;CACR;CAMA,IAAI,aAAa;CACjB,KAAK,MAAM,OAAO,OAAO,KAAK,OAAO,GAAG;EACtC,MAAM,QAAQ,WAAW;EACzB,IAAI,UAAU,iBAAiB,UAAU,oBAAoB;GAC3D,IAAI,eAAe,QAAQ,aAAa,EAAE,GAAG,OAAO;GACpD,WAAW,OAAO,gBAAgB,KAAK;EACzC;CACF;CAKA,MAAM,aAA2B,SAAS,eAAgB;EACxD,MAAM,iBAA0C,CAAC;EACjD,IAAI,YAAY,YAAY,KAAA,GAAW,eAAe,UAAU,WAAW;EAC3E,IAAI,YAAY,WAAW,KAAA,GAAW,eAAe,SAAS,WAAW;EACzE,IAAI,YAAY,YAAY,KAAA,GAAW,eAAe,UAAU,WAAW;EAS3E,IAAI,YAAqC;EACzC,KAAK,MAAM,OAAO,OAAO,KAAK,OAAO,GAAG;GACtC,MAAM,QAAQ,QAAQ;GACtB,IAAI,UAAU,KAAA,GAAW;IACvB,IAAI,cAAc,SAAS,YAAY,EAAE,GAAG,QAAQ;IACpD,UAAU,OAAO;GACnB,OAAO,IAAI,UAAU,MAAM;IACzB,IAAI,UAAyB;IAC7B,IAAI;KACF,UAAU,OAAO,IAAe,EAAE,UAAU,IAAkB,KAAK;IACrE,QAAQ,CAER;IACA,IAAI,YAAY,MAAM;KACpB,IAAI,cAAc,SAAS,YAAY,EAAE,GAAG,QAAQ;KACpD,UAAU,OAAO;IACnB;GACF;EACF;EAEA,UAAe,WAAkB,cAAc;CACjD;CAEA,OAAO,CAAC,YAAiB,SAAS;AACpC;;;;;AAQA,SAAgB,mBACd,YACqD;CACrD,QAAQ,YAAiC;EACvC,OAAO,iBAAkB,WAAW,QAAQ,SAAS,WAAW,OAAO;CACzE;AACF"}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { t as resolveSegmentParams } from "./slot-params-BCTmZkQB.js";
|
|
2
|
-
import { a as _setCurrentParams, d as currentParams, f as currentSlotParams, n as getSsrData, o as _setCurrentSlotParams } from "./ssr-data-14MXm7Pj.js";
|
|
3
|
-
import "./param-value-C8TNYchQ.js";
|
|
4
|
-
import React from "react";
|
|
5
|
-
//#region src/client/params-context.ts
|
|
6
|
-
/**
|
|
7
|
-
* Segment params context — the one channel params use to reach the browser.
|
|
8
|
-
*
|
|
9
|
-
* Params ride the RSC payload's root row as a sibling of the tree
|
|
10
|
-
* (`{ tree, params, slotParams }`), rather than in four side channels that
|
|
11
|
-
* raced to seed them: a response header, an inline script, and two build-time
|
|
12
|
-
* manifest fields all previously carried the same record, each with its own
|
|
13
|
-
* `JSON.stringify` (TIM-1294).
|
|
14
|
-
*
|
|
15
|
-
* Riding the payload is what makes them *typed*. `defineSchema` takes any
|
|
16
|
-
* `Codec<T>`, so a coerced param is whatever the codec returned — a `Date`, a
|
|
17
|
-
* `bigint` — and `JSON.stringify` either flattened it to a string or threw
|
|
18
|
-
* mid-response. React Flight carries those values natively, so the client
|
|
19
|
-
* reads the value the server produced instead of a lossy copy of it. See
|
|
20
|
-
* design/41-global-params.md §"Transport".
|
|
21
|
-
*
|
|
22
|
-
* **The client owns the provider.** There is exactly one `ParamsProvider` in
|
|
23
|
-
* the browser's tree, rendered by `PayloadRoot` above the point where a
|
|
24
|
-
* partial navigation splices the new payload into the retained tree. It has to
|
|
25
|
-
* be there and it has to be alone: a provider *inside* the payload lands below
|
|
26
|
-
* the retained region, whose own root is the departing route's provider, so
|
|
27
|
-
* every reader in a skipped layout resolves to the departing record and no
|
|
28
|
-
* amount of wrapping above it helps (TIM-1297).
|
|
29
|
-
*
|
|
30
|
-
* Ordering still holds without a bootstrap contract, for the same reason it
|
|
31
|
-
* did when the provider was in the tree: a provider renders before its own
|
|
32
|
-
* descendants by construction, so `useSegmentParams()` is correct during
|
|
33
|
-
* hydration without anything having to run before `hydrateRoot()`.
|
|
34
|
-
*/
|
|
35
|
-
/**
|
|
36
|
-
* SINGLETON GUARANTEE: globalThis + `Symbol.for`, the same pattern as
|
|
37
|
-
* `NavigationContext` and `SegmentUpdateContext`.
|
|
38
|
-
*
|
|
39
|
-
* The RSC client bundler can duplicate a module across chunks, and with ESM
|
|
40
|
-
* output each chunk gets its own module scope — so a bare `createContext` at
|
|
41
|
-
* module level yields one context per chunk. This module is now reached from
|
|
42
|
-
* *both* graphs: `PayloadRoot` is imported by the browser entry, while
|
|
43
|
-
* `useParamsContext()` arrives through the client-reference graph with the
|
|
44
|
-
* app's own components. A duplicate would put the provider on instance A and
|
|
45
|
-
* every reader on instance B, so `useContext` returns `null` and every
|
|
46
|
-
* `useSegmentParams()` call silently falls back to the module snapshot.
|
|
47
|
-
*
|
|
48
|
-
* This module was the one client context without the guard — harmless while
|
|
49
|
-
* the provider travelled inside the payload, in the same graph as its readers,
|
|
50
|
-
* and load-bearing the moment the client started rendering it (TIM-1297).
|
|
51
|
-
*
|
|
52
|
-
* The React APIs are reached through the namespace rather than named imports,
|
|
53
|
-
* for the same reason `segment-update-context.ts` and `navigation-context.ts`
|
|
54
|
-
* do it: React's `react-server` export provides neither `createContext` nor
|
|
55
|
-
* `useContext`, and a *named* ESM import of a missing export fails at module
|
|
56
|
-
* instantiation — before any feature check could run. This module is reachable
|
|
57
|
-
* from `@timber-js/app/segment-params`, which a Server Component imports for
|
|
58
|
-
* `defineSegmentParams`, so the named form crashed that entry outright
|
|
59
|
-
* (codex, PR #992; reproduced with
|
|
60
|
-
* `node --conditions react-server -e "import('./dist/segment-params/index.js')"`).
|
|
61
|
-
*
|
|
62
|
-
* See design/19-client-navigation.md §"Singleton Guarantee via globalThis"
|
|
63
|
-
*/
|
|
64
|
-
var PARAMS_CTX_KEY = Symbol.for("__timber_params_ctx");
|
|
65
|
-
function getOrCreateContext() {
|
|
66
|
-
const store = globalThis;
|
|
67
|
-
const existing = store[PARAMS_CTX_KEY];
|
|
68
|
-
if (existing !== void 0) return existing;
|
|
69
|
-
if (typeof React.createContext !== "function") return;
|
|
70
|
-
const ctx = React.createContext(null);
|
|
71
|
-
store[PARAMS_CTX_KEY] = ctx;
|
|
72
|
-
return ctx;
|
|
73
|
-
}
|
|
74
|
-
var ParamsContext = getOrCreateContext();
|
|
75
|
-
/**
|
|
76
|
-
* Read the params provided by the tree. Returns null when no provider is
|
|
77
|
-
* above the caller — a component rendered outside a timber route, a
|
|
78
|
-
* `useSegmentParams()` call from outside React entirely, or any component
|
|
79
|
-
* during SSR (where the params reach the hook through the ALS-backed SSR data
|
|
80
|
-
* context instead, and there is no client-owned tree to hold a provider).
|
|
81
|
-
*/
|
|
82
|
-
function useParamsContext() {
|
|
83
|
-
return React.useContext(ParamsContext);
|
|
84
|
-
}
|
|
85
|
-
//#endregion
|
|
86
|
-
//#region src/client/use-segment-params.ts
|
|
87
|
-
/**
|
|
88
|
-
* Set the current route params in the module-level store.
|
|
89
|
-
*
|
|
90
|
-
* Called by the router on each navigation. This updates the fallback
|
|
91
|
-
* snapshot used by tests and by the hook when called outside a React
|
|
92
|
-
* component (no NavigationContext available).
|
|
93
|
-
*
|
|
94
|
-
* On the client, the primary reactivity path is NavigationContext —
|
|
95
|
-
* the router calls setNavigationState() then renderRoot() which wraps
|
|
96
|
-
* the element in NavigationProvider. setCurrentParams is still called
|
|
97
|
-
* for the module-level fallback.
|
|
98
|
-
*
|
|
99
|
-
* During SSR, params are also available via getSsrData().params
|
|
100
|
-
* (ALS-backed).
|
|
101
|
-
*/
|
|
102
|
-
function setCurrentParams(params) {
|
|
103
|
-
_setCurrentParams(params);
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Set the per-slot params snapshot in the module-level store.
|
|
107
|
-
*
|
|
108
|
-
* Paired with `setCurrentParams`: the router calls both on every navigation,
|
|
109
|
-
* including with `null` when a response carries no slot params, so a slot's
|
|
110
|
-
* params from the *previous* route cannot be read on the next one. Fill and
|
|
111
|
-
* serve are paired; so are fill and clear. See TIM-1285.
|
|
112
|
-
*/
|
|
113
|
-
function setCurrentSlotParams(slotParams) {
|
|
114
|
-
_setCurrentSlotParams(slotParams);
|
|
115
|
-
}
|
|
116
|
-
function useSegmentParams(segmentPath) {
|
|
117
|
-
try {
|
|
118
|
-
const paramsContext = useParamsContext();
|
|
119
|
-
if (paramsContext !== null) return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);
|
|
120
|
-
} catch {}
|
|
121
|
-
const ssrData = getSsrData();
|
|
122
|
-
if (ssrData) return resolveSegmentParams(ssrData.params, ssrData.slotParams, segmentPath);
|
|
123
|
-
return resolveSegmentParams(currentParams, currentSlotParams, segmentPath);
|
|
124
|
-
}
|
|
125
|
-
//#endregion
|
|
126
|
-
export { setCurrentSlotParams as n, useSegmentParams as r, setCurrentParams as t };
|
|
127
|
-
|
|
128
|
-
//# sourceMappingURL=use-segment-params-ClyUNq4d.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-segment-params-ClyUNq4d.js","names":[],"sources":["../../src/client/params-context.ts","../../src/client/use-segment-params.ts"],"sourcesContent":["/**\n * Segment params context — the one channel params use to reach the browser.\n *\n * Params ride the RSC payload's root row as a sibling of the tree\n * (`{ tree, params, slotParams }`), rather than in four side channels that\n * raced to seed them: a response header, an inline script, and two build-time\n * manifest fields all previously carried the same record, each with its own\n * `JSON.stringify` (TIM-1294).\n *\n * Riding the payload is what makes them *typed*. `defineSchema` takes any\n * `Codec<T>`, so a coerced param is whatever the codec returned — a `Date`, a\n * `bigint` — and `JSON.stringify` either flattened it to a string or threw\n * mid-response. React Flight carries those values natively, so the client\n * reads the value the server produced instead of a lossy copy of it. See\n * design/41-global-params.md §\"Transport\".\n *\n * **The client owns the provider.** There is exactly one `ParamsProvider` in\n * the browser's tree, rendered by `PayloadRoot` above the point where a\n * partial navigation splices the new payload into the retained tree. It has to\n * be there and it has to be alone: a provider *inside* the payload lands below\n * the retained region, whose own root is the departing route's provider, so\n * every reader in a skipped layout resolves to the departing record and no\n * amount of wrapping above it helps (TIM-1297).\n *\n * Ordering still holds without a bootstrap contract, for the same reason it\n * did when the provider was in the tree: a provider renders before its own\n * descendants by construction, so `useSegmentParams()` is correct during\n * hydration without anything having to run before `hydrateRoot()`.\n */\n\n'use client';\n\nimport React, { createElement, useMemo, use } from 'react';\nimport { _setCurrentParams, _setCurrentSlotParams } from './state.js';\nimport { toNullProtoRecord } from '../shared/param-value.js';\nimport { readPublishedParams, type PublishedParams } from '../shared/payload-root.js';\nimport type { SlotParamsRecord } from '../shared/slot-params.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type ParamsContextValue = PublishedParams;\n\n// ─── Context ─────────────────────────────────────────────────────\n\n/**\n * SINGLETON GUARANTEE: globalThis + `Symbol.for`, the same pattern as\n * `NavigationContext` and `SegmentUpdateContext`.\n *\n * The RSC client bundler can duplicate a module across chunks, and with ESM\n * output each chunk gets its own module scope — so a bare `createContext` at\n * module level yields one context per chunk. This module is now reached from\n * *both* graphs: `PayloadRoot` is imported by the browser entry, while\n * `useParamsContext()` arrives through the client-reference graph with the\n * app's own components. A duplicate would put the provider on instance A and\n * every reader on instance B, so `useContext` returns `null` and every\n * `useSegmentParams()` call silently falls back to the module snapshot.\n *\n * This module was the one client context without the guard — harmless while\n * the provider travelled inside the payload, in the same graph as its readers,\n * and load-bearing the moment the client started rendering it (TIM-1297).\n *\n * The React APIs are reached through the namespace rather than named imports,\n * for the same reason `segment-update-context.ts` and `navigation-context.ts`\n * do it: React's `react-server` export provides neither `createContext` nor\n * `useContext`, and a *named* ESM import of a missing export fails at module\n * instantiation — before any feature check could run. This module is reachable\n * from `@timber-js/app/segment-params`, which a Server Component imports for\n * `defineSegmentParams`, so the named form crashed that entry outright\n * (codex, PR #992; reproduced with\n * `node --conditions react-server -e \"import('./dist/segment-params/index.js')\"`).\n *\n * See design/19-client-navigation.md §\"Singleton Guarantee via globalThis\"\n */\nconst PARAMS_CTX_KEY = Symbol.for('__timber_params_ctx');\n\nfunction getOrCreateContext(): React.Context<ParamsContextValue | null> {\n const store = globalThis as Record<symbol, unknown>;\n const existing = store[PARAMS_CTX_KEY] as React.Context<ParamsContextValue | null> | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext !== 'function') {\n // RSC environment — no contexts here. Nothing in this module runs on that\n // side; it only has to import cleanly.\n return undefined as unknown as React.Context<ParamsContextValue | null>;\n }\n const ctx = React.createContext<ParamsContextValue | null>(null);\n store[PARAMS_CTX_KEY] = ctx;\n return ctx;\n}\n\nconst ParamsContext = getOrCreateContext();\n\n/**\n * Read the params provided by the tree. Returns null when no provider is\n * above the caller — a component rendered outside a timber route, a\n * `useSegmentParams()` call from outside React entirely, or any component\n * during SSR (where the params reach the hook through the ALS-backed SSR data\n * context instead, and there is no client-owned tree to hold a provider).\n */\nexport function useParamsContext(): ParamsContextValue | null {\n return React.useContext(ParamsContext);\n}\n\n// ─── Provider ────────────────────────────────────────────────────\n\ninterface ParamsProviderProps {\n params: Record<string, string | string[]>;\n slotParams: SlotParamsRecord | null;\n children?: React.ReactNode;\n}\n\n/**\n * Provides the current navigation's params to everything below it.\n *\n * Rendered only by `PayloadRoot`. Not exported: a second provider anywhere in\n * the tree would shadow this one for the region below it, which is precisely\n * the defect TIM-1297 fixed.\n *\n * The module-level snapshot in `state.ts` is written during render rather\n * than in an effect. It is the fallback path for `useSegmentParams()` called\n * outside a component (tests, module scope), and an effect would leave that\n * path reading the *previous* route's params for the whole commit — the\n * window in which a navigation's components actually run. Writing during\n * render is safe here because the value is derived entirely from props: a\n * double-invoked render in StrictMode writes the same record twice.\n */\nfunction ParamsProvider({ params, slotParams, children }: ParamsProviderProps) {\n // Restore the null prototype the wire could not carry. Flight rejects a\n // null-prototype object, so `withPublishedParams` flattens the records;\n // rebuilding them here is what keeps `params.constructor` returning\n // `undefined` instead of a function for a param the route does not define\n // (design/13-security.md #36c). Memoized on the props so a re-render with\n // the same records does not rebuild — the identity of what the hook returns\n // is load-bearing for `useEffect` dependencies (TIM-1285).\n const value = useMemo(\n () => ({\n params: toNullProtoRecord(params),\n slotParams: toNullProtoRecord(slotParams),\n }),\n [params, slotParams]\n );\n\n // Keep the out-of-component fallback in step with the tree being rendered.\n _setCurrentParams(value.params);\n _setCurrentSlotParams(value.slotParams);\n\n return createElement(ParamsContext.Provider, { value }, children);\n}\n\n// ─── Payload root ────────────────────────────────────────────────\n\n/**\n * The client's root: publishes a payload's params over the tree being shown.\n *\n * Rendered at the same position in the wrapper chain on **every** render path\n * — hydration, full navigation, partial navigation, popstate replay, shallow\n * search sync, and revalidation from a server action. Being unconditional is\n * load-bearing twice over: an element type that appears on one render and not\n * the next remounts everything below it, destroying exactly the layout state a\n * partial navigation exists to preserve; and a reader in a skipped layout has\n * to have *some* provider above it on every path or it falls back to the\n * module-level snapshot.\n *\n * `children` is the tree to display, which is not always `source`'s tree:\n *\n * - Full navigation, hydration, replay — `source` is the payload being shown,\n * and `children` is its own tree.\n * - **Partial navigation** — `children` is the *retained* tree and `source` is\n * the *incoming* payload. This is the case the whole design exists for: the\n * retained tree is not re-rendered, so the destination's params can only\n * reach it from above, and this provider is above it.\n *\n * `source` may be a thenable, in which case this suspends on the payload's\n * root row. That happens on the hydration path only, where the payload\n * promise was going to be rendered at this position anyway. Every other path\n * resolves the row in the router — inside the navigation transition — and\n * hands over a settled value, so a decode rejection surfaces where React\n * renders the tree and is caught by the error boundary *around* it, rather\n * than here, above every boundary the app has.\n */\nexport function PayloadRoot({ source, children }: { source: unknown; children?: React.ReactNode }) {\n const resolved = isThenable(source) ? use(source) : source;\n const { params, slotParams } = readPublishedParams(resolved);\n return createElement(ParamsProvider, { params, slotParams }, children);\n}\n\nfunction isThenable(value: unknown): value is Promise<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function'\n );\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['segmentParams']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * During SSR, params are read from the ALS-backed SSR data context\n * (populated by ssr-entry.ts) to ensure correct per-request isolation\n * across concurrent requests with streaming Suspense.\n *\n * Reactivity: On the client, useParams() reads from ParamsContext, published\n * by the one provider the client renders above the merge point\n * (`PayloadRoot`). Params update atomically with the tree because they travel\n * on the same payload root — there is no separate channel that could be\n * seeded a render early or late (TIM-1294, TIM-1297).\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { Routes } from '../index.js';\nimport { getSsrData } from './ssr-data.js';\nimport {\n currentParams,\n currentSlotParams,\n _setCurrentParams,\n _setCurrentSlotParams,\n paramsListeners,\n} from './state.js';\nimport { resolveSegmentParams, type SlotParamsRecord } from '../shared/slot-params.js';\nimport { useParamsContext } from './params-context.js';\n\n// ---------------------------------------------------------------------------\n// Module-level subscribe/notify pattern — kept for backward compat and tests\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to params changes.\n * Retained for backward compatibility with tests that verify the\n * subscribe/notify contract. On the client, useParams() reads from\n * NavigationContext instead.\n */\nexport function subscribe(callback: () => void): () => void {\n paramsListeners.add(callback);\n return () => paramsListeners.delete(callback);\n}\n\n/**\n * Get the current params snapshot (module-level fallback).\n * Used by tests and by the hook when called outside a React component.\n */\nexport function getSnapshot(): Record<string, string | string[]> {\n return currentParams;\n}\n\n// ---------------------------------------------------------------------------\n// Framework API — called by the segment router on each navigation\n// ---------------------------------------------------------------------------\n\n/**\n * Set the current route params in the module-level store.\n *\n * Called by the router on each navigation. This updates the fallback\n * snapshot used by tests and by the hook when called outside a React\n * component (no NavigationContext available).\n *\n * On the client, the primary reactivity path is NavigationContext —\n * the router calls setNavigationState() then renderRoot() which wraps\n * the element in NavigationProvider. setCurrentParams is still called\n * for the module-level fallback.\n *\n * During SSR, params are also available via getSsrData().params\n * (ALS-backed).\n */\nexport function setCurrentParams(params: Record<string, string | string[]>): void {\n _setCurrentParams(params);\n}\n\n/**\n * Set the per-slot params snapshot in the module-level store.\n *\n * Paired with `setCurrentParams`: the router calls both on every navigation,\n * including with `null` when a response carries no slot params, so a slot's\n * params from the *previous* route cannot be read on the next one. Fill and\n * serve are paired; so are fill and clear. See TIM-1285.\n */\nexport function setCurrentSlotParams(slotParams: SlotParamsRecord | null): void {\n _setCurrentSlotParams(slotParams);\n}\n\n/**\n * Notify all legacy subscribers that params have changed.\n *\n * Retained for backward compatibility with tests. On the client,\n * the NavigationContext + renderRoot pattern replaces this — params\n * update atomically with the tree render, so explicit notification\n * is no longer needed.\n */\nexport function notifyParamsListeners(): void {\n for (const listener of paramsListeners) {\n listener();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public hook\n// ---------------------------------------------------------------------------\n\n/**\n * Read the current route's dynamic params.\n *\n * The optional `_route` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value.\n *\n * On the client, reads from ParamsContext, published by `PayloadRoot` above\n * everything the navigation renders. Params update atomically with the RSC\n * tree — no timing gap.\n *\n * During SSR, reads from the ALS-backed SSR data context to ensure\n * per-request isolation across concurrent requests with streaming Suspense.\n *\n * When called outside a React component (e.g., in test assertions),\n * falls back to the module-level snapshot.\n *\n * @overload Typed — when a known segment path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useSegmentParams<R extends keyof Routes>(\n segmentPath: R\n): Routes[R] extends { segmentParams: infer P } ? P : Record<string, string | string[]>;\nexport function useSegmentParams(segmentPath?: string): Record<string, string | string[]>;\nexport function useSegmentParams(segmentPath?: string): Record<string, string | string[]> {\n // Try the client-owned provider first. It sits above everything a navigation\n // renders, so any component on the page — initial document, full navigation,\n // or a layout the server skipped — has one above it. Absent during SSR,\n // where the ALS path below is the answer. When called outside a React\n // component, useContext throws — caught below.\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on environment, not render path\n const paramsContext = useParamsContext();\n if (paramsContext !== null) {\n return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to module-level snapshot below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n // Falls back to module-level currentParams for tests.\n const ssrData = getSsrData();\n if (ssrData) return resolveSegmentParams(ssrData.params, ssrData.slotParams, segmentPath);\n return resolveSegmentParams(currentParams, currentSlotParams, segmentPath);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyEA,IAAM,iBAAiB,OAAO,IAAI,qBAAqB;AAEvD,SAAS,qBAA+D;CACtE,MAAM,QAAQ;CACd,MAAM,WAAW,MAAM;CACvB,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,IAAI,OAAO,MAAM,kBAAkB,YAGjC;CAEF,MAAM,MAAM,MAAM,cAAyC,IAAI;CAC/D,MAAM,kBAAkB;CACxB,OAAO;AACT;AAEA,IAAM,gBAAgB,mBAAmB;;;;;;;;AASzC,SAAgB,mBAA8C;CAC5D,OAAO,MAAM,WAAW,aAAa;AACvC;;;;;;;;;;;;;;;;;;ACdA,SAAgB,iBAAiB,QAAiD;CAChF,kBAAkB,MAAM;AAC1B;;;;;;;;;AAUA,SAAgB,qBAAqB,YAA2C;CAC9E,sBAAsB,UAAU;AAClC;AA4CA,SAAgB,iBAAiB,aAAyD;CAMxF,IAAI;EAEF,MAAM,gBAAgB,iBAAiB;EACvC,IAAI,kBAAkB,MACpB,OAAO,qBAAqB,cAAc,QAAQ,cAAc,YAAY,WAAW;CAE3F,QAAQ,CAGR;CAIA,MAAM,UAAU,WAAW;CAC3B,IAAI,SAAS,OAAO,qBAAqB,QAAQ,QAAQ,QAAQ,YAAY,WAAW;CACxF,OAAO,qBAAqB,eAAe,mBAAmB,WAAW;AAC3E"}
|