@timber-js/app 0.2.0-alpha.186 → 0.2.0-alpha.188
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/segment-keys-BawYuNFO.js.map +1 -1
- 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 +33 -17
- 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 +14 -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/segment-keys.d.ts +22 -0
- package/dist/routing/segment-keys.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/chain-url-parts.d.ts +2 -3
- package/dist/server/chain-url-parts.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 +344 -237
- 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/skippable-prefix.d.ts +3 -0
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +18 -19
- 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/state-tree-diff.d.ts +9 -16
- package/dist/server/state-tree-diff.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 -43
- 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/dist/shared/segment-info.d.ts +7 -0
- package/dist/shared/segment-info.d.ts.map +1 -1
- 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 +26 -5
- 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/segment-keys.ts +37 -0
- 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 +61 -68
- package/src/server/action-handler.ts +1 -4
- package/src/server/als-registry.ts +16 -0
- package/src/server/chain-url-parts.ts +2 -3
- 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 +169 -194
- 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 +3 -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/skippable-prefix.ts +21 -39
- package/src/server/slot-resolver.ts +69 -189
- package/src/server/ssr-bridge-types.ts +12 -0
- package/src/server/ssr-entry.ts +8 -6
- package/src/server/state-tree-diff.ts +11 -62
- package/src/server/static-generator.ts +3 -2
- package/src/server/status-code-resolver.ts +28 -11
- package/src/server/tree-builder.ts +35 -228
- 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/src/shared/segment-info.ts +7 -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
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
'use client';
|
|
12
12
|
|
|
13
13
|
import { useQueryStates as nuqsUseQueryStates } from 'nuqs';
|
|
14
|
-
import type {
|
|
14
|
+
import type { MultiParser } from 'nuqs';
|
|
15
15
|
import type {
|
|
16
16
|
SearchParamCodec,
|
|
17
17
|
SearchParamsDefinition,
|
|
18
18
|
SetParams,
|
|
19
19
|
QueryStatesOptions,
|
|
20
20
|
} from '../search-params/define.js';
|
|
21
|
-
import {
|
|
21
|
+
import { parseTotal } from '../search-params/parse-total.js';
|
|
22
22
|
|
|
23
23
|
// ─── Codec Bridge ─────────────────────────────────────────────────
|
|
24
24
|
|
|
@@ -50,32 +50,80 @@ function unwrapNuqsValue(value: unknown): unknown {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Bridge a timber SearchParamCodec to a nuqs-compatible
|
|
53
|
+
* Bridge a timber SearchParamCodec to a nuqs-compatible MultiParser.
|
|
54
54
|
*
|
|
55
55
|
* nuqs parsers: { parse(string) → T|null, serialize?(T) → string, eq?, defaultValue? }
|
|
56
56
|
* timber codecs: { parse(string|string[]|undefined) → T, serialize(T) → string|null }
|
|
57
57
|
*
|
|
58
|
-
* The defaultValue is computed eagerly
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
58
|
+
* The defaultValue is computed eagerly, through `parseTotal` — the same
|
|
59
|
+
* entry point server-side `parse()` uses, so the hook and the server agree
|
|
60
|
+
* on what an absent param means (a bare nuqs parser answers `null`, not
|
|
61
|
+
* `undefined`; TIM-1350). Codecs are documented to return a default rather
|
|
62
|
+
* than throw, but a throwing codec must not crash every component that
|
|
63
|
+
* mounts the hook — treat its default as undefined and let its error
|
|
64
|
+
* surface from server-side parse() instead.
|
|
65
|
+
*
|
|
66
|
+
* A `null` absent-value is NOT registered as the nuqs default. nuqs
|
|
67
|
+
* already represents an absent key as `null`, so the hook reads the same
|
|
68
|
+
* value either way — but registering it makes `clearOnDefault` fire on
|
|
69
|
+
* `setParams({ q: null })` and delete the key before the bridged
|
|
70
|
+
* `serialize` runs. For a codec that encodes `null` as a real query value
|
|
71
|
+
* (`serialize(null) === 'none'`), that silently disagrees with
|
|
72
|
+
* `buildSearchParams({ q: null })`, which writes it. Same reasoning as
|
|
73
|
+
* `getDefaultSerialized` on the server: a codec with no value for an
|
|
74
|
+
* absent param has no default to register.
|
|
62
75
|
*/
|
|
63
|
-
function bridgeCodec<T>(codec: SearchParamCodec<T>):
|
|
64
|
-
let
|
|
76
|
+
function bridgeCodec<T>(codec: SearchParamCodec<T>): MultiParser<T> & { defaultValue: T } {
|
|
77
|
+
let absent: unknown;
|
|
65
78
|
try {
|
|
66
|
-
|
|
79
|
+
absent = parseTotal(codec, undefined);
|
|
67
80
|
} catch {
|
|
68
|
-
|
|
81
|
+
absent = undefined;
|
|
69
82
|
}
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
|
|
84
|
+
const parser = {
|
|
85
|
+
// `multi`, so nuqs reads the key with `searchParams.getAll()` and hands
|
|
86
|
+
// us EVERY value. A single parser reads `.get()` — the first value only
|
|
87
|
+
// — which is not the domain a timber codec is defined over. The server
|
|
88
|
+
// parses `?tags=a&tags=b` as `['a','b']`; a single parser made the hook
|
|
89
|
+
// answer `['a']` for the same URL, under a declared `string[]` that
|
|
90
|
+
// admitted no such disagreement (TIM-1352). Scalar codecs are unaffected:
|
|
91
|
+
// they receive the array and take `value[0]`, exactly as they do on the
|
|
92
|
+
// server, so first-value-wins is preserved through the same code path
|
|
93
|
+
// rather than through nuqs's reader.
|
|
94
|
+
//
|
|
95
|
+
// nuqs never calls this with an empty array — `isAbsentFromUrl` treats
|
|
96
|
+
// `[]` as absent and answers `defaultValue` directly — which is what
|
|
97
|
+
// keeps the absent case agreeing with the server's `undefined`.
|
|
98
|
+
//
|
|
99
|
+
// Reading every value is only half of it: the values must arrive in the
|
|
100
|
+
// SAME SHAPE the server would have produced, or the divergence just
|
|
101
|
+
// moves. `normalizeRaw` (search-params/define.ts) collapses a
|
|
102
|
+
// single-valued key to a bare string and keeps an array only for a
|
|
103
|
+
// repeated one, so this mirrors that rule exactly. Handing a codec
|
|
104
|
+
// `['3']` where the server hands it `'3'` breaks every codec whose
|
|
105
|
+
// `parse` is written for the scalar case — which is most hand-written
|
|
106
|
+
// ones, contract or no contract.
|
|
107
|
+
type: 'multi' as const,
|
|
108
|
+
// Through parseTotal, not codec.parse. nuqs's own `.withDefault(d)`
|
|
109
|
+
// overrides ONLY `parseServerSide`, so `parseAsInteger.withDefault(1)`
|
|
110
|
+
// on `?page=abc` returned 1 from the server and null from the hook —
|
|
111
|
+
// a divergence the declared non-nullable `number` did not admit.
|
|
112
|
+
parse: (v: readonly string[]) =>
|
|
113
|
+
wrapNuqsValue(parseTotal(codec, v.length === 1 ? v[0] : [...v])),
|
|
72
114
|
serialize: (v: unknown) => {
|
|
73
115
|
const value = unwrapNuqsValue(v);
|
|
74
116
|
// Delegate null to the codec — some codecs encode null as a real
|
|
75
117
|
// query value. undefined has no encoding; nuqs requires a string.
|
|
76
|
-
|
|
118
|
+
//
|
|
119
|
+
// ONE element, always. A multi parser may return several — nuqs
|
|
120
|
+
// appends one key per entry — but `Codec.serialize` returns a single
|
|
121
|
+
// string by construction, so timber cannot emit repeated keys here
|
|
122
|
+
// any more than `buildSearchParams` can. Widening that protocol is
|
|
123
|
+
// TIM-1353. Wrapping the same string keeps the URL the hook writes
|
|
124
|
+
// byte-identical to the one `buildSearchParams` writes.
|
|
125
|
+
return [value === undefined ? '' : (codec.serialize(value as T) ?? '')];
|
|
77
126
|
},
|
|
78
|
-
defaultValue: wrapNuqsValue(defaultValue),
|
|
79
127
|
eq: (a: unknown, b: unknown) => {
|
|
80
128
|
if (a === b) return true;
|
|
81
129
|
try {
|
|
@@ -86,7 +134,26 @@ function bridgeCodec<T>(codec: SearchParamCodec<T>): SingleParser<T> & { default
|
|
|
86
134
|
return false;
|
|
87
135
|
}
|
|
88
136
|
},
|
|
89
|
-
} as
|
|
137
|
+
} as MultiParser<T> & { defaultValue: T };
|
|
138
|
+
|
|
139
|
+
if (absent !== null) parser.defaultValue = wrapNuqsValue(absent) as T;
|
|
140
|
+
return parser;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Collect `withUrlKey` aliases off a codec map.
|
|
145
|
+
*
|
|
146
|
+
* `withUrlKey(codec, 'q')` returns a codec carrying `urlKey: 'q'`, so the map
|
|
147
|
+
* alone is enough to reconstruct the aliases — `defineSearchParams` builds its
|
|
148
|
+
* own `urlKeys` from exactly this property.
|
|
149
|
+
*/
|
|
150
|
+
function deriveUrlKeys(codecs: Record<string, SearchParamCodec<unknown>>): Record<string, string> {
|
|
151
|
+
const result: Record<string, string> = {};
|
|
152
|
+
for (const key of Object.keys(codecs)) {
|
|
153
|
+
const alias = codecs[key]?.urlKey;
|
|
154
|
+
if (alias) result[key] = alias;
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
90
157
|
}
|
|
91
158
|
|
|
92
159
|
/**
|
|
@@ -95,12 +162,12 @@ function bridgeCodec<T>(codec: SearchParamCodec<T>): SingleParser<T> & { default
|
|
|
95
162
|
function bridgeCodecs<T extends Record<string, unknown>>(codecs: {
|
|
96
163
|
[K in keyof T]: SearchParamCodec<T[K]>;
|
|
97
164
|
}) {
|
|
98
|
-
const result: Record<string,
|
|
165
|
+
const result: Record<string, MultiParser<unknown> & { defaultValue: unknown }> = {};
|
|
99
166
|
for (const key of Object.keys(codecs)) {
|
|
100
167
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
168
|
result[key] = bridgeCodec(codecs[key as keyof T]) as any;
|
|
102
169
|
}
|
|
103
|
-
return result as { [K in keyof T]:
|
|
170
|
+
return result as { [K in keyof T]: MultiParser<T[K]> & { defaultValue: T[K] } };
|
|
104
171
|
}
|
|
105
172
|
|
|
106
173
|
// ─── Hook ─────────────────────────────────────────────────────────
|
|
@@ -113,7 +180,7 @@ function bridgeCodecs<T extends Record<string, unknown>>(codecs: {
|
|
|
113
180
|
*
|
|
114
181
|
* Usage:
|
|
115
182
|
* ```ts
|
|
116
|
-
* // Via a SearchParamsDefinition
|
|
183
|
+
* // Via a SearchParamsDefinition imported from the route's params.ts
|
|
117
184
|
* const [params, setParams] = definition.useQueryStates()
|
|
118
185
|
*
|
|
119
186
|
* // Standalone with inline codecs
|
|
@@ -121,30 +188,18 @@ function bridgeCodecs<T extends Record<string, unknown>>(codecs: {
|
|
|
121
188
|
* page: fromSchema(z.coerce.number().int().min(1).default(1)),
|
|
122
189
|
* })
|
|
123
190
|
* ```
|
|
191
|
+
*
|
|
192
|
+
* There is deliberately no route-string form (`useQueryStates('/products')`).
|
|
193
|
+
* Importing the definition from `params.ts` is the documented way to reach
|
|
194
|
+
* another route's codecs — it needs no runtime registry lookup and so has no
|
|
195
|
+
* "not registered yet" failure mode. See design/23-search-params.md
|
|
196
|
+
* §"Client Access".
|
|
124
197
|
*/
|
|
125
198
|
export function useQueryStates<T extends Record<string, unknown>>(
|
|
126
|
-
|
|
199
|
+
codecs: { [K in keyof T]: SearchParamCodec<T[K]> },
|
|
127
200
|
_options?: QueryStatesOptions,
|
|
128
201
|
urlKeys?: Readonly<Record<string, string>>
|
|
129
202
|
): [T, SetParams<T>] {
|
|
130
|
-
// Route-string overload: resolve codecs from the registry
|
|
131
|
-
let codecs: { [K in keyof T]: SearchParamCodec<T[K]> };
|
|
132
|
-
let resolvedUrlKeys = urlKeys;
|
|
133
|
-
if (typeof codecsOrRoute === 'string') {
|
|
134
|
-
const definition = getSearchParamsDefinition(codecsOrRoute);
|
|
135
|
-
if (!definition) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
`useQueryStates('${codecsOrRoute}'): no search params registered for this route. ` +
|
|
138
|
-
`Either the route has no search-params.ts file, or it hasn't been loaded yet. ` +
|
|
139
|
-
`For cross-route usage, import the definition explicitly.`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
codecs = definition.codecs as { [K in keyof T]: SearchParamCodec<T[K]> };
|
|
143
|
-
resolvedUrlKeys = definition.urlKeys;
|
|
144
|
-
} else {
|
|
145
|
-
codecs = codecsOrRoute;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
203
|
const bridged = bridgeCodecs(codecs);
|
|
149
204
|
|
|
150
205
|
// Forward hook-level options (shallow, scroll, history) to nuqs.
|
|
@@ -155,7 +210,15 @@ export function useQueryStates<T extends Record<string, unknown>>(
|
|
|
155
210
|
if (_options?.shallow !== undefined) nuqsOptions.shallow = _options.shallow;
|
|
156
211
|
if (_options?.scroll !== undefined) nuqsOptions.scroll = _options.scroll;
|
|
157
212
|
if (_options?.history !== undefined) nuqsOptions.history = _options.history;
|
|
158
|
-
|
|
213
|
+
// `withUrlKey` attaches the alias to the codec itself — that is the design's
|
|
214
|
+
// "URL keys travel with codecs" principle — so the aliases are derivable
|
|
215
|
+
// here and must be, for the inline codec-map form: nobody passes `urlKeys`
|
|
216
|
+
// on that path, and without this an aliased bundle silently read and wrote
|
|
217
|
+
// the property name instead of the alias. `bindUseQueryStates` still passes
|
|
218
|
+
// the definition's precomputed map, which wins on conflict; it is built from
|
|
219
|
+
// these same codecs, so the two agree by construction rather than by luck.
|
|
220
|
+
const resolvedUrlKeys = { ...deriveUrlKeys(codecs), ...urlKeys };
|
|
221
|
+
if (Object.keys(resolvedUrlKeys).length > 0) {
|
|
159
222
|
nuqsOptions.urlKeys = resolvedUrlKeys;
|
|
160
223
|
}
|
|
161
224
|
|
|
@@ -147,7 +147,12 @@ function deleteClientCookie(name: string, options?: ClientCookieOptions): void {
|
|
|
147
147
|
// ─── Standard Schema Auto-Detection ───────────────────────────────────────
|
|
148
148
|
|
|
149
149
|
function resolveCodec<T>(codecOrSchema: CookieCodec<T> | StandardSchemaV1<T>): CookieCodec<T> {
|
|
150
|
-
|
|
150
|
+
// 'cookie', not 'search': a cookie carries one string per name, so the
|
|
151
|
+
// repeated-key array shape the search bridge tolerates does not exist
|
|
152
|
+
// here — and `serialize` returning `null` means DELETE for a cookie
|
|
153
|
+
// where it means "omit the key" for a search param. See
|
|
154
|
+
// schema-bridge.ts §fromCookieSchema.
|
|
155
|
+
return resolveCodecOrSchema('codec', codecOrSchema, 'cookie') as CookieCodec<T>;
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
// ─── Factory ──────────────────────────────────────────────────────────────
|
package/src/index.ts
CHANGED
|
@@ -42,7 +42,7 @@ import { resolveEncryptionKeyExpression, shouldEnableEncryption } from './server
|
|
|
42
42
|
import { createHoldingServer } from './dev-tools/holding-server.js';
|
|
43
43
|
import { resolveStartPort, startDevServerPort } from './server/port-resolution.js';
|
|
44
44
|
import type { TimberUserConfig } from './config-types.js';
|
|
45
|
-
import type { PluginContext } from './plugin-context.js';
|
|
45
|
+
import type { PluginContext, TimberPluginApi } from './plugin-context.js';
|
|
46
46
|
import {
|
|
47
47
|
createPluginContext,
|
|
48
48
|
loadTimberConfigFile,
|
|
@@ -69,7 +69,10 @@ export type { Metadata, MetadataRoute, MetadataHandler, MetadataResult } from '.
|
|
|
69
69
|
*
|
|
70
70
|
* Each key is a route path pattern. Values have:
|
|
71
71
|
* segmentParams: shape of URL segment params (e.g. { id: string })
|
|
72
|
-
*
|
|
72
|
+
*
|
|
73
|
+
* There is no `searchParams` member: since TIM-1343 the framework does not
|
|
74
|
+
* scan for search-param definitions, so it has nothing to type them with. A
|
|
75
|
+
* definition carries its own type to call sites through an ordinary import.
|
|
73
76
|
*
|
|
74
77
|
* This interface is empty by default and populated via codegen.
|
|
75
78
|
* See design/09-typescript.md §"Typed Routes".
|
|
@@ -245,6 +248,21 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
245
248
|
// config() hook must NOT return a `plugins` field (Vite ignores it).
|
|
246
249
|
const rootSync: Plugin = {
|
|
247
250
|
name: 'timber-root-sync',
|
|
251
|
+
|
|
252
|
+
// The plugin context, published for out-of-band callers that need the
|
|
253
|
+
// *resolved* configuration rather than a second reading of the files it
|
|
254
|
+
// came from. `timber check` is the one today: it runs Vite's
|
|
255
|
+
// `resolveConfig()` and reads root, appDir, and pageExtensions from here,
|
|
256
|
+
// so it scans exactly the tree `timber build` builds — including config
|
|
257
|
+
// passed inline as `timber({ ... })`, which lives only in this closure and
|
|
258
|
+
// is invisible to anything that re-reads `timber.config.ts`. See
|
|
259
|
+
// `cli-check.ts` and design/11-platform.md §`timber check`.
|
|
260
|
+
//
|
|
261
|
+
// Valid from `configResolved` onward; `appDir` and `root` are unset before
|
|
262
|
+
// that, which is why the reader runs a full `resolveConfig()` rather than
|
|
263
|
+
// just loading the config file.
|
|
264
|
+
api: { getContext: () => ctx } satisfies TimberPluginApi,
|
|
265
|
+
|
|
248
266
|
async config(userConfig, { command, isPreview }) {
|
|
249
267
|
// ── Load timber.config.ts from the correct root ───────────────
|
|
250
268
|
// Vite's `config` hook fires before `configResolved`. The user's
|
|
@@ -388,7 +406,6 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
388
406
|
'@timber-js/app/cookies',
|
|
389
407
|
'@timber-js/app/cache',
|
|
390
408
|
'@timber-js/app/search-params',
|
|
391
|
-
'@timber-js/app/segment-params',
|
|
392
409
|
'@timber-js/app/params',
|
|
393
410
|
'@timber-js/app/codec',
|
|
394
411
|
];
|
package/src/plugin-context.ts
CHANGED
|
@@ -248,6 +248,32 @@ export class ParseMemo {
|
|
|
248
248
|
|
|
249
249
|
// ── App directory resolution ──────────────────────────────────────────────
|
|
250
250
|
|
|
251
|
+
// ── Plugin API (Vite `plugin.api`) ────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* What `timber-root-sync` publishes on its Vite `plugin.api`.
|
|
255
|
+
*
|
|
256
|
+
* The single supported way to read timber's *resolved* configuration from
|
|
257
|
+
* outside the plugin pipeline. Re-reading `timber.config.ts` is not equivalent:
|
|
258
|
+
* config passed inline as `timber({ ... })` in `vite.config.ts` exists only in
|
|
259
|
+
* the plugin's closure, and `root`/`appDir` are resolved against Vite's root
|
|
260
|
+
* rather than the caller's cwd.
|
|
261
|
+
*/
|
|
262
|
+
export interface TimberPluginApi {
|
|
263
|
+
/** The live plugin context. Fully populated from `configResolved` onward. */
|
|
264
|
+
getContext(): PluginContext;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Narrow an unknown Vite `plugin.api` to timber's. */
|
|
268
|
+
export function isTimberPluginApi(api: unknown): api is TimberPluginApi {
|
|
269
|
+
return (
|
|
270
|
+
typeof api === 'object' &&
|
|
271
|
+
api !== null &&
|
|
272
|
+
'getContext' in api &&
|
|
273
|
+
typeof api.getContext === 'function'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
251
277
|
/**
|
|
252
278
|
* Resolve the app directory. Checks (in order):
|
|
253
279
|
* 1. Explicit `configAppDir` from timber.config.ts
|
package/src/plugins/routing.ts
CHANGED
|
@@ -9,21 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { Plugin, ViteDevServer } from 'vite';
|
|
12
|
-
import { writeFile, mkdir } from 'node:fs/promises';
|
|
13
12
|
import { join } from 'node:path';
|
|
14
13
|
import { scanRoutes } from '../routing/scanner.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
buildDirToRouteMap,
|
|
18
|
-
writeSegmentTypes,
|
|
19
|
-
type SegmentInfo,
|
|
20
|
-
} from '../routing/segment-codegen.js';
|
|
14
|
+
import { buildDirToRouteMap, type SegmentInfo } from '../routing/segment-codegen.js';
|
|
15
|
+
import { writeRouteCodegen, flushCodegenWrites } from '../routing/codegen-write.js';
|
|
21
16
|
import { validateSchemaAgainstRoutes } from '../routing/schema-validation.js';
|
|
22
|
-
import {
|
|
23
|
-
generateManifestModule,
|
|
24
|
-
generateSearchParamsRegistryModule,
|
|
25
|
-
generateSchemaModule,
|
|
26
|
-
} from '../routing/manifest-codegen.js';
|
|
17
|
+
import { generateManifestModule, generateSchemaModule } from '../routing/manifest-codegen.js';
|
|
27
18
|
import {
|
|
28
19
|
lintConventions,
|
|
29
20
|
checkAppDirExists,
|
|
@@ -34,21 +25,6 @@ import type { PluginContext } from '../plugin-context.js';
|
|
|
34
25
|
const VIRTUAL_MODULE_ID = 'virtual:timber-route-manifest';
|
|
35
26
|
const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_MODULE_ID}`;
|
|
36
27
|
|
|
37
|
-
// TIM-830: Search-params registry virtual module.
|
|
38
|
-
//
|
|
39
|
-
// Statically imports the `searchParams` named export from every page
|
|
40
|
-
// that exports one, and registers each definition into the shared
|
|
41
|
-
// search-params registry keyed by the un-interpolated route pattern
|
|
42
|
-
// (e.g. '/products/[id]'). This lets `<Link>` serialize flat
|
|
43
|
-
// `Partial<T>` values at runtime without callers importing the
|
|
44
|
-
// definition at the call site.
|
|
45
|
-
//
|
|
46
|
-
// The imports are STATIC (`import { searchParams as r0 } from '...'`)
|
|
47
|
-
// so Vite/Rolldown tree-shakes each page's component body out of the
|
|
48
|
-
// registry chunk — only the `searchParams` named export is pulled in.
|
|
49
|
-
const SEARCH_PARAMS_REGISTRY_ID = 'virtual:timber-search-params-registry';
|
|
50
|
-
const RESOLVED_SEARCH_PARAMS_REGISTRY_ID = `\0${SEARCH_PARAMS_REGISTRY_ID}`;
|
|
51
|
-
|
|
52
28
|
// TIM-931: Schema virtual module.
|
|
53
29
|
//
|
|
54
30
|
// When app/schema.ts exists, this module imports it and resolves the
|
|
@@ -69,112 +45,26 @@ const SEGMENT_PREFIX = '\0$segment:';
|
|
|
69
45
|
* File convention names we track for changes that require manifest regeneration.
|
|
70
46
|
*/
|
|
71
47
|
const ROUTE_FILE_PATTERNS =
|
|
72
|
-
/\/(page|layout|middleware|access|route|error|global-error|default|denied|
|
|
48
|
+
/\/(page|layout|middleware|access|route|error|global-error|default|denied|schema|\d{3}|[45]xx|sitemap|robots|manifest|favicon|icon|opengraph-image|apple-icon)\./;
|
|
73
49
|
|
|
74
50
|
/**
|
|
75
51
|
* Create the timber-routing Vite plugin.
|
|
76
52
|
*
|
|
77
53
|
* Hooks: resolveId, load, buildStart, configureServer
|
|
78
54
|
*/
|
|
79
|
-
/** Absolute path to the generated route map declaration file. */
|
|
80
|
-
const CODEGEN_OUTPUT = '.timber/timber-routes.d.ts';
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Content of the timber-env.d.ts file written to the project root.
|
|
84
|
-
*
|
|
85
|
-
* Uses a triple-slash reference so TypeScript auto-discovers the generated
|
|
86
|
-
* route map without the user needing to edit tsconfig.json — same pattern
|
|
87
|
-
* as Next.js's next-env.d.ts.
|
|
88
|
-
*
|
|
89
|
-
* Also references `vite/client` so side-effect imports like
|
|
90
|
-
* `import './globals.css'` type-check without ts(2882), and so
|
|
91
|
-
* `import.meta.env`, `import.meta.hot`, `import.meta.glob`, asset
|
|
92
|
-
* imports (`*.png`, `*.svg`, …), CSS modules, and `?url`/`?raw`/
|
|
93
|
-
* `?inline` query variants all resolve without the user editing
|
|
94
|
-
* tsconfig.json themselves. Next.js's next-env.d.ts plays the same
|
|
95
|
-
* role via `next/types/global`.
|
|
96
|
-
*
|
|
97
|
-
* `vite` is a peer dep of `@timber-js/app`, so the reference always
|
|
98
|
-
* resolves in a correctly-installed project.
|
|
99
|
-
*/
|
|
100
|
-
const TIMBER_ENV_DTS = [
|
|
101
|
-
'// This file is auto-generated by timber.js. Do not edit.',
|
|
102
|
-
'// It ensures TypeScript picks up the generated route types and',
|
|
103
|
-
'// Vite-provided ambient types (CSS, assets, import.meta.env, HMR).',
|
|
104
|
-
'/* eslint-disable @typescript-eslint/triple-slash-reference */',
|
|
105
|
-
'/// <reference path=".timber/timber-routes.d.ts" />',
|
|
106
|
-
'/// <reference types="vite/client" />',
|
|
107
|
-
'',
|
|
108
|
-
].join('\n');
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Codegen writes that have been started but have not yet landed on disk.
|
|
112
|
-
*
|
|
113
|
-
* `writeCodegen` is deliberately unawaited on the hot path, which leaves a
|
|
114
|
-
* caller no way to know the files exist yet. Anything that tears down the
|
|
115
|
-
* project root — a build process exiting, a test removing its temp root —
|
|
116
|
-
* then races the writes: `rmSync` empties a directory that a pending `mkdir`
|
|
117
|
-
* immediately recreates, and the removal fails with ENOTEMPTY.
|
|
118
|
-
*
|
|
119
|
-
* Every started write registers here and deregisters when it settles, so
|
|
120
|
-
* `flushCodegenWrites()` can await them instead of guessing at a delay.
|
|
121
|
-
*/
|
|
122
|
-
const pendingCodegenWrites = new Set<Promise<void>>();
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Resolve once every codegen write started so far has landed on disk.
|
|
126
|
-
*
|
|
127
|
-
* Safe to call at any time and from anywhere — writes are tracked per process,
|
|
128
|
-
* not per plugin instance, so a caller does not need a handle on the plugin
|
|
129
|
-
* that started them. Never rejects: individual write failures are already
|
|
130
|
-
* swallowed and logged by `writeCodegen`.
|
|
131
|
-
*/
|
|
132
|
-
export async function flushCodegenWrites(): Promise<void> {
|
|
133
|
-
// Loop rather than a single Promise.all: a write started while we were
|
|
134
|
-
// awaiting would otherwise be missed, and the set would still be non-empty
|
|
135
|
-
// on return.
|
|
136
|
-
//
|
|
137
|
-
// Each round drops what it awaited rather than trusting the per-write
|
|
138
|
-
// `finally` to have run, so the two removal paths are independent — a
|
|
139
|
-
// deregistration that stopped firing would leak memory in dev, but it can
|
|
140
|
-
// never turn this into a spin.
|
|
141
|
-
while (pendingCodegenWrites.size > 0) {
|
|
142
|
-
const inFlight = [...pendingCodegenWrites];
|
|
143
|
-
await Promise.all(inFlight);
|
|
144
|
-
for (const write of inFlight) pendingCodegenWrites.delete(write);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
55
|
/**
|
|
149
56
|
* Write the generated route map and the timber-env.d.ts reference file.
|
|
150
57
|
*
|
|
151
|
-
* Not awaited
|
|
152
|
-
*
|
|
153
|
-
*
|
|
58
|
+
* Not awaited, to avoid blocking the hot path — use `flushCodegenWrites()` to
|
|
59
|
+
* wait for the writes to land. Failures are logged, not thrown: a missing .d.ts
|
|
60
|
+
* is a developer UX issue in dev/build, not a runtime failure. (`timber check`
|
|
61
|
+
* calls `writeRouteCodegen` directly and does treat a failure as fatal.)
|
|
154
62
|
*/
|
|
155
63
|
function writeCodegen(ctx: PluginContext): void {
|
|
156
64
|
if (!ctx.routeTree) return;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const envPath = join(ctx.root, 'timber-env.d.ts');
|
|
161
|
-
const done = mkdir(timberDir, { recursive: true })
|
|
162
|
-
.then(() =>
|
|
163
|
-
Promise.all([
|
|
164
|
-
writeFile(routesPath, content, 'utf-8'),
|
|
165
|
-
writeFile(envPath, TIMBER_ENV_DTS, 'utf-8'),
|
|
166
|
-
writeSegmentTypes(ctx.routeTree!, ctx.appDir, ctx.root),
|
|
167
|
-
])
|
|
168
|
-
)
|
|
169
|
-
.then(() => undefined)
|
|
170
|
-
.catch((err) => {
|
|
171
|
-
// Non-fatal — types are a dev convenience, but log so issues are visible
|
|
172
|
-
console.warn('[timber] Failed to write codegen output:', err);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
pendingCodegenWrites.add(done);
|
|
176
|
-
// `done` has a catch attached, so it never rejects and this stays quiet.
|
|
177
|
-
void done.finally(() => pendingCodegenWrites.delete(done));
|
|
65
|
+
void writeRouteCodegen(ctx.routeTree, ctx.appDir, ctx.root).catch((err) => {
|
|
66
|
+
console.warn('[timber] Failed to write codegen output:', err);
|
|
67
|
+
});
|
|
178
68
|
}
|
|
179
69
|
|
|
180
70
|
export function timberRouting(ctx: PluginContext): Plugin {
|
|
@@ -204,10 +94,26 @@ export function timberRouting(ctx: PluginContext): Plugin {
|
|
|
204
94
|
}
|
|
205
95
|
|
|
206
96
|
ctx.timer.start('route-scan');
|
|
207
|
-
|
|
97
|
+
const tree = scanRoutes(ctx.appDir, {
|
|
208
98
|
pageExtensions: ctx.config.pageExtensions,
|
|
209
99
|
});
|
|
210
100
|
ctx.timer.end('route-scan');
|
|
101
|
+
|
|
102
|
+
// Lint conventions (empty app, missing methods, missing root layout).
|
|
103
|
+
// Runs on the scanned tree *before* it is published: error-level findings
|
|
104
|
+
// are proven-fatal misconfigurations — the app cannot render at all — so
|
|
105
|
+
// the tree must not reach codegen, the manifest, or a live dev server's
|
|
106
|
+
// module graph. Fail loudly rather than let the developer discover it as
|
|
107
|
+
// an unattributed React error on an unrelated page (TIM-1329). Never
|
|
108
|
+
// deduplicated against `warnedFiles`: an error must not be downgraded to
|
|
109
|
+
// silence by an earlier scan in the same process.
|
|
110
|
+
const conventionWarnings = lintConventions(tree, ctx.appDir);
|
|
111
|
+
const fatal = conventionWarnings.filter((w) => w.level === 'error');
|
|
112
|
+
if (fatal.length > 0) {
|
|
113
|
+
throw new Error(formatConventionWarnings(fatal, { color: false }));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
ctx.routeTree = tree;
|
|
211
117
|
dirToRoute = buildDirToRouteMap(ctx.routeTree, ctx.appDir);
|
|
212
118
|
writeCodegen(ctx);
|
|
213
119
|
|
|
@@ -216,8 +122,6 @@ export function timberRouting(ctx: PluginContext): Plugin {
|
|
|
216
122
|
// See design/41-global-params.md §Auto-Scaffolding
|
|
217
123
|
warnSchemaValidation(ctx);
|
|
218
124
|
|
|
219
|
-
// Lint conventions (empty app, missing methods, missing root layout)
|
|
220
|
-
const conventionWarnings = lintConventions(ctx.routeTree, ctx.appDir);
|
|
221
125
|
const newConventionWarnings = conventionWarnings.filter((w) => !warnedFiles.has(w.id));
|
|
222
126
|
if (newConventionWarnings.length > 0) {
|
|
223
127
|
for (const w of newConventionWarnings) warnedFiles.add(w.id);
|
|
@@ -250,14 +154,6 @@ export function timberRouting(ctx: PluginContext): Plugin {
|
|
|
250
154
|
return RESOLVED_VIRTUAL_ID;
|
|
251
155
|
}
|
|
252
156
|
|
|
253
|
-
// TIM-830: Search-params registry virtual module
|
|
254
|
-
if (
|
|
255
|
-
cleanId === SEARCH_PARAMS_REGISTRY_ID ||
|
|
256
|
-
cleanId.endsWith(`/${SEARCH_PARAMS_REGISTRY_ID}`)
|
|
257
|
-
) {
|
|
258
|
-
return RESOLVED_SEARCH_PARAMS_REGISTRY_ID;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
157
|
// TIM-931: Schema virtual module
|
|
262
158
|
if (cleanId === SCHEMA_ID || cleanId.endsWith(`/${SCHEMA_ID}`)) {
|
|
263
159
|
return RESOLVED_SCHEMA_ID;
|
|
@@ -297,13 +193,6 @@ export function timberRouting(ctx: PluginContext): Plugin {
|
|
|
297
193
|
return generateManifestModule(ctx.routeTree!, ctx.root);
|
|
298
194
|
}
|
|
299
195
|
|
|
300
|
-
if (id === RESOLVED_SEARCH_PARAMS_REGISTRY_ID) {
|
|
301
|
-
if (!ctx.routeTree) {
|
|
302
|
-
rescan();
|
|
303
|
-
}
|
|
304
|
-
return generateSearchParamsRegistryModule(ctx.routeTree!);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
196
|
if (id === RESOLVED_SCHEMA_ID) {
|
|
308
197
|
return generateSchemaModule(ctx.appDir);
|
|
309
198
|
}
|
|
@@ -362,6 +251,59 @@ export function timberRouting(ctx: PluginContext): Plugin {
|
|
|
362
251
|
/** Snapshot of the last generated manifest, used to detect structural changes. */
|
|
363
252
|
let lastManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree, ctx.root) : '';
|
|
364
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Rescan without letting a rejected route tree kill the dev server.
|
|
256
|
+
*
|
|
257
|
+
* `rescan()` throws on hard misconfigurations — duplicate conventions,
|
|
258
|
+
* route collisions, a status file that is not a client component. Those
|
|
259
|
+
* throws are correct at startup and in `vite build`, but here they run
|
|
260
|
+
* inside a chokidar listener, where an exception is an unhandled
|
|
261
|
+
* rejection that takes the process down mid-edit. Report and keep the
|
|
262
|
+
* last good tree so the server stays up and the fix is one save away.
|
|
263
|
+
*
|
|
264
|
+
* Returns false when the scan failed, so callers skip the manifest
|
|
265
|
+
* regeneration that would otherwise publish a stale tree as fresh.
|
|
266
|
+
*/
|
|
267
|
+
/**
|
|
268
|
+
* True while the last rescan is still failing.
|
|
269
|
+
*
|
|
270
|
+
* Recovery has to force a reload even when the manifest is byte-identical.
|
|
271
|
+
* Adding a missing `'use client'` back to a status file changes no route
|
|
272
|
+
* metadata, so the content-change path would send nothing and leave the
|
|
273
|
+
* error overlay on screen over a page that is now fine.
|
|
274
|
+
*/
|
|
275
|
+
let scanFailed = false;
|
|
276
|
+
|
|
277
|
+
const rescanSafely = (): boolean => {
|
|
278
|
+
try {
|
|
279
|
+
rescan();
|
|
280
|
+
if (scanFailed) {
|
|
281
|
+
scanFailed = false;
|
|
282
|
+
// Force the reload: see `scanFailed`.
|
|
283
|
+
lastManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree, ctx.root) : '';
|
|
284
|
+
invalidateManifest(devServer);
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
} catch (err) {
|
|
288
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
289
|
+
process.stderr.write(
|
|
290
|
+
`[timber] Route scan failed — keeping previous routes.\n${message}\n`
|
|
291
|
+
);
|
|
292
|
+
scanFailed = true;
|
|
293
|
+
|
|
294
|
+
// The terminal is not the only surface that needs this. Keeping the
|
|
295
|
+
// previous tree keeps the *server* alive, but it does not un-edit the
|
|
296
|
+
// file: Vite still re-transforms it, so the very next render hits the
|
|
297
|
+
// failure this scan just diagnosed — and the browser's version of it
|
|
298
|
+
// names nothing. Put the diagnosis where the developer is looking.
|
|
299
|
+
devServer.hot.send({
|
|
300
|
+
type: 'error',
|
|
301
|
+
err: { message, stack: '', plugin: 'timber-routing' },
|
|
302
|
+
});
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
365
307
|
/**
|
|
366
308
|
* Handle a route-significant file being added or removed.
|
|
367
309
|
* Always triggers a full-reload since the route tree structure changed.
|
|
@@ -370,7 +312,7 @@ export function timberRouting(ctx: PluginContext): Plugin {
|
|
|
370
312
|
if (!filePath.startsWith(ctx.appDir)) return;
|
|
371
313
|
if (!ROUTE_FILE_PATTERNS.test(filePath)) return;
|
|
372
314
|
|
|
373
|
-
|
|
315
|
+
if (!rescanSafely()) return;
|
|
374
316
|
lastManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree, ctx.root) : '';
|
|
375
317
|
invalidateManifest(devServer);
|
|
376
318
|
};
|
|
@@ -387,7 +329,7 @@ export function timberRouting(ctx: PluginContext): Plugin {
|
|
|
387
329
|
if (!filePath.startsWith(ctx.appDir)) return;
|
|
388
330
|
if (!ROUTE_FILE_PATTERNS.test(filePath)) return;
|
|
389
331
|
|
|
390
|
-
|
|
332
|
+
if (!rescanSafely()) return;
|
|
391
333
|
const newManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree, ctx.root) : '';
|
|
392
334
|
if (newManifest !== lastManifest) {
|
|
393
335
|
lastManifest = newManifest;
|
|
@@ -449,11 +391,7 @@ function invalidateManifest(server: ViteDevServer): void {
|
|
|
449
391
|
const env = server.environments[envName];
|
|
450
392
|
if (!env?.moduleGraph) continue;
|
|
451
393
|
|
|
452
|
-
for (const virtualId of [
|
|
453
|
-
RESOLVED_VIRTUAL_ID,
|
|
454
|
-
RESOLVED_SEARCH_PARAMS_REGISTRY_ID,
|
|
455
|
-
RESOLVED_SCHEMA_ID,
|
|
456
|
-
]) {
|
|
394
|
+
for (const virtualId of [RESOLVED_VIRTUAL_ID, RESOLVED_SCHEMA_ID]) {
|
|
457
395
|
const mod = env.moduleGraph.getModuleById(virtualId);
|
|
458
396
|
if (mod) {
|
|
459
397
|
env.moduleGraph.invalidateModule(mod);
|
package/src/plugins/shims.ts
CHANGED
|
@@ -55,7 +55,6 @@ export const SUBPATH_SRC_MAP: Record<string, string> = {
|
|
|
55
55
|
'cookies': 'cookies/index.ts',
|
|
56
56
|
'params': 'params/index.ts',
|
|
57
57
|
'search-params': 'search-params/index.ts',
|
|
58
|
-
'segment-params': 'segment-params/index.ts',
|
|
59
58
|
|
|
60
59
|
'cache/stores/memory': 'cache/stores/memory.ts',
|
|
61
60
|
'cache/stores/redis': 'cache/stores/redis.ts',
|