@timber-js/app 0.2.0-alpha.200 → 0.2.0-alpha.202

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 (143) hide show
  1. package/bin/timber.mjs +15 -1
  2. package/dist/_chunks/{actions-d1hCqnU3.js → actions-HUdJADAD.js} +3 -3
  3. package/dist/_chunks/{actions-d1hCqnU3.js.map → actions-HUdJADAD.js.map} +1 -1
  4. package/dist/_chunks/{build-manifest-DTmSGLRz.js → build-manifest-DWppEdLB.js} +2 -51
  5. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  6. package/dist/_chunks/{cache-api-ByagcC-J.js → cache-api-CAPbZTga.js} +2 -2
  7. package/dist/_chunks/{cache-api-ByagcC-J.js.map → cache-api-CAPbZTga.js.map} +1 -1
  8. package/dist/_chunks/{chains-Bpb0W4ax.js → chains-CBNA0Ozj.js} +3 -3
  9. package/dist/_chunks/{chains-Bpb0W4ax.js.map → chains-CBNA0Ozj.js.map} +1 -1
  10. package/dist/_chunks/{cli-check-D6VolrDV.js → cli-check-BzMGuIH6.js} +3 -3
  11. package/dist/_chunks/{cli-check-D6VolrDV.js.map → cli-check-BzMGuIH6.js.map} +1 -1
  12. package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js → cli-schema-sync-DnXqcIIj.js} +2 -2
  13. package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js.map → cli-schema-sync-DnXqcIIj.js.map} +1 -1
  14. package/dist/_chunks/{cloudflare-BFb__LYG.js → cloudflare-DxX1SU0g.js} +3 -3
  15. package/dist/_chunks/{cloudflare-BFb__LYG.js.map → cloudflare-DxX1SU0g.js.map} +1 -1
  16. package/dist/_chunks/{convention-lint-fRkwVwEH.js → convention-lint-DHOFvX5s.js} +5 -95
  17. package/dist/_chunks/convention-lint-DHOFvX5s.js.map +1 -0
  18. package/dist/_chunks/csp-nonce-hOGniaG4.js +227 -0
  19. package/dist/_chunks/csp-nonce-hOGniaG4.js.map +1 -0
  20. package/dist/_chunks/dev-server-DioP7tkQ.js +2288 -0
  21. package/dist/_chunks/dev-server-DioP7tkQ.js.map +1 -0
  22. package/dist/_chunks/{error-boundary-BfPHZjm0.js → error-boundary-BQKxl6EX.js} +11 -17
  23. package/dist/_chunks/{error-boundary-BfPHZjm0.js.map → error-boundary-BQKxl6EX.js.map} +1 -1
  24. package/dist/_chunks/{graph-cache-CP4GEmf9.js → graph-cache-Cv3njEH8.js} +2 -2
  25. package/dist/_chunks/{graph-cache-CP4GEmf9.js.map → graph-cache-Cv3njEH8.js.map} +1 -1
  26. package/dist/_chunks/{live-graph-D_2D32Ad.js → live-graph-VjHFF5EV.js} +3 -3
  27. package/dist/_chunks/{live-graph-D_2D32Ad.js.map → live-graph-VjHFF5EV.js.map} +1 -1
  28. package/dist/_chunks/{logger-uLBuGKDI.js → logger-DqJ2VoAY.js} +450 -458
  29. package/dist/_chunks/logger-DqJ2VoAY.js.map +1 -0
  30. package/dist/_chunks/{segment-keys-lqtdookO.js → metadata-routes-DSDjM_hJ.js} +2 -61
  31. package/dist/_chunks/metadata-routes-DSDjM_hJ.js.map +1 -0
  32. package/dist/_chunks/{poison-scan-Bm9Yyqk9.js → poison-scan-lEbz4pQE.js} +2 -2
  33. package/dist/_chunks/{poison-scan-Bm9Yyqk9.js.map → poison-scan-lEbz4pQE.js.map} +1 -1
  34. package/dist/_chunks/{scanner-AiazgH_f.js → scanner-B_tnqFcF.js} +37 -137
  35. package/dist/_chunks/scanner-B_tnqFcF.js.map +1 -0
  36. package/dist/_chunks/segment-keys-D5hu1hz4.js +62 -0
  37. package/dist/_chunks/segment-keys-D5hu1hz4.js.map +1 -0
  38. package/dist/_chunks/tree-match-CdbvYTBz.js +122 -0
  39. package/dist/_chunks/tree-match-CdbvYTBz.js.map +1 -0
  40. package/dist/_chunks/{walkers-B6XUtmqK.js → walkers-Cm3PC5JT.js} +2 -2
  41. package/dist/_chunks/{walkers-B6XUtmqK.js.map → walkers-Cm3PC5JT.js.map} +1 -1
  42. package/dist/adapters/cloudflare-dev.js +1 -1
  43. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  44. package/dist/adapters/cloudflare.js +1 -1
  45. package/dist/analyze/crawl-entry.js +2 -2
  46. package/dist/analyze/graph-command.js +3 -3
  47. package/dist/cache/index.js +1 -1
  48. package/dist/cli.d.ts +7 -2
  49. package/dist/cli.d.ts.map +1 -1
  50. package/dist/cli.js +44 -7
  51. package/dist/cli.js.map +1 -1
  52. package/dist/client/browser-dev.d.ts +5 -1
  53. package/dist/client/browser-dev.d.ts.map +1 -1
  54. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  55. package/dist/client/browser-entry/action-queue.d.ts +20 -1
  56. package/dist/client/browser-entry/action-queue.d.ts.map +1 -1
  57. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  58. package/dist/client/error-boundary.d.ts +3 -15
  59. package/dist/client/error-boundary.d.ts.map +1 -1
  60. package/dist/client/error-boundary.js +1 -1
  61. package/dist/client/error-reconstituter.d.ts +4 -4
  62. package/dist/client/error-reconstituter.d.ts.map +1 -1
  63. package/dist/client/internal.js +1 -1
  64. package/dist/dev-tools/debug-channel.d.ts +55 -0
  65. package/dist/dev-tools/debug-channel.d.ts.map +1 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +302 -2245
  68. package/dist/index.js.map +1 -1
  69. package/dist/plugins/dev-server.d.ts +7 -0
  70. package/dist/plugins/dev-server.d.ts.map +1 -1
  71. package/dist/plugins/mdx.d.ts.map +1 -1
  72. package/dist/routing/convention-lint.d.ts.map +1 -1
  73. package/dist/routing/index.js +2 -2
  74. package/dist/routing/scanner.d.ts +4 -4
  75. package/dist/routing/scanner.d.ts.map +1 -1
  76. package/dist/server/access-gate.d.ts +2 -2
  77. package/dist/server/deny-boundary.d.ts +3 -5
  78. package/dist/server/deny-boundary.d.ts.map +1 -1
  79. package/dist/server/deny-renderer.d.ts +0 -1
  80. package/dist/server/deny-renderer.d.ts.map +1 -1
  81. package/dist/server/error-boundary-wrapper.d.ts +5 -12
  82. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  83. package/dist/server/index.js +2 -2
  84. package/dist/server/internal.js +20 -195
  85. package/dist/server/internal.js.map +1 -1
  86. package/dist/server/pipeline.d.ts +8 -0
  87. package/dist/server/pipeline.d.ts.map +1 -1
  88. package/dist/server/primitives.d.ts +15 -13
  89. package/dist/server/primitives.d.ts.map +1 -1
  90. package/dist/server/rsc-entry/error-renderer.d.ts +3 -7
  91. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  92. package/dist/server/rsc-entry/helpers.d.ts +6 -27
  93. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  94. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  95. package/dist/server/rsc-entry/render-route.d.ts +1 -0
  96. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  97. package/dist/server/rsc-entry/rsc-stream.d.ts +4 -1
  98. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  99. package/dist/server/rsc-entry/ssr-renderer.d.ts +1 -0
  100. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  101. package/dist/server/slot-resolver.d.ts.map +1 -1
  102. package/dist/server/slot-subtree-contain.d.ts +1 -1
  103. package/dist/server/slot-subtree-contain.d.ts.map +1 -1
  104. package/docs/api/30-api-server.mdx +5 -5
  105. package/docs/api/34-api-config.mdx +2 -2
  106. package/docs/api/36-cli.mdx +1 -1
  107. package/docs/learn/12-error-handling.mdx +7 -8
  108. package/package.json +2 -3
  109. package/src/cli.ts +62 -8
  110. package/src/client/browser-dev.ts +35 -3
  111. package/src/client/browser-entry/action-dispatch.ts +12 -0
  112. package/src/client/browser-entry/action-queue.ts +70 -8
  113. package/src/client/browser-entry/rsc-stream.ts +78 -22
  114. package/src/client/error-boundary.tsx +13 -39
  115. package/src/client/error-reconstituter.tsx +5 -5
  116. package/src/dev-tools/debug-channel.ts +151 -0
  117. package/src/index.ts +8 -12
  118. package/src/plugins/dev-server.ts +58 -7
  119. package/src/plugins/mdx.ts +2 -1
  120. package/src/routing/convention-lint.ts +4 -111
  121. package/src/routing/scanner.ts +33 -22
  122. package/src/server/access-gate.tsx +3 -3
  123. package/src/server/deny-boundary.ts +5 -23
  124. package/src/server/deny-renderer.ts +4 -7
  125. package/src/server/error-boundary-wrapper.ts +11 -35
  126. package/src/server/pipeline.ts +9 -0
  127. package/src/server/primitives.ts +20 -26
  128. package/src/server/rsc-entry/error-renderer.ts +15 -43
  129. package/src/server/rsc-entry/helpers.ts +10 -67
  130. package/src/server/rsc-entry/index.ts +1 -0
  131. package/src/server/rsc-entry/render-route.ts +12 -1
  132. package/src/server/rsc-entry/rsc-stream.ts +27 -24
  133. package/src/server/rsc-entry/ssr-renderer.ts +7 -1
  134. package/src/server/slot-resolver.ts +29 -7
  135. package/src/server/slot-subtree-contain.ts +13 -4
  136. package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
  137. package/dist/_chunks/convention-lint-fRkwVwEH.js.map +0 -1
  138. package/dist/_chunks/logger-uLBuGKDI.js.map +0 -1
  139. package/dist/_chunks/scanner-AiazgH_f.js.map +0 -1
  140. package/dist/_chunks/segment-keys-lqtdookO.js.map +0 -1
  141. package/dist/server/utils/mdx-file.d.ts +0 -17
  142. package/dist/server/utils/mdx-file.d.ts.map +0 -1
  143. package/src/server/utils/mdx-file.ts +0 -22
@@ -0,0 +1,62 @@
1
+ //#region src/routing/segment-keys.ts
2
+ /**
3
+ * Compute tree paths for a segment chain — the directory path of each segment
4
+ * with slots elided, e.g. `app/(browse)/feed` → `/(browse)/feed`.
5
+ *
6
+ * This is `computeSegmentKeys` with the group branch applied to *every*
7
+ * segment rather than only to groups. That one difference is the whole point:
8
+ * `computeSegmentKeys` resets to `urlPath` at each URL-visible segment, so
9
+ * `(browse)/feed` and `(landing)/feed` both key as `/feed`. Two directories
10
+ * that render different layouts must not share an identity.
11
+ *
12
+ * Used for interception scoping (`routing/interception.ts`), where the
13
+ * question is "does this route pass through *that* directory?" — not "does it
14
+ * render this URL?". Not interchangeable with `computeSegmentKeys`: the state
15
+ * tree is keyed by URL on purpose, because the client caches by URL.
16
+ */
17
+ function computeSegmentTreePaths(segments) {
18
+ const paths = [];
19
+ let prev = "";
20
+ for (const segment of segments) {
21
+ const name = segment.segmentName ?? "";
22
+ if (!name) {
23
+ paths.push("/");
24
+ prev = "/";
25
+ continue;
26
+ }
27
+ const path = `${prev === "/" ? "" : prev}/${name}`;
28
+ paths.push(path);
29
+ prev = path;
30
+ }
31
+ return paths;
32
+ }
33
+ /**
34
+ * The directory names a tree path is built from — `/` is none, `/feed/(a)` is
35
+ * `['feed', '(a)']`.
36
+ *
37
+ * The inverse of the walk above, and it lives beside it so the format is
38
+ * stated once: `findChainByTreePath` (`server/children-interception.ts`) walks
39
+ * these names back down the tree to the directory the path addresses, and
40
+ * `treePathDepth` counts them. Both are readings of the same string, and a
41
+ * reader that disagreed with the producer about what separates two names
42
+ * resolves an interception to the wrong node or to none.
43
+ */
44
+ function treePathNames(treePath) {
45
+ return treePath === "/" ? [] : treePath.slice(1).split("/");
46
+ }
47
+ /**
48
+ * How many directories a tree path names — `/` is 0, `/feed` is 1,
49
+ * `/feed/(a)` is 2.
50
+ *
51
+ * Two tree paths on a single rendered chain are strictly nested, so on that
52
+ * chain depth orders them totally — which is what makes it usable as "the
53
+ * deeper of these two directories" wherever a route passes through both
54
+ * (`routing/interception.ts`).
55
+ */
56
+ function treePathDepth(treePath) {
57
+ return treePathNames(treePath).length;
58
+ }
59
+ //#endregion
60
+ export { treePathDepth as n, computeSegmentTreePaths as t };
61
+
62
+ //# sourceMappingURL=segment-keys-D5hu1hz4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segment-keys-D5hu1hz4.js","names":[],"sources":["../../src/routing/segment-keys.ts"],"sourcesContent":["/**\n * Segment key computation — the stable identity of a segment in the route tree.\n *\n * A segment key is the string that names a segment across the RSC/SSR/client\n * boundary: it appears in the X-Timber-State-Tree header, the X-Timber-Segments\n * header, SegmentOutlet props, the client segment cache, and (since TIM-1279)\n * interception scoping.\n *\n * This module lives in `routing/` rather than `server/` because the keys are a\n * property of the route tree itself — both the build-time scanner tree and the\n * runtime manifest tree produce identical keys for the same directory, which is\n * what lets a build-time value (an interception scope) be compared against a\n * request-time matched chain.\n *\n * See design/19-client-navigation.md §\"X-Timber-State-Tree Header\"\n */\n\n/**\n * Segment node shape expected by computeSegmentKeys.\n *\n * Structurally satisfied by both `SegmentNode` (build time) and\n * `ManifestSegmentNode` (request time) — keys depend only on the\n * URL path and the segment classification, never on file payloads.\n */\n\nimport type { CoercedParams } from '../shared/param-value.ts';\n\nexport interface SegmentKeyInput {\n urlPath: string;\n segmentName?: string;\n segmentType?: string;\n}\n\n/**\n * Compute state-tree keys for a segment chain.\n *\n * Non-group segments use their urlPath as-is. Route groups accumulate\n * ancestor group names to produce globally unique keys:\n * app/(a)/(shared)/dashboard → keys: ['/', '/(a)', '/(a)/(shared)', '/dashboard']\n *\n * **An intercepting segment switches the chain to name accumulation for good.**\n * A children-path interception (TIM-1280) puts an intercepting node and its\n * descendants into the *main* rendered chain, and their `urlPath`s are not\n * URLs: interception adds no URL depth, so `app/(browse)/(...)[artistSlug]/[year]`\n * carries `/` and `/[year]`. Keyed by urlPath the intercepting node would\n * collide with its own owner, and `[year]` with any top-level `[year]` route —\n * so a client that had one mounted would reuse the wrong cached layout. The\n * switch is sticky rather than per-node because the *descendants* are where\n * the false URLs are; the intercepting node alone is not the problem.\n *\n * This is the single source of truth for segment keys — used by the\n * element builder (skip decisions, SegmentOutlet props), segment info\n * (X-Timber-Segments header), the client cache/state tree, and\n * interception scoping (`routing/interception.ts`).\n */\nexport function computeSegmentKeys(segments: SegmentKeyInput[]): string[] {\n const keys: string[] = [];\n let prevKey = '';\n let insideIntercepting = false;\n\n for (const segment of segments) {\n if (segment.segmentType === 'intercepting') insideIntercepting = true;\n if (segment.segmentType === 'group' || insideIntercepting) {\n const base = prevKey === '/' ? '' : prevKey;\n const key = `${base}/${segment.segmentName}`;\n keys.push(key);\n prevKey = key;\n } else {\n keys.push(segment.urlPath);\n prevKey = segment.urlPath;\n }\n }\n\n return keys;\n}\n\n/**\n * Compute tree paths for a segment chain — the directory path of each segment\n * with slots elided, e.g. `app/(browse)/feed` → `/(browse)/feed`.\n *\n * This is `computeSegmentKeys` with the group branch applied to *every*\n * segment rather than only to groups. That one difference is the whole point:\n * `computeSegmentKeys` resets to `urlPath` at each URL-visible segment, so\n * `(browse)/feed` and `(landing)/feed` both key as `/feed`. Two directories\n * that render different layouts must not share an identity.\n *\n * Used for interception scoping (`routing/interception.ts`), where the\n * question is \"does this route pass through *that* directory?\" — not \"does it\n * render this URL?\". Not interchangeable with `computeSegmentKeys`: the state\n * tree is keyed by URL on purpose, because the client caches by URL.\n */\nexport function computeSegmentTreePaths(segments: SegmentKeyInput[]): string[] {\n const paths: string[] = [];\n let prev = '';\n\n for (const segment of segments) {\n const name = segment.segmentName ?? '';\n if (!name) {\n // The app root — no directory name of its own.\n paths.push('/');\n prev = '/';\n continue;\n }\n const base = prev === '/' ? '' : prev;\n const path = `${base}/${name}`;\n paths.push(path);\n prev = path;\n }\n\n return paths;\n}\n\n/**\n * The directory names a tree path is built from — `/` is none, `/feed/(a)` is\n * `['feed', '(a)']`.\n *\n * The inverse of the walk above, and it lives beside it so the format is\n * stated once: `findChainByTreePath` (`server/children-interception.ts`) walks\n * these names back down the tree to the directory the path addresses, and\n * `treePathDepth` counts them. Both are readings of the same string, and a\n * reader that disagreed with the producer about what separates two names\n * resolves an interception to the wrong node or to none.\n */\nexport function treePathNames(treePath: string): string[] {\n return treePath === '/' ? [] : treePath.slice(1).split('/');\n}\n\n/**\n * How many directories a tree path names — `/` is 0, `/feed` is 1,\n * `/feed/(a)` is 2.\n *\n * Two tree paths on a single rendered chain are strictly nested, so on that\n * chain depth orders them totally — which is what makes it usable as \"the\n * deeper of these two directories\" wherever a route passes through both\n * (`routing/interception.ts`).\n */\nexport function treePathDepth(treePath: string): number {\n return treePathNames(treePath).length;\n}\n\n/**\n * Compute a unique key for a parallel route slot.\n * Format: `{parentSegmentId}/@{slotName}`, e.g. `/@sidebar` or `/dashboard/@modal`.\n */\nexport function computeSlotKey(parentSegmentId: string, slotName: string): string {\n const name = slotName.startsWith('@') ? slotName : `@${slotName}`;\n const prefix = parentSegmentId === '/' ? '' : parentSegmentId;\n return `${prefix}/${name}`;\n}\n\n/**\n * Compute a content key for a parallel route slot.\n *\n * The content key encodes everything that determines the slot's rendered\n * output for a given navigation: the slot key (which includes the owning\n * segment's identity and the slot name), the URL parts consumed by the\n * owning segment (which determine parent params the slot page might\n * read), the matched page file (or a sentinel for `default.tsx`), and\n * the slot's own extracted params.\n *\n * The slot key prevents cross-group collisions: two route groups at the\n * same URL level each owning a `@sidebar` slot produce different keys\n * even when both fall back to `default.tsx`.\n *\n * The key is opaque to the client — it stores whatever the server sent\n * and advertises it back. The server computes the destination key and\n * checks membership. This eliminates the need for the server to\n * reconstruct the client's state from a departing URL.\n *\n * See design/07-routing.md §\"Segment Tree Diffing on Navigation\"\n */\nexport function computeSlotContentKey(\n slotKey: string,\n ownerParts: string[],\n entryFile: string | null,\n slotParams: CoercedParams\n): string {\n const owner = ownerParts.join('/');\n const entry = entryFile ?? '\\x01';\n const paramKeys = Object.keys(slotParams).sort();\n const paramParts = paramKeys.map((k) => {\n const v = slotParams[k];\n return Array.isArray(v) ? `${k}=${v.map(String).join('\\x02')}` : `${k}=${String(v)}`;\n });\n return [slotKey, owner, entry, ...paramParts].join('\\0');\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA2FA,SAAgB,wBAAwB,UAAuC;CAC7E,MAAM,QAAkB,CAAC;CACzB,IAAI,OAAO;CAEX,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,OAAO,QAAQ,eAAe;EACpC,IAAI,CAAC,MAAM;GAET,MAAM,KAAK,GAAG;GACd,OAAO;GACP;EACF;EAEA,MAAM,OAAO,GADA,SAAS,MAAM,KAAK,KACZ,GAAG;EACxB,MAAM,KAAK,IAAI;EACf,OAAO;CACT;CAEA,OAAO;AACT;;;;;;;;;;;;AAaA,SAAgB,cAAc,UAA4B;CACxD,OAAO,aAAa,MAAM,CAAC,IAAI,SAAS,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG;AAC5D;;;;;;;;;;AAWA,SAAgB,cAAc,UAA0B;CACtD,OAAO,cAAc,QAAQ,CAAC,CAAC;AACjC"}
@@ -0,0 +1,122 @@
1
+ //#region src/server/tree-match.ts
2
+ /**
3
+ * Match URL path parts against a segment tree.
4
+ *
5
+ * Walks the tree depth-first with backtracking, applying priority
6
+ * ordering globally across group boundaries. Returns the matched
7
+ * segment chain and collected params, or null if no route matches.
8
+ *
9
+ * By default, a match requires a terminal node (one with `page` or
10
+ * `route`) at the leaf. Pass `{ requireTerminal: false }` to match
11
+ * any segment (used by metadata route matching).
12
+ *
13
+ * @param root The root segment node to start matching from.
14
+ * @param parts URL path parts (e.g. ['blog', 'hello-world']).
15
+ * Empty array matches the root itself (if terminal).
16
+ */
17
+ function matchUrlParts(root, parts, options) {
18
+ const chain = [];
19
+ const params = {};
20
+ if (matchNode(root, parts, 0, chain, params, options?.requireTerminal ?? true)) return {
21
+ chain,
22
+ params
23
+ };
24
+ return null;
25
+ }
26
+ /**
27
+ * Flatten children through group segments.
28
+ *
29
+ * Groups are transparent for URL matching — their non-group descendants
30
+ * are returned with the chain of group nodes leading to them. This lets
31
+ * the caller apply priority ordering (static > dynamic > catch-all >
32
+ * optional-catch-all) across all groups uniformly instead of per-group.
33
+ */
34
+ function collectEffectiveChildren(node, groupChain = []) {
35
+ const result = [];
36
+ for (const child of node.children) if (child.segmentType === "group") {
37
+ const nested = collectEffectiveChildren(child, [...groupChain, child]);
38
+ result.push(...nested);
39
+ } else result.push({
40
+ child,
41
+ groupChain
42
+ });
43
+ return result;
44
+ }
45
+ /** A node is terminal if it can serve a response (has a page or route handler). */
46
+ function isTerminal(node) {
47
+ return !!(node.page || node.route);
48
+ }
49
+ /** Combined affix length for specificity ordering. */
50
+ function affixLength(node) {
51
+ return (node.paramPrefix?.length ?? 0) + (node.paramSuffix?.length ?? 0);
52
+ }
53
+ /**
54
+ * Recursive depth-first matcher with backtracking.
55
+ *
56
+ * Mutates `chain` and `params` in place for performance; backtracks
57
+ * (restores previous state) when a candidate doesn't lead to a match.
58
+ */
59
+ function matchNode(node, parts, index, chain, params, requireTerminal) {
60
+ chain.push(node);
61
+ if (index >= parts.length) {
62
+ if (!requireTerminal || isTerminal(node)) return true;
63
+ for (const child of node.children) if (child.segmentType === "group") {
64
+ if (matchNode(child, parts, index, chain, params, requireTerminal)) return true;
65
+ }
66
+ const effective = collectEffectiveChildren(node);
67
+ for (const { child, groupChain } of effective) if (child.segmentType === "optional-catch-all" && (!requireTerminal || isTerminal(child))) {
68
+ chain.push(...groupChain, child);
69
+ return true;
70
+ }
71
+ chain.pop();
72
+ return false;
73
+ }
74
+ const part = parts[index];
75
+ const effective = collectEffectiveChildren(node);
76
+ for (const { child, groupChain } of effective) if (child.segmentType === "static" && child.segmentName === part) {
77
+ chain.push(...groupChain);
78
+ if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
79
+ chain.length -= groupChain.length;
80
+ }
81
+ const affixed = effective.filter(({ child }) => child.segmentType === "dynamic" && child.paramName && (child.paramPrefix || child.paramSuffix));
82
+ affixed.sort((a, b) => affixLength(b.child) - affixLength(a.child));
83
+ for (const { child, groupChain } of affixed) {
84
+ const prefix = child.paramPrefix ?? "";
85
+ const suffix = child.paramSuffix ?? "";
86
+ if (part.length > prefix.length + suffix.length && (!prefix || part.startsWith(prefix)) && (!suffix || part.endsWith(suffix))) {
87
+ const value = part.slice(prefix.length, part.length - (suffix.length || 0));
88
+ chain.push(...groupChain);
89
+ const prev = params[child.paramName];
90
+ params[child.paramName] = value;
91
+ if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
92
+ chain.length -= groupChain.length;
93
+ if (prev !== void 0) params[child.paramName] = prev;
94
+ else delete params[child.paramName];
95
+ }
96
+ }
97
+ for (const { child, groupChain } of effective) if (child.segmentType === "dynamic" && child.paramName && !child.paramPrefix && !child.paramSuffix) {
98
+ chain.push(...groupChain);
99
+ const prev = params[child.paramName];
100
+ params[child.paramName] = part;
101
+ if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
102
+ chain.length -= groupChain.length;
103
+ if (prev !== void 0) params[child.paramName] = prev;
104
+ else delete params[child.paramName];
105
+ }
106
+ for (const { child, groupChain } of effective) if (child.segmentType === "catch-all" && child.paramName && (!requireTerminal || isTerminal(child))) {
107
+ chain.push(...groupChain, child);
108
+ params[child.paramName] = parts.slice(index);
109
+ return true;
110
+ }
111
+ for (const { child, groupChain } of effective) if (child.segmentType === "optional-catch-all" && child.paramName && (!requireTerminal || isTerminal(child))) {
112
+ chain.push(...groupChain, child);
113
+ params[child.paramName] = parts.slice(index);
114
+ return true;
115
+ }
116
+ chain.pop();
117
+ return false;
118
+ }
119
+ //#endregion
120
+ export { matchUrlParts as t };
121
+
122
+ //# sourceMappingURL=tree-match-CdbvYTBz.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-match-CdbvYTBz.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// ─── 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/** 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 for (const { child, groupChain } of effective) {\n if (\n child.segmentType === 'dynamic' &&\n child.paramName &&\n !child.paramPrefix &&\n !child.paramSuffix\n ) {\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\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"],"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-D6rO-VcS.js";
3
+ import { n as findSchemaFile, r as parseExistingSchemaKeys } from "./cli-schema-sync-DnXqcIIj.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-B6XUtmqK.js.map
144
+ //# sourceMappingURL=walkers-Cm3PC5JT.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"walkers-B6XUtmqK.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-Cm3PC5JT.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,4 +1,4 @@
1
- import { f as runWithBindings } from "../_chunks/cloudflare-BFb__LYG.js";
1
+ import { f as runWithBindings } from "../_chunks/cloudflare-DxX1SU0g.js";
2
2
  //#region src/adapters/cloudflare-dev.ts
3
3
  /**
4
4
  * Dynamically import wrangler. Extracted as a named export so tests
@@ -1,5 +1,5 @@
1
1
  import { t as warnLossyJsonValue } from "../_chunks/json-lossy-check-ClNvBM_3.js";
2
- import { l as getCloudflareBindings } from "../_chunks/cloudflare-BFb__LYG.js";
2
+ import { l as getCloudflareBindings } from "../_chunks/cloudflare-DxX1SU0g.js";
3
3
  //#region src/adapters/cloudflare-kv-cache.ts
4
4
  var LEGACY_GRACE_MS = 2592e6;
5
5
  /**
@@ -1,2 +1,2 @@
1
- import { a as generatePreviewCommand, c as generateWranglerConfig, d as readUserWranglerConfig, f as runWithBindings, i as generatePrebuiltSourceModule, l as getCloudflareBindings, n as cloudflare, o as generateRuntimeInitModule, r as generateBindingsConfig, s as generateWorkerEntry, t as buildStaticOutput, u as readPrebuiltData } from "../_chunks/cloudflare-BFb__LYG.js";
1
+ import { a as generatePreviewCommand, c as generateWranglerConfig, d as readUserWranglerConfig, f as runWithBindings, i as generatePrebuiltSourceModule, l as getCloudflareBindings, n as cloudflare, o as generateRuntimeInitModule, r as generateBindingsConfig, s as generateWorkerEntry, t as buildStaticOutput, u as readPrebuiltData } from "../_chunks/cloudflare-DxX1SU0g.js";
2
2
  export { buildStaticOutput, cloudflare, generateBindingsConfig, generatePrebuiltSourceModule, generatePreviewCommand, generateRuntimeInitModule, generateWorkerEntry, generateWranglerConfig, getCloudflareBindings, readPrebuiltData, readUserWranglerConfig, runWithBindings };
@@ -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-Bpb0W4ax.js";
4
- import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "../_chunks/poison-scan-Bm9Yyqk9.js";
3
+ import { a as detectDirective, i as classifyModules, n as stitchImportChains } from "../_chunks/chains-CBNA0Ozj.js";
4
+ import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "../_chunks/poison-scan-lEbz4pQE.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";
@@ -1,4 +1,4 @@
1
- import { a as readGraphCache, c as isPidAlive, i as hashFingerprintEntries, n as computeProjectFingerprint, o as writeGraphCache, r as fingerprintEntriesForFiles, s as DEV_DISCOVERY_DIR, t as collectBaseFingerprintEntries } from "../_chunks/graph-cache-CP4GEmf9.js";
1
+ import { a as readGraphCache, c as isPidAlive, i as hashFingerprintEntries, n as computeProjectFingerprint, o as writeGraphCache, r as fingerprintEntriesForFiles, s as DEV_DISCOVERY_DIR, t as collectBaseFingerprintEntries } from "../_chunks/graph-cache-Cv3njEH8.js";
2
2
  import { r as normalizeSlashes } from "../_chunks/scan-BoP17W3w.js";
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { createRequire } from "node:module";
@@ -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-Bpb0W4ax.js").then((n) => n.t);
381
+ const { formatChains } = await import("../_chunks/chains-CBNA0Ozj.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-Bpb0W4ax.js").then((n) => n.t);
400
+ const { formatChains } = await import("../_chunks/chains-CBNA0Ozj.js").then((n) => n.t);
401
401
  console.log(" import chains:");
402
402
  console.log(formatChains(data.chains, configDir));
403
403
  }
@@ -1,3 +1,3 @@
1
- import { _ as setDefaultSingleflightTimeout, f as MemoryCacheHandler, g as setCacheHandler, h as getCacheHandler, m as RedisCacheHandler, p as TagAwareCacheHandler, t as cache } from "../_chunks/cache-api-ByagcC-J.js";
1
+ import { _ as setDefaultSingleflightTimeout, f as MemoryCacheHandler, g as setCacheHandler, h as getCacheHandler, m as RedisCacheHandler, p as TagAwareCacheHandler, t as cache } from "../_chunks/cache-api-CAPbZTga.js";
2
2
  import { t as estimateByteSize } from "../_chunks/sizeof-lXffQ1Vr.js";
3
3
  export { MemoryCacheHandler, RedisCacheHandler, TagAwareCacheHandler, cache, estimateByteSize, getCacheHandler, setCacheHandler, setDefaultSingleflightTimeout };
package/dist/cli.d.ts CHANGED
@@ -52,13 +52,18 @@ export interface ViteDeps {
52
52
  preview?: typeof import('vite').preview;
53
53
  }
54
54
  /**
55
- * Start the Vite dev server.
55
+ * Start the Vite dev server with a supervisor that respawns the process
56
+ * when timber.config.ts changes. Node's require cache for ESM modules
57
+ * loaded via require() cannot be evicted, so an in-process restart
58
+ * serves stale config. The supervisor forks a worker; when the worker
59
+ * exits with RESTART_EXIT_CODE (set by timber-dev-server on config file
60
+ * change), the supervisor spawns a fresh worker with a clean cache.
56
61
  *
57
62
  * The timber plugin binds the port immediately with a holding page
58
63
  * (in rootSync's config hook) so browsers see a "starting..." page
59
64
  * instead of ERR_CONNECTION_REFUSED during initialization.
60
65
  *
61
- * See design/21-dev-server.md, TIM-665.
66
+ * See design/21-dev-server.md, TIM-665, TIM-1444.
62
67
  */
63
68
  export declare function runDev(options: CommandOptions, _deps?: ViteDeps): Promise<void>;
64
69
  /**
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B,QAAA,MAAM,QAAQ,YAAI,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAU,CAAC;AAC1F,KAAK,OAAO,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzC,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,+DAA+D;IAC/D,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAC;IACd,uDAAuD;IACvD,GAAG,EAAE,OAAO,CAAC;IACb,8EAA8E;IAC9E,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAmEpD;AAID,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACvB,YAAY,CAAC,EAAE,cAAc,MAAM,EAAE,YAAY,CAAC;IAClD,aAAa,CAAC,EAAE,cAAc,MAAM,EAAE,aAAa,CAAC;IACpD,OAAO,CAAC,EAAE,cAAc,MAAM,EAAE,OAAO,CAAC;CACzC;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAOrF;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAMvF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,qBAAqB,EAAE,qBAAqB,GAAG,SAAS,GACvE,SAAS,GAAG,MAAM,CAKpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBzF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAMxF;AAID;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA6C7D;AAID;;;;;;;GAOG;AACH,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAsD7C"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B,QAAA,MAAM,QAAQ,YAAI,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAU,CAAC;AAC1F,KAAK,OAAO,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzC,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,+DAA+D;IAC/D,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAC;IACd,uDAAuD;IACvD,GAAG,EAAE,OAAO,CAAC;IACb,8EAA8E;IAC9E,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAmEpD;AAID,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACvB,YAAY,CAAC,EAAE,cAAc,MAAM,EAAE,YAAY,CAAC;IAClD,aAAa,CAAC,EAAE,cAAc,MAAM,EAAE,aAAa,CAAC;IACpD,OAAO,CAAC,EAAE,cAAc,MAAM,EAAE,OAAO,CAAC;CACzC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDrF;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAMvF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,qBAAqB,EAAE,qBAAqB,GAAG,SAAS,GACvE,SAAS,GAAG,MAAM,CAKpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBzF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAMxF;AAID;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA6C7D;AAID;;;;;;;GAOG;AACH,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAsD7C"}
package/dist/cli.js CHANGED
@@ -69,18 +69,55 @@ function parseArgs(args) {
69
69
  };
70
70
  }
71
71
  /**
72
- * Start the Vite dev server.
72
+ * Start the Vite dev server with a supervisor that respawns the process
73
+ * when timber.config.ts changes. Node's require cache for ESM modules
74
+ * loaded via require() cannot be evicted, so an in-process restart
75
+ * serves stale config. The supervisor forks a worker; when the worker
76
+ * exits with RESTART_EXIT_CODE (set by timber-dev-server on config file
77
+ * change), the supervisor spawns a fresh worker with a clean cache.
73
78
  *
74
79
  * The timber plugin binds the port immediately with a holding page
75
80
  * (in rootSync's config hook) so browsers see a "starting..." page
76
81
  * instead of ERR_CONNECTION_REFUSED during initialization.
77
82
  *
78
- * See design/21-dev-server.md, TIM-665.
83
+ * See design/21-dev-server.md, TIM-665, TIM-1444.
79
84
  */
80
85
  async function runDev(options, _deps) {
81
- const server = await (_deps?.createServer ?? (await import("vite")).createServer)({ configFile: options.config });
82
- await server.listen();
83
- server.printUrls();
86
+ if (process.env.__TIMBER_DEV_WORKER) {
87
+ const server = await (_deps?.createServer ?? (await import("vite")).createServer)({ configFile: options.config });
88
+ await server.listen();
89
+ server.printUrls();
90
+ return;
91
+ }
92
+ const { fork } = await import("node:child_process");
93
+ const { RESTART_EXIT_CODE } = await import("./_chunks/dev-server-DioP7tkQ.js").then((n) => n.t);
94
+ while (true) {
95
+ const result = await new Promise((resolve) => {
96
+ const child = fork(process.argv[1], process.argv.slice(2), {
97
+ stdio: "inherit",
98
+ env: {
99
+ ...process.env,
100
+ __TIMBER_DEV_WORKER: "1"
101
+ }
102
+ });
103
+ const onSigint = () => child.kill("SIGINT");
104
+ const onSigterm = () => child.kill("SIGTERM");
105
+ process.on("SIGINT", onSigint);
106
+ process.on("SIGTERM", onSigterm);
107
+ child.on("exit", (code, signal) => {
108
+ process.off("SIGINT", onSigint);
109
+ process.off("SIGTERM", onSigterm);
110
+ resolve({
111
+ code,
112
+ signal
113
+ });
114
+ });
115
+ });
116
+ if (result.code === RESTART_EXIT_CODE) continue;
117
+ if (result.signal) process.kill(process.pid, result.signal);
118
+ if (result.code != null && result.code !== 0) process.exit(result.code);
119
+ break;
120
+ }
84
121
  }
85
122
  /**
86
123
  * Run the production build using createBuilder + buildApp.
@@ -136,7 +173,7 @@ async function runPreview(options, _deps) {
136
173
  * `--project` names the tsconfig handed to tsgo.
137
174
  */
138
175
  async function runCheck(options, _deps) {
139
- const { checkRoutes, runTypeCheck } = await import("./_chunks/cli-check-D6VolrDV.js");
176
+ const { checkRoutes, runTypeCheck } = await import("./_chunks/cli-check-BzMGuIH6.js");
140
177
  const { root } = await checkRoutes({ viteConfigFile: options.config }, _deps);
141
178
  await runTypeCheck({
142
179
  root,
@@ -152,7 +189,7 @@ async function runCheck(options, _deps) {
152
189
  async function runSchema(args) {
153
190
  const subcommand = args[0];
154
191
  if (subcommand !== "sync") throw new Error(`Unknown schema subcommand: ${subcommand ?? "(none)"}. Usage: timber schema sync`);
155
- const { runSchemaSync, defaultCodecForSegment } = await import("./_chunks/cli-schema-sync-D6rO-VcS.js").then((n) => n.t);
192
+ const { runSchemaSync, defaultCodecForSegment } = await import("./_chunks/cli-schema-sync-DnXqcIIj.js").then((n) => n.t);
156
193
  const result = runSchemaSync(process.cwd());
157
194
  if (result.filesystemSegments.length === 0) {
158
195
  console.log("[timber] No dynamic segments found in app/.");
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n// timber.js CLI\n//\n// Wraps Vite commands with timber-specific behavior.\n// See design/18-build-system.md §\"CLI\".\n//\n// Commands:\n// timber dev — Start Vite dev server with HMR\n// timber build — Run multi-environment build via createBuilder/buildApp\n// timber preview — Serve the production build\n// timber check — Validate types + routes without building\n// timber graph — Classify a file's module environment(s) (design/47)\n\n// Type-only — the implementation is imported dynamically inside runCheck so the\n// CLI's other commands never load the route scanner.\nimport type { CheckDeps } from './cli-check.ts';\n\nexport type { CheckDeps };\n\nconst COMMANDS = ['dev', 'build', 'preview', 'check', 'schema', 'init', 'graph'] as const;\ntype Command = (typeof COMMANDS)[number];\n\nexport interface ParsedArgs {\n command: Command;\n config: string | undefined;\n /** `--project`: the tsconfig `timber check` passes to tsgo. */\n project: string | undefined;\n /** `timber graph` only: emit the machine-readable JSON contract. */\n json: boolean;\n /** `timber graph` only: classify the whole project. */\n all: boolean;\n /** Positional arguments after the command (`schema sync`, `graph <file>`). */\n rest: string[];\n}\n\nexport interface CommandOptions {\n /** Path to a Vite config file. Means the same thing for every command. */\n config?: string;\n /** `timber check` only: the tsconfig to type-check. */\n project?: string;\n}\n\n/**\n * Parse CLI arguments into a structured command + options.\n * Accepts: timber <command> [--config|-c <path>] [--project <path>]\n *\n * `--config` names a **Vite** config file for every command, `check` included\n * (TIM-1332): `check` resolves the project through Vite, so a project that\n * builds with `--config ./custom.vite.config.ts` has to be checkable the same\n * way. `--project` names the tsconfig, spelled as tsgo and tsc spell it, and\n * is meaningful only for `check`.\n *\n * Before TIM-1332 `--config` meant the tsconfig for `check` alone — one flag\n * with two meanings, which the docs had to carry a note to explain and which\n * left custom Vite configs unreachable from `check` entirely.\n *\n * Unknown flags are an error, not silently ignored — `timber dev --port 4000`\n * starting on the default port would contradict fail-loudly. The surface stays\n * deliberately small (design/11-platform.md §CLI); port selection goes through\n * the PORT env var or server.port in vite.config.ts.\n *\n * Each subcommand declares its exact flags (the `check --project` precedent,\n * TIM-1332): `graph` additionally accepts a file positional plus `--json` and\n * `--all`, and those two are rejected everywhere else.\n */\nexport function parseArgs(args: string[]): ParsedArgs {\n if (args.length === 0) {\n throw new Error(\n 'No command provided. Usage: timber <dev|build|preview|check|schema|init|graph> [--config <path>]'\n );\n }\n\n const command = args[0];\n if (!COMMANDS.includes(command as Command)) {\n throw new Error(`Unknown command: ${command}. Available commands: ${COMMANDS.join(', ')}`);\n }\n\n let config: string | undefined;\n let project: string | undefined;\n let json = false;\n let all = false;\n const rest: string[] = [];\n for (let i = 1; i < args.length; i++) {\n const arg = args[i];\n if (arg === '--config' || arg === '-c') {\n config = args[++i];\n if (!config) {\n throw new Error('--config requires a path argument');\n }\n } else if (arg === '--json' || arg === '--all') {\n if (command !== 'graph') {\n throw new Error(\n `${arg} is only valid for \\`timber graph\\` (the module-environment query).`\n );\n }\n if (arg === '--json') json = true;\n else all = true;\n } else if (arg === '--project') {\n if (command !== 'check') {\n throw new Error(\n `--project is only valid for \\`timber check\\` (it names the tsconfig to type-check). ` +\n `To point \\`timber ${command}\\` at a different Vite config, use --config <path>.`\n );\n }\n project = args[++i];\n if (!project) {\n throw new Error('--project requires a path argument');\n }\n } else if (arg.startsWith('-')) {\n const portHint =\n arg === '--port' || arg === '-p'\n ? ' To change the port, set the PORT environment variable (PORT=4000 timber dev) ' +\n 'or server.port in vite.config.ts.'\n : '';\n throw new Error(\n `Unknown option: ${arg}. The timber CLI accepts --config <path>` +\n ` (and --project <path> for \\`check\\`).${portHint}`\n );\n } else if (command === 'schema' || command === 'init' || command === 'graph') {\n rest.push(arg);\n } else {\n throw new Error(`Unexpected argument: ${arg}. Usage: timber ${command} [--config <path>]`);\n }\n }\n\n if (command === 'graph' && rest.length > 1) {\n throw new Error(\n `timber graph takes one file. Usage: timber graph <file> [--json], or timber graph --all --json`\n );\n }\n\n return { command: command as Command, config, project, json, all, rest };\n}\n\n// ─── Command Implementations ─────────────────────────────────────────────────\n\n/** @internal Dependency injection for testing. */\nexport interface ViteDeps {\n createServer?: typeof import('vite').createServer;\n createBuilder?: typeof import('vite').createBuilder;\n preview?: typeof import('vite').preview;\n}\n\n/**\n * Start the Vite dev server.\n *\n * The timber plugin binds the port immediately with a holding page\n * (in rootSync's config hook) so browsers see a \"starting...\" page\n * instead of ERR_CONNECTION_REFUSED during initialization.\n *\n * See design/21-dev-server.md, TIM-665.\n */\nexport async function runDev(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n const createServer = _deps?.createServer ?? (await import('vite')).createServer;\n const server = await createServer({\n configFile: options.config,\n });\n await server.listen();\n server.printUrls();\n}\n\n/**\n * Run the production build using createBuilder + buildApp.\n * Direct build() calls do NOT trigger the RSC plugin's multi-environment\n * pipeline — createBuilder/buildApp is required.\n */\nexport async function runBuild(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n const createBuilder = _deps?.createBuilder ?? (await import('vite')).createBuilder;\n const builder = await createBuilder({\n configFile: options.config,\n });\n await builder.buildApp();\n}\n\n/**\n * Determine whether to use the adapter's preview or Vite's built-in preview.\n * Exported for testing — the actual runPreview function uses this internally.\n */\nexport function resolvePreviewStrategy(\n adapter: import('./adapters/types.ts').TimberPlatformAdapter | undefined\n): 'adapter' | 'vite' {\n if (adapter && typeof adapter.preview === 'function') {\n return 'adapter';\n }\n return 'vite';\n}\n\n/**\n * Serve the production build for local testing.\n * If the adapter provides a preview() method, it takes priority.\n * Otherwise falls back to Vite's built-in preview server.\n *\n * Uses the same loadTimberConfigFile loader as dev/build — one loader,\n * one set of module semantics. A config file that exists but fails to\n * load throws (naming the file) instead of silently degrading to plain\n * Vite preview. See TIM-1066.\n */\nexport async function runPreview(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n const { loadTimberConfigFile, resolveBuildDir } = await import('./plugin-context.ts');\n\n const root = process.cwd();\n const config = loadTimberConfigFile(root);\n const adapter = config?.adapter as\n | import('./adapters/types.ts').TimberPlatformAdapter\n | undefined;\n\n if (resolvePreviewStrategy(adapter) === 'adapter') {\n const buildDir = resolveBuildDir(root, config?.buildDir);\n const timberConfig = { output: (config?.output ?? 'server') as 'server' | 'static' };\n await adapter!.preview!(timberConfig, buildDir);\n return;\n }\n\n // Fallback: Vite's built-in preview server\n const preview = _deps?.preview ?? (await import('vite')).preview;\n const server = await preview({\n configFile: options.config,\n });\n server.printUrls();\n}\n\n/**\n * Validate routes and types without producing build output.\n *\n * Runs the route pass first — resolve the project through Vite, scan it, fail\n * on proven-fatal convention errors, print warnings, and regenerate the route\n * types — then `tsgo --noEmit`. Codegen has to precede the type pass: the\n * generated `.timber/*.d.ts` files are what type `<Link href>` and\n * `useSegmentParams()`, and a fresh clone has never produced them. See\n * `cli-check.ts` for the full reasoning.\n *\n * `--config` names the **Vite** config, as it does for every other command, so\n * an app that builds with a custom one is checkable the same way.\n * `--project` names the tsconfig handed to tsgo.\n */\nexport async function runCheck(options: CommandOptions, _deps?: CheckDeps): Promise<void> {\n const { checkRoutes, runTypeCheck } = await import('./cli-check.ts');\n const { root } = await checkRoutes({ viteConfigFile: options.config }, _deps);\n // tsgo runs from the root the route pass resolved, not from cwd — that is\n // where it just wrote the declarations tsgo has to read.\n await runTypeCheck({ root, config: options.project }, _deps);\n}\n\n// ─── Schema Subcommand ───────────────────────────────────────────────────────\n\n/**\n * `timber schema sync` — scan filesystem for dynamic segments, diff against\n * app/schema.ts, and append missing entries with type-appropriate codec defaults.\n *\n * See design/41-global-params.md §Auto-Scaffolding\n */\nexport async function runSchema(args: string[]): Promise<void> {\n const subcommand = args[0];\n\n if (subcommand !== 'sync') {\n throw new Error(\n `Unknown schema subcommand: ${subcommand ?? '(none)'}. Usage: timber schema sync`\n );\n }\n\n const { runSchemaSync, defaultCodecForSegment } = await import('./cli-schema-sync.ts');\n const result = runSchemaSync(process.cwd());\n\n if (result.filesystemSegments.length === 0) {\n console.log('[timber] No dynamic segments found in app/.');\n return;\n }\n\n if (result.created) {\n console.log(`[timber] Created app/schema.ts with ${result.addedSegments.length} segment(s):`);\n for (const seg of result.addedSegments) {\n console.log(` + ${seg}: ${defaultCodecForSegment(seg)}`);\n }\n return;\n }\n\n if (result.addedSegments.length === 0 && result.removedSegments.length === 0) {\n console.log('[timber] Schema is up to date — all segments are registered.');\n return;\n }\n\n if (result.addedSegments.length > 0) {\n console.log(`[timber] Added ${result.addedSegments.length} segment(s) to app/schema.ts:`);\n for (const seg of result.addedSegments) {\n console.log(` + ${seg}: ${defaultCodecForSegment(seg)}`);\n }\n }\n\n if (result.removedSegments.length > 0) {\n console.log(\n `[timber] Removed ${result.removedSegments.length} stale segment(s) from app/schema.ts:`\n );\n for (const seg of result.removedSegments) {\n console.log(` - ${seg}`);\n }\n }\n}\n\n// ─── Init Subcommand ─────────────────────────────────────────────────────────\n\n/**\n * `timber init` — write AGENTS.md (and a CLAUDE.md symlink to it) to the\n * project root so AI coding agents can discover timber.js documentation and\n * conventions.\n *\n * The content is read from the installed @timber-js/app package itself, so it\n * stays in sync with the installed version.\n */\nexport async function runInit(): Promise<void> {\n const { readFileSync, writeFileSync, existsSync, symlinkSync } = await import('node:fs');\n const { resolve, dirname } = await import('node:path');\n const { fileURLToPath } = await import('node:url');\n\n const root = process.cwd();\n const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');\n\n const agentSkillPath = resolve(packageRoot, 'agent-skill.md');\n if (!existsSync(agentSkillPath)) {\n throw new Error(\n 'agent-skill.md not found in @timber-js/app package. ' +\n 'This may be a development install — try reinstalling.'\n );\n }\n\n const agentSkill = readFileSync(agentSkillPath, 'utf-8');\n\n const agentsPath = resolve(root, 'AGENTS.md');\n const claudePath = resolve(root, 'CLAUDE.md');\n\n const wrote: string[] = [];\n\n if (existsSync(agentsPath)) {\n console.log('[timber] AGENTS.md already exists — skipping (delete it first to regenerate)');\n } else {\n writeFileSync(agentsPath, agentSkill);\n wrote.push('AGENTS.md');\n }\n\n if (existsSync(claudePath)) {\n console.log('[timber] CLAUDE.md already exists — skipping (delete it first to regenerate)');\n } else {\n try {\n // Relative target so the link survives the project directory moving.\n // The 'file' type argument is required for Windows (ignored elsewhere).\n symlinkSync('AGENTS.md', claudePath, 'file');\n wrote.push('CLAUDE.md (symlink)');\n } catch {\n // Windows without Developer Mode or admin rights can't create symlinks\n // (EPERM) — fall back to a plain pointer file.\n writeFileSync(claudePath, 'Read AGENTS.md for project context and conventions.\\n');\n wrote.push('CLAUDE.md');\n }\n }\n\n if (wrote.length > 0) {\n console.log(\n `[timber] Created ${wrote.join(' and ')} — AI agents can now discover timber.js docs.`\n );\n console.log('[timber] Full documentation: node_modules/@timber-js/app/docs/');\n } else {\n console.log('[timber] Nothing to do — both files already exist.');\n }\n}\n\n// ─── Main Entry Point ────────────────────────────────────────────────────────\n\nasync function main(): Promise<void> {\n const parsed = parseArgs(process.argv.slice(2));\n const options: CommandOptions = { config: parsed.config, project: parsed.project };\n\n switch (parsed.command) {\n case 'dev':\n await runDev(options);\n break;\n case 'build':\n await runBuild(options);\n break;\n case 'preview':\n await runPreview(options);\n break;\n case 'check':\n await runCheck(options);\n break;\n case 'schema':\n await runSchema(parsed.rest);\n break;\n case 'init':\n await runInit();\n break;\n case 'graph': {\n // Lazy import — the graph implementation (and, on a cache miss,\n // Vite + the crawl child) never loads for the other commands.\n const { runGraph } = await import('./analyze/graph-command.ts');\n await runGraph({\n file: parsed.rest[0],\n config: parsed.config,\n json: parsed.json,\n all: parsed.all,\n });\n break;\n }\n }\n}\n\n// Run main when executed as a CLI (not imported in tests).\n// The bin shim (bin/timber.mjs) does `import '../dist/cli.js'`, so\n// process.argv[1] points to the shim, not this file. We check both:\n// direct execution AND being imported by the timber bin shim.\nconst isDirectExecution =\n typeof process !== 'undefined' &&\n process.argv[1] &&\n (import.meta.url.endsWith(process.argv[1]) ||\n process.argv[1].endsWith('bin/timber.mjs') ||\n process.argv[1].endsWith('bin/timber'));\n\nif (isDirectExecution) {\n main().catch((err) => {\n console.error(err.message);\n process.exit(1);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n// timber.js CLI\n//\n// Wraps Vite commands with timber-specific behavior.\n// See design/18-build-system.md §\"CLI\".\n//\n// Commands:\n// timber dev — Start Vite dev server with HMR\n// timber build — Run multi-environment build via createBuilder/buildApp\n// timber preview — Serve the production build\n// timber check — Validate types + routes without building\n// timber graph — Classify a file's module environment(s) (design/47)\n\n// Type-only — the implementation is imported dynamically inside runCheck so the\n// CLI's other commands never load the route scanner.\nimport type { CheckDeps } from './cli-check.ts';\n\nexport type { CheckDeps };\n\nconst COMMANDS = ['dev', 'build', 'preview', 'check', 'schema', 'init', 'graph'] as const;\ntype Command = (typeof COMMANDS)[number];\n\nexport interface ParsedArgs {\n command: Command;\n config: string | undefined;\n /** `--project`: the tsconfig `timber check` passes to tsgo. */\n project: string | undefined;\n /** `timber graph` only: emit the machine-readable JSON contract. */\n json: boolean;\n /** `timber graph` only: classify the whole project. */\n all: boolean;\n /** Positional arguments after the command (`schema sync`, `graph <file>`). */\n rest: string[];\n}\n\nexport interface CommandOptions {\n /** Path to a Vite config file. Means the same thing for every command. */\n config?: string;\n /** `timber check` only: the tsconfig to type-check. */\n project?: string;\n}\n\n/**\n * Parse CLI arguments into a structured command + options.\n * Accepts: timber <command> [--config|-c <path>] [--project <path>]\n *\n * `--config` names a **Vite** config file for every command, `check` included\n * (TIM-1332): `check` resolves the project through Vite, so a project that\n * builds with `--config ./custom.vite.config.ts` has to be checkable the same\n * way. `--project` names the tsconfig, spelled as tsgo and tsc spell it, and\n * is meaningful only for `check`.\n *\n * Before TIM-1332 `--config` meant the tsconfig for `check` alone — one flag\n * with two meanings, which the docs had to carry a note to explain and which\n * left custom Vite configs unreachable from `check` entirely.\n *\n * Unknown flags are an error, not silently ignored — `timber dev --port 4000`\n * starting on the default port would contradict fail-loudly. The surface stays\n * deliberately small (design/11-platform.md §CLI); port selection goes through\n * the PORT env var or server.port in vite.config.ts.\n *\n * Each subcommand declares its exact flags (the `check --project` precedent,\n * TIM-1332): `graph` additionally accepts a file positional plus `--json` and\n * `--all`, and those two are rejected everywhere else.\n */\nexport function parseArgs(args: string[]): ParsedArgs {\n if (args.length === 0) {\n throw new Error(\n 'No command provided. Usage: timber <dev|build|preview|check|schema|init|graph> [--config <path>]'\n );\n }\n\n const command = args[0];\n if (!COMMANDS.includes(command as Command)) {\n throw new Error(`Unknown command: ${command}. Available commands: ${COMMANDS.join(', ')}`);\n }\n\n let config: string | undefined;\n let project: string | undefined;\n let json = false;\n let all = false;\n const rest: string[] = [];\n for (let i = 1; i < args.length; i++) {\n const arg = args[i];\n if (arg === '--config' || arg === '-c') {\n config = args[++i];\n if (!config) {\n throw new Error('--config requires a path argument');\n }\n } else if (arg === '--json' || arg === '--all') {\n if (command !== 'graph') {\n throw new Error(\n `${arg} is only valid for \\`timber graph\\` (the module-environment query).`\n );\n }\n if (arg === '--json') json = true;\n else all = true;\n } else if (arg === '--project') {\n if (command !== 'check') {\n throw new Error(\n `--project is only valid for \\`timber check\\` (it names the tsconfig to type-check). ` +\n `To point \\`timber ${command}\\` at a different Vite config, use --config <path>.`\n );\n }\n project = args[++i];\n if (!project) {\n throw new Error('--project requires a path argument');\n }\n } else if (arg.startsWith('-')) {\n const portHint =\n arg === '--port' || arg === '-p'\n ? ' To change the port, set the PORT environment variable (PORT=4000 timber dev) ' +\n 'or server.port in vite.config.ts.'\n : '';\n throw new Error(\n `Unknown option: ${arg}. The timber CLI accepts --config <path>` +\n ` (and --project <path> for \\`check\\`).${portHint}`\n );\n } else if (command === 'schema' || command === 'init' || command === 'graph') {\n rest.push(arg);\n } else {\n throw new Error(`Unexpected argument: ${arg}. Usage: timber ${command} [--config <path>]`);\n }\n }\n\n if (command === 'graph' && rest.length > 1) {\n throw new Error(\n `timber graph takes one file. Usage: timber graph <file> [--json], or timber graph --all --json`\n );\n }\n\n return { command: command as Command, config, project, json, all, rest };\n}\n\n// ─── Command Implementations ─────────────────────────────────────────────────\n\n/** @internal Dependency injection for testing. */\nexport interface ViteDeps {\n createServer?: typeof import('vite').createServer;\n createBuilder?: typeof import('vite').createBuilder;\n preview?: typeof import('vite').preview;\n}\n\n/**\n * Start the Vite dev server with a supervisor that respawns the process\n * when timber.config.ts changes. Node's require cache for ESM modules\n * loaded via require() cannot be evicted, so an in-process restart\n * serves stale config. The supervisor forks a worker; when the worker\n * exits with RESTART_EXIT_CODE (set by timber-dev-server on config file\n * change), the supervisor spawns a fresh worker with a clean cache.\n *\n * The timber plugin binds the port immediately with a holding page\n * (in rootSync's config hook) so browsers see a \"starting...\" page\n * instead of ERR_CONNECTION_REFUSED during initialization.\n *\n * See design/21-dev-server.md, TIM-665, TIM-1444.\n */\nexport async function runDev(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n // Worker mode: run the dev server directly (forked by supervisor below)\n if (process.env.__TIMBER_DEV_WORKER) {\n const createServer = _deps?.createServer ?? (await import('vite')).createServer;\n const server = await createServer({\n configFile: options.config,\n });\n await server.listen();\n server.printUrls();\n return;\n }\n\n // Supervisor mode: fork a worker and respawn on config changes.\n const { fork } = await import('node:child_process');\n const { RESTART_EXIT_CODE } = await import('./plugins/dev-server.ts');\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(\n (resolve) => {\n const child = fork(process.argv[1], process.argv.slice(2), {\n stdio: 'inherit',\n env: { ...process.env, __TIMBER_DEV_WORKER: '1' },\n });\n\n // Forward signals to the child. SIGINT from Ctrl+C is sent to\n // the entire process group, but a targeted SIGTERM (from a\n // process manager or test teardown) only reaches the parent.\n const onSigint = () => child.kill('SIGINT');\n const onSigterm = () => child.kill('SIGTERM');\n process.on('SIGINT', onSigint);\n process.on('SIGTERM', onSigterm);\n\n child.on('exit', (code, signal) => {\n process.off('SIGINT', onSigint);\n process.off('SIGTERM', onSigterm);\n resolve({ code, signal });\n });\n }\n );\n\n if (result.code === RESTART_EXIT_CODE) {\n continue;\n }\n\n // Child was killed by a signal (e.g. SIGTERM) — re-raise on ourselves\n if (result.signal) {\n process.kill(process.pid, result.signal);\n }\n\n if (result.code != null && result.code !== 0) {\n process.exit(result.code);\n }\n\n break;\n }\n}\n\n/**\n * Run the production build using createBuilder + buildApp.\n * Direct build() calls do NOT trigger the RSC plugin's multi-environment\n * pipeline — createBuilder/buildApp is required.\n */\nexport async function runBuild(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n const createBuilder = _deps?.createBuilder ?? (await import('vite')).createBuilder;\n const builder = await createBuilder({\n configFile: options.config,\n });\n await builder.buildApp();\n}\n\n/**\n * Determine whether to use the adapter's preview or Vite's built-in preview.\n * Exported for testing — the actual runPreview function uses this internally.\n */\nexport function resolvePreviewStrategy(\n adapter: import('./adapters/types.ts').TimberPlatformAdapter | undefined\n): 'adapter' | 'vite' {\n if (adapter && typeof adapter.preview === 'function') {\n return 'adapter';\n }\n return 'vite';\n}\n\n/**\n * Serve the production build for local testing.\n * If the adapter provides a preview() method, it takes priority.\n * Otherwise falls back to Vite's built-in preview server.\n *\n * Uses the same loadTimberConfigFile loader as dev/build — one loader,\n * one set of module semantics. A config file that exists but fails to\n * load throws (naming the file) instead of silently degrading to plain\n * Vite preview. See TIM-1066.\n */\nexport async function runPreview(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n const { loadTimberConfigFile, resolveBuildDir } = await import('./plugin-context.ts');\n\n const root = process.cwd();\n const config = loadTimberConfigFile(root);\n const adapter = config?.adapter as\n | import('./adapters/types.ts').TimberPlatformAdapter\n | undefined;\n\n if (resolvePreviewStrategy(adapter) === 'adapter') {\n const buildDir = resolveBuildDir(root, config?.buildDir);\n const timberConfig = { output: (config?.output ?? 'server') as 'server' | 'static' };\n await adapter!.preview!(timberConfig, buildDir);\n return;\n }\n\n // Fallback: Vite's built-in preview server\n const preview = _deps?.preview ?? (await import('vite')).preview;\n const server = await preview({\n configFile: options.config,\n });\n server.printUrls();\n}\n\n/**\n * Validate routes and types without producing build output.\n *\n * Runs the route pass first — resolve the project through Vite, scan it, fail\n * on proven-fatal convention errors, print warnings, and regenerate the route\n * types — then `tsgo --noEmit`. Codegen has to precede the type pass: the\n * generated `.timber/*.d.ts` files are what type `<Link href>` and\n * `useSegmentParams()`, and a fresh clone has never produced them. See\n * `cli-check.ts` for the full reasoning.\n *\n * `--config` names the **Vite** config, as it does for every other command, so\n * an app that builds with a custom one is checkable the same way.\n * `--project` names the tsconfig handed to tsgo.\n */\nexport async function runCheck(options: CommandOptions, _deps?: CheckDeps): Promise<void> {\n const { checkRoutes, runTypeCheck } = await import('./cli-check.ts');\n const { root } = await checkRoutes({ viteConfigFile: options.config }, _deps);\n // tsgo runs from the root the route pass resolved, not from cwd — that is\n // where it just wrote the declarations tsgo has to read.\n await runTypeCheck({ root, config: options.project }, _deps);\n}\n\n// ─── Schema Subcommand ───────────────────────────────────────────────────────\n\n/**\n * `timber schema sync` — scan filesystem for dynamic segments, diff against\n * app/schema.ts, and append missing entries with type-appropriate codec defaults.\n *\n * See design/41-global-params.md §Auto-Scaffolding\n */\nexport async function runSchema(args: string[]): Promise<void> {\n const subcommand = args[0];\n\n if (subcommand !== 'sync') {\n throw new Error(\n `Unknown schema subcommand: ${subcommand ?? '(none)'}. Usage: timber schema sync`\n );\n }\n\n const { runSchemaSync, defaultCodecForSegment } = await import('./cli-schema-sync.ts');\n const result = runSchemaSync(process.cwd());\n\n if (result.filesystemSegments.length === 0) {\n console.log('[timber] No dynamic segments found in app/.');\n return;\n }\n\n if (result.created) {\n console.log(`[timber] Created app/schema.ts with ${result.addedSegments.length} segment(s):`);\n for (const seg of result.addedSegments) {\n console.log(` + ${seg}: ${defaultCodecForSegment(seg)}`);\n }\n return;\n }\n\n if (result.addedSegments.length === 0 && result.removedSegments.length === 0) {\n console.log('[timber] Schema is up to date — all segments are registered.');\n return;\n }\n\n if (result.addedSegments.length > 0) {\n console.log(`[timber] Added ${result.addedSegments.length} segment(s) to app/schema.ts:`);\n for (const seg of result.addedSegments) {\n console.log(` + ${seg}: ${defaultCodecForSegment(seg)}`);\n }\n }\n\n if (result.removedSegments.length > 0) {\n console.log(\n `[timber] Removed ${result.removedSegments.length} stale segment(s) from app/schema.ts:`\n );\n for (const seg of result.removedSegments) {\n console.log(` - ${seg}`);\n }\n }\n}\n\n// ─── Init Subcommand ─────────────────────────────────────────────────────────\n\n/**\n * `timber init` — write AGENTS.md (and a CLAUDE.md symlink to it) to the\n * project root so AI coding agents can discover timber.js documentation and\n * conventions.\n *\n * The content is read from the installed @timber-js/app package itself, so it\n * stays in sync with the installed version.\n */\nexport async function runInit(): Promise<void> {\n const { readFileSync, writeFileSync, existsSync, symlinkSync } = await import('node:fs');\n const { resolve, dirname } = await import('node:path');\n const { fileURLToPath } = await import('node:url');\n\n const root = process.cwd();\n const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');\n\n const agentSkillPath = resolve(packageRoot, 'agent-skill.md');\n if (!existsSync(agentSkillPath)) {\n throw new Error(\n 'agent-skill.md not found in @timber-js/app package. ' +\n 'This may be a development install — try reinstalling.'\n );\n }\n\n const agentSkill = readFileSync(agentSkillPath, 'utf-8');\n\n const agentsPath = resolve(root, 'AGENTS.md');\n const claudePath = resolve(root, 'CLAUDE.md');\n\n const wrote: string[] = [];\n\n if (existsSync(agentsPath)) {\n console.log('[timber] AGENTS.md already exists — skipping (delete it first to regenerate)');\n } else {\n writeFileSync(agentsPath, agentSkill);\n wrote.push('AGENTS.md');\n }\n\n if (existsSync(claudePath)) {\n console.log('[timber] CLAUDE.md already exists — skipping (delete it first to regenerate)');\n } else {\n try {\n // Relative target so the link survives the project directory moving.\n // The 'file' type argument is required for Windows (ignored elsewhere).\n symlinkSync('AGENTS.md', claudePath, 'file');\n wrote.push('CLAUDE.md (symlink)');\n } catch {\n // Windows without Developer Mode or admin rights can't create symlinks\n // (EPERM) — fall back to a plain pointer file.\n writeFileSync(claudePath, 'Read AGENTS.md for project context and conventions.\\n');\n wrote.push('CLAUDE.md');\n }\n }\n\n if (wrote.length > 0) {\n console.log(\n `[timber] Created ${wrote.join(' and ')} — AI agents can now discover timber.js docs.`\n );\n console.log('[timber] Full documentation: node_modules/@timber-js/app/docs/');\n } else {\n console.log('[timber] Nothing to do — both files already exist.');\n }\n}\n\n// ─── Main Entry Point ────────────────────────────────────────────────────────\n\nasync function main(): Promise<void> {\n const parsed = parseArgs(process.argv.slice(2));\n const options: CommandOptions = { config: parsed.config, project: parsed.project };\n\n switch (parsed.command) {\n case 'dev':\n await runDev(options);\n break;\n case 'build':\n await runBuild(options);\n break;\n case 'preview':\n await runPreview(options);\n break;\n case 'check':\n await runCheck(options);\n break;\n case 'schema':\n await runSchema(parsed.rest);\n break;\n case 'init':\n await runInit();\n break;\n case 'graph': {\n // Lazy import — the graph implementation (and, on a cache miss,\n // Vite + the crawl child) never loads for the other commands.\n const { runGraph } = await import('./analyze/graph-command.ts');\n await runGraph({\n file: parsed.rest[0],\n config: parsed.config,\n json: parsed.json,\n all: parsed.all,\n });\n break;\n }\n }\n}\n\n// Run main when executed as a CLI (not imported in tests).\n// The bin shim (bin/timber.mjs) does `import '../dist/cli.js'`, so\n// process.argv[1] points to the shim, not this file. We check both:\n// direct execution AND being imported by the timber bin shim.\nconst isDirectExecution =\n typeof process !== 'undefined' &&\n process.argv[1] &&\n (import.meta.url.endsWith(process.argv[1]) ||\n process.argv[1].endsWith('bin/timber.mjs') ||\n process.argv[1].endsWith('bin/timber'));\n\nif (isDirectExecution) {\n main().catch((err) => {\n console.error(err.message);\n process.exit(1);\n });\n}\n"],"mappings":"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"}
@@ -50,8 +50,12 @@ export declare function hideCompilingOverlay(): void;
50
50
  *
51
51
  * The server receives 'timber:client-error' events, and echoes them
52
52
  * back as Vite '{ type: "error" }' payloads to trigger the overlay.
53
+ *
54
+ * Errors that fire before the Vite HMR WebSocket has connected are
55
+ * buffered and flushed once the connection is established, so
56
+ * `hot.send()` never throws `SendBeforeConnectError`. See TIM-1509.
53
57
  */
54
- export declare function setupClientErrorForwarding(hot: Pick<HotInterface, 'send'>): void;
58
+ export declare function setupClientErrorForwarding(hot: HotInterface): void;
55
59
  /**
56
60
  * Detect errors whose `digest` marks them as a framework control-flow signal
57
61
  * (RedirectSignal, DenySignal). These must never surface as dev overlays —
@@ -1 +1 @@
1
- {"version":3,"file":"browser-dev.d.ts","sourceRoot":"","sources":["../../src/client/browser-dev.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,gDAAgD;AAChD,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAC1D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;CAC1C;AAqDD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,GAAG,IAAI,CAwBxE;AAoED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAmB3C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAc3C;AAID;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,GAAG,IAAI,CAoChF;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAW9D"}
1
+ {"version":3,"file":"browser-dev.d.ts","sourceRoot":"","sources":["../../src/client/browser-dev.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,gDAAgD;AAChD,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAC1D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;CAC1C;AAqDD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,GAAG,IAAI,CAwBxE;AAoED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAmB3C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAc3C;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAgElE;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAW9D"}