@timber-js/app 0.2.0-alpha.120 → 0.2.0-alpha.121

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.
Files changed (94) hide show
  1. package/dist/_chunks/{actions-Cw-7NfJJ.js → actions-Dy8vOjFt.js} +6 -6
  2. package/dist/_chunks/{actions-Cw-7NfJJ.js.map → actions-Dy8vOjFt.js.map} +1 -1
  3. package/dist/_chunks/{build-output-helper-DhxiT18k.js → build-output-helper-B1n0sUqj.js} +1 -1
  4. package/dist/_chunks/{build-output-helper-DhxiT18k.js.map → build-output-helper-B1n0sUqj.js.map} +1 -1
  5. package/dist/_chunks/cli-schema-sync-ByZPGGmS.js +724 -0
  6. package/dist/_chunks/cli-schema-sync-ByZPGGmS.js.map +1 -0
  7. package/dist/_chunks/cli-schema-sync-DHQLjFu3.js +2 -0
  8. package/dist/_chunks/{codec-registry-V7ARyeLN.js → codec-registry-G9hD5LOq.js} +1 -1
  9. package/dist/_chunks/{codec-registry-V7ARyeLN.js.map → codec-registry-G9hD5LOq.js.map} +1 -1
  10. package/dist/_chunks/{define-zQX6e1tN.js → define-CPVQaWbx.js} +3 -3
  11. package/dist/_chunks/{define-zQX6e1tN.js.map → define-CPVQaWbx.js.map} +1 -1
  12. package/dist/_chunks/{define-8zQ8Vm7x.js → define-jeHDXZGq.js} +3 -3
  13. package/dist/_chunks/{define-8zQ8Vm7x.js.map → define-jeHDXZGq.js.map} +1 -1
  14. package/dist/_chunks/{format-CR6v1pWE.js → format-BjN9HLu9.js} +1 -1
  15. package/dist/_chunks/{format-CR6v1pWE.js.map → format-BjN9HLu9.js.map} +1 -1
  16. package/dist/_chunks/{handler-store-iqnt2ghN.js → handler-store-CcwNX-gk.js} +1 -1
  17. package/dist/_chunks/{handler-store-iqnt2ghN.js.map → handler-store-CcwNX-gk.js.map} +1 -1
  18. package/dist/_chunks/{merge-search-params-CwxJzBLS.js → merge-search-params-az6ZiBeU.js} +1 -1
  19. package/dist/_chunks/{merge-search-params-CwxJzBLS.js.map → merge-search-params-az6ZiBeU.js.map} +1 -1
  20. package/dist/_chunks/{navigation-root-DKsJ5Di7.js → navigation-root-DoRTeBm5.js} +2 -2
  21. package/dist/_chunks/{navigation-root-DKsJ5Di7.js.map → navigation-root-DoRTeBm5.js.map} +1 -1
  22. package/dist/_chunks/{registry-Dp_hJL4s.js → registry-BI9cgCt7.js} +1 -1
  23. package/dist/_chunks/{registry-Dp_hJL4s.js.map → registry-BI9cgCt7.js.map} +1 -1
  24. package/dist/_chunks/{resolve-schema-23Mc8aLi.js → resolve-schema-Bf8eC6q6.js} +11 -3
  25. package/dist/_chunks/resolve-schema-Bf8eC6q6.js.map +1 -0
  26. package/dist/_chunks/{router-ref-sektkr-t.js → router-ref-CpvjBqdu.js} +2 -2
  27. package/dist/_chunks/{router-ref-sektkr-t.js.map → router-ref-CpvjBqdu.js.map} +1 -1
  28. package/dist/_chunks/{schema-bridge-eL5vQDSA.js → schema-bridge-o0SwFtXW.js} +1 -1
  29. package/dist/_chunks/{schema-bridge-eL5vQDSA.js.map → schema-bridge-o0SwFtXW.js.map} +1 -1
  30. package/dist/_chunks/{segment-context-Bg0pFbLT.js → segment-context-EsfQpS--.js} +1 -1
  31. package/dist/_chunks/{segment-context-Bg0pFbLT.js.map → segment-context-EsfQpS--.js.map} +1 -1
  32. package/dist/_chunks/{ssr-data-BO5OvEvg.js → ssr-data-m9-cNEIw.js} +1 -1
  33. package/dist/_chunks/{ssr-data-BO5OvEvg.js.map → ssr-data-m9-cNEIw.js.map} +1 -1
  34. package/dist/_chunks/{stale-reload-BCPRG0Cv.js → stale-reload-BN2kcE06.js} +1 -1
  35. package/dist/_chunks/{stale-reload-BCPRG0Cv.js.map → stale-reload-BN2kcE06.js.map} +1 -1
  36. package/dist/_chunks/{use-query-states-D9SBlAfp.js → use-query-states-C9G6LP0F.js} +2 -2
  37. package/dist/_chunks/{use-query-states-D9SBlAfp.js.map → use-query-states-C9G6LP0F.js.map} +1 -1
  38. package/dist/_chunks/{use-segment-params-CskiK-59.js → use-segment-params-CtIAjqK0.js} +2 -2
  39. package/dist/_chunks/{use-segment-params-CskiK-59.js.map → use-segment-params-CtIAjqK0.js.map} +1 -1
  40. package/dist/_chunks/{validation-Bm3Je-09.js → validation-BMU5MPzP.js} +1 -1
  41. package/dist/_chunks/{validation-Bm3Je-09.js.map → validation-BMU5MPzP.js.map} +1 -1
  42. package/dist/_chunks/{walkers-DgOXkTts.js → walkers-DNp9tHyV.js} +116 -6
  43. package/dist/_chunks/walkers-DNp9tHyV.js.map +1 -0
  44. package/dist/adapters/cloudflare.js +1 -1
  45. package/dist/adapters/nitro.js +1 -1
  46. package/dist/cache/index.js +1 -1
  47. package/dist/cli.js +1 -1
  48. package/dist/client/error-boundary.js +2 -2
  49. package/dist/client/index.js +9 -9
  50. package/dist/client/internal.js +7 -7
  51. package/dist/codec.d.ts +18 -0
  52. package/dist/codec.d.ts.map +1 -1
  53. package/dist/codec.js +30 -1
  54. package/dist/codec.js.map +1 -1
  55. package/dist/cookies/index.js +3 -3
  56. package/dist/index.js +30 -21
  57. package/dist/index.js.map +1 -1
  58. package/dist/params/define-schema.d.ts +32 -4
  59. package/dist/params/define-schema.d.ts.map +1 -1
  60. package/dist/params/index.d.ts +1 -1
  61. package/dist/params/index.d.ts.map +1 -1
  62. package/dist/params/index.js +2 -2
  63. package/dist/params/index.js.map +1 -1
  64. package/dist/params/resolve-schema.d.ts.map +1 -1
  65. package/dist/plugins/routing.d.ts.map +1 -1
  66. package/dist/routing/index.d.ts +2 -0
  67. package/dist/routing/index.d.ts.map +1 -1
  68. package/dist/routing/index.js +3 -3
  69. package/dist/routing/scanner.d.ts.map +1 -1
  70. package/dist/routing/schema-validation.d.ts +35 -0
  71. package/dist/routing/schema-validation.d.ts.map +1 -0
  72. package/dist/search-params/index.js +3 -3
  73. package/dist/segment-params/index.js +1 -1
  74. package/dist/server/index.js +2 -2
  75. package/dist/server/internal.js +3 -3
  76. package/package.json +7 -6
  77. package/src/cli.ts +0 -0
  78. package/src/codec.ts +51 -1
  79. package/src/params/define-schema.ts +46 -5
  80. package/src/params/index.ts +7 -1
  81. package/src/params/resolve-schema.ts +13 -0
  82. package/src/plugins/routing.ts +30 -33
  83. package/src/routing/codegen.ts +54 -5
  84. package/src/routing/index.ts +5 -0
  85. package/src/routing/scanner.ts +58 -0
  86. package/src/routing/schema-validation.ts +149 -0
  87. package/LICENSE +0 -8
  88. package/dist/_chunks/cli-schema-sync-XV0I47Gj.js +0 -2
  89. package/dist/_chunks/cli-schema-sync-q2wB0DyJ.js +0 -358
  90. package/dist/_chunks/cli-schema-sync-q2wB0DyJ.js.map +0 -1
  91. package/dist/_chunks/resolve-schema-23Mc8aLi.js.map +0 -1
  92. package/dist/_chunks/scanner-DmfoB7iK.js +0 -336
  93. package/dist/_chunks/scanner-DmfoB7iK.js.map +0 -1
  94. package/dist/_chunks/walkers-DgOXkTts.js.map +0 -1
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Design doc: design/41-global-params.md
9
9
  */
10
- import type { Codec } from '../codec.js';
10
+ import type { Codec, CatchAllCodec, OptionalCatchAllCodec } from '../codec.js';
11
11
  import type { StandardSchemaV1 } from '../schema-bridge.js';
12
12
  /**
13
13
  * Infer the output type from a Codec, StandardSchemaV1, or nuqs parser.
@@ -19,7 +19,30 @@ export type InferCodecType<V> = V extends Codec<infer T> ? T : V extends Standar
19
19
  parseServerSide: (value: string) => infer T;
20
20
  } ? T : string;
21
21
  /** Acceptable codec value for segment params: a Codec or Standard Schema. */
22
- type SegmentParamCodec = Codec<unknown> | StandardSchemaV1<unknown>;
22
+ export type SegmentParamCodec = Codec<unknown> | StandardSchemaV1<unknown>;
23
+ /** Codec constraint for catch-all segment keys (`[...name]`). */
24
+ export type CatchAllParamCodec = CatchAllCodec<unknown> | StandardSchemaV1<readonly unknown[]>;
25
+ /** Codec constraint for optional catch-all segment keys (`[[...name]]`). */
26
+ export type OptionalCatchAllParamCodec = OptionalCatchAllCodec<unknown> | StandardSchemaV1<readonly unknown[] | undefined>;
27
+ /**
28
+ * Resolves the required codec type for a given bracket key pattern.
29
+ * - `[...name]` → requires a catch-all codec
30
+ * - `[[...name]]` → requires an optional catch-all codec
31
+ * - `[name]` → accepts any segment param codec
32
+ */
33
+ export type CodecForKey<K extends string> = K extends `[[...${string}]]` ? OptionalCatchAllParamCodec : K extends `[...${string}]` ? CatchAllParamCodec : SegmentParamCodec;
34
+ /**
35
+ * Registry interface augmented by codegen with known filesystem bracket keys.
36
+ * When empty (no codegen yet), ValidDynamicSegmentKeys falls back to string.
37
+ */
38
+ export interface ValidDynamicSegmentKeysRegistry {
39
+ }
40
+ /**
41
+ * Union of valid bracket keys for defineSchema.
42
+ * After codegen runs, this is the set of filesystem segment names.
43
+ * Before codegen (fresh project), accepts any string.
44
+ */
45
+ export type ValidDynamicSegmentKeys = keyof ValidDynamicSegmentKeysRegistry extends never ? string : keyof ValidDynamicSegmentKeysRegistry & string;
23
46
  /** Configuration object for defineSchema. */
24
47
  export interface SchemaConfig<S extends Record<string, SegmentParamCodec>> {
25
48
  segmentParams: S;
@@ -48,6 +71,11 @@ export interface SchemaConfig<S extends Record<string, SegmentParamCodec>> {
48
71
  * });
49
72
  * ```
50
73
  */
51
- export declare function defineSchema<S extends Record<string, SegmentParamCodec>>(config: SchemaConfig<S>): SchemaConfig<S>;
52
- export {};
74
+ export declare function defineSchema<S extends {
75
+ [K in keyof S]: K extends ValidDynamicSegmentKeys ? K extends string ? CodecForKey<K> : SegmentParamCodec : never;
76
+ }>(config: {
77
+ segmentParams: S;
78
+ }): {
79
+ segmentParams: S;
80
+ };
53
81
  //# sourceMappingURL=define-schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-schema.d.ts","sourceRoot":"","sources":["../../src/params/define-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAM5D;;;;;GAKG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAC1B,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GACpB,CAAC,GACD,CAAC,SAAS,gBAAgB,CAAC,MAAM,CAAC,CAAC,GACjC,CAAC,GACD,CAAC,SAAS;IAAE,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;CAAE,GACvD,CAAC,GACD,MAAM,CAAC;AAEjB,6EAA6E;AAC7E,KAAK,iBAAiB,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAEpE,6CAA6C;AAC7C,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACvE,aAAa,EAAE,CAAC,CAAC;CAClB;AAMD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACtE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,YAAY,CAAC,CAAC,CAAC,CAEjB"}
1
+ {"version":3,"file":"define-schema.d.ts","sourceRoot":"","sources":["../../src/params/define-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAM5D;;;;;GAKG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAC1B,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GACpB,CAAC,GACD,CAAC,SAAS,gBAAgB,CAAC,MAAM,CAAC,CAAC,GACjC,CAAC,GACD,CAAC,SAAS;IAAE,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;CAAE,GACvD,CAAC,GACD,MAAM,CAAC;AAEjB,6EAA6E;AAC7E,MAAM,MAAM,iBAAiB,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAE3E,iEAAiE;AACjE,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG,gBAAgB,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;AAE/F,4EAA4E;AAC5E,MAAM,MAAM,0BAA0B,GAClC,qBAAqB,CAAC,OAAO,CAAC,GAC9B,gBAAgB,CAAC,SAAS,OAAO,EAAE,GAAG,SAAS,CAAC,CAAC;AAErD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,QAAQ,MAAM,IAAI,GACpE,0BAA0B,GAC1B,CAAC,SAAS,OAAO,MAAM,GAAG,GACxB,kBAAkB,GAClB,iBAAiB,CAAC;AAExB;;;GAGG;AACH,MAAM,WAAW,+BAA+B;CAAG;AAEnD;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,MAAM,+BAA+B,SAAS,KAAK,GACrF,MAAM,GACN,MAAM,+BAA+B,GAAG,MAAM,CAAC;AAEnD,6CAA6C;AAC7C,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACvE,aAAa,EAAE,CAAC,CAAC;CAClB;AAMD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,CAC1B,CAAC,SAAS;KACP,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,SAAS,uBAAuB,GAC7C,CAAC,SAAS,MAAM,GACd,WAAW,CAAC,CAAC,CAAC,GACd,iBAAiB,GACnB,KAAK;CACV,EACD,MAAM,EAAE;IAAE,aAAa,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,aAAa,EAAE,CAAC,CAAA;CAAE,CAEpD"}
@@ -1,5 +1,5 @@
1
1
  export { defineSchema } from './define-schema.js';
2
- export type { InferCodecType, SchemaConfig } from './define-schema.js';
2
+ export type { InferCodecType, SchemaConfig, SegmentParamCodec, ValidDynamicSegmentKeys, ValidDynamicSegmentKeysRegistry, } from './define-schema.js';
3
3
  export { resolveSchemaCodecs, toBracketKey } from './resolve-schema.js';
4
4
  export { setLinkCodecs, getLinkCodec } from './codec-registry.js';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/params/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/params/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,+BAA+B,GAChC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC"}
@@ -1,5 +1,5 @@
1
- import { n as toBracketKey, t as resolveSchemaCodecs } from "../_chunks/resolve-schema-23Mc8aLi.js";
2
- import { n as setLinkCodecs, t as getLinkCodec } from "../_chunks/codec-registry-V7ARyeLN.js";
1
+ import { n as toBracketKey, t as resolveSchemaCodecs } from "../_chunks/resolve-schema-Bf8eC6q6.js";
2
+ import { n as setLinkCodecs, t as getLinkCodec } from "../_chunks/codec-registry-G9hD5LOq.js";
3
3
  //#region src/params/define-schema.ts
4
4
  /**
5
5
  * Define the global segment param schema.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/params/define-schema.ts"],"sourcesContent":["/**\n * defineSchema — global segment param schema definition.\n *\n * Identity function at runtime; the type information is what matters.\n * The Vite plugin reads the schema at build time for codegen, and\n * the pipeline reads it at runtime for coercion.\n *\n * Design doc: design/41-global-params.md\n */\n\nimport type { Codec } from '../codec.js';\nimport type { StandardSchemaV1 } from '../schema-bridge.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Infer the output type from a Codec, StandardSchemaV1, or nuqs parser.\n *\n * Used by codegen to reference schema types without hardcoding:\n * `InferCodecType<typeof schema.segmentParams['[year]']>`\n */\nexport type InferCodecType<V> =\n V extends Codec<infer T>\n ? T\n : V extends StandardSchemaV1<infer T>\n ? T\n : V extends { parseServerSide: (value: string) => infer T }\n ? T\n : string;\n\n/** Acceptable codec value for segment params: a Codec or Standard Schema. */\ntype SegmentParamCodec = Codec<unknown> | StandardSchemaV1<unknown>;\n\n/** Configuration object for defineSchema. */\nexport interface SchemaConfig<S extends Record<string, SegmentParamCodec>> {\n segmentParams: S;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Define the global segment param schema.\n *\n * Each key uses the bracket syntax from the filesystem (`[id]`, `[...slug]`,\n * `[[...slug]]`) mapped to a codec. This makes the relationship between\n * filesystem segments and codecs visually obvious.\n *\n * A dynamic segment with no key here defaults to `string` at codegen time.\n *\n * ```ts\n * // app/schema.ts\n * import { defineSchema } from '@timber-js/app/params';\n * import { codec } from '@timber-js/app/codec';\n * import { z } from 'zod';\n *\n * export default defineSchema({\n * segmentParams: {\n * '[artistSlug]': codec.string,\n * '[year]': codec.integer,\n * '[id]': z.coerce.number().int(),\n * },\n * });\n * ```\n */\nexport function defineSchema<S extends Record<string, SegmentParamCodec>>(\n config: SchemaConfig<S>\n): SchemaConfig<S> {\n return config;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,SAAgB,aACd,QACiB;AACjB,QAAO"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/params/define-schema.ts"],"sourcesContent":["/**\n * defineSchema — global segment param schema definition.\n *\n * Identity function at runtime; the type information is what matters.\n * The Vite plugin reads the schema at build time for codegen, and\n * the pipeline reads it at runtime for coercion.\n *\n * Design doc: design/41-global-params.md\n */\n\nimport type { Codec, CatchAllCodec, OptionalCatchAllCodec } from '../codec.js';\nimport type { StandardSchemaV1 } from '../schema-bridge.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Infer the output type from a Codec, StandardSchemaV1, or nuqs parser.\n *\n * Used by codegen to reference schema types without hardcoding:\n * `InferCodecType<typeof schema.segmentParams['[year]']>`\n */\nexport type InferCodecType<V> =\n V extends Codec<infer T>\n ? T\n : V extends StandardSchemaV1<infer T>\n ? T\n : V extends { parseServerSide: (value: string) => infer T }\n ? T\n : string;\n\n/** Acceptable codec value for segment params: a Codec or Standard Schema. */\nexport type SegmentParamCodec = Codec<unknown> | StandardSchemaV1<unknown>;\n\n/** Codec constraint for catch-all segment keys (`[...name]`). */\nexport type CatchAllParamCodec = CatchAllCodec<unknown> | StandardSchemaV1<readonly unknown[]>;\n\n/** Codec constraint for optional catch-all segment keys (`[[...name]]`). */\nexport type OptionalCatchAllParamCodec =\n | OptionalCatchAllCodec<unknown>\n | StandardSchemaV1<readonly unknown[] | undefined>;\n\n/**\n * Resolves the required codec type for a given bracket key pattern.\n * - `[...name]` → requires a catch-all codec\n * - `[[...name]]` → requires an optional catch-all codec\n * - `[name]` → accepts any segment param codec\n */\nexport type CodecForKey<K extends string> = K extends `[[...${string}]]`\n ? OptionalCatchAllParamCodec\n : K extends `[...${string}]`\n ? CatchAllParamCodec\n : SegmentParamCodec;\n\n/**\n * Registry interface augmented by codegen with known filesystem bracket keys.\n * When empty (no codegen yet), ValidDynamicSegmentKeys falls back to string.\n */\nexport interface ValidDynamicSegmentKeysRegistry {}\n\n/**\n * Union of valid bracket keys for defineSchema.\n * After codegen runs, this is the set of filesystem segment names.\n * Before codegen (fresh project), accepts any string.\n */\nexport type ValidDynamicSegmentKeys = keyof ValidDynamicSegmentKeysRegistry extends never\n ? string\n : keyof ValidDynamicSegmentKeysRegistry & string;\n\n/** Configuration object for defineSchema. */\nexport interface SchemaConfig<S extends Record<string, SegmentParamCodec>> {\n segmentParams: S;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Define the global segment param schema.\n *\n * Each key uses the bracket syntax from the filesystem (`[id]`, `[...slug]`,\n * `[[...slug]]`) mapped to a codec. This makes the relationship between\n * filesystem segments and codecs visually obvious.\n *\n * A dynamic segment with no key here defaults to `string` at codegen time.\n *\n * ```ts\n * // app/schema.ts\n * import { defineSchema } from '@timber-js/app/params';\n * import { codec } from '@timber-js/app/codec';\n * import { z } from 'zod';\n *\n * export default defineSchema({\n * segmentParams: {\n * '[artistSlug]': codec.string,\n * '[year]': codec.integer,\n * '[id]': z.coerce.number().int(),\n * },\n * });\n * ```\n */\nexport function defineSchema<\n S extends {\n [K in keyof S]: K extends ValidDynamicSegmentKeys\n ? K extends string\n ? CodecForKey<K>\n : SegmentParamCodec\n : never;\n },\n>(config: { segmentParams: S }): { segmentParams: S } {\n return config;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAuGA,SAAgB,aAQd,QAAoD;AACpD,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-schema.d.ts","sourceRoot":"","sources":["../../src/params/resolve-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAmCzC;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAS3E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAQhC"}
1
+ {"version":3,"file":"resolve-schema.d.ts","sourceRoot":"","sources":["../../src/params/resolve-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAmCzC;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAS3E;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAchC"}
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/plugins/routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAwBlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AA2F1D,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAuNxD"}
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/plugins/routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAoBlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AA2F1D,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAuNxD"}
@@ -9,4 +9,6 @@ export { classifyUrlSegment, classifySegment } from './segment-classify.js';
9
9
  export type { UrlSegment, SegmentClassification } from './segment-classify.js';
10
10
  export { collectLeafRoutes } from './walkers.js';
11
11
  export type { LeafRoute, CollectLeafRoutesOptions } from './walkers.js';
12
+ export { validateSchemaAgainstRoutes, collectDynamicSegmentsFromTree, } from './schema-validation.js';
13
+ export type { SchemaWarning } from './schema-validation.js';
12
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EACV,SAAS,EACT,WAAW,EACX,WAAW,EACX,SAAS,EACT,aAAa,EACb,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAChE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC5E,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,SAAS,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EACV,SAAS,EACT,WAAW,EACX,WAAW,EACX,SAAS,EACT,aAAa,EACb,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAChE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC5E,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,SAAS,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC"}
@@ -1,4 +1,4 @@
1
1
  import { i as INTERCEPTION_MARKERS, n as classifyUrlSegment, r as DEFAULT_PAGE_EXTENSIONS, t as classifySegment } from "../_chunks/segment-classify-CnfLLvir.js";
2
- import { t as scanRoutes } from "../_chunks/scanner-DmfoB7iK.js";
3
- import { n as collectInterceptionRewrites, r as generateRouteMap, t as collectLeafRoutes } from "../_chunks/walkers-DgOXkTts.js";
4
- export { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS, classifySegment, classifyUrlSegment, collectInterceptionRewrites, collectLeafRoutes, generateRouteMap, scanRoutes };
2
+ import { p as scanRoutes } from "../_chunks/cli-schema-sync-ByZPGGmS.js";
3
+ import { a as generateRouteMap, i as collectInterceptionRewrites, n as collectDynamicSegmentsFromTree, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "../_chunks/walkers-DNp9tHyV.js";
4
+ export { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS, classifySegment, classifyUrlSegment, collectDynamicSegmentsFromTree, collectInterceptionRewrites, collectLeafRoutes, generateRouteMap, scanRoutes, validateSchemaAgainstRoutes };
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/routing/scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EACV,SAAS,EAIT,aAAa,EAEd,MAAM,YAAY,CAAC;AA+CpB;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,aAAkB,GAAG,SAAS,CAoChF"}
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/routing/scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EACV,SAAS,EAIT,aAAa,EAEd,MAAM,YAAY,CAAC;AA+CpB;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,aAAkB,GAAG,SAAS,CAwChF"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Schema key/value validation against the filesystem route tree.
3
+ *
4
+ * Runs at codegen time (build + dev) to catch mismatches between
5
+ * app/schema.ts keys and the actual dynamic segments on disk.
6
+ *
7
+ * Design doc: design/41-global-params.md
8
+ */
9
+ import type { RouteTree } from './types.js';
10
+ export interface SchemaWarning {
11
+ type: 'stale-key' | 'missing-key' | 'bracket-mismatch';
12
+ key: string;
13
+ message: string;
14
+ filesystemKey?: string;
15
+ }
16
+ /**
17
+ * Collect all unique bracket-keyed dynamic segment names from a route tree.
18
+ * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.
19
+ */
20
+ export declare function collectDynamicSegmentsFromTree(tree: RouteTree): Set<string>;
21
+ /**
22
+ * Validate schema keys against the filesystem route tree.
23
+ *
24
+ * Returns warnings for:
25
+ * - Stale keys: schema keys that don't match any filesystem segment
26
+ * - Bracket form mismatches: same param name, different bracket form
27
+ * - Missing keys: filesystem segments not registered in schema (informational)
28
+ */
29
+ export declare function validateSchemaAgainstRoutes(tree: RouteTree, appDir: string): SchemaWarning[];
30
+ /**
31
+ * Core validation: compare schema keys against filesystem segments.
32
+ * Exported separately for testing without filesystem dependencies.
33
+ */
34
+ export declare function validateKeys(schemaKeys: string[], filesystemSegments: Set<string>): SchemaWarning[];
35
+ //# sourceMappingURL=schema-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-validation.d.ts","sourceRoot":"","sources":["../../src/routing/schema-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,YAAY,CAAC;AAIzD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,GAAG,aAAa,GAAG,kBAAkB,CAAC;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,CAI3E;AA2BD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAY5F;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,MAAM,EAAE,EACpB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,GAC9B,aAAa,EAAE,CA+DjB"}
@@ -1,6 +1,6 @@
1
- import { n as registerSearchParams, t as getSearchParamsDefinition } from "../_chunks/registry-Dp_hJL4s.js";
2
- import { i as isStandardSchema, n as fromSchema, r as isCodec } from "../_chunks/schema-bridge-eL5vQDSA.js";
3
- import { n as defineSearchParams } from "../_chunks/define-8zQ8Vm7x.js";
1
+ import { n as registerSearchParams, t as getSearchParamsDefinition } from "../_chunks/registry-BI9cgCt7.js";
2
+ import { i as isStandardSchema, n as fromSchema, r as isCodec } from "../_chunks/schema-bridge-o0SwFtXW.js";
3
+ import { n as defineSearchParams } from "../_chunks/define-jeHDXZGq.js";
4
4
  //#region src/search-params/wrappers.ts
5
5
  /**
6
6
  * Wrap a nullable codec with a default value. When the inner codec returns
@@ -1,2 +1,2 @@
1
- import { r as defineSegmentParams } from "../_chunks/define-zQX6e1tN.js";
1
+ import { r as defineSegmentParams } from "../_chunks/define-CPVQaWbx.js";
2
2
  export { defineSegmentParams };
@@ -1,6 +1,6 @@
1
1
  import { a as getSpanId, d as withSpan, o as getTraceId, p as formFlashAls, t as addSpanEvent, v as isDebug, y as isDevMode } from "../_chunks/tracing-DvKLzCFa.js";
2
- import { t as formatSize } from "../_chunks/format-CR6v1pWE.js";
3
- import { D as getCookieJar, a as revalidateTag, b as getSegmentParams, c as DenySignal, f as deny, h as waitUntil, i as revalidatePath, l as RedirectSignal, m as redirectExternal, o as coerce, p as redirect, s as parseFormData, u as RedirectType, v as getHeaders } from "../_chunks/actions-Cw-7NfJJ.js";
2
+ import { t as formatSize } from "../_chunks/format-BjN9HLu9.js";
3
+ import { D as getCookieJar, a as revalidateTag, b as getSegmentParams, c as DenySignal, f as deny, h as waitUntil, i as revalidatePath, l as RedirectSignal, m as redirectExternal, o as coerce, p as redirect, s as parseFormData, u as RedirectType, v as getHeaders } from "../_chunks/actions-Dy8vOjFt.js";
4
4
  //#region src/server/sensitive-fields.ts
5
5
  /**
6
6
  * Sensitive field stripping — removes password/token/CVV-style fields
@@ -3,10 +3,10 @@ import { a as warnRedirectInSuspense, c as warnSuspenseWrappingChildren, i as wa
3
3
  import { n as classifyUrlSegment } from "../_chunks/segment-classify-CnfLLvir.js";
4
4
  import { i as getMetadataRouteServePath, n as classifyMetadataRoute, r as getMetadataRouteAutoLink, t as METADATA_ROUTE_CONVENTIONS } from "../_chunks/metadata-routes-Cjmvi3rQ.js";
5
5
  import { a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, h as swallow, i as logMiddlewareShortCircuit, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest } from "../_chunks/logger-DDmK5skc.js";
6
- import { C as setMatchedSegmentPath, E as getCookie, O as getSetCookieHeaders, S as runWithRequestContext, T as setSegmentParams, _ as getHeader, b as getSegmentParams, c as DenySignal, d as RenderError, g as applyRequestHeaderOverlay, l as RedirectSignal, n as executeAction, o as coerce, r as isRscActionRequest, t as buildNoJsResponse, w as setMutableCookieContext, x as markResponseFlushed, y as getSearchParams } from "../_chunks/actions-Cw-7NfJJ.js";
6
+ import { C as setMatchedSegmentPath, E as getCookie, O as getSetCookieHeaders, S as runWithRequestContext, T as setSegmentParams, _ as getHeader, b as getSegmentParams, c as DenySignal, d as RenderError, g as applyRequestHeaderOverlay, l as RedirectSignal, n as executeAction, o as coerce, r as isRscActionRequest, t as buildNoJsResponse, w as setMutableCookieContext, x as markResponseFlushed, y as getSearchParams } from "../_chunks/actions-Dy8vOjFt.js";
7
7
  import "../client/error-boundary.js";
8
- import { n as toBracketKey } from "../_chunks/resolve-schema-23Mc8aLi.js";
9
- import "../_chunks/segment-context-Bg0pFbLT.js";
8
+ import { n as toBracketKey } from "../_chunks/resolve-schema-Bf8eC6q6.js";
9
+ import "../_chunks/segment-context-EsfQpS--.js";
10
10
  import { readFile } from "node:fs/promises";
11
11
  import { createElement } from "react";
12
12
  //#region src/server/server-timing.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.120",
3
+ "version": "0.2.0-alpha.121",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -114,6 +114,11 @@
114
114
  "publishConfig": {
115
115
  "access": "public"
116
116
  },
117
+ "scripts": {
118
+ "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
119
+ "typecheck": "tsgo --noEmit",
120
+ "prepublishOnly": "pnpm run build"
121
+ },
117
122
  "dependencies": {
118
123
  "@opentelemetry/api": "^1.9.1",
119
124
  "@opentelemetry/context-async-hooks": "^2.6.1",
@@ -157,9 +162,5 @@
157
162
  },
158
163
  "engines": {
159
164
  "node": ">=22.12.0"
160
- },
161
- "scripts": {
162
- "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
163
- "typecheck": "tsgo --noEmit"
164
165
  }
165
- }
166
+ }
package/src/cli.ts CHANGED
File without changes
package/src/codec.ts CHANGED
@@ -22,6 +22,23 @@ export interface Codec<T> {
22
22
  serialize(value: T): string | null;
23
23
  }
24
24
 
25
+ /**
26
+ * Marker interface for codecs designed for catch-all segments (`[...slug]`).
27
+ * These codecs expect `string[]` input from the router.
28
+ */
29
+ export interface CatchAllCodec<T> extends Codec<T> {
30
+ readonly __catchAll: true;
31
+ }
32
+
33
+ /**
34
+ * Marker interface for codecs designed for optional catch-all segments (`[[...slug]]`).
35
+ * These codecs expect `string[] | undefined` input from the router.
36
+ */
37
+ export interface OptionalCatchAllCodec<T> extends Codec<T> {
38
+ readonly __catchAll: true;
39
+ readonly __optional: true;
40
+ }
41
+
25
42
  /**
26
43
  * Recursive JSON-serializable type. The canonical definition for data that
27
44
  * can safely cross the RSC → client boundary.
@@ -86,6 +103,7 @@ export const codec = {
86
103
  } satisfies Codec<string>,
87
104
 
88
105
  stringArray: {
106
+ __catchAll: true as const,
89
107
  parse(value: string | string[] | undefined): string[] {
90
108
  if (Array.isArray(value)) return value;
91
109
  return value ? [value] : [];
@@ -93,5 +111,37 @@ export const codec = {
93
111
  serialize(value: string[]): string | null {
94
112
  return value.join('/');
95
113
  },
96
- } satisfies Codec<string[]>,
114
+ } satisfies CatchAllCodec<string[]>,
115
+
116
+ catchAll<T>(inner: Codec<T>): CatchAllCodec<T[]> {
117
+ return {
118
+ __catchAll: true as const,
119
+ parse(value: string | string[] | undefined): T[] {
120
+ const arr = Array.isArray(value) ? value : value !== undefined ? [value] : [];
121
+ if (arr.length === 0) {
122
+ throw new Error('Catch-all segment requires at least one value');
123
+ }
124
+ return arr.map((item) => inner.parse(item));
125
+ },
126
+ serialize(value: T[]): string | null {
127
+ return value.map((item) => inner.serialize(item)).join('/');
128
+ },
129
+ };
130
+ },
131
+
132
+ optionalCatchAll<T>(inner: Codec<T>): OptionalCatchAllCodec<T[] | undefined> {
133
+ return {
134
+ __catchAll: true as const,
135
+ __optional: true as const,
136
+ parse(value: string | string[] | undefined): T[] | undefined {
137
+ const arr = Array.isArray(value) ? value : value !== undefined ? [value] : [];
138
+ if (arr.length === 0) return undefined;
139
+ return arr.map((item) => inner.parse(item));
140
+ },
141
+ serialize(value: T[] | undefined): string | null {
142
+ if (!value || value.length === 0) return null;
143
+ return value.map((item) => inner.serialize(item)).join('/');
144
+ },
145
+ };
146
+ },
97
147
  } as const;
@@ -8,7 +8,7 @@
8
8
  * Design doc: design/41-global-params.md
9
9
  */
10
10
 
11
- import type { Codec } from '../codec.js';
11
+ import type { Codec, CatchAllCodec, OptionalCatchAllCodec } from '../codec.js';
12
12
  import type { StandardSchemaV1 } from '../schema-bridge.js';
13
13
 
14
14
  // ---------------------------------------------------------------------------
@@ -31,7 +31,42 @@ export type InferCodecType<V> =
31
31
  : string;
32
32
 
33
33
  /** Acceptable codec value for segment params: a Codec or Standard Schema. */
34
- type SegmentParamCodec = Codec<unknown> | StandardSchemaV1<unknown>;
34
+ export type SegmentParamCodec = Codec<unknown> | StandardSchemaV1<unknown>;
35
+
36
+ /** Codec constraint for catch-all segment keys (`[...name]`). */
37
+ export type CatchAllParamCodec = CatchAllCodec<unknown> | StandardSchemaV1<readonly unknown[]>;
38
+
39
+ /** Codec constraint for optional catch-all segment keys (`[[...name]]`). */
40
+ export type OptionalCatchAllParamCodec =
41
+ | OptionalCatchAllCodec<unknown>
42
+ | StandardSchemaV1<readonly unknown[] | undefined>;
43
+
44
+ /**
45
+ * Resolves the required codec type for a given bracket key pattern.
46
+ * - `[...name]` → requires a catch-all codec
47
+ * - `[[...name]]` → requires an optional catch-all codec
48
+ * - `[name]` → accepts any segment param codec
49
+ */
50
+ export type CodecForKey<K extends string> = K extends `[[...${string}]]`
51
+ ? OptionalCatchAllParamCodec
52
+ : K extends `[...${string}]`
53
+ ? CatchAllParamCodec
54
+ : SegmentParamCodec;
55
+
56
+ /**
57
+ * Registry interface augmented by codegen with known filesystem bracket keys.
58
+ * When empty (no codegen yet), ValidDynamicSegmentKeys falls back to string.
59
+ */
60
+ export interface ValidDynamicSegmentKeysRegistry {}
61
+
62
+ /**
63
+ * Union of valid bracket keys for defineSchema.
64
+ * After codegen runs, this is the set of filesystem segment names.
65
+ * Before codegen (fresh project), accepts any string.
66
+ */
67
+ export type ValidDynamicSegmentKeys = keyof ValidDynamicSegmentKeysRegistry extends never
68
+ ? string
69
+ : keyof ValidDynamicSegmentKeysRegistry & string;
35
70
 
36
71
  /** Configuration object for defineSchema. */
37
72
  export interface SchemaConfig<S extends Record<string, SegmentParamCodec>> {
@@ -66,8 +101,14 @@ export interface SchemaConfig<S extends Record<string, SegmentParamCodec>> {
66
101
  * });
67
102
  * ```
68
103
  */
69
- export function defineSchema<S extends Record<string, SegmentParamCodec>>(
70
- config: SchemaConfig<S>
71
- ): SchemaConfig<S> {
104
+ export function defineSchema<
105
+ S extends {
106
+ [K in keyof S]: K extends ValidDynamicSegmentKeys
107
+ ? K extends string
108
+ ? CodecForKey<K>
109
+ : SegmentParamCodec
110
+ : never;
111
+ },
112
+ >(config: { segmentParams: S }): { segmentParams: S } {
72
113
  return config;
73
114
  }
@@ -6,6 +6,12 @@
6
6
  // See design/41-global-params.md
7
7
 
8
8
  export { defineSchema } from './define-schema.js';
9
- export type { InferCodecType, SchemaConfig } from './define-schema.js';
9
+ export type {
10
+ InferCodecType,
11
+ SchemaConfig,
12
+ SegmentParamCodec,
13
+ ValidDynamicSegmentKeys,
14
+ ValidDynamicSegmentKeysRegistry,
15
+ } from './define-schema.js';
10
16
  export { resolveSchemaCodecs, toBracketKey } from './resolve-schema.js';
11
17
  export { setLinkCodecs, getLinkCodec } from './codec-registry.js';
@@ -63,6 +63,13 @@ export function toBracketKey(segmentType: string, paramName: string): string {
63
63
  }
64
64
  }
65
65
 
66
+ const CATCH_ALL_RE = /^\[\.\.\..+\]$/;
67
+ const OPTIONAL_CATCH_ALL_RE = /^\[\[\.\.\..+\]\]$/;
68
+
69
+ function isCatchAllKey(key: string): boolean {
70
+ return CATCH_ALL_RE.test(key) || OPTIONAL_CATCH_ALL_RE.test(key);
71
+ }
72
+
66
73
  /**
67
74
  * Resolve the global schema's segmentParams into a map of bracket-keyed
68
75
  * param name → resolved Codec.
@@ -81,6 +88,12 @@ export function resolveSchemaCodecs(
81
88
  const resolved: Record<string, Codec<unknown>> = Object.create(null);
82
89
 
83
90
  for (const [bracketName, value] of Object.entries(segmentParams)) {
91
+ if (isCatchAllKey(bracketName) && isCodec(value) && !('__catchAll' in value)) {
92
+ throw new Error(
93
+ `[timber] Schema error: '${bracketName}' is a catch-all segment and requires an array codec ` +
94
+ `(e.g. codec.catchAll(codec.string)). Got a scalar codec.`
95
+ );
96
+ }
84
97
  resolved[bracketName] = resolveCodec(bracketName, value);
85
98
  }
86
99
 
@@ -20,11 +20,7 @@ import {
20
20
  writeSegmentTypes,
21
21
  type SegmentInfo,
22
22
  } from '../routing/segment-codegen.js';
23
- import {
24
- collectDynamicSegments,
25
- findSchemaFile,
26
- parseExistingSchemaKeys,
27
- } from '../cli-schema-sync.js';
23
+ import { validateSchemaAgainstRoutes } from '../routing/schema-validation.js';
28
24
 
29
25
  import {
30
26
  lintConventions,
@@ -157,10 +153,10 @@ export function timberRouting(ctx: PluginContext): Plugin {
157
153
  dirToRoute = buildDirToRouteMap(ctx.routeTree, ctx.appDir);
158
154
  writeCodegen(ctx);
159
155
 
160
- // Dev: warn about dynamic segments not registered in app/schema.ts.
161
- // Only logs a suggestion, never auto-modifies the schema file.
156
+ // Validate schema keys against filesystem segments.
157
+ // Warns on stale keys, bracket form mismatches, and missing keys.
162
158
  // See design/41-global-params.md §Auto-Scaffolding
163
- warnUnregisteredSegments(ctx);
159
+ warnSchemaValidation(ctx);
164
160
 
165
161
  // Lint conventions (empty app, missing methods, missing root layout)
166
162
  const conventionWarnings = lintConventions(ctx.routeTree, ctx.appDir);
@@ -346,34 +342,35 @@ export function timberRouting(ctx: PluginContext): Plugin {
346
342
  * When the route tree changes, any environment that has imported the manifest
347
343
  * needs to be invalidated so the next request/transform picks up the new tree.
348
344
  */
349
- /** Set of segments we've already warned about — avoid repeating on every rescan. */
350
- const _warnedSegments = new Set<string>();
345
+ /** Set of warning keys we've already emitted — avoid repeating on every rescan. */
346
+ const _warnedKeys = new Set<string>();
351
347
 
352
348
  /**
353
- * Warn about dynamic segments not registered in app/schema.ts.
354
- * Only fires when a schema file exists. Logs once per segment per session.
349
+ * Validate schema keys against the filesystem route tree.
350
+ * Warns on stale keys, bracket form mismatches, and missing keys.
351
+ * Uses the already-scanned route tree instead of re-scanning.
355
352
  */
356
- function warnUnregisteredSegments(ctx: PluginContext): void {
357
- const schemaPath = findSchemaFile(ctx.appDir);
358
- if (!schemaPath) return;
359
-
360
- const filesystemSegments = collectDynamicSegments(ctx.appDir);
361
- if (filesystemSegments.length === 0) return;
362
-
363
- const content = readFileSync(schemaPath, 'utf-8');
364
- const existingKeys = parseExistingSchemaKeys(content);
365
- const missing = filesystemSegments.filter(
366
- (seg) => !existingKeys.includes(seg) && !_warnedSegments.has(seg)
367
- );
368
-
369
- if (missing.length === 0) return;
370
-
371
- for (const seg of missing) _warnedSegments.add(seg);
372
-
373
- console.warn(
374
- `[timber] New segment${missing.length > 1 ? 's' : ''} not in schema: ${missing.join(', ')}\n` +
375
- ` Run: timber schema sync`
376
- );
353
+ function warnSchemaValidation(ctx: PluginContext): void {
354
+ if (!ctx.routeTree) return;
355
+ const warnings = validateSchemaAgainstRoutes(ctx.routeTree, ctx.appDir);
356
+
357
+ for (const w of warnings) {
358
+ const warnKey = `${w.type}:${w.key}`;
359
+ if (_warnedKeys.has(warnKey)) continue;
360
+ _warnedKeys.add(warnKey);
361
+
362
+ switch (w.type) {
363
+ case 'stale-key':
364
+ console.warn(`[timber] ${w.message}`);
365
+ break;
366
+ case 'bracket-mismatch':
367
+ console.warn(`[timber] ${w.message}`);
368
+ break;
369
+ case 'missing-key':
370
+ console.warn(`[timber] ${w.message}`);
371
+ break;
372
+ }
373
+ }
377
374
  }
378
375
 
379
376
  function invalidateManifest(server: ViteDevServer): void {