@timber-js/app 0.2.0-alpha.205 → 0.2.0-alpha.207

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 (46) hide show
  1. package/dist/_chunks/{chains-BjGKq9Je.js → chains-B4bgOl2g.js} +2 -2
  2. package/dist/_chunks/{chains-BjGKq9Je.js.map → chains-B4bgOl2g.js.map} +1 -1
  3. package/dist/_chunks/{cli-check-3WE_GyoL.js → cli-check-G8o30ldg.js} +3 -3
  4. package/dist/_chunks/{cli-check-3WE_GyoL.js.map → cli-check-G8o30ldg.js.map} +1 -1
  5. package/dist/_chunks/{cli-schema-sync-D3iIZjku.js → cli-schema-sync--LZKs8Zn.js} +2 -2
  6. package/dist/_chunks/{cli-schema-sync-D3iIZjku.js.map → cli-schema-sync--LZKs8Zn.js.map} +1 -1
  7. package/dist/_chunks/{convention-lint-BnO5TyHD.js → convention-lint-5KzoMg_A.js} +35 -1
  8. package/dist/_chunks/convention-lint-5KzoMg_A.js.map +1 -0
  9. package/dist/_chunks/{dev-server-Dbdgx2dD.js → dev-server-BzBxgYAV.js} +3 -3
  10. package/dist/_chunks/{dev-server-Dbdgx2dD.js.map → dev-server-BzBxgYAV.js.map} +1 -1
  11. package/dist/_chunks/{live-graph-BM2UbbNx.js → live-graph-Bm5WTvcr.js} +3 -3
  12. package/dist/_chunks/{live-graph-BM2UbbNx.js.map → live-graph-Bm5WTvcr.js.map} +1 -1
  13. package/dist/_chunks/{poison-scan-g83aAXhs.js → poison-scan-DG5eI2_z.js} +2 -2
  14. package/dist/_chunks/{poison-scan-g83aAXhs.js.map → poison-scan-DG5eI2_z.js.map} +1 -1
  15. package/dist/_chunks/{scanner-BhNruzv0.js → scanner-BQ5sPIJ7.js} +2 -2
  16. package/dist/_chunks/{scanner-BhNruzv0.js.map → scanner-BQ5sPIJ7.js.map} +1 -1
  17. package/dist/_chunks/schema-bridge-C83xa9lT.js.map +1 -1
  18. package/dist/_chunks/{tree-match-CdbvYTBz.js → tree-match-6yihorhk.js} +49 -2
  19. package/dist/_chunks/tree-match-6yihorhk.js.map +1 -0
  20. package/dist/_chunks/{walkers-BNsswm-h.js → walkers-CN2N_bA-.js} +2 -2
  21. package/dist/_chunks/{walkers-BNsswm-h.js.map → walkers-CN2N_bA-.js.map} +1 -1
  22. package/dist/analyze/crawl-entry.js +2 -2
  23. package/dist/analyze/graph-command.js +2 -2
  24. package/dist/cli.js +3 -3
  25. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  26. package/dist/index.js +5 -5
  27. package/dist/index.js.map +1 -1
  28. package/dist/routing/convention-lint.d.ts.map +1 -1
  29. package/dist/routing/index.js +2 -2
  30. package/dist/schema-bridge.d.ts +2 -2
  31. package/dist/schema-bridge.d.ts.map +1 -1
  32. package/dist/server/action-client.d.ts +2 -2
  33. package/dist/server/action-client.d.ts.map +1 -1
  34. package/dist/server/index.js.map +1 -1
  35. package/dist/server/tree-match.d.ts +10 -0
  36. package/dist/server/tree-match.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/client/browser-entry/action-dispatch.ts +8 -2
  39. package/src/plugins/routing.ts +1 -1
  40. package/src/routing/convention-lint.ts +60 -0
  41. package/src/schema-bridge.ts +4 -2
  42. package/src/server/action-client.ts +4 -2
  43. package/src/server/route-matcher.ts +73 -20
  44. package/src/server/tree-match.ts +250 -18
  45. package/dist/_chunks/convention-lint-BnO5TyHD.js.map +0 -1
  46. package/dist/_chunks/tree-match-CdbvYTBz.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"schema-bridge-C83xa9lT.js","names":[],"sources":["../../src/schema-bridge.ts"],"sourcesContent":["/**\n * Standard Schema bridge — shared helpers for bridging Standard Schema-compatible\n * validation libraries (Zod, Valibot, ArkType) to the Codec<T> protocol.\n *\n * This module is the single source of truth for:\n * - StandardSchemaV1 interface (subset of the Standard Schema spec)\n * - validateSync() helper\n * - fromSchema() — search-param bridge; scalar shape first, array second\n * - fromArraySchema() — same loop, array shape first\n * - fromCookieSchema() — scalar shape only; a cookie has no array shape\n * - fromParamSchema() — route params; throws on failure (invalid param → 404)\n *\n * One parse loop, four candidate orders. Which shapes a bridge offers, and\n * in what order, is the ONLY thing that differs — see §\"Shape tolerance\"\n * below and in design/23-search-params.md. `serialize` is deliberately NOT\n * shared: `null` means \"omit the key\" for a search param and \"delete the\n * cookie\" for a cookie, so unifying it silently changed what\n * `cookie.set([])` did (TIM-1352).\n *\n * These are re-exported from @timber-js/app/search-params, @timber-js/app/segment-params,\n * and @timber-js/app/cookies for convenience. The canonical import is\n * @timber-js/app/codec.\n *\n * Design doc: design/23a-search-params-triage.md §\"Unify Codec<T> type\"\n */\n\nimport type { Codec } from './codec.ts';\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset)\n//\n// Standard Schema (https://github.com/standard-schema/standard-schema) defines\n// a minimal interface that Zod ≥3.24, Valibot ≥1.0, and ArkType all implement.\n// We depend only on `~standard.validate` to avoid coupling to any specific lib.\n// ---------------------------------------------------------------------------\n\n/** Minimal Standard Schema interface for auto-detection. */\nexport interface StandardSchemaV1<Output = unknown> {\n '~standard': {\n validate(value: unknown): StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;\n };\n}\n\nexport type StandardSchemaResult<Output> =\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> };\n\n// ---------------------------------------------------------------------------\n// Sync validate helper\n// ---------------------------------------------------------------------------\n\n/**\n * Run a Standard Schema's `~standard.validate()` synchronously.\n *\n * Zod v4's signature includes `Promise` in the return union to satisfy the\n * Standard Schema spec, but in practice Zod always validates synchronously\n * for the schema types we use. We assert the result is sync and throw if\n * it isn't — codec parsing must be synchronous.\n */\nexport function validateSync<Output>(\n schema: StandardSchemaV1<Output>,\n value: unknown\n): StandardSchemaResult<Output> {\n const result = schema['~standard'].validate(value);\n if (result instanceof Promise) {\n throw new Error(\n '[timber] fromSchema: schema returned a Promise — only sync schemas are supported.'\n );\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Type guards\n// ---------------------------------------------------------------------------\n\n/** Check if a value is a Standard Schema object. */\nexport function isStandardSchema(value: unknown): value is StandardSchemaV1 {\n return (\n typeof value === 'object' &&\n value !== null &&\n '~standard' in value &&\n typeof (value as StandardSchemaV1)['~standard']?.validate === 'function'\n );\n}\n\n/** Check if a value is a Codec (has parse + serialize methods). */\nexport function isCodec(value: unknown): value is Codec<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as Codec<unknown>).parse === 'function' &&\n typeof (value as Codec<unknown>).serialize === 'function'\n );\n}\n\n// ---------------------------------------------------------------------------\n// fromParamSchema — bridge from Standard Schema to Codec<T> for route params\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema to a Codec for route params.\n * Parse throws on failure (invalid param → 404). Serialize returns string.\n */\nexport function fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n const result = validateSync(schema, value);\n if (!result.issues) {\n return result.value;\n }\n const messages = result.issues.map((i) => i.message).join(', ');\n throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);\n },\n serialize(value: T): string | null {\n if (value === null || value === undefined) return null;\n if (Array.isArray(value)) return value.join('/');\n return String(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// resolveCodecOrSchema — generic resolver for any codec-or-schema field\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a field value to a Codec. Accepts Codec<T>, StandardSchemaV1<T>,\n * or (for route params) auto-wraps via fromParamSchema.\n *\n * @param fieldName - used in error messages\n * @param value - the codec or schema to resolve\n * @param mode - 'param' uses fromParamSchema (throws on parse failure),\n * 'search' uses fromSchema (falls back to default on failure,\n * tolerant of the repeated-key array shape),\n * 'cookie' uses fromCookieSchema (same, minus the array shape,\n * which a cookie value cannot have)\n */\nexport function resolveCodecOrSchema(\n fieldName: string,\n value: unknown,\n mode: 'param' | 'search' | 'cookie' = 'search'\n): Codec<unknown> {\n if (isCodec(value)) return value;\n if (isStandardSchema(value)) {\n if (mode === 'param') return fromParamSchema(fieldName, value);\n if (mode === 'cookie') return fromCookieSchema(value) as Codec<unknown>;\n return fromSchema(value) as Codec<unknown>;\n }\n throw new Error(\n `[timber] Field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n// ---------------------------------------------------------------------------\n// fromSchema — bridge from Standard Schema to Codec<T>\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Shape tolerance — the shared parse loop for both bridges\n//\n// A URL value is `string | string[] | undefined`, but a schema is written\n// against ONE of those: `z.string()` wants `'a'`, `z.array(z.string())`\n// wants `['a']`. Nothing in Standard Schema says which — `~standard.types`\n// exists at the type level only, and probing at runtime is not reliable\n// (design/23-search-params.md §\"Shape tolerance\"). So a bridge tries BOTH\n// shapes rather than committing to one and discarding the value when it\n// guessed wrong.\n//\n// Each bridge only chooses WHICH candidates, and in what order. The first\n// candidate is always the shape that bridge already passed, so nothing that\n// parses today changes meaning; a later one is reached only where the old\n// code fell through to the default. `fromCookieSchema` offers ONE candidate,\n// because a cookie has no array shape at all.\n// ---------------------------------------------------------------------------\n\n/**\n * The scalar shape, and only it. A value that cannot be repeated — a cookie —\n * has no array shape to tolerate, so its bridge stays exactly where it was.\n */\nfunction scalarOnly(value: string | string[] | undefined): unknown[] {\n return [Array.isArray(value) ? value[0] : value];\n}\n\n/** Candidate inputs for the scalar bridge: scalar first, array second. */\nfunction scalarFirst(value: string | string[] | undefined): unknown[] {\n if (value === undefined) return [undefined];\n if (typeof value === 'string') return [value, [value]];\n // Repeated keys: `value[0]` matches URLSearchParams.get().\n //\n // A raw `[]` carries no value at all. `value[0]` is `undefined`, which is\n // the shape the scalar bridge has always passed for it, so `undefined`\n // stays FIRST — an empty array must keep meaning \"absent\" and land on the\n // schema's default. Only `defineSearchParams().parse({ tags: [] })`, the\n // record form, can produce one: URLSearchParams never yields an empty list.\n return value.length > 0 ? [value[0], value] : [undefined, value];\n}\n\n/** Candidate inputs for the array bridge: array first, scalar second. */\nfunction arrayFirst(value: string | string[] | undefined): unknown[] {\n if (value === undefined) return [undefined];\n if (typeof value === 'string') return [[value], value];\n // `[]` first here for the same reason, inverted: the array bridge has\n // always passed the empty array straight through.\n return value.length > 0 ? [value, value[0]] : [value, undefined];\n}\n\n/**\n * Validate `value` against `schema` in each candidate shape, then fall back\n * to the schema's default. Shared by every bridge — they differ only in\n * which candidates they offer, and in what order.\n */\nfunction parseThroughSchema<T>(\n schema: StandardSchemaV1<T>,\n candidates: (value: string | string[] | undefined) => unknown[],\n value: string | string[] | undefined\n): T {\n const inputs = candidates(value);\n for (let i = 0; i < inputs.length; i++) {\n // A throw from the FIRST candidate propagates, unchanged: that is the\n // shape the schema was written against, and a throw from it is the\n // deliberate signal design/23 protects — an app schema may `redirect()`\n // or `notFound()` on a value it refuses, and `validateSync` throws on\n // an async schema.\n //\n // A throw from a LATER candidate is ours, not the app's. We invented\n // that shape; the schema never agreed to receive it. A scalar-only\n // hand-written validator doing `value.toUpperCase()`, or a schema that\n // is async for the array shape only, would turn a field that used to\n // fall back to its default into a render-phase 500. Treat it as \"this\n // shape does not fit\" and keep going.\n let result: StandardSchemaResult<T>;\n try {\n result = validateSync(schema, inputs[i]);\n } catch (error) {\n if (i === 0) throw error;\n continue;\n }\n if (!result.issues) {\n return result.value;\n }\n }\n\n // No shape validated — try parsing undefined to get the default.\n // Re-validate each time so factory defaults (e.g. .default(() => []))\n // produce fresh values.\n const defaultResult = validateSync(schema, undefined);\n if (!defaultResult.issues) {\n return defaultResult.value;\n }\n\n // No default available — the field is implicitly optional. Return\n // undefined; defineSearchParams widens the field's inferred type to\n // T | undefined via InferField so this doesn't lie.\n // design/23-search-params.md §\"Implicit Optionality\"\n return undefined as T;\n}\n\n/**\n * Bridge a Standard Schema-compatible schema (Zod, Valibot, ArkType) to a\n * Codec<T>.\n *\n * Parse: coerces the raw URL value through the schema, trying the scalar\n * shape and then the array shape (see `scalarFirst`), so an array schema\n * works bare: `z.array(z.string()).default([])` yields `[]` when absent,\n * `['a']` for `?tags=a`, and `['a','b']` for `?tags=a&tags=b`. When no\n * shape validates, parses `undefined` to get the schema's default (the\n * schema should have a `.default()` call). If that also fails, returns\n * `undefined`.\n *\n * Serialize: `String()` for primitives, `null` for null/undefined. Note\n * that `String(['a','b'])` is `'a,b'` — the same string `fromArraySchema`\n * writes — so a bare array schema round-trips exactly as design/09\n * §\"Array params\" describes.\n *\n * This is the SEARCH-PARAM bridge. Cookies use `fromCookieSchema`; a\n * cookie has no array shape (see below).\n *\n * ```ts\n * import { z } from 'zod/v4'\n *\n * const pageCodec = fromSchema(z.coerce.number().int().min(1).default(1))\n * const tagsCodec = fromSchema(z.array(z.string()).default([]))\n * ```\n */\nexport function fromSchema<T>(schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse: (value: string | string[] | undefined): T =>\n parseThroughSchema(schema, scalarFirst, value),\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n return String(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// fromCookieSchema — bridge for cookie values\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema for a COOKIE value.\n *\n * Identical to `fromSchema` minus the array shape, because a cookie cannot\n * carry one: `Cookie:` headers and `document.cookie` yield a single string\n * per name, and there is no repeated-key concept to be tolerant of. Sharing\n * the search-param bridge here meant a cookie written by this very codec —\n * `serialize(['a','b'])` → `a,b` — read back as the one-element array\n * `['a,b']` instead of failing to its default: no repeated key in sight,\n * just a delimiter reinterpreted as data.\n *\n * The `serialize` contract also differs by domain and must not be unified:\n * for a search param `null` means \"omit the key\", for a cookie it means\n * **delete the cookie** (design/29-cookies.md).\n */\nexport function fromCookieSchema<T>(schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse: (value: string | string[] | undefined): T =>\n parseThroughSchema(schema, scalarOnly, value),\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n return String(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// fromArraySchema — bridge for array-valued codecs\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema for array values. Handles both single strings\n * and repeated query keys (`?tag=a&tag=b`).\n *\n * ```ts\n * import { fromArraySchema } from '@timber-js/app/codec'\n * import { z } from 'zod/v4'\n *\n * const tagsCodec = fromArraySchema(z.array(z.string()).default([]))\n * ```\n *\n * Since TIM-1352 a bare array schema works through `fromSchema` too, so\n * this is no longer required to make an array field parse. Two cases still\n * want it, both listed in design/23 §\"Shape tolerance\":\n *\n * 1. A **permissive** schema meant as an array — one that accepts a string\n * as readily as an array (`z.any()`, a hand-written coercing validator)\n * — where scalar-first ordering would settle on the scalar shape.\n * 2. An array schema wrapped in `.catch([])`, which swallows the failure\n * that would otherwise trigger the array attempt.\n *\n * It also serializes an empty array to `null` (omitting the key) where\n * `fromSchema` writes `''`.\n */\nexport function fromArraySchema<T>(schema: StandardSchemaV1<T>): {\n parse(value: string | string[] | undefined): T;\n serialize(value: T): string | string[] | null;\n} {\n return {\n parse: (value: string | string[] | undefined): T =>\n parseThroughSchema(schema, arrayFirst, value),\n serialize(value: T): string | string[] | null {\n if (value === null || value === undefined) {\n return null;\n }\n // TIM-1353: return string[] so buildSearchParams emits repeated keys\n // (?tag=a&tag=b) instead of comma-joining into one key (?tag=a%2Cb).\n if (Array.isArray(value)) {\n return value.length === 0 ? null : value.map(String);\n }\n return String(value);\n },\n };\n}\n"],"mappings":";;;;;;;;;AA2DA,SAAgB,aACd,QACA,OAC8B;CAC9B,MAAM,SAAS,OAAO,YAAY,CAAC,SAAS,KAAK;CACjD,IAAI,kBAAkB,SACpB,MAAM,IAAI,MACR,mFACF;CAEF,OAAO;AACT;;AAOA,SAAgB,iBAAiB,OAA2C;CAC1E,OACE,OAAO,UAAU,YACjB,UAAU,QACV,eAAe,SACf,OAAQ,MAA2B,YAAY,EAAE,aAAa;AAElE;;AAGA,SAAgB,QAAQ,OAAyC;CAC/D,OACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAAyB,UAAU,cAC3C,OAAQ,MAAyB,cAAc;AAEnD;;;;;AAUA,SAAgB,gBAAmB,WAAmB,QAAuC;CAC3F,OAAO;EACL,MAAM,OAAyC;GAC7C,MAAM,SAAS,aAAa,QAAQ,KAAK;GACzC,IAAI,CAAC,OAAO,QACV,OAAO,OAAO;GAEhB,MAAM,WAAW,OAAO,OAAO,KAAK,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI;GAC9D,MAAM,IAAI,MAAM,mBAAmB,UAAU,qBAAqB,UAAU;EAC9E;EACA,UAAU,OAAyB;GACjC,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;GAClD,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,KAAK,GAAG;GAC/C,OAAO,OAAO,KAAK;EACrB;CACF;AACF;;;;;;;;;;;;;AAkBA,SAAgB,qBACd,WACA,OACA,OAAsC,UACtB;CAChB,IAAI,QAAQ,KAAK,GAAG,OAAO;CAC3B,IAAI,iBAAiB,KAAK,GAAG;EAC3B,IAAI,SAAS,SAAS,OAAO,gBAAgB,WAAW,KAAK;EAC7D,IAAI,SAAS,UAAU,OAAO,iBAAiB,KAAK;EACpD,OAAO,WAAW,KAAK;CACzB;CACA,MAAM,IAAI,MACR,mBAAmB,UAAU,sJAG/B;AACF;;;;;AA4BA,SAAS,WAAW,OAAiD;CACnE,OAAO,CAAC,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,KAAK;AACjD;;AAGA,SAAS,YAAY,OAAiD;CACpE,IAAI,UAAU,KAAA,GAAW,OAAO,CAAC,KAAA,CAAS;CAC1C,IAAI,OAAO,UAAU,UAAU,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC;CAQrD,OAAO,MAAM,SAAS,IAAI,CAAC,MAAM,IAAI,KAAK,IAAI,CAAC,KAAA,GAAW,KAAK;AACjE;;AAGA,SAAS,WAAW,OAAiD;CACnE,IAAI,UAAU,KAAA,GAAW,OAAO,CAAC,KAAA,CAAS;CAC1C,IAAI,OAAO,UAAU,UAAU,OAAO,CAAC,CAAC,KAAK,GAAG,KAAK;CAGrD,OAAO,MAAM,SAAS,IAAI,CAAC,OAAO,MAAM,EAAE,IAAI,CAAC,OAAO,KAAA,CAAS;AACjE;;;;;;AAOA,SAAS,mBACP,QACA,YACA,OACG;CACH,MAAM,SAAS,WAAW,KAAK;CAC/B,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;EAatC,IAAI;EACJ,IAAI;GACF,SAAS,aAAa,QAAQ,OAAO,EAAE;EACzC,SAAS,OAAO;GACd,IAAI,MAAM,GAAG,MAAM;GACnB;EACF;EACA,IAAI,CAAC,OAAO,QACV,OAAO,OAAO;CAElB;CAKA,MAAM,gBAAgB,aAAa,QAAQ,KAAA,CAAS;CACpD,IAAI,CAAC,cAAc,QACjB,OAAO,cAAc;AAQzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,WAAc,QAAuC;CACnE,OAAO;EACL,QAAQ,UACN,mBAAmB,QAAQ,aAAa,KAAK;EAC/C,UAAU,OAAyB;GACjC,IAAI,UAAU,QAAQ,UAAU,KAAA,GAC9B,OAAO;GAET,OAAO,OAAO,KAAK;EACrB;CACF;AACF;;;;;;;;;;;;;;;;AAqBA,SAAgB,iBAAoB,QAAuC;CACzE,OAAO;EACL,QAAQ,UACN,mBAAmB,QAAQ,YAAY,KAAK;EAC9C,UAAU,OAAyB;GACjC,IAAI,UAAU,QAAQ,UAAU,KAAA,GAC9B,OAAO;GAET,OAAO,OAAO,KAAK;EACrB;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,gBAAmB,QAGjC;CACA,OAAO;EACL,QAAQ,UACN,mBAAmB,QAAQ,YAAY,KAAK;EAC9C,UAAU,OAAoC;GAC5C,IAAI,UAAU,QAAQ,UAAU,KAAA,GAC9B,OAAO;GAIT,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,WAAW,IAAI,OAAO,MAAM,IAAI,MAAM;GAErD,OAAO,OAAO,KAAK;EACrB;CACF;AACF"}
1
+ {"version":3,"file":"schema-bridge-C83xa9lT.js","names":[],"sources":["../../src/schema-bridge.ts"],"sourcesContent":["/**\n * Standard Schema bridge — shared helpers for bridging Standard Schema-compatible\n * validation libraries (Zod, Valibot, ArkType) to the Codec<T> protocol.\n *\n * This module is the single source of truth for:\n * - StandardSchemaV1 interface (subset of the Standard Schema spec)\n * - validateSync() helper\n * - fromSchema() — search-param bridge; scalar shape first, array second\n * - fromArraySchema() — same loop, array shape first\n * - fromCookieSchema() — scalar shape only; a cookie has no array shape\n * - fromParamSchema() — route params; throws on failure (invalid param → 404)\n *\n * One parse loop, four candidate orders. Which shapes a bridge offers, and\n * in what order, is the ONLY thing that differs — see §\"Shape tolerance\"\n * below and in design/23-search-params.md. `serialize` is deliberately NOT\n * shared: `null` means \"omit the key\" for a search param and \"delete the\n * cookie\" for a cookie, so unifying it silently changed what\n * `cookie.set([])` did (TIM-1352).\n *\n * These are re-exported from @timber-js/app/search-params, @timber-js/app/segment-params,\n * and @timber-js/app/cookies for convenience. The canonical import is\n * @timber-js/app/codec.\n *\n * Design doc: design/23a-search-params-triage.md §\"Unify Codec<T> type\"\n */\n\nimport type { Codec } from './codec.ts';\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset)\n//\n// Standard Schema (https://github.com/standard-schema/standard-schema) defines\n// a minimal interface that Zod ≥3.24, Valibot ≥1.0, and ArkType all implement.\n// We depend only on `~standard.validate` to avoid coupling to any specific lib.\n// ---------------------------------------------------------------------------\n\n/** Minimal Standard Schema interface for auto-detection. */\nexport interface StandardSchemaV1<Output = unknown> {\n readonly '~standard': {\n readonly validate: (\n value: unknown\n ) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;\n };\n}\n\nexport type StandardSchemaResult<Output> =\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> };\n\n// ---------------------------------------------------------------------------\n// Sync validate helper\n// ---------------------------------------------------------------------------\n\n/**\n * Run a Standard Schema's `~standard.validate()` synchronously.\n *\n * Zod v4's signature includes `Promise` in the return union to satisfy the\n * Standard Schema spec, but in practice Zod always validates synchronously\n * for the schema types we use. We assert the result is sync and throw if\n * it isn't — codec parsing must be synchronous.\n */\nexport function validateSync<Output>(\n schema: StandardSchemaV1<Output>,\n value: unknown\n): StandardSchemaResult<Output> {\n const result = schema['~standard'].validate(value);\n if (result instanceof Promise) {\n throw new Error(\n '[timber] fromSchema: schema returned a Promise — only sync schemas are supported.'\n );\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Type guards\n// ---------------------------------------------------------------------------\n\n/** Check if a value is a Standard Schema object. */\nexport function isStandardSchema(value: unknown): value is StandardSchemaV1 {\n return (\n typeof value === 'object' &&\n value !== null &&\n '~standard' in value &&\n typeof (value as StandardSchemaV1)['~standard']?.validate === 'function'\n );\n}\n\n/** Check if a value is a Codec (has parse + serialize methods). */\nexport function isCodec(value: unknown): value is Codec<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as Codec<unknown>).parse === 'function' &&\n typeof (value as Codec<unknown>).serialize === 'function'\n );\n}\n\n// ---------------------------------------------------------------------------\n// fromParamSchema — bridge from Standard Schema to Codec<T> for route params\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema to a Codec for route params.\n * Parse throws on failure (invalid param → 404). Serialize returns string.\n */\nexport function fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n const result = validateSync(schema, value);\n if (!result.issues) {\n return result.value;\n }\n const messages = result.issues.map((i) => i.message).join(', ');\n throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);\n },\n serialize(value: T): string | null {\n if (value === null || value === undefined) return null;\n if (Array.isArray(value)) return value.join('/');\n return String(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// resolveCodecOrSchema — generic resolver for any codec-or-schema field\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a field value to a Codec. Accepts Codec<T>, StandardSchemaV1<T>,\n * or (for route params) auto-wraps via fromParamSchema.\n *\n * @param fieldName - used in error messages\n * @param value - the codec or schema to resolve\n * @param mode - 'param' uses fromParamSchema (throws on parse failure),\n * 'search' uses fromSchema (falls back to default on failure,\n * tolerant of the repeated-key array shape),\n * 'cookie' uses fromCookieSchema (same, minus the array shape,\n * which a cookie value cannot have)\n */\nexport function resolveCodecOrSchema(\n fieldName: string,\n value: unknown,\n mode: 'param' | 'search' | 'cookie' = 'search'\n): Codec<unknown> {\n if (isCodec(value)) return value;\n if (isStandardSchema(value)) {\n if (mode === 'param') return fromParamSchema(fieldName, value);\n if (mode === 'cookie') return fromCookieSchema(value) as Codec<unknown>;\n return fromSchema(value) as Codec<unknown>;\n }\n throw new Error(\n `[timber] Field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n// ---------------------------------------------------------------------------\n// fromSchema — bridge from Standard Schema to Codec<T>\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Shape tolerance — the shared parse loop for both bridges\n//\n// A URL value is `string | string[] | undefined`, but a schema is written\n// against ONE of those: `z.string()` wants `'a'`, `z.array(z.string())`\n// wants `['a']`. Nothing in Standard Schema says which — `~standard.types`\n// exists at the type level only, and probing at runtime is not reliable\n// (design/23-search-params.md §\"Shape tolerance\"). So a bridge tries BOTH\n// shapes rather than committing to one and discarding the value when it\n// guessed wrong.\n//\n// Each bridge only chooses WHICH candidates, and in what order. The first\n// candidate is always the shape that bridge already passed, so nothing that\n// parses today changes meaning; a later one is reached only where the old\n// code fell through to the default. `fromCookieSchema` offers ONE candidate,\n// because a cookie has no array shape at all.\n// ---------------------------------------------------------------------------\n\n/**\n * The scalar shape, and only it. A value that cannot be repeated — a cookie —\n * has no array shape to tolerate, so its bridge stays exactly where it was.\n */\nfunction scalarOnly(value: string | string[] | undefined): unknown[] {\n return [Array.isArray(value) ? value[0] : value];\n}\n\n/** Candidate inputs for the scalar bridge: scalar first, array second. */\nfunction scalarFirst(value: string | string[] | undefined): unknown[] {\n if (value === undefined) return [undefined];\n if (typeof value === 'string') return [value, [value]];\n // Repeated keys: `value[0]` matches URLSearchParams.get().\n //\n // A raw `[]` carries no value at all. `value[0]` is `undefined`, which is\n // the shape the scalar bridge has always passed for it, so `undefined`\n // stays FIRST — an empty array must keep meaning \"absent\" and land on the\n // schema's default. Only `defineSearchParams().parse({ tags: [] })`, the\n // record form, can produce one: URLSearchParams never yields an empty list.\n return value.length > 0 ? [value[0], value] : [undefined, value];\n}\n\n/** Candidate inputs for the array bridge: array first, scalar second. */\nfunction arrayFirst(value: string | string[] | undefined): unknown[] {\n if (value === undefined) return [undefined];\n if (typeof value === 'string') return [[value], value];\n // `[]` first here for the same reason, inverted: the array bridge has\n // always passed the empty array straight through.\n return value.length > 0 ? [value, value[0]] : [value, undefined];\n}\n\n/**\n * Validate `value` against `schema` in each candidate shape, then fall back\n * to the schema's default. Shared by every bridge — they differ only in\n * which candidates they offer, and in what order.\n */\nfunction parseThroughSchema<T>(\n schema: StandardSchemaV1<T>,\n candidates: (value: string | string[] | undefined) => unknown[],\n value: string | string[] | undefined\n): T {\n const inputs = candidates(value);\n for (let i = 0; i < inputs.length; i++) {\n // A throw from the FIRST candidate propagates, unchanged: that is the\n // shape the schema was written against, and a throw from it is the\n // deliberate signal design/23 protects — an app schema may `redirect()`\n // or `notFound()` on a value it refuses, and `validateSync` throws on\n // an async schema.\n //\n // A throw from a LATER candidate is ours, not the app's. We invented\n // that shape; the schema never agreed to receive it. A scalar-only\n // hand-written validator doing `value.toUpperCase()`, or a schema that\n // is async for the array shape only, would turn a field that used to\n // fall back to its default into a render-phase 500. Treat it as \"this\n // shape does not fit\" and keep going.\n let result: StandardSchemaResult<T>;\n try {\n result = validateSync(schema, inputs[i]);\n } catch (error) {\n if (i === 0) throw error;\n continue;\n }\n if (!result.issues) {\n return result.value;\n }\n }\n\n // No shape validated — try parsing undefined to get the default.\n // Re-validate each time so factory defaults (e.g. .default(() => []))\n // produce fresh values.\n const defaultResult = validateSync(schema, undefined);\n if (!defaultResult.issues) {\n return defaultResult.value;\n }\n\n // No default available — the field is implicitly optional. Return\n // undefined; defineSearchParams widens the field's inferred type to\n // T | undefined via InferField so this doesn't lie.\n // design/23-search-params.md §\"Implicit Optionality\"\n return undefined as T;\n}\n\n/**\n * Bridge a Standard Schema-compatible schema (Zod, Valibot, ArkType) to a\n * Codec<T>.\n *\n * Parse: coerces the raw URL value through the schema, trying the scalar\n * shape and then the array shape (see `scalarFirst`), so an array schema\n * works bare: `z.array(z.string()).default([])` yields `[]` when absent,\n * `['a']` for `?tags=a`, and `['a','b']` for `?tags=a&tags=b`. When no\n * shape validates, parses `undefined` to get the schema's default (the\n * schema should have a `.default()` call). If that also fails, returns\n * `undefined`.\n *\n * Serialize: `String()` for primitives, `null` for null/undefined. Note\n * that `String(['a','b'])` is `'a,b'` — the same string `fromArraySchema`\n * writes — so a bare array schema round-trips exactly as design/09\n * §\"Array params\" describes.\n *\n * This is the SEARCH-PARAM bridge. Cookies use `fromCookieSchema`; a\n * cookie has no array shape (see below).\n *\n * ```ts\n * import { z } from 'zod/v4'\n *\n * const pageCodec = fromSchema(z.coerce.number().int().min(1).default(1))\n * const tagsCodec = fromSchema(z.array(z.string()).default([]))\n * ```\n */\nexport function fromSchema<T>(schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse: (value: string | string[] | undefined): T =>\n parseThroughSchema(schema, scalarFirst, value),\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n return String(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// fromCookieSchema — bridge for cookie values\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema for a COOKIE value.\n *\n * Identical to `fromSchema` minus the array shape, because a cookie cannot\n * carry one: `Cookie:` headers and `document.cookie` yield a single string\n * per name, and there is no repeated-key concept to be tolerant of. Sharing\n * the search-param bridge here meant a cookie written by this very codec —\n * `serialize(['a','b'])` → `a,b` — read back as the one-element array\n * `['a,b']` instead of failing to its default: no repeated key in sight,\n * just a delimiter reinterpreted as data.\n *\n * The `serialize` contract also differs by domain and must not be unified:\n * for a search param `null` means \"omit the key\", for a cookie it means\n * **delete the cookie** (design/29-cookies.md).\n */\nexport function fromCookieSchema<T>(schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse: (value: string | string[] | undefined): T =>\n parseThroughSchema(schema, scalarOnly, value),\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n return String(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// fromArraySchema — bridge for array-valued codecs\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema for array values. Handles both single strings\n * and repeated query keys (`?tag=a&tag=b`).\n *\n * ```ts\n * import { fromArraySchema } from '@timber-js/app/codec'\n * import { z } from 'zod/v4'\n *\n * const tagsCodec = fromArraySchema(z.array(z.string()).default([]))\n * ```\n *\n * Since TIM-1352 a bare array schema works through `fromSchema` too, so\n * this is no longer required to make an array field parse. Two cases still\n * want it, both listed in design/23 §\"Shape tolerance\":\n *\n * 1. A **permissive** schema meant as an array — one that accepts a string\n * as readily as an array (`z.any()`, a hand-written coercing validator)\n * — where scalar-first ordering would settle on the scalar shape.\n * 2. An array schema wrapped in `.catch([])`, which swallows the failure\n * that would otherwise trigger the array attempt.\n *\n * It also serializes an empty array to `null` (omitting the key) where\n * `fromSchema` writes `''`.\n */\nexport function fromArraySchema<T>(schema: StandardSchemaV1<T>): {\n parse(value: string | string[] | undefined): T;\n serialize(value: T): string | string[] | null;\n} {\n return {\n parse: (value: string | string[] | undefined): T =>\n parseThroughSchema(schema, arrayFirst, value),\n serialize(value: T): string | string[] | null {\n if (value === null || value === undefined) {\n return null;\n }\n // TIM-1353: return string[] so buildSearchParams emits repeated keys\n // (?tag=a&tag=b) instead of comma-joining into one key (?tag=a%2Cb).\n if (Array.isArray(value)) {\n return value.length === 0 ? null : value.map(String);\n }\n return String(value);\n },\n };\n}\n"],"mappings":"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"}
@@ -46,6 +46,47 @@ function collectEffectiveChildren(node, groupChain = []) {
46
46
  function isTerminal(node) {
47
47
  return !!(node.page || node.route);
48
48
  }
49
+ /**
50
+ * Specificity tier for a segment. Lower = more specific. Matches the
51
+ * priority order in `matchNode`: static > affixed-dynamic > bare-dynamic >
52
+ * catch-all > optional-catch-all.
53
+ */
54
+ function segmentTier(node) {
55
+ if (node.segmentType === "static") return 0;
56
+ if (node.segmentType === "dynamic" && (node.paramPrefix || node.paramSuffix)) return 1;
57
+ if (node.segmentType === "dynamic") return 2;
58
+ if (node.segmentType === "catch-all") return 3;
59
+ if (node.segmentType === "optional-catch-all") return 4;
60
+ return 2;
61
+ }
62
+ /**
63
+ * Score a bare-dynamic candidate by running `matchNode` against its subtree
64
+ * with the remaining URL parts, then reading the tier of each segment in
65
+ * the matched chain. Returns a tier array that is compared lexicographically
66
+ * — the candidate whose chain has a lower tier at the first differing
67
+ * position is more specific.
68
+ *
69
+ * Delegates entirely to `matchNode`, so every segment type and priority
70
+ * rule is inherited automatically — no second copy to drift.
71
+ */
72
+ function subtreeMatchScore(node, parts, index, requireTerminal) {
73
+ const chain = [];
74
+ if (!matchNode(node, parts, index, chain, {}, requireTerminal)) return [];
75
+ return chain.slice(1).map((seg) => segmentTier(seg));
76
+ }
77
+ /**
78
+ * Compare two tier arrays lexicographically. Negative means `a` is more
79
+ * specific (should be tried first).
80
+ */
81
+ function compareTierArrays(a, b) {
82
+ const len = Math.max(a.length, b.length);
83
+ for (let i = 0; i < len; i++) {
84
+ const ta = i < a.length ? a[i] : 5;
85
+ const tb = i < b.length ? b[i] : 5;
86
+ if (ta !== tb) return ta - tb;
87
+ }
88
+ return 0;
89
+ }
49
90
  /** Combined affix length for specificity ordering. */
50
91
  function affixLength(node) {
51
92
  return (node.paramPrefix?.length ?? 0) + (node.paramSuffix?.length ?? 0);
@@ -94,7 +135,13 @@ function matchNode(node, parts, index, chain, params, requireTerminal) {
94
135
  else delete params[child.paramName];
95
136
  }
96
137
  }
97
- for (const { child, groupChain } of effective) if (child.segmentType === "dynamic" && child.paramName && !child.paramPrefix && !child.paramSuffix) {
138
+ const bareDynamic = effective.filter(({ child }) => child.segmentType === "dynamic" && child.paramName && !child.paramPrefix && !child.paramSuffix);
139
+ if (bareDynamic.length > 1 && index + 1 < parts.length) {
140
+ const scores = /* @__PURE__ */ new Map();
141
+ for (const { child } of bareDynamic) scores.set(child, subtreeMatchScore(child, parts, index + 1, requireTerminal));
142
+ bareDynamic.sort((a, b) => compareTierArrays(scores.get(a.child), scores.get(b.child)));
143
+ }
144
+ for (const { child, groupChain } of bareDynamic) {
98
145
  chain.push(...groupChain);
99
146
  const prev = params[child.paramName];
100
147
  params[child.paramName] = part;
@@ -119,4 +166,4 @@ function matchNode(node, parts, index, chain, params, requireTerminal) {
119
166
  //#endregion
120
167
  export { matchUrlParts as t };
121
168
 
122
- //# sourceMappingURL=tree-match-CdbvYTBz.js.map
169
+ //# sourceMappingURL=tree-match-6yihorhk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-match-6yihorhk.js","names":[],"sources":["../../src/server/tree-match.ts"],"sourcesContent":["/**\n * Shared URL-matching tree walker for segment trees.\n *\n * Both the main route matcher (`route-matcher.ts`) and slot resolver\n * (`slot-resolver.ts`) need to walk a SegmentNode tree, matching URL\n * parts against children with the same priority semantics:\n *\n * static > dynamic > catch-all > optional-catch-all\n *\n * Before this module, each had its own walker with subtle divergences\n * (different group handling, missing backtracking, incomplete\n * optional-catch-all support). This shared walker is the single\n * source of truth for URL-to-segment matching.\n *\n * See design/07-routing.md §\"Request Lifecycle\" and TIM-940.\n */\n\nimport type { SegmentNode } from '../routing/types.ts';\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/** Result of matching URL parts against a segment tree. */\nexport interface TreeMatchResult<TFile> {\n /** Matched segment chain from root to leaf, inclusive. */\n chain: SegmentNode<TFile>[];\n /** Collected params, keyed by param name. Catch-all params are string[]. */\n params: Record<string, string | string[]>;\n}\n\nexport interface TreeMatchOptions {\n /**\n * When true (default), only match if the leaf node has a page or route\n * handler. Set to false for metadata route matching, where we just need\n * to find the owning segment regardless of whether it has a page.\n */\n requireTerminal?: boolean;\n}\n\n/**\n * Match URL path parts against a segment tree.\n *\n * Walks the tree depth-first with backtracking, applying priority\n * ordering globally across group boundaries. Returns the matched\n * segment chain and collected params, or null if no route matches.\n *\n * By default, a match requires a terminal node (one with `page` or\n * `route`) at the leaf. Pass `{ requireTerminal: false }` to match\n * any segment (used by metadata route matching).\n *\n * @param root The root segment node to start matching from.\n * @param parts URL path parts (e.g. ['blog', 'hello-world']).\n * Empty array matches the root itself (if terminal).\n */\nexport function matchUrlParts<TFile>(\n root: SegmentNode<TFile>,\n parts: string[],\n options?: TreeMatchOptions\n): TreeMatchResult<TFile> | null {\n const chain: SegmentNode<TFile>[] = [];\n const params: Record<string, string | string[]> = {};\n const requireTerminal = options?.requireTerminal ?? true;\n\n if (matchNode(root, parts, 0, chain, params, requireTerminal)) {\n return { chain, params };\n }\n return null;\n}\n\n/**\n * Yield every match for the given URL parts, in priority order.\n *\n * Same matching semantics as `matchUrlParts` — static > affixed-dynamic >\n * bare-dynamic > catch-all > optional-catch-all — but instead of returning\n * the first match, it yields all of them. Used by the metadata route matcher,\n * which needs to try alternate owner segments when the first match doesn't\n * own the requested metadata file.\n */\nexport function* matchUrlPartsAll<TFile>(\n root: SegmentNode<TFile>,\n parts: string[],\n options?: TreeMatchOptions\n): Generator<TreeMatchResult<TFile>> {\n const chain: SegmentNode<TFile>[] = [];\n const params: Record<string, string | string[]> = {};\n const requireTerminal = options?.requireTerminal ?? true;\n\n yield* matchNodeAll(root, parts, 0, chain, params, requireTerminal);\n}\n\n// ─── Internal ─────────────────────────────────────────────────────────────\n\n/**\n * A child node flattened through group segments, along with the\n * chain of group nodes that must appear in the segment chain\n * before the child to preserve the tree path.\n */\ninterface EffectiveChild<TFile> {\n child: SegmentNode<TFile>;\n groupChain: SegmentNode<TFile>[];\n}\n\n/**\n * Flatten children through group segments.\n *\n * Groups are transparent for URL matching — their non-group descendants\n * are returned with the chain of group nodes leading to them. This lets\n * the caller apply priority ordering (static > dynamic > catch-all >\n * optional-catch-all) across all groups uniformly instead of per-group.\n */\nfunction collectEffectiveChildren<TFile>(\n node: SegmentNode<TFile>,\n groupChain: SegmentNode<TFile>[] = []\n): EffectiveChild<TFile>[] {\n const result: EffectiveChild<TFile>[] = [];\n for (const child of node.children) {\n if (child.segmentType === 'group') {\n const nested = collectEffectiveChildren(child, [...groupChain, child]);\n result.push(...nested);\n } else {\n result.push({ child, groupChain });\n }\n }\n return result;\n}\n\n/** A node is terminal if it can serve a response (has a page or route handler). */\nfunction isTerminal<TFile>(node: SegmentNode<TFile>): boolean {\n return !!(node.page || node.route);\n}\n\n/**\n * Specificity tier for a segment. Lower = more specific. Matches the\n * priority order in `matchNode`: static > affixed-dynamic > bare-dynamic >\n * catch-all > optional-catch-all.\n */\nfunction segmentTier<TFile>(node: SegmentNode<TFile>): number {\n if (node.segmentType === 'static') return 0;\n if (node.segmentType === 'dynamic' && (node.paramPrefix || node.paramSuffix)) return 1;\n if (node.segmentType === 'dynamic') return 2;\n if (node.segmentType === 'catch-all') return 3;\n if (node.segmentType === 'optional-catch-all') return 4;\n return 2;\n}\n\n/**\n * Score a bare-dynamic candidate by running `matchNode` against its subtree\n * with the remaining URL parts, then reading the tier of each segment in\n * the matched chain. Returns a tier array that is compared lexicographically\n * — the candidate whose chain has a lower tier at the first differing\n * position is more specific.\n *\n * Delegates entirely to `matchNode`, so every segment type and priority\n * rule is inherited automatically — no second copy to drift.\n */\nfunction subtreeMatchScore<TFile>(\n node: SegmentNode<TFile>,\n parts: string[],\n index: number,\n requireTerminal: boolean\n): number[] {\n const chain: SegmentNode<TFile>[] = [];\n const params: Record<string, string | string[]> = {};\n if (!matchNode(node, parts, index, chain, params, requireTerminal)) return [];\n // Skip the first entry (the candidate node itself — it's the bare-dynamic\n // we're scoring, which is the same tier for all candidates).\n return chain.slice(1).map((seg) => segmentTier(seg));\n}\n\n/**\n * Compare two tier arrays lexicographically. Negative means `a` is more\n * specific (should be tried first).\n */\nfunction compareTierArrays(a: number[], b: number[]): number {\n const len = Math.max(a.length, b.length);\n for (let i = 0; i < len; i++) {\n // A shorter match is less specific — missing entries treated as worst tier.\n const ta = i < a.length ? a[i] : 5;\n const tb = i < b.length ? b[i] : 5;\n if (ta !== tb) return ta - tb;\n }\n return 0;\n}\n\n/** Combined affix length for specificity ordering. */\nfunction affixLength<TFile>(node: SegmentNode<TFile>): number {\n return (node.paramPrefix?.length ?? 0) + (node.paramSuffix?.length ?? 0);\n}\n\n/**\n * Recursive depth-first matcher with backtracking.\n *\n * Mutates `chain` and `params` in place for performance; backtracks\n * (restores previous state) when a candidate doesn't lead to a match.\n */\nfunction matchNode<TFile>(\n node: SegmentNode<TFile>,\n parts: string[],\n index: number,\n chain: SegmentNode<TFile>[],\n params: Record<string, string | string[]>,\n requireTerminal: boolean\n): boolean {\n chain.push(node);\n\n // All parts consumed — check if this node is terminal (or any node if !requireTerminal)\n if (index >= parts.length) {\n if (!requireTerminal || isTerminal(node)) return true;\n\n // Groups don't consume URL segments, so a group child's page can\n // match at the same URL depth. Recurse into groups transparently.\n for (const child of node.children) {\n if (child.segmentType === 'group') {\n if (matchNode(child, parts, index, chain, params, requireTerminal)) {\n return true;\n }\n }\n }\n\n // Optional catch-all with zero segments (direct and through groups).\n // The param is NOT set — matching Next.js semantics for [[...slug]]\n // with no path segments.\n const effective = collectEffectiveChildren(node);\n for (const { child, groupChain } of effective) {\n if (child.segmentType === 'optional-catch-all' && (!requireTerminal || isTerminal(child))) {\n chain.push(...groupChain, child);\n return true;\n }\n }\n\n chain.pop();\n return false;\n }\n\n const part = parts[index];\n\n // Flatten children through groups so priority ordering applies globally.\n const effective = collectEffectiveChildren(node);\n\n // 1. Static segments (exact name match)\n for (const { child, groupChain } of effective) {\n if (child.segmentType === 'static' && child.segmentName === part) {\n chain.push(...groupChain);\n if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) {\n return true;\n }\n chain.length -= groupChain.length;\n }\n }\n\n // 2. Affixed dynamic segments (e.g. img-[id].png) — more specific than bare dynamic.\n // Sorted by specificity: longer combined prefix+suffix wins.\n const affixed = effective.filter(\n ({ child }) =>\n child.segmentType === 'dynamic' && child.paramName && (child.paramPrefix || child.paramSuffix)\n );\n affixed.sort((a, b) => affixLength(b.child) - affixLength(a.child));\n for (const { child, groupChain } of affixed) {\n const prefix = child.paramPrefix ?? '';\n const suffix = child.paramSuffix ?? '';\n if (\n part.length > prefix.length + suffix.length &&\n (!prefix || part.startsWith(prefix)) &&\n (!suffix || part.endsWith(suffix))\n ) {\n const value = part.slice(prefix.length, part.length - (suffix.length || 0));\n chain.push(...groupChain);\n const prev = params[child.paramName!];\n params[child.paramName!] = value;\n if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) {\n return true;\n }\n // Backtrack\n chain.length -= groupChain.length;\n if (prev !== undefined) {\n params[child.paramName!] = prev;\n } else {\n delete params[child.paramName!];\n }\n }\n }\n\n // 3. Bare dynamic segments ([param])\n // When multiple bare dynamics compete (common with route groups that\n // flatten into the same parent), sort by subtree specificity so a\n // branch containing a static match for the NEXT URL part is tried\n // first. Without this, the winner depends on children order and\n // renaming a route group can flip it — the exact shape the collision\n // validator exists to reject.\n const bareDynamic = effective.filter(\n ({ child }) =>\n child.segmentType === 'dynamic' && child.paramName && !child.paramPrefix && !child.paramSuffix\n );\n if (bareDynamic.length > 1 && index + 1 < parts.length) {\n const scores = new Map<SegmentNode<TFile>, number[]>();\n for (const { child } of bareDynamic) {\n scores.set(child, subtreeMatchScore(child, parts, index + 1, requireTerminal));\n }\n bareDynamic.sort((a, b) => compareTierArrays(scores.get(a.child)!, scores.get(b.child)!));\n }\n for (const { child, groupChain } of bareDynamic) {\n chain.push(...groupChain);\n const prev = params[child.paramName!];\n params[child.paramName!] = part;\n if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) {\n return true;\n }\n // Backtrack\n chain.length -= groupChain.length;\n if (prev !== undefined) {\n params[child.paramName!] = prev;\n } else {\n delete params[child.paramName!];\n }\n }\n\n // 4. Catch-all segments ([...param]) — must be terminal unless the\n // caller passed requireTerminal:false (metadata route matching needs to\n // find the owning segment even when it has no page).\n for (const { child, groupChain } of effective) {\n if (\n child.segmentType === 'catch-all' &&\n child.paramName &&\n (!requireTerminal || isTerminal(child))\n ) {\n chain.push(...groupChain, child);\n params[child.paramName] = parts.slice(index);\n return true;\n }\n }\n\n // 5. Optional catch-all segments ([[...param]]) — same terminal rule\n for (const { child, groupChain } of effective) {\n if (\n child.segmentType === 'optional-catch-all' &&\n child.paramName &&\n (!requireTerminal || isTerminal(child))\n ) {\n chain.push(...groupChain, child);\n params[child.paramName] = parts.slice(index);\n return true;\n }\n }\n\n chain.pop();\n return false;\n}\n\n/**\n * Generator twin of `matchNode` — yields every match instead of returning\n * the first. Each yield snapshots `chain` and `params` so the caller gets\n * independent results. After yielding, backtracking continues to find the\n * next match.\n */\nfunction* matchNodeAll<TFile>(\n node: SegmentNode<TFile>,\n parts: string[],\n index: number,\n chain: SegmentNode<TFile>[],\n params: Record<string, string | string[]>,\n requireTerminal: boolean\n): Generator<TreeMatchResult<TFile>> {\n chain.push(node);\n\n if (index >= parts.length) {\n if (!requireTerminal || isTerminal(node)) {\n yield { chain: [...chain], params: { ...params } };\n }\n\n for (const child of node.children) {\n if (child.segmentType === 'group') {\n yield* matchNodeAll(child, parts, index, chain, params, requireTerminal);\n }\n }\n\n const effective = collectEffectiveChildren(node);\n for (const { child, groupChain } of effective) {\n if (child.segmentType === 'optional-catch-all' && (!requireTerminal || isTerminal(child))) {\n chain.push(...groupChain, child);\n yield { chain: [...chain], params: { ...params } };\n chain.length -= groupChain.length + 1;\n }\n }\n\n chain.pop();\n return;\n }\n\n const part = parts[index];\n const effective = collectEffectiveChildren(node);\n\n // 1. Static segments\n for (const { child, groupChain } of effective) {\n if (child.segmentType === 'static' && child.segmentName === part) {\n chain.push(...groupChain);\n yield* matchNodeAll(child, parts, index + 1, chain, params, requireTerminal);\n chain.length -= groupChain.length;\n }\n }\n\n // 2. Affixed dynamic segments\n const affixed = effective.filter(\n ({ child }) =>\n child.segmentType === 'dynamic' && child.paramName && (child.paramPrefix || child.paramSuffix)\n );\n affixed.sort((a, b) => affixLength(b.child) - affixLength(a.child));\n for (const { child, groupChain } of affixed) {\n const prefix = child.paramPrefix ?? '';\n const suffix = child.paramSuffix ?? '';\n if (\n part.length > prefix.length + suffix.length &&\n (!prefix || part.startsWith(prefix)) &&\n (!suffix || part.endsWith(suffix))\n ) {\n const value = part.slice(prefix.length, part.length - (suffix.length || 0));\n chain.push(...groupChain);\n const prev = params[child.paramName!];\n params[child.paramName!] = value;\n yield* matchNodeAll(child, parts, index + 1, chain, params, requireTerminal);\n chain.length -= groupChain.length;\n if (prev !== undefined) {\n params[child.paramName!] = prev;\n } else {\n delete params[child.paramName!];\n }\n }\n }\n\n // 3. Bare dynamic segments\n const bareDynamic = effective.filter(\n ({ child }) =>\n child.segmentType === 'dynamic' && child.paramName && !child.paramPrefix && !child.paramSuffix\n );\n if (bareDynamic.length > 1 && index + 1 < parts.length) {\n const scores = new Map<SegmentNode<TFile>, number[]>();\n for (const { child } of bareDynamic) {\n scores.set(child, subtreeMatchScore(child, parts, index + 1, requireTerminal));\n }\n bareDynamic.sort((a, b) => compareTierArrays(scores.get(a.child)!, scores.get(b.child)!));\n }\n for (const { child, groupChain } of bareDynamic) {\n chain.push(...groupChain);\n const prev = params[child.paramName!];\n params[child.paramName!] = part;\n yield* matchNodeAll(child, parts, index + 1, chain, params, requireTerminal);\n chain.length -= groupChain.length;\n if (prev !== undefined) {\n params[child.paramName!] = prev;\n } else {\n delete params[child.paramName!];\n }\n }\n\n // 4. Catch-all segments\n for (const { child, groupChain } of effective) {\n if (\n child.segmentType === 'catch-all' &&\n child.paramName &&\n (!requireTerminal || isTerminal(child))\n ) {\n chain.push(...groupChain, child);\n const prev = params[child.paramName];\n params[child.paramName] = parts.slice(index);\n yield { chain: [...chain], params: { ...params } };\n chain.length -= groupChain.length + 1;\n if (prev !== undefined) {\n params[child.paramName] = prev;\n } else {\n delete params[child.paramName];\n }\n }\n }\n\n // 5. Optional catch-all segments\n for (const { child, groupChain } of effective) {\n if (\n child.segmentType === 'optional-catch-all' &&\n child.paramName &&\n (!requireTerminal || isTerminal(child))\n ) {\n chain.push(...groupChain, child);\n const prev = params[child.paramName];\n params[child.paramName] = parts.slice(index);\n yield { chain: [...chain], params: { ...params } };\n chain.length -= groupChain.length + 1;\n if (prev !== undefined) {\n params[child.paramName] = prev;\n } else {\n delete params[child.paramName];\n }\n }\n }\n\n chain.pop();\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { n as classifyUrlSegment } from "./segment-classify-C539Pa2O.js";
2
2
  import { t as readFileCached } from "./file-cache-Dw6BJPG7.js";
3
- import { n as findSchemaFile, r as parseExistingSchemaKeys } from "./cli-schema-sync-D3iIZjku.js";
3
+ import { n as findSchemaFile, r as parseExistingSchemaKeys } from "./cli-schema-sync--LZKs8Zn.js";
4
4
  //#region src/routing/schema-validation.ts
5
5
  /**
6
6
  * Schema key/value validation against the filesystem route tree.
@@ -141,4 +141,4 @@ function walk(node, chain, result, includeSlots) {
141
141
  //#endregion
142
142
  export { collectDynamicSegmentsFromTree as n, validateSchemaAgainstRoutes as r, collectLeafRoutes as t };
143
143
 
144
- //# sourceMappingURL=walkers-BNsswm-h.js.map
144
+ //# sourceMappingURL=walkers-CN2N_bA-.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"walkers-BNsswm-h.js","names":[],"sources":["../../src/routing/schema-validation.ts","../../src/routing/walkers.ts"],"sourcesContent":["/**\n * Schema key/value validation against the filesystem route tree.\n *\n * Runs at codegen time (build + dev) to catch mismatches between\n * app/schema.ts keys and the actual dynamic segments on disk.\n *\n * Design doc: design/41-global-params.md\n */\n\nimport { readFileCached } from './file-cache.ts';\nimport type { RouteTree, SegmentNode } from './types.ts';\nimport { findSchemaFile, parseExistingSchemaKeys } from '../cli-schema-sync.ts';\nimport { classifyUrlSegment } from './segment-classify.ts';\n\nexport interface SchemaWarning {\n type: 'stale-key' | 'missing-key' | 'bracket-mismatch';\n key: string;\n message: string;\n filesystemKey?: string;\n}\n\n/**\n * Collect all unique bracket-keyed dynamic segment names from a route tree.\n * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.\n */\nexport function collectDynamicSegmentsFromTree(tree: RouteTree): Set<string> {\n const segments = new Set<string>();\n walkTree(tree.root, segments);\n return segments;\n}\n\nfunction walkTree(node: SegmentNode, segments: Set<string>): void {\n if (node.paramName && node.segmentName) {\n const classified = classifyUrlSegment(node.segmentName);\n if (classified.kind !== 'static') {\n // Use bare bracket form for schema keys — affixed segments like\n // `img-[id].png` register as `[id]`, matching runtime codec lookup.\n if (classified.kind === 'dynamic' && (classified.prefix || classified.suffix)) {\n segments.add(`[${classified.name}]`);\n } else {\n segments.add(node.segmentName);\n }\n }\n }\n for (const child of node.children) {\n walkTree(child, segments);\n }\n for (const slot of Object.values(node.slots)) {\n walkTree(slot, segments);\n }\n}\n\n/**\n * Extract the param name from a bracket key.\n * '[id]' → 'id', '[...slug]' → 'slug', '[[...topic]]' → 'topic'\n */\nfunction extractParamName(bracketKey: string): string | null {\n const seg = classifyUrlSegment(bracketKey);\n if (seg.kind === 'static') return null;\n return seg.name;\n}\n\n/**\n * Validate schema keys against the filesystem route tree.\n *\n * Returns warnings for:\n * - Stale keys: schema keys that don't match any filesystem segment\n * - Bracket form mismatches: same param name, different bracket form\n * - Missing keys: filesystem segments not registered in schema (informational)\n */\nexport function validateSchemaAgainstRoutes(tree: RouteTree, appDir: string): SchemaWarning[] {\n const schemaPath = findSchemaFile(appDir);\n if (!schemaPath) return [];\n\n const content = readFileCached(schemaPath);\n const schemaKeys = parseExistingSchemaKeys(content);\n\n if (schemaKeys.length === 0) return [];\n\n const filesystemSegments = collectDynamicSegmentsFromTree(tree);\n\n return validateKeys(schemaKeys, filesystemSegments);\n}\n\n/**\n * Core validation: compare schema keys against filesystem segments.\n * Exported separately for testing without filesystem dependencies.\n */\nexport function validateKeys(\n schemaKeys: string[],\n filesystemSegments: Set<string>\n): SchemaWarning[] {\n const warnings: SchemaWarning[] = [];\n\n // Build a map of param name → bracket form for filesystem segments\n const fsParamToKey = new Map<string, string>();\n for (const fsKey of filesystemSegments) {\n const name = extractParamName(fsKey);\n if (name) {\n fsParamToKey.set(name, fsKey);\n }\n }\n\n // Build a set of schema param names for the missing-key check\n const schemaParamNames = new Set<string>();\n\n for (const schemaKey of schemaKeys) {\n const schemaParamName = extractParamName(schemaKey);\n if (!schemaParamName) continue;\n\n schemaParamNames.add(schemaParamName);\n\n if (filesystemSegments.has(schemaKey)) {\n // Exact match — no issue\n continue;\n }\n\n // Check if the param name exists but with a different bracket form\n const fsKey = fsParamToKey.get(schemaParamName);\n if (fsKey) {\n warnings.push({\n type: 'bracket-mismatch',\n key: schemaKey,\n filesystemKey: fsKey,\n message:\n `Schema key '${schemaKey}' does not match filesystem bracket form '${fsKey}'. ` +\n `Update the schema key to '${fsKey}'.`,\n });\n } else {\n warnings.push({\n type: 'stale-key',\n key: schemaKey,\n message:\n `Schema key '${schemaKey}' does not match any dynamic segment in the filesystem. ` +\n `Remove it from app/schema.ts or create the corresponding route segment.`,\n });\n }\n }\n\n // Check for filesystem segments not in schema (informational)\n for (const fsKey of filesystemSegments) {\n const name = extractParamName(fsKey);\n if (name && !schemaParamNames.has(name)) {\n warnings.push({\n type: 'missing-key',\n key: fsKey,\n message:\n `Dynamic segment '${fsKey}' has no codec in app/schema.ts (defaults to string). ` +\n `Run: timber schema sync`,\n });\n }\n }\n\n return warnings;\n}\n","/**\n * Shared route-tree walkers (TIM-848).\n *\n * Tiny helpers that walk a `SegmentNode<TFile>` tree generically. Both the\n * build-time tree (`SegmentNode<RouteFile>`) and the runtime manifest\n * tree (`SegmentNode<ManifestFile>`) flow through these helpers because\n * the walker only reads the structural fields shared by both shapes\n * (`children`, `slots`, `page`, `route`, `urlPath`).\n *\n * Before this module, three near-identical `collectRoutes` functions\n * lived in `plugins/dev-404-page.ts`, `plugins/build-report.ts`, and\n * `routing/codegen.ts`. The codegen one is special-purpose (it\n * accumulates `ParamEntry[]` and resolves codec chains) and stays\n * local; the other two now share `collectLeafRoutes` from this file.\n */\n\nimport type { SegmentNode } from './types.ts';\n\n/** A leaf route discovered while walking the segment tree. */\nexport interface LeafRoute<TFile> {\n /** URL path of the leaf (root is \"/\"). */\n urlPath: string;\n /** Segment chain from root to this leaf, inclusive. */\n segments: SegmentNode<TFile>[];\n /** The page file at this leaf, if any. */\n page?: TFile;\n /** The route handler file at this leaf, if any. */\n route?: TFile;\n}\n\n/** Options for `collectLeafRoutes`. */\nexport interface CollectLeafRoutesOptions {\n /**\n * If true, recurse into parallel slots and emit slot leaves alongside\n * the main route tree. Defaults to `false` because slots render\n * alongside their parent at the same URL and are not separately\n * URL-addressable. The build report excludes slots; route-listing\n * UIs that want to show \"all leaves with a page handler\" can opt in.\n */\n includeSlots?: boolean;\n}\n\n/**\n * Walk a segment tree and collect every leaf with a `page` or `route`\n * handler. Generic over `TFile` so it works on both the build-time\n * scanner output and the runtime manifest tree.\n *\n * - Pages and route handlers at the same URL produce two distinct\n * entries (the build report deduplicates by URL afterward).\n * - Parallel slots are skipped unless `includeSlots: true` (slots\n * share their parent's URL and are not addressable on their own).\n * - Intercepting subtrees are skipped unconditionally. They render only\n * on soft navigation and add no URL depth, so their computed `urlPath`s\n * are not addresses — listing them would advertise routes (`/[year]`)\n * that no request can reach. This matches every other consumer of the\n * tree that produces URLs: the sitemap generator, the static generator,\n * prerendering and the prebuilt-payload builder all skip them too.\n * - Result is sorted by `urlPath` for deterministic output.\n */\nexport function collectLeafRoutes<TFile>(\n root: SegmentNode<TFile>,\n options: CollectLeafRoutesOptions = {}\n): LeafRoute<TFile>[] {\n const { includeSlots = false } = options;\n const result: LeafRoute<TFile>[] = [];\n walk(root, [], result, includeSlots);\n result.sort((a, b) => a.urlPath.localeCompare(b.urlPath));\n return result;\n}\n\nfunction walk<TFile>(\n node: SegmentNode<TFile>,\n chain: SegmentNode<TFile>[],\n result: LeafRoute<TFile>[],\n includeSlots: boolean\n): void {\n if (node.segmentType === 'intercepting') return;\n\n const currentChain = [...chain, node];\n const path = node.urlPath || '/';\n\n if (node.page) {\n result.push({ urlPath: path, segments: currentChain, page: node.page });\n }\n if (node.route) {\n result.push({ urlPath: path, segments: currentChain, route: node.route });\n }\n\n for (const child of node.children) {\n walk(child, currentChain, result, includeSlots);\n }\n\n if (includeSlots) {\n for (const slotNode of Object.values(node.slots)) {\n walk(slotNode, currentChain, result, includeSlots);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"walkers-CN2N_bA-.js","names":[],"sources":["../../src/routing/schema-validation.ts","../../src/routing/walkers.ts"],"sourcesContent":["/**\n * Schema key/value validation against the filesystem route tree.\n *\n * Runs at codegen time (build + dev) to catch mismatches between\n * app/schema.ts keys and the actual dynamic segments on disk.\n *\n * Design doc: design/41-global-params.md\n */\n\nimport { readFileCached } from './file-cache.ts';\nimport type { RouteTree, SegmentNode } from './types.ts';\nimport { findSchemaFile, parseExistingSchemaKeys } from '../cli-schema-sync.ts';\nimport { classifyUrlSegment } from './segment-classify.ts';\n\nexport interface SchemaWarning {\n type: 'stale-key' | 'missing-key' | 'bracket-mismatch';\n key: string;\n message: string;\n filesystemKey?: string;\n}\n\n/**\n * Collect all unique bracket-keyed dynamic segment names from a route tree.\n * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.\n */\nexport function collectDynamicSegmentsFromTree(tree: RouteTree): Set<string> {\n const segments = new Set<string>();\n walkTree(tree.root, segments);\n return segments;\n}\n\nfunction walkTree(node: SegmentNode, segments: Set<string>): void {\n if (node.paramName && node.segmentName) {\n const classified = classifyUrlSegment(node.segmentName);\n if (classified.kind !== 'static') {\n // Use bare bracket form for schema keys — affixed segments like\n // `img-[id].png` register as `[id]`, matching runtime codec lookup.\n if (classified.kind === 'dynamic' && (classified.prefix || classified.suffix)) {\n segments.add(`[${classified.name}]`);\n } else {\n segments.add(node.segmentName);\n }\n }\n }\n for (const child of node.children) {\n walkTree(child, segments);\n }\n for (const slot of Object.values(node.slots)) {\n walkTree(slot, segments);\n }\n}\n\n/**\n * Extract the param name from a bracket key.\n * '[id]' → 'id', '[...slug]' → 'slug', '[[...topic]]' → 'topic'\n */\nfunction extractParamName(bracketKey: string): string | null {\n const seg = classifyUrlSegment(bracketKey);\n if (seg.kind === 'static') return null;\n return seg.name;\n}\n\n/**\n * Validate schema keys against the filesystem route tree.\n *\n * Returns warnings for:\n * - Stale keys: schema keys that don't match any filesystem segment\n * - Bracket form mismatches: same param name, different bracket form\n * - Missing keys: filesystem segments not registered in schema (informational)\n */\nexport function validateSchemaAgainstRoutes(tree: RouteTree, appDir: string): SchemaWarning[] {\n const schemaPath = findSchemaFile(appDir);\n if (!schemaPath) return [];\n\n const content = readFileCached(schemaPath);\n const schemaKeys = parseExistingSchemaKeys(content);\n\n if (schemaKeys.length === 0) return [];\n\n const filesystemSegments = collectDynamicSegmentsFromTree(tree);\n\n return validateKeys(schemaKeys, filesystemSegments);\n}\n\n/**\n * Core validation: compare schema keys against filesystem segments.\n * Exported separately for testing without filesystem dependencies.\n */\nexport function validateKeys(\n schemaKeys: string[],\n filesystemSegments: Set<string>\n): SchemaWarning[] {\n const warnings: SchemaWarning[] = [];\n\n // Build a map of param name → bracket form for filesystem segments\n const fsParamToKey = new Map<string, string>();\n for (const fsKey of filesystemSegments) {\n const name = extractParamName(fsKey);\n if (name) {\n fsParamToKey.set(name, fsKey);\n }\n }\n\n // Build a set of schema param names for the missing-key check\n const schemaParamNames = new Set<string>();\n\n for (const schemaKey of schemaKeys) {\n const schemaParamName = extractParamName(schemaKey);\n if (!schemaParamName) continue;\n\n schemaParamNames.add(schemaParamName);\n\n if (filesystemSegments.has(schemaKey)) {\n // Exact match — no issue\n continue;\n }\n\n // Check if the param name exists but with a different bracket form\n const fsKey = fsParamToKey.get(schemaParamName);\n if (fsKey) {\n warnings.push({\n type: 'bracket-mismatch',\n key: schemaKey,\n filesystemKey: fsKey,\n message:\n `Schema key '${schemaKey}' does not match filesystem bracket form '${fsKey}'. ` +\n `Update the schema key to '${fsKey}'.`,\n });\n } else {\n warnings.push({\n type: 'stale-key',\n key: schemaKey,\n message:\n `Schema key '${schemaKey}' does not match any dynamic segment in the filesystem. ` +\n `Remove it from app/schema.ts or create the corresponding route segment.`,\n });\n }\n }\n\n // Check for filesystem segments not in schema (informational)\n for (const fsKey of filesystemSegments) {\n const name = extractParamName(fsKey);\n if (name && !schemaParamNames.has(name)) {\n warnings.push({\n type: 'missing-key',\n key: fsKey,\n message:\n `Dynamic segment '${fsKey}' has no codec in app/schema.ts (defaults to string). ` +\n `Run: timber schema sync`,\n });\n }\n }\n\n return warnings;\n}\n","/**\n * Shared route-tree walkers (TIM-848).\n *\n * Tiny helpers that walk a `SegmentNode<TFile>` tree generically. Both the\n * build-time tree (`SegmentNode<RouteFile>`) and the runtime manifest\n * tree (`SegmentNode<ManifestFile>`) flow through these helpers because\n * the walker only reads the structural fields shared by both shapes\n * (`children`, `slots`, `page`, `route`, `urlPath`).\n *\n * Before this module, three near-identical `collectRoutes` functions\n * lived in `plugins/dev-404-page.ts`, `plugins/build-report.ts`, and\n * `routing/codegen.ts`. The codegen one is special-purpose (it\n * accumulates `ParamEntry[]` and resolves codec chains) and stays\n * local; the other two now share `collectLeafRoutes` from this file.\n */\n\nimport type { SegmentNode } from './types.ts';\n\n/** A leaf route discovered while walking the segment tree. */\nexport interface LeafRoute<TFile> {\n /** URL path of the leaf (root is \"/\"). */\n urlPath: string;\n /** Segment chain from root to this leaf, inclusive. */\n segments: SegmentNode<TFile>[];\n /** The page file at this leaf, if any. */\n page?: TFile;\n /** The route handler file at this leaf, if any. */\n route?: TFile;\n}\n\n/** Options for `collectLeafRoutes`. */\nexport interface CollectLeafRoutesOptions {\n /**\n * If true, recurse into parallel slots and emit slot leaves alongside\n * the main route tree. Defaults to `false` because slots render\n * alongside their parent at the same URL and are not separately\n * URL-addressable. The build report excludes slots; route-listing\n * UIs that want to show \"all leaves with a page handler\" can opt in.\n */\n includeSlots?: boolean;\n}\n\n/**\n * Walk a segment tree and collect every leaf with a `page` or `route`\n * handler. Generic over `TFile` so it works on both the build-time\n * scanner output and the runtime manifest tree.\n *\n * - Pages and route handlers at the same URL produce two distinct\n * entries (the build report deduplicates by URL afterward).\n * - Parallel slots are skipped unless `includeSlots: true` (slots\n * share their parent's URL and are not addressable on their own).\n * - Intercepting subtrees are skipped unconditionally. They render only\n * on soft navigation and add no URL depth, so their computed `urlPath`s\n * are not addresses — listing them would advertise routes (`/[year]`)\n * that no request can reach. This matches every other consumer of the\n * tree that produces URLs: the sitemap generator, the static generator,\n * prerendering and the prebuilt-payload builder all skip them too.\n * - Result is sorted by `urlPath` for deterministic output.\n */\nexport function collectLeafRoutes<TFile>(\n root: SegmentNode<TFile>,\n options: CollectLeafRoutesOptions = {}\n): LeafRoute<TFile>[] {\n const { includeSlots = false } = options;\n const result: LeafRoute<TFile>[] = [];\n walk(root, [], result, includeSlots);\n result.sort((a, b) => a.urlPath.localeCompare(b.urlPath));\n return result;\n}\n\nfunction walk<TFile>(\n node: SegmentNode<TFile>,\n chain: SegmentNode<TFile>[],\n result: LeafRoute<TFile>[],\n includeSlots: boolean\n): void {\n if (node.segmentType === 'intercepting') return;\n\n const currentChain = [...chain, node];\n const path = node.urlPath || '/';\n\n if (node.page) {\n result.push({ urlPath: path, segments: currentChain, page: node.page });\n }\n if (node.route) {\n result.push({ urlPath: path, segments: currentChain, route: node.route });\n }\n\n for (const child of node.children) {\n walk(child, currentChain, result, includeSlots);\n }\n\n if (includeSlots) {\n for (const slotNode of Object.values(node.slots)) {\n walk(slotNode, currentChain, result, includeSlots);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,SAAgB,+BAA+B,MAA8B;CAC3E,MAAM,2BAAW,IAAI,IAAY;CACjC,SAAS,KAAK,MAAM,QAAQ;CAC5B,OAAO;AACT;AAEA,SAAS,SAAS,MAAmB,UAA6B;CAChE,IAAI,KAAK,aAAa,KAAK,aAAa;EACtC,MAAM,aAAa,mBAAmB,KAAK,WAAW;EACtD,IAAI,WAAW,SAAS,UAAU;GAGhC,IAAI,WAAW,SAAS,cAAc,WAAW,UAAU,WAAW,SACpE,SAAS,IAAI,IAAI,WAAW,KAAK,EAAE;QAEnC,SAAS,IAAI,KAAK,WAAW;EAEjC;CACF;CACA,KAAK,MAAM,SAAS,KAAK,UACvB,SAAS,OAAO,QAAQ;CAE1B,KAAK,MAAM,QAAQ,OAAO,OAAO,KAAK,KAAK,GACzC,SAAS,MAAM,QAAQ;AAE3B;;;;;AAMA,SAAS,iBAAiB,YAAmC;CAC3D,MAAM,MAAM,mBAAmB,UAAU;CACzC,IAAI,IAAI,SAAS,UAAU,OAAO;CAClC,OAAO,IAAI;AACb;;;;;;;;;AAUA,SAAgB,4BAA4B,MAAiB,QAAiC;CAC5F,MAAM,aAAa,eAAe,MAAM;CACxC,IAAI,CAAC,YAAY,OAAO,CAAC;CAEzB,MAAM,UAAU,eAAe,UAAU;CACzC,MAAM,aAAa,wBAAwB,OAAO;CAElD,IAAI,WAAW,WAAW,GAAG,OAAO,CAAC;CAIrC,OAAO,aAAa,YAFO,+BAA+B,IAE1B,CAAkB;AACpD;;;;;AAMA,SAAgB,aACd,YACA,oBACiB;CACjB,MAAM,WAA4B,CAAC;CAGnC,MAAM,+BAAe,IAAI,IAAoB;CAC7C,KAAK,MAAM,SAAS,oBAAoB;EACtC,MAAM,OAAO,iBAAiB,KAAK;EACnC,IAAI,MACF,aAAa,IAAI,MAAM,KAAK;CAEhC;CAGA,MAAM,mCAAmB,IAAI,IAAY;CAEzC,KAAK,MAAM,aAAa,YAAY;EAClC,MAAM,kBAAkB,iBAAiB,SAAS;EAClD,IAAI,CAAC,iBAAiB;EAEtB,iBAAiB,IAAI,eAAe;EAEpC,IAAI,mBAAmB,IAAI,SAAS,GAElC;EAIF,MAAM,QAAQ,aAAa,IAAI,eAAe;EAC9C,IAAI,OACF,SAAS,KAAK;GACZ,MAAM;GACN,KAAK;GACL,eAAe;GACf,SACE,eAAe,UAAU,4CAA4C,MAAM,+BAC9C,MAAM;EACvC,CAAC;OAED,SAAS,KAAK;GACZ,MAAM;GACN,KAAK;GACL,SACE,eAAe,UAAU;EAE7B,CAAC;CAEL;CAGA,KAAK,MAAM,SAAS,oBAAoB;EACtC,MAAM,OAAO,iBAAiB,KAAK;EACnC,IAAI,QAAQ,CAAC,iBAAiB,IAAI,IAAI,GACpC,SAAS,KAAK;GACZ,MAAM;GACN,KAAK;GACL,SACE,oBAAoB,MAAM;EAE9B,CAAC;CAEL;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;AC/FA,SAAgB,kBACd,MACA,UAAoC,CAAC,GACjB;CACpB,MAAM,EAAE,eAAe,UAAU;CACjC,MAAM,SAA6B,CAAC;CACpC,KAAK,MAAM,CAAC,GAAG,QAAQ,YAAY;CACnC,OAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,OAAO,CAAC;CACxD,OAAO;AACT;AAEA,SAAS,KACP,MACA,OACA,QACA,cACM;CACN,IAAI,KAAK,gBAAgB,gBAAgB;CAEzC,MAAM,eAAe,CAAC,GAAG,OAAO,IAAI;CACpC,MAAM,OAAO,KAAK,WAAW;CAE7B,IAAI,KAAK,MACP,OAAO,KAAK;EAAE,SAAS;EAAM,UAAU;EAAc,MAAM,KAAK;CAAK,CAAC;CAExE,IAAI,KAAK,OACP,OAAO,KAAK;EAAE,SAAS;EAAM,UAAU;EAAc,OAAO,KAAK;CAAM,CAAC;CAG1E,KAAK,MAAM,SAAS,KAAK,UACvB,KAAK,OAAO,cAAc,QAAQ,YAAY;CAGhD,IAAI,cACF,KAAK,MAAM,YAAY,OAAO,OAAO,KAAK,KAAK,GAC7C,KAAK,UAAU,cAAc,QAAQ,YAAY;AAGvD"}
@@ -1,7 +1,7 @@
1
1
  import { i as scanProjectSourceFiles, n as isScannableSourceFile, t as buildSourceExtensions } from "../_chunks/scan-BoP17W3w.js";
2
2
  import { n as isTimberPluginApi, o as resolveAppDir } from "../_chunks/plugin-context-8idfR3UZ.js";
3
- import { a as detectDirective, i as classifyModules, n as stitchImportChains } from "../_chunks/chains-BjGKq9Je.js";
4
- import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "../_chunks/poison-scan-g83aAXhs.js";
3
+ import { a as detectDirective, i as classifyModules, n as stitchImportChains } from "../_chunks/chains-B4bgOl2g.js";
4
+ import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "../_chunks/poison-scan-DG5eI2_z.js";
5
5
  import { normalizePath } from "vite";
6
6
  import { join, resolve } from "node:path";
7
7
  import { readFile, realpath, writeFile } from "node:fs/promises";
@@ -378,7 +378,7 @@ async function printSingleFile(input, target, module, answer, json) {
378
378
  console.log(` environments: ${envs.length > 0 ? envs.join(", ") : "none"}`);
379
379
  if (module.subtreeUnknown) console.log(" note: reachable, but its subtree is unknown (never transformed)");
380
380
  if (module.chains.length > 0) {
381
- const { formatChains } = await import("../_chunks/chains-BjGKq9Je.js").then((n) => n.t);
381
+ const { formatChains } = await import("../_chunks/chains-B4bgOl2g.js").then((n) => n.t);
382
382
  console.log(" import chains:");
383
383
  console.log(formatChains(module.chains, root));
384
384
  }
@@ -397,7 +397,7 @@ async function printDevServerText(data, configDir) {
397
397
  console.log(` environments: ${envList.length > 0 ? envList.join(", ") : "none"}`);
398
398
  if (data.subtreeUnknown) console.log(" note: reachable, but its subtree is unknown (never transformed)");
399
399
  if (data.chains.length > 0) {
400
- const { formatChains } = await import("../_chunks/chains-BjGKq9Je.js").then((n) => n.t);
400
+ const { formatChains } = await import("../_chunks/chains-B4bgOl2g.js").then((n) => n.t);
401
401
  console.log(" import chains:");
402
402
  console.log(formatChains(data.chains, configDir));
403
403
  }
package/dist/cli.js CHANGED
@@ -90,7 +90,7 @@ async function runDev(options, _deps) {
90
90
  return;
91
91
  }
92
92
  const { fork } = await import("node:child_process");
93
- const { RESTART_EXIT_CODE } = await import("./_chunks/dev-server-Dbdgx2dD.js").then((n) => n.t);
93
+ const { RESTART_EXIT_CODE } = await import("./_chunks/dev-server-BzBxgYAV.js").then((n) => n.t);
94
94
  while (true) {
95
95
  const result = await new Promise((resolve) => {
96
96
  const child = fork(process.argv[1], process.argv.slice(2), {
@@ -173,7 +173,7 @@ async function runPreview(options, _deps) {
173
173
  * `--project` names the tsconfig handed to tsgo.
174
174
  */
175
175
  async function runCheck(options, _deps) {
176
- const { checkRoutes, runTypeCheck } = await import("./_chunks/cli-check-3WE_GyoL.js");
176
+ const { checkRoutes, runTypeCheck } = await import("./_chunks/cli-check-G8o30ldg.js");
177
177
  const { root } = await checkRoutes({ viteConfigFile: options.config }, _deps);
178
178
  await runTypeCheck({
179
179
  root,
@@ -189,7 +189,7 @@ async function runCheck(options, _deps) {
189
189
  async function runSchema(args) {
190
190
  const subcommand = args[0];
191
191
  if (subcommand !== "sync") throw new Error(`Unknown schema subcommand: ${subcommand ?? "(none)"}. Usage: timber schema sync`);
192
- const { runSchemaSync, defaultCodecForSegment } = await import("./_chunks/cli-schema-sync-D3iIZjku.js").then((n) => n.t);
192
+ const { runSchemaSync, defaultCodecForSegment } = await import("./_chunks/cli-schema-sync--LZKs8Zn.js").then((n) => n.t);
193
193
  const result = runSchemaSync(process.cwd());
194
194
  if (result.filesystemSegments.length === 0) {
195
195
  console.log("[timber] No dynamic segments found in app/.");
@@ -1 +1 @@
1
- {"version":3,"file":"action-dispatch.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/action-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAwBH,wBAAgB,kBAAkB,IAAI,IAAI,CAwKzC"}
1
+ {"version":3,"file":"action-dispatch.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/action-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAwBH,wBAAgB,kBAAkB,IAAI,IAAI,CA8KzC"}
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { a as formatMissingPeers, i as formatConfigErrors, n as timberDevServer, o as validateConfig, r as checkPeerDependencies } from "./_chunks/dev-server-Dbdgx2dD.js";
1
+ import { a as formatMissingPeers, i as formatConfigErrors, n as timberDevServer, o as validateConfig, r as checkPeerDependencies } from "./_chunks/dev-server-BzBxgYAV.js";
2
2
  import { a as isDynamicMetadataExtension } from "./_chunks/metadata-routes-DSDjM_hJ.js";
3
- import { n as scanRoutes, r as collectInterceptionRewrites } from "./_chunks/scanner-BhNruzv0.js";
3
+ import { n as scanRoutes, r as collectInterceptionRewrites } from "./_chunks/scanner-BQ5sPIJ7.js";
4
4
  import { h as swallow } from "./_chunks/logger-k2DQ4EUf.js";
5
- import { c as writeRouteCodegen, i as getPrerenderExport, l as buildDirToRouteMap, n as formatConventionWarnings, r as lintConventions, s as flushCodegenWrites, t as checkAppDirExists } from "./_chunks/convention-lint-BnO5TyHD.js";
5
+ import { c as writeRouteCodegen, i as getPrerenderExport, l as buildDirToRouteMap, n as formatConventionWarnings, r as lintConventions, s as flushCodegenWrites, t as checkAppDirExists } from "./_chunks/convention-lint-5KzoMg_A.js";
6
6
  import { c as resolveClientJavascript, i as mergeFileConfig, l as createNoopTimer, o as resolveAppDir, r as loadTimberConfigFile, s as resolveBuildDir, t as createPluginContext } from "./_chunks/plugin-context-8idfR3UZ.js";
7
- import { r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-BNsswm-h.js";
7
+ import { r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-CN2N_bA-.js";
8
8
  import { i as poisonPillSource, r as markerFor, t as EXPLICIT_MARKERS } from "./_chunks/poison-rules-DoEhbqaY.js";
9
9
  import { i as withBasePath, n as normalizeBasePath, t as isRelativeBase } from "./_chunks/base-path-DaQrzbez.js";
10
10
  import { t as EMPTY_BUILD_MANIFEST } from "./_chunks/build-manifest-DTmSGLRz.js";
@@ -662,7 +662,7 @@ var SEGMENT_PREFIX = "\0$segment:";
662
662
  /**
663
663
  * File convention names we track for changes that require manifest regeneration.
664
664
  */
665
- var ROUTE_FILE_PATTERNS = /\/(page|layout|middleware|access|route|error|global-error|default|denied|schema|\d{3}|[45]xx|sitemap|robots|manifest|favicon|icon|opengraph-image|apple-icon)\./;
665
+ var ROUTE_FILE_PATTERNS = /\/(page|layout|middleware|access|route|error|global-error|default|denied|schema|proxy|\d{3}|[45]xx|sitemap|robots|manifest|favicon|icon|opengraph-image|apple-icon)\./;
666
666
  /**
667
667
  * Create the timber-routing Vite plugin.
668
668
  *