@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
package/src/cli.ts
CHANGED
|
@@ -11,28 +11,49 @@
|
|
|
11
11
|
// timber preview — Serve the production build
|
|
12
12
|
// timber check — Validate types + routes without building
|
|
13
13
|
|
|
14
|
+
// Type-only — the implementation is imported dynamically inside runCheck so the
|
|
15
|
+
// CLI's other commands never load the route scanner.
|
|
16
|
+
import type { CheckDeps } from './cli-check.js';
|
|
17
|
+
|
|
18
|
+
export type { CheckDeps };
|
|
19
|
+
|
|
14
20
|
const COMMANDS = ['dev', 'build', 'preview', 'check', 'schema', 'init'] as const;
|
|
15
21
|
type Command = (typeof COMMANDS)[number];
|
|
16
22
|
|
|
17
23
|
export interface ParsedArgs {
|
|
18
24
|
command: Command;
|
|
19
25
|
config: string | undefined;
|
|
26
|
+
/** `--project`: the tsconfig `timber check` passes to tsgo. */
|
|
27
|
+
project: string | undefined;
|
|
20
28
|
/** Positional arguments after the command (used by `timber schema sync`). */
|
|
21
29
|
rest: string[];
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
export interface CommandOptions {
|
|
33
|
+
/** Path to a Vite config file. Means the same thing for every command. */
|
|
25
34
|
config?: string;
|
|
35
|
+
/** `timber check` only: the tsconfig to type-check. */
|
|
36
|
+
project?: string;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
/**
|
|
29
40
|
* Parse CLI arguments into a structured command + options.
|
|
30
|
-
* Accepts: timber <command> [--config|-c <path>]
|
|
41
|
+
* Accepts: timber <command> [--config|-c <path>] [--project <path>]
|
|
42
|
+
*
|
|
43
|
+
* `--config` names a **Vite** config file for every command, `check` included
|
|
44
|
+
* (TIM-1332): `check` resolves the project through Vite, so a project that
|
|
45
|
+
* builds with `--config ./custom.vite.config.ts` has to be checkable the same
|
|
46
|
+
* way. `--project` names the tsconfig, spelled as tsgo and tsc spell it, and
|
|
47
|
+
* is meaningful only for `check`.
|
|
48
|
+
*
|
|
49
|
+
* Before TIM-1332 `--config` meant the tsconfig for `check` alone — one flag
|
|
50
|
+
* with two meanings, which the docs had to carry a note to explain and which
|
|
51
|
+
* left custom Vite configs unreachable from `check` entirely.
|
|
31
52
|
*
|
|
32
53
|
* Unknown flags are an error, not silently ignored — `timber dev --port 4000`
|
|
33
|
-
* starting on the default port would contradict fail-loudly. The
|
|
34
|
-
*
|
|
35
|
-
*
|
|
54
|
+
* starting on the default port would contradict fail-loudly. The surface stays
|
|
55
|
+
* deliberately small (design/11-platform.md §CLI); port selection goes through
|
|
56
|
+
* the PORT env var or server.port in vite.config.ts.
|
|
36
57
|
*/
|
|
37
58
|
export function parseArgs(args: string[]): ParsedArgs {
|
|
38
59
|
if (args.length === 0) {
|
|
@@ -47,6 +68,7 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
let config: string | undefined;
|
|
71
|
+
let project: string | undefined;
|
|
50
72
|
const rest: string[] = [];
|
|
51
73
|
for (let i = 1; i < args.length; i++) {
|
|
52
74
|
const arg = args[i];
|
|
@@ -55,6 +77,17 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
55
77
|
if (!config) {
|
|
56
78
|
throw new Error('--config requires a path argument');
|
|
57
79
|
}
|
|
80
|
+
} else if (arg === '--project') {
|
|
81
|
+
if (command !== 'check') {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`--project is only valid for \`timber check\` (it names the tsconfig to type-check). ` +
|
|
84
|
+
`To point \`timber ${command}\` at a different Vite config, use --config <path>.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
project = args[++i];
|
|
88
|
+
if (!project) {
|
|
89
|
+
throw new Error('--project requires a path argument');
|
|
90
|
+
}
|
|
58
91
|
} else if (arg.startsWith('-')) {
|
|
59
92
|
const portHint =
|
|
60
93
|
arg === '--port' || arg === '-p'
|
|
@@ -62,7 +95,8 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
62
95
|
'or server.port in vite.config.ts.'
|
|
63
96
|
: '';
|
|
64
97
|
throw new Error(
|
|
65
|
-
`Unknown option: ${arg}. The timber CLI
|
|
98
|
+
`Unknown option: ${arg}. The timber CLI accepts --config <path>` +
|
|
99
|
+
` (and --project <path> for \`check\`).${portHint}`
|
|
66
100
|
);
|
|
67
101
|
} else if (command === 'schema' || command === 'init') {
|
|
68
102
|
rest.push(arg);
|
|
@@ -71,7 +105,7 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
71
105
|
}
|
|
72
106
|
}
|
|
73
107
|
|
|
74
|
-
return { command: command as Command, config, rest };
|
|
108
|
+
return { command: command as Command, config, project, rest };
|
|
75
109
|
}
|
|
76
110
|
|
|
77
111
|
// ─── Command Implementations ─────────────────────────────────────────────────
|
|
@@ -160,24 +194,25 @@ export async function runPreview(options: CommandOptions, _deps?: ViteDeps): Pro
|
|
|
160
194
|
}
|
|
161
195
|
|
|
162
196
|
/**
|
|
163
|
-
* Validate
|
|
164
|
-
*
|
|
197
|
+
* Validate routes and types without producing build output.
|
|
198
|
+
*
|
|
199
|
+
* Runs the route pass first — resolve the project through Vite, scan it, fail
|
|
200
|
+
* on proven-fatal convention errors, print warnings, and regenerate the route
|
|
201
|
+
* types — then `tsgo --noEmit`. Codegen has to precede the type pass: the
|
|
202
|
+
* generated `.timber/*.d.ts` files are what type `<Link href>` and
|
|
203
|
+
* `useSegmentParams()`, and a fresh clone has never produced them. See
|
|
204
|
+
* `cli-check.ts` for the full reasoning.
|
|
205
|
+
*
|
|
206
|
+
* `--config` names the **Vite** config, as it does for every other command, so
|
|
207
|
+
* an app that builds with a custom one is checkable the same way.
|
|
208
|
+
* `--project` names the tsconfig handed to tsgo.
|
|
165
209
|
*/
|
|
166
|
-
export async function runCheck(options: CommandOptions): Promise<void> {
|
|
167
|
-
const {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (stdout) process.stdout.write(stdout);
|
|
173
|
-
if (stderr) process.stderr.write(stderr);
|
|
174
|
-
if (err) {
|
|
175
|
-
reject(new Error(`Type check failed with exit code ${err.code}`));
|
|
176
|
-
} else {
|
|
177
|
-
resolve();
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
});
|
|
210
|
+
export async function runCheck(options: CommandOptions, _deps?: CheckDeps): Promise<void> {
|
|
211
|
+
const { checkRoutes, runTypeCheck } = await import('./cli-check.js');
|
|
212
|
+
const { root } = await checkRoutes({ viteConfigFile: options.config }, _deps);
|
|
213
|
+
// tsgo runs from the root the route pass resolved, not from cwd — that is
|
|
214
|
+
// where it just wrote the declarations tsgo has to read.
|
|
215
|
+
await runTypeCheck({ root, config: options.project }, _deps);
|
|
181
216
|
}
|
|
182
217
|
|
|
183
218
|
// ─── Schema Subcommand ───────────────────────────────────────────────────────
|
|
@@ -305,7 +340,7 @@ export async function runInit(): Promise<void> {
|
|
|
305
340
|
|
|
306
341
|
async function main(): Promise<void> {
|
|
307
342
|
const parsed = parseArgs(process.argv.slice(2));
|
|
308
|
-
const options: CommandOptions = { config: parsed.config };
|
|
343
|
+
const options: CommandOptions = { config: parsed.config, project: parsed.project };
|
|
309
344
|
|
|
310
345
|
switch (parsed.command) {
|
|
311
346
|
case 'dev':
|
|
@@ -19,6 +19,7 @@ import { getRouter, getRouterOrNull } from '#client-internal';
|
|
|
19
19
|
import { setHardNavigating } from '../navigation-root.js';
|
|
20
20
|
import { getClientDeploymentId, DEPLOYMENT_ID_HEADER, RELOAD_HEADER } from '../rsc-fetch.js';
|
|
21
21
|
import { markClientStale } from '../stale-client.js';
|
|
22
|
+
import { RSC_CONTENT_TYPE } from '../../shared/rsc-media-type.js';
|
|
22
23
|
export function setupServerActions(): void {
|
|
23
24
|
setServerCallback(async (id: string, args: unknown[]) => {
|
|
24
25
|
const body = await encodeReply(args);
|
|
@@ -32,7 +33,7 @@ export function setupServerActions(): void {
|
|
|
32
33
|
// Build action request headers. Include deployment ID for version
|
|
33
34
|
// skew detection (TIM-446) — the server rejects stale actions gracefully.
|
|
34
35
|
const actionHeaders: Record<string, string> = {
|
|
35
|
-
'Accept':
|
|
36
|
+
'Accept': RSC_CONTENT_TYPE,
|
|
36
37
|
'x-rsc-action': id,
|
|
37
38
|
};
|
|
38
39
|
const actionDeploymentId = getClientDeploymentId();
|
|
@@ -28,11 +28,6 @@
|
|
|
28
28
|
|
|
29
29
|
// @ts-expect-error — virtual module provided by timber-entries plugin
|
|
30
30
|
import config from 'virtual:timber-config';
|
|
31
|
-
// TIM-830: Populate the search-params registry eagerly so <Link> on the
|
|
32
|
-
// client can serialize flat `Partial<T>` values synchronously during
|
|
33
|
-
// render and click handlers. Side-effect-only import.
|
|
34
|
-
// @ts-expect-error — virtual module provided by timber-routing plugin
|
|
35
|
-
import 'virtual:timber-search-params-registry';
|
|
36
31
|
// TIM-932: Populate the codec registry so Link's interpolateParams can
|
|
37
32
|
// use codec.serialize() for URL construction. Side-effect-only import.
|
|
38
33
|
// @ts-expect-error — virtual module provided by timber-routing plugin
|
|
@@ -35,6 +35,12 @@ interface DenyDigest {
|
|
|
35
35
|
type: 'deny';
|
|
36
36
|
status: number;
|
|
37
37
|
data: unknown;
|
|
38
|
+
/**
|
|
39
|
+
* Segment key of the segment that owns the matched deny page, present when
|
|
40
|
+
* an in-tree catch site matched a file it may not render at its own
|
|
41
|
+
* position and re-threw to reach the owner. See TIM-1356.
|
|
42
|
+
*/
|
|
43
|
+
ownerKey?: string;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
/** Structured digest returned by RSC onError for RenderError. */
|
|
@@ -78,6 +84,12 @@ export interface TimberErrorBoundaryProps {
|
|
|
78
84
|
* graceful degradation — they must NOT change the HTTP status code.
|
|
79
85
|
*/
|
|
80
86
|
isSlotBoundary?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Segment key of the segment that declared this boundary. Only meaningful
|
|
89
|
+
* against a deny digest carrying an `ownerKey`. See the decline rule in
|
|
90
|
+
* `render()` and TIM-1356.
|
|
91
|
+
*/
|
|
92
|
+
segmentKey?: string;
|
|
81
93
|
children: ReactNode;
|
|
82
94
|
}
|
|
83
95
|
|
|
@@ -209,6 +221,34 @@ export class TimberErrorBoundary extends Component<
|
|
|
209
221
|
return null;
|
|
210
222
|
}
|
|
211
223
|
|
|
224
|
+
// Owner-addressed denies: an in-tree catch site already resolved the
|
|
225
|
+
// fallback chain and matched a file it may not render at its own
|
|
226
|
+
// position, so it re-threw to reach the segment that owns that file.
|
|
227
|
+
// Every boundary but that one declines — otherwise the nearest boundary
|
|
228
|
+
// whose status filter happens to match catches first and renders the
|
|
229
|
+
// deny page at its own depth, wrapped in the layouts the hoist existed
|
|
230
|
+
// to skip, and from a different file than the chain chose. (Boundary
|
|
231
|
+
// nesting is per-segment `{status} → category → error`; the 4xx chain
|
|
232
|
+
// runs status files across ALL segments before any error.tsx, so a
|
|
233
|
+
// leaf error.tsx would otherwise beat a root 404.tsx.)
|
|
234
|
+
//
|
|
235
|
+
// Declining is the default: an unkeyed boundary — a slot boundary, or
|
|
236
|
+
// one from a construction site that builds no deny chains — never
|
|
237
|
+
// claims a deny addressed to a segment. See TIM-1356.
|
|
238
|
+
// `typeof === 'string'`, not a null check: the digest is untrusted JSON —
|
|
239
|
+
// `parseDigest` validates only `type`, and any component can set
|
|
240
|
+
// `error.digest` itself. A non-string `ownerKey` is unequal to every
|
|
241
|
+
// boundary's key, so a truthiness test would let one error decline its
|
|
242
|
+
// way past the entire tree and blank the page, where it would otherwise
|
|
243
|
+
// have hit the nearest matching boundary.
|
|
244
|
+
if (
|
|
245
|
+
parsed?.type === 'deny' &&
|
|
246
|
+
typeof parsed.ownerKey === 'string' &&
|
|
247
|
+
parsed.ownerKey !== this.props.segmentKey
|
|
248
|
+
) {
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
|
|
212
252
|
// If this boundary has a status filter, check whether the error matches.
|
|
213
253
|
// Non-matching errors re-throw so an outer boundary can catch them.
|
|
214
254
|
if (this.props.status != null) {
|
|
@@ -272,7 +312,7 @@ export class TimberErrorBoundary extends Component<
|
|
|
272
312
|
});
|
|
273
313
|
}
|
|
274
314
|
return createElement(this.props.fallbackComponent as never, {
|
|
275
|
-
error,
|
|
315
|
+
error: denyError(parsed.status, error),
|
|
276
316
|
digest: null,
|
|
277
317
|
reset: this.reset,
|
|
278
318
|
status: parsed.status,
|
|
@@ -312,6 +352,30 @@ function parseDigest(error: Error): ParsedDigest | null {
|
|
|
312
352
|
return null;
|
|
313
353
|
}
|
|
314
354
|
|
|
355
|
+
/**
|
|
356
|
+
* The Error an `error.tsx` serving a deny should receive.
|
|
357
|
+
*
|
|
358
|
+
* The error that actually arrives here is React's, not the framework's: a
|
|
359
|
+
* server-thrown error crossing Flight has its message and name replaced with
|
|
360
|
+
* a generic redacted string in production, by design. An `error.tsx` written
|
|
361
|
+
* per the docs branches on `error.message` — so handing it the redacted error
|
|
362
|
+
* gives it nothing to branch on, while the in-tree path hands the same file a
|
|
363
|
+
* `DenySignal` named error reading `Access denied with status <N>` (see
|
|
364
|
+
* `renderDenyEntry`). This reconstructs that shape from the digest, which is
|
|
365
|
+
* where the status already is.
|
|
366
|
+
*
|
|
367
|
+
* Derived from the status alone — no stack, no server data — so it is safe on
|
|
368
|
+
* the client for the same reason the digest is. See design/13-security.md
|
|
369
|
+
* §"Errors don't leak", TIM-1081.
|
|
370
|
+
*/
|
|
371
|
+
function denyError(status: number, original: Error): Error {
|
|
372
|
+
// A dev build may have preserved something more useful than the redaction.
|
|
373
|
+
if (original.name === 'DenySignal') return original;
|
|
374
|
+
const error = new Error(`Access denied with status ${status}`);
|
|
375
|
+
error.name = 'DenySignal';
|
|
376
|
+
return error;
|
|
377
|
+
}
|
|
378
|
+
|
|
315
379
|
/**
|
|
316
380
|
* Extract the HTTP status code from a parsed digest or error message.
|
|
317
381
|
* Falls back to message pattern matching for errors without a digest.
|
package/src/client/index.ts
CHANGED
|
@@ -75,11 +75,22 @@ export { useSelectedLayoutSegment, useSelectedLayoutSegments } from './use-selec
|
|
|
75
75
|
export { useActionState, useFormAction } from './form';
|
|
76
76
|
export type { UseActionStateFn, UseActionStateReturn, FormErrorsResult } from './form';
|
|
77
77
|
|
|
78
|
-
// Params
|
|
78
|
+
// Params. Called with no argument this returns the untyped accumulated params;
|
|
79
|
+
// pass the route's SEGMENT_PATH (from its generated `./$segment` module) to get
|
|
80
|
+
// the exact types `app/schema.ts` declares — `useSegmentParams(SEGMENT_PATH)`.
|
|
79
81
|
export { useSegmentParams } from './use-segment-params';
|
|
80
82
|
|
|
81
|
-
// Query states
|
|
82
|
-
//
|
|
83
|
+
// Query states. `definition.useQueryStates()` is the usual form; this standalone
|
|
84
|
+
// export is the inline codec-map form, for shared param bundles reused across
|
|
85
|
+
// routes (design/23-search-params.md §"Client Access").
|
|
86
|
+
//
|
|
87
|
+
// Until TIM-1341 this export did not exist, yet four doc sites told users to
|
|
88
|
+
// import it from here. It type-checked only because the generated `.d.ts`
|
|
89
|
+
// augmented this module with a `useQueryStates` overload — so TypeScript
|
|
90
|
+
// believed in a binding that was never emitted, and the documented import
|
|
91
|
+
// failed at build time. Deleting that overload removed the lie; exporting the
|
|
92
|
+
// hook makes the docs true.
|
|
93
|
+
export { useQueryStates } from './use-query-states';
|
|
83
94
|
|
|
84
95
|
// Cookies — useCookie lives on defineCookie().useCookie().
|
|
85
96
|
// Types still exported for advanced use.
|
package/src/client/link.tsx
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
// Typed Link: design/09-typescript.md §"Typed Link"
|
|
15
15
|
// - href validated against known routes (via codegen overloads, not runtime)
|
|
16
16
|
// - params prop typed per-route, URL interpolated at runtime
|
|
17
|
-
// - searchParams prop
|
|
17
|
+
// - searchParams prop is a query string (from `definition.buildSearchParams()`)
|
|
18
|
+
// or a plain object whose values are String()-coerced
|
|
18
19
|
// - params and fully-resolved string href are mutually exclusive
|
|
19
20
|
// - searchParams and inline query string are mutually exclusive
|
|
20
21
|
|
|
@@ -25,8 +26,6 @@ import {
|
|
|
25
26
|
type ReactNode,
|
|
26
27
|
type MouseEvent as ReactMouseEvent,
|
|
27
28
|
} from 'react';
|
|
28
|
-
import type { SearchParamsDefinition } from '../search-params/define.js';
|
|
29
|
-
import { getSearchParamsDefinition } from '../search-params/registry.js';
|
|
30
29
|
import type { LinkFunction } from './index.js';
|
|
31
30
|
import { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.js';
|
|
32
31
|
import {
|
|
@@ -131,26 +130,37 @@ export type LinkSegmentParams<T> = {
|
|
|
131
130
|
|
|
132
131
|
// ─── searchParams prop shapes ────────────────────────────────────
|
|
133
132
|
//
|
|
134
|
-
//
|
|
135
|
-
// searchParams={{ page: 2, q: 'boots' }}
|
|
136
|
-
// The framework looks up the route's SearchParamsDefinition from the
|
|
137
|
-
// search-params registry at runtime (see TIM-830).
|
|
133
|
+
// Two shapes, discriminated at runtime by `typeof === 'string'`:
|
|
138
134
|
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
135
|
+
// 1. Codec-aware (preferred) — a query string built from a definition:
|
|
136
|
+
// searchParams={productParams.buildSearchParams({ page: 2, search: 'boots' })}
|
|
137
|
+
// `buildSearchParams` applies each codec, honours `withUrlKey` aliases,
|
|
138
|
+
// and omits values equal to their default. It is typed `Partial<T>` at
|
|
139
|
+
// the call site, so the definition supplies the type checking.
|
|
143
140
|
//
|
|
144
|
-
//
|
|
141
|
+
// 2. Plain object (escape hatch) — every value is String()-coerced:
|
|
142
|
+
// searchParams={{ ref: 'email' }}
|
|
143
|
+
// No codecs, no aliases. This is for one-off params that have no
|
|
144
|
+
// definition. Passing a *definition's* keys this way is a mistake the
|
|
145
|
+
// types cannot catch: `{ search: 'x' }` emits `?search=x`, where the
|
|
146
|
+
// definition would emit `?q=x`. Prefer (1) whenever a definition exists.
|
|
147
|
+
//
|
|
148
|
+
// Why a STRING and not a URLSearchParams: a Link is routinely rendered by a
|
|
149
|
+
// Server Component, so this prop crosses the RSC Flight boundary before the
|
|
150
|
+
// client Link runs. `URLSearchParams` is iterable, and React serializes any
|
|
151
|
+
// iterable as an array — so the prop arrived as `[['pg','2'], …]`, fell into
|
|
152
|
+
// the plain-object branch, and rendered `?0=pg&0=2`. Strings survive Flight
|
|
153
|
+
// unchanged. (Codex P1 on PR #1021; reproduced against a live dev server.)
|
|
154
|
+
//
|
|
155
|
+
// TIM-1343 removed the third shape — a flat values object resolved against a
|
|
156
|
+
// runtime registry keyed by href. It required scanning a `params.ts`
|
|
157
|
+
// convention file, a generated registry module, and eager imports of every
|
|
158
|
+
// route's definition into all three entries, to save one `.buildSearchParams`
|
|
159
|
+
// call. See design/23-search-params.md §"Link Integration".
|
|
145
160
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
146
161
|
export type ParamValue = string | number | string[] | { toString(): string; [key: string]: any };
|
|
147
162
|
|
|
148
|
-
type
|
|
149
|
-
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
150
|
-
values: Record<string, unknown>;
|
|
151
|
-
};
|
|
152
|
-
type FlatSearchParamsProp = Record<string, unknown>;
|
|
153
|
-
type LinkSearchParamsProp = WrappedSearchParamsProp | FlatSearchParamsProp;
|
|
163
|
+
type LinkSearchParamsProp = string | Record<string, unknown>;
|
|
154
164
|
|
|
155
165
|
/**
|
|
156
166
|
* Runtime-only loose props used internally by the Link implementation.
|
|
@@ -303,16 +313,35 @@ export function interpolateParams(pattern: string, params: Record<string, ParamV
|
|
|
303
313
|
*
|
|
304
314
|
* Handles:
|
|
305
315
|
* - params interpolation into route patterns
|
|
306
|
-
* - searchParams serialization
|
|
316
|
+
* - searchParams serialization (see the two shapes documented above)
|
|
307
317
|
* - Validation that searchParams and inline query strings are exclusive
|
|
308
318
|
*/
|
|
309
319
|
/**
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
320
|
+
* Tolerate a leading '?'. `buildSearchParams()` never emits one, but callers
|
|
321
|
+
* hand-rolling a query string reasonably might, and silently producing
|
|
322
|
+
* `?%3Fa=b` for it would be a worse failure than accepting both.
|
|
323
|
+
*/
|
|
324
|
+
function stripLeadingQuestionMark(qs: string): string {
|
|
325
|
+
return qs.startsWith('?') ? qs.slice(1) : qs;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Escape-hatch serialization for a plain object: String()-coerce every
|
|
330
|
+
* value, append arrays as repeated keys, skip null/undefined.
|
|
331
|
+
*
|
|
332
|
+
* Deliberately codec-free and alias-free — see the shape docs above.
|
|
313
333
|
*/
|
|
314
|
-
function
|
|
315
|
-
|
|
334
|
+
function coerceToQueryString(values: Record<string, unknown>): string {
|
|
335
|
+
const usp = new URLSearchParams();
|
|
336
|
+
for (const [key, val] of Object.entries(values)) {
|
|
337
|
+
if (val === undefined || val === null) continue;
|
|
338
|
+
if (Array.isArray(val)) {
|
|
339
|
+
for (const item of val) usp.append(key, String(item));
|
|
340
|
+
} else {
|
|
341
|
+
usp.set(key, String(val));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return usp.toString();
|
|
316
345
|
}
|
|
317
346
|
|
|
318
347
|
export function resolveHref(
|
|
@@ -337,46 +366,18 @@ export function resolveHref(
|
|
|
337
366
|
);
|
|
338
367
|
}
|
|
339
368
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// virtual:timber-search-params-registry module generated by the
|
|
353
|
-
// timber-routing Vite plugin.
|
|
354
|
-
definition = getSearchParamsDefinition(href) as
|
|
355
|
-
| SearchParamsDefinition<Record<string, unknown>>
|
|
356
|
-
| undefined;
|
|
357
|
-
values = searchParams;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (definition) {
|
|
361
|
-
const qs = definition.serialize(values);
|
|
362
|
-
if (qs) {
|
|
363
|
-
resolvedPath = `${resolvedPath}?${qs}`;
|
|
364
|
-
}
|
|
365
|
-
} else {
|
|
366
|
-
// No registered definition — serialize flat object as plain query params.
|
|
367
|
-
const usp = new URLSearchParams();
|
|
368
|
-
for (const [key, val] of Object.entries(values)) {
|
|
369
|
-
if (val === undefined || val === null) continue;
|
|
370
|
-
if (Array.isArray(val)) {
|
|
371
|
-
for (const item of val) usp.append(key, String(item));
|
|
372
|
-
} else {
|
|
373
|
-
usp.set(key, String(val));
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
const qs = usp.toString();
|
|
377
|
-
if (qs) {
|
|
378
|
-
resolvedPath = `${resolvedPath}?${qs}`;
|
|
379
|
-
}
|
|
369
|
+
// A string is already a serialized query (from
|
|
370
|
+
// `definition.buildSearchParams()`), so it passes through untouched.
|
|
371
|
+
// `typeof` is the only discriminator that survives the RSC Flight
|
|
372
|
+
// boundary — see the shape docs above for what happened when this was
|
|
373
|
+
// an object test.
|
|
374
|
+
const qs =
|
|
375
|
+
typeof searchParams === 'string'
|
|
376
|
+
? stripLeadingQuestionMark(searchParams)
|
|
377
|
+
: coerceToQueryString(searchParams);
|
|
378
|
+
|
|
379
|
+
if (qs) {
|
|
380
|
+
resolvedPath = `${resolvedPath}?${qs}`;
|
|
380
381
|
}
|
|
381
382
|
}
|
|
382
383
|
|
|
@@ -38,6 +38,22 @@ export interface NavigationCommitInput {
|
|
|
38
38
|
/** When true, clear the segment cache if segmentInfo is absent
|
|
39
39
|
* (popstate replay for entries without layout metadata). */
|
|
40
40
|
clearSegmentCacheOnEmpty?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* HTTP status of the response this navigation rendered, when it came from a
|
|
43
|
+
* fetch. Omitted by replays, which re-publish a tree already judged.
|
|
44
|
+
*
|
|
45
|
+
* An error response rendered a status page, and a status page replaces a
|
|
46
|
+
* subtree: the deny page lands at the segment owning the matched file and
|
|
47
|
+
* nothing below it mounts. `segmentInfo` nonetheless describes the whole
|
|
48
|
+
* chain the payload was built against — it must, because the merge point is
|
|
49
|
+
* chosen out of it — so the cache has to be told not to learn from this one.
|
|
50
|
+
*
|
|
51
|
+
* Enforced here rather than at the call sites because there are three of
|
|
52
|
+
* them (`performNavigationFetch`, `refresh()`, and uncached traversal, the
|
|
53
|
+
* last two through `fetchCommitAndRender`) and only one had the check.
|
|
54
|
+
* See TIM-1356.
|
|
55
|
+
*/
|
|
56
|
+
status?: number;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
/**
|
|
@@ -151,12 +167,19 @@ export function createNavigationCommitter(deps: {
|
|
|
151
167
|
prepareNavigation(url: string, opts: NavigationCommitInput): PreparedNavigation {
|
|
152
168
|
const navState = deriveNavigationState(url);
|
|
153
169
|
|
|
170
|
+
// A tree rendered by an error response is not one to cache — see
|
|
171
|
+
// `status` on NavigationCommitInput. Normalized here, once, so no fetch
|
|
172
|
+
// caller can forget it; the history entry records the same thing, or a
|
|
173
|
+
// traversal back to this URL would replay the metadata this drops.
|
|
174
|
+
const cacheable = opts.status === undefined || opts.status < 400;
|
|
175
|
+
const segmentInfo = cacheable ? opts.segmentInfo : [];
|
|
176
|
+
|
|
154
177
|
return {
|
|
155
178
|
navState,
|
|
156
179
|
commit() {
|
|
157
|
-
if (
|
|
158
|
-
updateSegmentCache(
|
|
159
|
-
} else if (
|
|
180
|
+
if (segmentInfo && segmentInfo.length > 0) {
|
|
181
|
+
updateSegmentCache(segmentInfo);
|
|
182
|
+
} else if (segmentInfo?.length === 0 || opts.clearSegmentCacheOnEmpty) {
|
|
160
183
|
segmentCache.clear();
|
|
161
184
|
}
|
|
162
185
|
|
|
@@ -165,7 +188,7 @@ export function createNavigationCommitter(deps: {
|
|
|
165
188
|
historyStack.push(url, {
|
|
166
189
|
payload: opts.payload,
|
|
167
190
|
params: opts.params,
|
|
168
|
-
segmentInfo
|
|
191
|
+
segmentInfo,
|
|
169
192
|
});
|
|
170
193
|
},
|
|
171
194
|
};
|
|
@@ -64,10 +64,10 @@ export type ParamsContextValue = PublishedParams;
|
|
|
64
64
|
* do it: React's `react-server` export provides neither `createContext` nor
|
|
65
65
|
* `useContext`, and a *named* ESM import of a missing export fails at module
|
|
66
66
|
* instantiation — before any feature check could run. This module is reachable
|
|
67
|
-
* from
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* from every entry a Server Component imports, so the named form crashed
|
|
68
|
+
* those entries outright (codex, PR #992; originally reproduced against
|
|
69
|
+
* `@timber-js/app/segment-params`, an entry point since deleted by TIM-1342 —
|
|
70
|
+
* the hazard is unchanged for the entries that remain).
|
|
71
71
|
*
|
|
72
72
|
* See design/19-client-navigation.md §"Singleton Guarantee via globalThis"
|
|
73
73
|
*/
|
|
@@ -314,6 +314,9 @@ export function createNavigationPipeline({
|
|
|
314
314
|
decodePromise: prefetched.decodePromise ?? null,
|
|
315
315
|
segmentInfo: prefetched.segmentInfo ?? null,
|
|
316
316
|
skippedSegments: prefetched.skippedSegments ?? null,
|
|
317
|
+
// Entries written before this field carried it are treated as
|
|
318
|
+
// cacheable, which is what they were before a deny could be one.
|
|
319
|
+
status: prefetched.status ?? 200,
|
|
317
320
|
}
|
|
318
321
|
: undefined;
|
|
319
322
|
|
|
@@ -364,6 +367,7 @@ export function createNavigationPipeline({
|
|
|
364
367
|
payload: isPartial || hasSkippedSlots ? null : payload,
|
|
365
368
|
params,
|
|
366
369
|
segmentInfo: result.segmentInfo,
|
|
370
|
+
status: result.status,
|
|
367
371
|
});
|
|
368
372
|
|
|
369
373
|
return {
|
package/src/client/router.ts
CHANGED
package/src/client/rsc-fetch.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { normalizeBasePath, stripBasePath, withBasePath } from '../shared/base-p
|
|
|
27
27
|
import { rscPayloadPath } from '../shared/rsc-payload-path.js';
|
|
28
28
|
import type { SegmentInfo, StateTree } from '../shared/segment-info.js';
|
|
29
29
|
import { TIMBER_ERROR_HEADER } from '../shared/rsc-error-envelope.js';
|
|
30
|
+
import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
|
|
30
31
|
import type { RouterDeps } from './router-types.js';
|
|
31
32
|
|
|
32
33
|
// ─── Types ───────────────────────────────────────────────────────
|
|
@@ -57,12 +58,17 @@ export interface FetchResult {
|
|
|
57
58
|
segmentInfo: SegmentInfo[] | null;
|
|
58
59
|
/** Segment paths that were skipped by the server (for client-side merging). */
|
|
59
60
|
skippedSegments: string[] | null;
|
|
61
|
+
/**
|
|
62
|
+
* HTTP status of the navigation response.
|
|
63
|
+
*
|
|
64
|
+
* Read at the commit site to decide whether the tree this payload produced
|
|
65
|
+
* is one worth caching. A deny page replaced a subtree, so `segmentInfo`
|
|
66
|
+
* describes more than actually mounted — but it cannot simply be emptied,
|
|
67
|
+
* because the merge point is chosen out of it. See TIM-1356.
|
|
68
|
+
*/
|
|
69
|
+
status: number;
|
|
60
70
|
}
|
|
61
71
|
|
|
62
|
-
// ─── Constants ───────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
export const RSC_CONTENT_TYPE = 'text/x-component';
|
|
65
|
-
|
|
66
72
|
// ─── URL Helpers ─────────────────────────────────────────────────
|
|
67
73
|
|
|
68
74
|
/**
|
|
@@ -531,6 +537,7 @@ export async function fetchRscPayload(
|
|
|
531
537
|
const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual', signal });
|
|
532
538
|
let segmentInfo: SegmentInfo[] | null = null;
|
|
533
539
|
let skippedSegments: string[] | null = null;
|
|
540
|
+
let status = 200;
|
|
534
541
|
// Track when the full RSC body stream is consumed (not just shell).
|
|
535
542
|
// Initialized to resolved for bodyless responses; overwritten when
|
|
536
543
|
// the response has a body.
|
|
@@ -586,6 +593,7 @@ export async function fetchRscPayload(
|
|
|
586
593
|
// as React elements — React 19 Float handles them. See TIM-1151.
|
|
587
594
|
segmentInfo = extractSegmentInfo(response);
|
|
588
595
|
skippedSegments = extractSkippedSegments(response);
|
|
596
|
+
status = response.status;
|
|
589
597
|
|
|
590
598
|
// Wrap the body to track full stream consumption. createFromFetch's
|
|
591
599
|
// thenable resolves when the root model (shell) arrives, but we need
|
|
@@ -634,6 +642,7 @@ export async function fetchRscPayload(
|
|
|
634
642
|
decodePromise,
|
|
635
643
|
segmentInfo,
|
|
636
644
|
skippedSegments,
|
|
645
|
+
status,
|
|
637
646
|
};
|
|
638
647
|
}
|
|
639
648
|
// Test/fallback path: return raw text
|
|
@@ -677,5 +686,6 @@ export async function fetchRscPayload(
|
|
|
677
686
|
decodePromise: null,
|
|
678
687
|
segmentInfo: extractSegmentInfo(response),
|
|
679
688
|
skippedSegments: extractSkippedSegments(response),
|
|
689
|
+
status: response.status,
|
|
680
690
|
};
|
|
681
691
|
}
|
|
@@ -26,6 +26,14 @@ export interface PrefetchResult {
|
|
|
26
26
|
params?: PublishedParams | Promise<PublishedParams>;
|
|
27
27
|
/** Segment paths skipped by the server (for client-side merging). */
|
|
28
28
|
skippedSegments?: string[] | null;
|
|
29
|
+
/**
|
|
30
|
+
* HTTP status of the prefetched response. A deny payload is an ordinary
|
|
31
|
+
* Flight response carrying a 4xx, so it prefetches and caches like any
|
|
32
|
+
* other — and the tree it renders is one the segment cache must not learn
|
|
33
|
+
* from. Absent on entries that predate the field, and on the negative
|
|
34
|
+
* sentinel. See TIM-1356.
|
|
35
|
+
*/
|
|
36
|
+
status?: number;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/**
|