@timber-js/app 0.2.0-alpha.210 → 0.2.0-alpha.212

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 (158) hide show
  1. package/agent-skill.md +10 -5
  2. package/dist/_chunks/{actions-Rjk4htmA.js → actions-CCdnVtWm.js} +8 -6
  3. package/dist/_chunks/actions-CCdnVtWm.js.map +1 -0
  4. package/dist/_chunks/{als-registry-DaxkVjt5.js → als-registry-BZqHCtq-.js} +2 -4
  5. package/dist/_chunks/als-registry-BZqHCtq-.js.map +1 -0
  6. package/dist/_chunks/{cache-api-DGdYfNJn.js → cache-api-LA3sBpUS.js} +5 -5
  7. package/dist/_chunks/{cache-api-DGdYfNJn.js.map → cache-api-LA3sBpUS.js.map} +1 -1
  8. package/dist/_chunks/{chains-BoO51joc.js → chains-BfoPFraI.js} +5 -146
  9. package/dist/_chunks/chains-BfoPFraI.js.map +1 -0
  10. package/dist/_chunks/classify-BT66U83D.js +147 -0
  11. package/dist/_chunks/classify-BT66U83D.js.map +1 -0
  12. package/dist/_chunks/{cli-check-ajNY3B2e.js → cli-check-BfQ54-UJ.js} +3 -3
  13. package/dist/_chunks/{cli-check-ajNY3B2e.js.map → cli-check-BfQ54-UJ.js.map} +1 -1
  14. package/dist/_chunks/{cli-schema-sync-D2eI8jEg.js → cli-schema-sync-czh2dsLs.js} +2 -2
  15. package/dist/_chunks/{cli-schema-sync-D2eI8jEg.js.map → cli-schema-sync-czh2dsLs.js.map} +1 -1
  16. package/dist/_chunks/client-dep-entries-CQwpb8dI.js +66 -0
  17. package/dist/_chunks/client-dep-entries-CQwpb8dI.js.map +1 -0
  18. package/dist/_chunks/{convention-lint-DLmhGsRS.js → convention-lint-BEVW4EID.js} +3 -3
  19. package/dist/_chunks/{convention-lint-DLmhGsRS.js.map → convention-lint-BEVW4EID.js.map} +1 -1
  20. package/dist/_chunks/{dev-server-v97rQH4b.js → dev-server-FKxptbnI.js} +2 -2
  21. package/dist/_chunks/{dev-server-v97rQH4b.js.map → dev-server-FKxptbnI.js.map} +1 -1
  22. package/dist/_chunks/{json-lossy-check-CVuRs2hG.js → json-lossy-check-C8zBY2uZ.js} +2 -2
  23. package/dist/_chunks/{json-lossy-check-CVuRs2hG.js.map → json-lossy-check-C8zBY2uZ.js.map} +1 -1
  24. package/dist/_chunks/{live-graph-9cSnn_h9.js → live-graph-C_4v-fHv.js} +4 -3
  25. package/dist/_chunks/{live-graph-9cSnn_h9.js.map → live-graph-C_4v-fHv.js.map} +1 -1
  26. package/dist/_chunks/{logger-BP0LN6vP.js → logger-CbLdcy-W.js} +2 -2
  27. package/dist/_chunks/{logger-BP0LN6vP.js.map → logger-CbLdcy-W.js.map} +1 -1
  28. package/dist/_chunks/{poison-scan-vGV7Re0B.js → poison-scan-C92liMAr.js} +2 -4
  29. package/dist/_chunks/poison-scan-C92liMAr.js.map +1 -0
  30. package/dist/_chunks/{scanner-DmqdxzbW.js → scanner-CQt12vE2.js} +2 -2
  31. package/dist/_chunks/{scanner-DmqdxzbW.js.map → scanner-CQt12vE2.js.map} +1 -1
  32. package/dist/_chunks/{sizeof-BM1409x2.js → sizeof-QPE5nd3u.js} +2 -2
  33. package/dist/_chunks/{sizeof-BM1409x2.js.map → sizeof-QPE5nd3u.js.map} +1 -1
  34. package/dist/_chunks/{walkers-Czu2jXFq.js → walkers-DAT4avhZ.js} +3 -3
  35. package/dist/_chunks/{walkers-Czu2jXFq.js.map → walkers-DAT4avhZ.js.map} +1 -1
  36. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  37. package/dist/analyze/classify.d.ts +2 -0
  38. package/dist/analyze/classify.d.ts.map +1 -1
  39. package/dist/analyze/crawl-entry.js +3 -2
  40. package/dist/analyze/crawl-entry.js.map +1 -1
  41. package/dist/analyze/graph-command.js +2 -2
  42. package/dist/analyze/poison-scan.d.ts.map +1 -1
  43. package/dist/cache/index.js +2 -2
  44. package/dist/cache/stores/memory.js +1 -1
  45. package/dist/cdn/workers-cache-purge.js +1 -1
  46. package/dist/cli.js +3 -3
  47. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  48. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  49. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  50. package/dist/client/form.d.ts +17 -64
  51. package/dist/client/form.d.ts.map +1 -1
  52. package/dist/client/index.d.ts +2 -2
  53. package/dist/client/index.d.ts.map +1 -1
  54. package/dist/client/index.js +23 -51
  55. package/dist/client/index.js.map +1 -1
  56. package/dist/client/internal.js +25 -25
  57. package/dist/client/internal.js.map +1 -1
  58. package/dist/client/navigation-api.d.ts +25 -63
  59. package/dist/client/navigation-api.d.ts.map +1 -1
  60. package/dist/client/router-lifecycle.d.ts +17 -11
  61. package/dist/client/router-lifecycle.d.ts.map +1 -1
  62. package/dist/client/router-pipeline.d.ts +0 -1
  63. package/dist/client/router-pipeline.d.ts.map +1 -1
  64. package/dist/client/router-types.d.ts +14 -45
  65. package/dist/client/router-types.d.ts.map +1 -1
  66. package/dist/client/router.d.ts.map +1 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +44 -7
  69. package/dist/index.js.map +1 -1
  70. package/dist/plugins/client-dep-entries.d.ts +44 -0
  71. package/dist/plugins/client-dep-entries.d.ts.map +1 -0
  72. package/dist/plugins/content.d.ts.map +1 -1
  73. package/dist/plugins/entries.d.ts +6 -0
  74. package/dist/plugins/entries.d.ts.map +1 -1
  75. package/dist/plugins/shims.d.ts.map +1 -1
  76. package/dist/routing/index.js +2 -2
  77. package/dist/rsc-runtime/rsc.d.ts +1 -1
  78. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  79. package/dist/server/action-client.d.ts +9 -5
  80. package/dist/server/action-client.d.ts.map +1 -1
  81. package/dist/server/action-handler.d.ts +27 -8
  82. package/dist/server/action-handler.d.ts.map +1 -1
  83. package/dist/server/als-registry.d.ts +22 -2
  84. package/dist/server/als-registry.d.ts.map +1 -1
  85. package/dist/server/client-error-message.d.ts +12 -0
  86. package/dist/server/client-error-message.d.ts.map +1 -0
  87. package/dist/server/form-state-embed.d.ts +32 -0
  88. package/dist/server/form-state-embed.d.ts.map +1 -0
  89. package/dist/server/index.d.ts +0 -2
  90. package/dist/server/index.d.ts.map +1 -1
  91. package/dist/server/index.js +7 -40
  92. package/dist/server/index.js.map +1 -1
  93. package/dist/server/internal.js +10 -6
  94. package/dist/server/internal.js.map +1 -1
  95. package/dist/server/logger.d.ts +1 -0
  96. package/dist/server/logger.d.ts.map +1 -1
  97. package/dist/server/pipeline.d.ts +20 -6
  98. package/dist/server/pipeline.d.ts.map +1 -1
  99. package/dist/server/request-context.d.ts +27 -2
  100. package/dist/server/request-context.d.ts.map +1 -1
  101. package/dist/server/route-element-builder.d.ts.map +1 -1
  102. package/dist/server/rsc-entry/action-dispatcher.d.ts +6 -5
  103. package/dist/server/rsc-entry/action-dispatcher.d.ts.map +1 -1
  104. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  105. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  106. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  107. package/dist/server/ssr-bridge-types.d.ts +8 -0
  108. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  109. package/dist/server/ssr-entry.d.ts.map +1 -1
  110. package/dist/server/ssr-render.d.ts +3 -0
  111. package/dist/server/ssr-render.d.ts.map +1 -1
  112. package/docs/api/31-api-client.mdx +9 -3
  113. package/docs/learn/08-forms-and-actions.mdx +21 -28
  114. package/package.json +1 -1
  115. package/src/analyze/classify.ts +1 -1
  116. package/src/analyze/poison-scan.ts +1 -4
  117. package/src/client/browser-entry/action-dispatch.ts +74 -18
  118. package/src/client/browser-entry/hydrate.ts +18 -0
  119. package/src/client/browser-entry/router-init.ts +4 -18
  120. package/src/client/form.tsx +33 -98
  121. package/src/client/index.ts +2 -2
  122. package/src/client/navigation-api.ts +47 -173
  123. package/src/client/navigation-transition.ts +2 -2
  124. package/src/client/router-lifecycle.ts +40 -20
  125. package/src/client/router-pipeline.ts +3 -9
  126. package/src/client/router-types.ts +14 -49
  127. package/src/client/router.ts +38 -58
  128. package/src/index.ts +21 -2
  129. package/src/plugins/client-dep-entries.ts +78 -0
  130. package/src/plugins/content.ts +26 -1
  131. package/src/plugins/entries.ts +7 -0
  132. package/src/plugins/shims.ts +0 -2
  133. package/src/rsc-runtime/rsc.ts +3 -0
  134. package/src/rsc-runtime/vendor-types.d.ts +14 -0
  135. package/src/server/action-client.ts +18 -20
  136. package/src/server/action-handler.ts +133 -95
  137. package/src/server/als-registry.ts +23 -9
  138. package/src/server/client-error-message.ts +18 -0
  139. package/src/server/form-state-embed.ts +63 -0
  140. package/src/server/index.ts +0 -4
  141. package/src/server/logger.ts +6 -1
  142. package/src/server/pipeline.ts +27 -8
  143. package/src/server/request-context.ts +39 -2
  144. package/src/server/route-element-builder.ts +12 -1
  145. package/src/server/rsc-entry/action-dispatcher.ts +37 -34
  146. package/src/server/rsc-entry/error-renderer.ts +2 -1
  147. package/src/server/rsc-entry/rsc-stream.ts +20 -11
  148. package/src/server/rsc-entry/ssr-renderer.ts +14 -1
  149. package/src/server/ssr-bridge-types.ts +9 -0
  150. package/src/server/ssr-entry.ts +1 -0
  151. package/src/server/ssr-render.ts +5 -0
  152. package/dist/_chunks/actions-Rjk4htmA.js.map +0 -1
  153. package/dist/_chunks/als-registry-DaxkVjt5.js.map +0 -1
  154. package/dist/_chunks/chains-BoO51joc.js.map +0 -1
  155. package/dist/_chunks/poison-scan-vGV7Re0B.js.map +0 -1
  156. package/dist/server/form-flash.d.ts +0 -78
  157. package/dist/server/form-flash.d.ts.map +0 -1
  158. package/src/server/form-flash.ts +0 -89
@@ -1,148 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { t as isRouteConventionFile } from "./scanner-DmqdxzbW.js";
3
- import { a as isDynamicMetadataExtension, s as isStaticMetadataExtension } from "./metadata-routes-DSDjM_hJ.js";
4
- import { normalizePath, parseAst } from "vite";
5
- import { basename, extname, isAbsolute, relative } from "node:path";
6
- import { hasDirective } from "@vitejs/plugin-rsc/transforms";
7
- //#region src/analyze/classify.ts
8
- /**
9
- * classify — environment-graph classification for project modules.
10
- *
11
- * The one classification module every frontend shares (design/47): the
12
- * `timber graph` CLI, the dev-server endpoint, the VS Code extension,
13
- * and the cold crawl must never disagree about a file's environment.
14
- * Classification intersects RSC/SSR module-graph membership with
15
- * first-directive detection to produce the decided taxonomy:
16
- *
17
- * - `server` — RSC graph only
18
- * - `client-boundary` — has `'use client'`
19
- * - `client-internal` — reachable only behind a boundary, no directive
20
- * - `shared` — instantiated in both environments; INFO, never a
21
- * warning — shared-by-design is a legitimate pattern
22
- * - `server-action` — has `'use server'`
23
- * - `unreferenced` — in no graph and no provisional signal; files with
24
- * a directive or filename convention get that
25
- * provisional kind instead, marked `pending`
26
- *
27
- * The browser (`client`) Vite graph stays empty until a real browser
28
- * requests modules, so membership is RSC × SSR — the SSR graph is a
29
- * faithful proxy for the client tree (design/47 §8, with the frontier
30
- * caveat below).
31
- *
32
- * Design docs: 47-module-environment-tooling.md (taxonomy, §8),
33
- * 43-analyze-cli.md (directive detection rules).
34
- */
35
- /** File extensions eligible for directive detection. */
36
- var PARSEABLE_EXTENSIONS = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;
37
- /**
38
- * Parse language for a source file, from its extension. Never force
39
- * `tsx` onto plain `.ts`: JSX-ambiguous TypeScript such as
40
- * `const identity = <T>(value: T) => value` is valid TS but rejected
41
- * by a TSX parse, and the resulting parse failure would silently drop
42
- * the file's directive and poison signals. Plain JS parses as `jsx`
43
- * (JSX-in-.js is common in Vite projects and JS has no generic-arrow
44
- * ambiguity for the x-syntax to break).
45
- */
46
- function langForFile(file) {
47
- if (/\.tsx$/.test(file)) return "tsx";
48
- if (/\.[mc]?ts$/.test(file)) return "ts";
49
- return "jsx";
50
- }
51
- /**
52
- * Detect the first directive of a module's original source.
53
- *
54
- * Never string-match or regex directives (design/43): `hasDirective`
55
- * from @vitejs/plugin-rsc checks for the `directive` property on
56
- * ExpressionStatements — the same check the RSC plugin runs to decide
57
- * boundaries, so classification agrees with what the framework actually
58
- * does with the file. The directive must be in prologue position (before
59
- * any import or statement) per the ECMAScript spec. Non-JS sources
60
- * (CSS, MDX, assets) and sources that fail to parse return null — no
61
- * directive is a safe default; classification then falls back to graph
62
- * membership.
63
- */
64
- function detectDirective(code, file) {
65
- if (!PARSEABLE_EXTENSIONS.test(file)) return null;
66
- let body;
67
- try {
68
- body = parseAst(code, { lang: langForFile(file) }).body;
69
- } catch {
70
- return null;
71
- }
72
- if (hasDirective(body, "use client")) return "use client";
73
- if (hasDirective(body, "use server")) return "use server";
74
- return null;
75
- }
76
- /**
77
- * Whether a file is a route-convention source the framework itself
78
- * imports (page/layout/route/… plus metadata routes). Conventions are
79
- * derived from the routing scanner and the metadata route table — never
80
- * a local duplicate list (drift lesson). Each family carries the
81
- * scanner's own extension rules.
82
- *
83
- * Shared by the provisional classifier below and by chains.ts, whose
84
- * RSC-side walks terminate at the first route-convention file
85
- * (design/47 §7) — the two must never disagree about what counts.
86
- *
87
- * `pageExtensions` mirrors the scanner's parameter: callers that have
88
- * resolved the app config pass the configured set (a `page.mdx` app);
89
- * callers without config access fall back to the defaults and tolerate
90
- * the mismatch.
91
- */
92
- function isConventionSourceFile(file, pageExtensions) {
93
- const ext = extname(file).slice(1);
94
- const base = basename(file, `.${ext}`);
95
- return isRouteConventionFile(base, ext, pageExtensions) || isStaticMetadataExtension(base, ext) || isDynamicMetadataExtension(base, ext);
96
- }
97
- /**
98
- * Provisional kind for a file no graph has confirmed (design/47 §2):
99
- * directive first, then filename convention, else `unreferenced`.
100
- */
101
- function provisionalKind(file, directive, ctx) {
102
- if (directive === "use client") return "client-boundary";
103
- if (directive === "use server") return "server-action";
104
- if (ctx?.appDir && !normalizePath(file).startsWith(normalizePath(ctx.appDir) + "/")) return "unreferenced";
105
- if (isConventionSourceFile(file, ctx?.pageExtensions)) return "server";
106
- return "unreferenced";
107
- }
108
- /**
109
- * Classify one module from its observation. Pure — all frontends call
110
- * this with membership data from their own graph source.
111
- */
112
- function classifyModule(obs, ctx) {
113
- const referenced = obs.inRsc || obs.inSsr || obs.inClient === true;
114
- const subtreeUnknown = obs.inSsr && obs.ssrTransformed === false;
115
- const envs = {
116
- rsc: obs.inRsc,
117
- ssr: obs.inSsr,
118
- client: obs.inClient === true
119
- };
120
- if (!referenced) return {
121
- file: obs.file,
122
- kind: provisionalKind(obs.file, obs.directive, ctx),
123
- envs,
124
- pending: true,
125
- subtreeUnknown: false
126
- };
127
- let kind;
128
- if (obs.directive === "use client") kind = "client-boundary";
129
- else if (obs.directive === "use server") kind = "server-action";
130
- else if (obs.inRsc && obs.inSsr) kind = "shared";
131
- else if (obs.inRsc) kind = "server";
132
- else kind = "client-internal";
133
- return {
134
- file: obs.file,
135
- kind,
136
- envs,
137
- pending: false,
138
- subtreeUnknown
139
- };
140
- }
141
- /** Classify a batch of observations, sorted by file for stable output. */
142
- function classifyModules(observations, ctx) {
143
- return [...observations].map((obs) => classifyModule(obs, ctx)).sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
144
- }
145
- //#endregion
2
+ import { a as isConventionSourceFile } from "./classify-BT66U83D.js";
3
+ import { normalizePath } from "vite";
4
+ import { isAbsolute, relative } from "node:path";
146
5
  //#region src/analyze/chains.ts
147
6
  /**
148
7
  * chains — boundary-aware import-chain stitching (the "why" answer).
@@ -408,6 +267,6 @@ function formatChains(chains, projectRoot) {
408
267
  return lines.join("\n");
409
268
  }
410
269
  //#endregion
411
- export { detectDirective as a, classifyModules as i, stitchImportChains as n, langForFile as o, classifyModule as r, chains_exports as t };
270
+ export { stitchImportChains as n, chains_exports as t };
412
271
 
413
- //# sourceMappingURL=chains-BoO51joc.js.map
272
+ //# sourceMappingURL=chains-BfoPFraI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chains-BfoPFraI.js","names":[],"sources":["../../src/analyze/chains.ts"],"sourcesContent":["/**\n * chains — boundary-aware import-chain stitching (the \"why\" answer).\n *\n * The human-meaningful import chain for a client-reachable module is a\n * JOIN of two graphs: RSC-graph importers ABOVE the `'use client'`\n * boundary and SSR-graph importers BELOW it. Walking `importers` naively\n * in one graph produces virtual-entry noise (`virtual:vite-rsc/css`)\n * instead of semantic importers — the design/47 §7 findings this module\n * bakes in:\n *\n * - RSC-side walks terminate at `virtual:timber-route-manifest` or the\n * first route-convention file (page/layout/…), never the literal RSC\n * entry — route files' importer is the manifest, whose importers are\n * framework internals; the manifest is the semantic root.\n * - `\\0virtual:vite-rsc/*` importers are filtered so they can never\n * surface in a chain.\n * - When multiple boundaries import the target, one chain is reported\n * PER BOUNDARY — a single BFS shortest path would pick arbitrarily.\n *\n * The chain/hop shapes and text formatting are shared with doc 43's\n * `timber why` so both commands emit the same contract: hops carry\n * `{ id, directive, boundary }`, and one shortest chain is emitted per\n * (boundary, reaching route entry) pair — doc 43's \"one shortest path\n * per reaching entry\", crossed with §7's per-boundary rule.\n *\n * Design docs: 47-module-environment-tooling.md §7, 43-analyze-cli.md\n * §Output Formats.\n */\n\nimport { relative, isAbsolute } from 'node:path';\nimport { normalizePath } from 'vite';\nimport { isConventionSourceFile, type Directive } from './classify.ts';\n\n/**\n * Duck-typed view of a Vite environment module node — a supertype of\n * `EnvironmentModuleNode` for the members the walk reads, so live\n * dev-server graphs and crawl graphs both satisfy it structurally.\n */\nexport interface ChainModuleNode {\n id: string | null;\n url: string;\n file: string | null;\n importers: Set<ChainModuleNode>;\n}\n\n/** Duck-typed view of a Vite environment module graph. */\nexport interface ChainGraph {\n getModulesByFile(file: string): Set<ChainModuleNode> | undefined;\n}\n\n/** Per-hop directive in doc 43's JSON vocabulary. */\nexport type HopDirective = 'client' | 'server' | null;\n\nexport interface ChainHop {\n /** Absolute file path, or a virtual module id printed verbatim. */\n id: string;\n directive: HopDirective;\n /** True only on the hop that crosses the server→client boundary. */\n boundary: boolean;\n}\n\nexport interface ImportChain {\n /**\n * Semantic root: a route-convention file, or the route-manifest\n * virtual id when the walk reached it without passing one. Null when\n * the RSC-side walk dead-ended (no root found) — the chain then\n * starts at its deepest known importer and consumers must present it\n * as partial.\n */\n entry: string | null;\n /**\n * The `'use client'` file this chain crosses; null for a pure\n * server-side chain.\n */\n boundary: string | null;\n /** Hops in forward import order, entry → … → target. */\n hops: ChainHop[];\n}\n\nexport interface ChainContext {\n rsc: ChainGraph;\n ssr: ChainGraph;\n /** Absolute app directory — scopes route-convention root detection. */\n appDir: string;\n /**\n * Configured pageExtensions, when the caller has resolved the app\n * config — a `page.mdx` app's route files are only roots with it.\n * Omitted → scanner defaults.\n */\n pageExtensions?: readonly string[];\n /**\n * First directive of a file's ORIGINAL source (design/43: never the\n * transformed output, where directives are stripped/proxied).\n */\n getDirective(file: string): Directive;\n}\n\n/**\n * The route manifest appears under several ids depending on environment\n * and build phase: `virtual:timber-route-manifest`,\n * `\\0virtual:timber-route-manifest`, and a root-prefixed form\n * (plugins/routing.ts) — match by substring.\n */\nfunction isRouteManifest(key: string): boolean {\n return key.includes('virtual:timber-route-manifest');\n}\n\n/** plugin-rsc internal virtuals (observed: virtual:vite-rsc/css). */\nfunction isViteRscInternal(key: string): boolean {\n return key.includes('virtual:vite-rsc/');\n}\n\n/**\n * File-level vertex key for a node: the file path when Vite resolved\n * one, else the virtual id. Multiple nodes can exist per file (query\n * variants); collapsing to the file keeps chains at source granularity.\n */\nfunction vertexKey(node: ChainModuleNode): string | null {\n return node.file ?? node.id ?? node.url ?? null;\n}\n\n/**\n * Separator for path signatures. Sorts below every printable character\n * so comparing joined signatures equals comparing key sequences.\n */\nconst SIG_SEP = '\\u0000';\n\n/**\n * Backwards level-order BFS over `importers` from `start`, collapsing\n * nodes to file-level vertices and filtering plugin-rsc internals.\n *\n * Shortest-path ties are broken GLOBALLY: each vertex's predecessor is\n * the candidate whose full path signature (the id sequence from the\n * target outward) is lexicographically smallest — per-frontier\n * candidate selection, not merely per-parent importer sorting, so\n * reconverging equal-length branches still yield the advertised\n * representative path (doc 43 §Tie-Breaking).\n *\n * `stopAt` marks terminal vertices. By default their importers are not\n * expanded (RSC walks must not proceed past a route root into\n * framework internals); with `continuePastTerminals` the traversal\n * records the terminal and keeps walking — the SSR walk needs this so\n * an outer boundary that imports an inner boundary is still\n * discovered (one chain per boundary).\n */\nfunction walkImporters(\n graph: ChainGraph,\n start: string,\n stopAt: (key: string) => boolean,\n continuePastTerminals = false\n): { predecessors: Map<string, string | null>; terminals: string[] } {\n const startNodes = graph.getModulesByFile(start);\n const predecessors = new Map<string, string | null>();\n const terminals: string[] = [];\n if (!startNodes || startNodes.size === 0) return { predecessors, terminals };\n\n predecessors.set(start, null);\n const signatures = new Map<string, string>([[start, start]]);\n const nodesOf = new Map<string, ChainModuleNode[]>([[start, [...startNodes]]]);\n let frontier = [start];\n\n while (frontier.length > 0) {\n interface Candidate {\n sig: string;\n parent: string;\n observed: ChainModuleNode[];\n }\n const candidates = new Map<string, Candidate>();\n for (const key of frontier) {\n if (stopAt(key)) {\n terminals.push(key);\n if (!continuePastTerminals) continue;\n }\n for (const node of nodesOf.get(key) ?? []) {\n for (const importer of node.importers) {\n const importerKey = vertexKey(importer);\n if (!importerKey || isViteRscInternal(importerKey)) continue;\n if (predecessors.has(importerKey)) continue;\n // Every frontier vertex has a signature (set when its level\n // was fixed); the fallback only satisfies the type.\n const sig = (signatures.get(key) ?? key) + SIG_SEP + importerKey;\n const existing = candidates.get(importerKey);\n if (!existing) {\n candidates.set(importerKey, { sig, parent: key, observed: [importer] });\n } else {\n // Keep every observed node object (virtual ids have no file\n // index to re-fetch from), but the smallest signature wins\n // the predecessor slot.\n existing.observed.push(importer);\n if (sig < existing.sig) {\n existing.sig = sig;\n existing.parent = key;\n }\n }\n }\n }\n }\n // Fix the next level, ordered by signature so terminal discovery\n // order — and therefore chain order — is deterministic.\n const next = [...candidates.entries()].sort(([, a], [, b]) =>\n a.sig < b.sig ? -1 : a.sig > b.sig ? 1 : 0\n );\n frontier = [];\n for (const [importerKey, candidate] of next) {\n predecessors.set(importerKey, candidate.parent);\n signatures.set(importerKey, candidate.sig);\n // Re-fetch the file's FULL node set from the graph: the importer\n // edges seen so far may cover only one of several nodes per file\n // (query variants — ?direct, ?v=…), and expanding a partial set\n // would silently drop the sibling nodes' importers and dead-end\n // the chain. Virtual ids have no file index; use observed nodes.\n const fullNodes = graph.getModulesByFile(importerKey);\n nodesOf.set(\n importerKey,\n fullNodes && fullNodes.size > 0 ? [...fullNodes] : candidate.observed\n );\n frontier.push(importerKey);\n }\n }\n return { predecessors, terminals };\n}\n\n/** Follow predecessors from `from` back to the walk's start vertex. */\nfunction pathToStart(predecessors: Map<string, string | null>, from: string): string[] {\n const path: string[] = [];\n for (let key: string | null = from; key !== null; key = predecessors.get(key) ?? null) {\n path.push(key);\n }\n return path;\n}\n\nfunction isRouteRoot(key: string, ctx: ChainContext): boolean {\n // Manifest matching applies to virtual ids only — a real file whose\n // path happens to contain the substring must not become a root.\n if (!isAbsolute(key) && isRouteManifest(key)) return true;\n if (!isAbsolute(key)) return false;\n if (key !== ctx.appDir && !key.startsWith(ctx.appDir + '/')) return false;\n return isConventionSourceFile(key, ctx.pageExtensions);\n}\n\n/**\n * RSC-side walk: importers of `file` in the RSC graph up to the\n * semantic roots. Returns one chain PER reaching route entry (doc 43:\n * \"one shortest path per reaching entry\"), each with hops in forward\n * order (root → … → file), in discovery order (shallowest first,\n * signature tie-break). When no root was reached, a single partial\n * result (entry: null) carrying the deepest known path.\n */\nfunction rscSideChains(\n file: string,\n ctx: ChainContext\n): Array<{ entry: string | null; files: string[] }> {\n const { predecessors, terminals } = walkImporters(ctx.rsc, file, (key) => isRouteRoot(key, ctx));\n if (terminals.length > 0) {\n return terminals.map((root) => ({ entry: root, files: pathToStart(predecessors, root) }));\n }\n if (predecessors.size <= 1) {\n // Nothing above the file at all (or file absent from the graph).\n return [{ entry: null, files: [file] }];\n }\n // Dead end — no root found. Report from the deepest vertex on the\n // last-discovered shortest path so the partial chain is still useful.\n const keys = [...predecessors.keys()];\n const deepest = keys[keys.length - 1];\n return [{ entry: null, files: pathToStart(predecessors, deepest) }];\n}\n\n/** Map a source directive onto the doc-43 hop vocabulary. */\nfunction hopDirective(key: string, ctx: ChainContext): HopDirective {\n if (!isAbsolute(key)) return null;\n const directive = ctx.getDirective(key);\n if (directive === 'use client') return 'client';\n if (directive === 'use server') return 'server';\n return null;\n}\n\nfunction toChain(\n entry: string | null,\n boundary: string | null,\n files: string[],\n ctx: ChainContext\n): ImportChain {\n return {\n entry,\n boundary,\n hops: files.map((key) => ({\n id: key,\n directive: hopDirective(key, ctx),\n boundary: boundary !== null && key === boundary,\n })),\n };\n}\n\n/**\n * Stitch the import chain(s) for a target file.\n *\n * - Target in the SSR graph (client-side): walk SSR importers up to\n * each `'use client'` boundary that the RSC graph knows (nested\n * boundaries only reachable from other client components are walked\n * THROUGH — they exist in no RSC graph, so the chain must continue\n * to a boundary a server component actually references), then walk\n * RSC importers from that boundary to the semantic root. One chain\n * per boundary.\n * - Target in the RSC graph without `'use client'`: a pure server\n * chain (boundary: null). Shared modules get both kinds.\n * - Target IS an RSC-known boundary: an RSC-side chain from the target\n * itself, PLUS any outer-boundary chains the SSR walk finds — a\n * boundary imported by another boundary is pulled in by both. A\n * nested `'use client'` target the RSC graph does NOT know is\n * handled like any client-internal file — the SSR walk continues to\n * the boundary a server component references.\n */\nexport function stitchImportChains(rawTargetFile: string, rawCtx: ChainContext): ImportChain[] {\n // Vite module-node `file` values are slash-normalized even on\n // Windows, while node:path targets/appDir arrive backslashed there —\n // normalize both once so graph lookups and containment checks agree\n // (normalizePath only rewrites separators on Windows, so posix paths\n // pass through untouched).\n const targetFile = normalizePath(rawTargetFile);\n const ctx: ChainContext = { ...rawCtx, appDir: normalizePath(rawCtx.appDir) };\n\n const chains: ImportChain[] = [];\n const targetDirective = ctx.getDirective(targetFile);\n const targetInRsc = (ctx.rsc.getModulesByFile(targetFile)?.size ?? 0) > 0;\n\n if (targetDirective === 'use client' && targetInRsc) {\n for (const above of rscSideChains(targetFile, ctx)) {\n chains.push(toChain(above.entry, targetFile, above.files, ctx));\n }\n }\n\n // Client-side chains: one per RSC-known boundary above the target.\n const isRscKnownBoundary = (key: string): boolean =>\n isAbsolute(key) &&\n ctx.getDirective(key) === 'use client' &&\n (ctx.rsc.getModulesByFile(key)?.size ?? 0) > 0;\n\n // continuePastTerminals: an outer boundary that imports an inner\n // boundary must still be discovered — one chain per boundary.\n const ssrWalk = walkImporters(ctx.ssr, targetFile, isRscKnownBoundary, true);\n for (const boundary of [...ssrWalk.terminals].sort()) {\n // A boundary target records itself as a terminal; its own chain is\n // the RSC-side one already pushed above.\n if (boundary === targetFile) continue;\n const below = pathToStart(ssrWalk.predecessors, boundary);\n for (const above of rscSideChains(boundary, ctx)) {\n // Stitch, dropping the duplicated boundary vertex from the RSC side.\n chains.push(toChain(above.entry, boundary, [...above.files.slice(0, -1), ...below], ctx));\n }\n }\n\n // Pure server chains when the RSC graph reaches the target directly.\n if (targetInRsc && targetDirective !== 'use client') {\n for (const above of rscSideChains(targetFile, ctx)) {\n chains.push(toChain(above.entry, null, above.files, ctx));\n }\n }\n\n // SSR-side dead end with nothing else to show: mirror the RSC side's\n // honesty and emit a partial chain from the deepest importer instead\n // of returning [] as if the file were in no graph. A singleton walk\n // (target in the SSR graph with no importers at all) still emits its\n // one-hop partial — graph membership is information.\n if (chains.length === 0 && ssrWalk.predecessors.size > 0) {\n const keys = [...ssrWalk.predecessors.keys()];\n const deepest = keys[keys.length - 1];\n chains.push(toChain(null, null, pathToStart(ssrWalk.predecessors, deepest), ctx));\n }\n\n return chains;\n}\n\n/**\n * Render chains in doc 43's text shape — shared by `timber graph` and\n * `timber why` so both commands print identically:\n *\n * ```\n * app/page.tsx\n * → app/nav.tsx ← 'use client' [boundary]\n * → src/lib/icons/chevron.ts\n * ```\n *\n * Paths under `projectRoot` print relative; virtual ids print verbatim\n * (doc 43 §Relative-vs-Absolute Paths). A partial chain (entry: null)\n * is prefixed with an honest marker instead of silently posing as\n * complete.\n */\nexport function formatChains(chains: ImportChain[], projectRoot: string): string {\n const root = normalizePath(projectRoot);\n const lines: string[] = [];\n for (const chain of chains) {\n if (lines.length > 0) lines.push('');\n if (chain.entry === null) {\n lines.push(' (partial chain — no route root found)');\n }\n chain.hops.forEach((hop, index) => {\n const display =\n isAbsolute(hop.id) && hop.id.startsWith(root + '/') ? relative(root, hop.id) : hop.id;\n const indent = ' '.repeat(index + 1);\n const arrow = index === 0 ? '' : '→ ';\n let annotation = '';\n if (hop.directive === 'client') {\n annotation = ` ← 'use client'${hop.boundary ? ' [boundary]' : ''}`;\n } else if (hop.directive === 'server') {\n annotation = ` ← 'use server'`;\n }\n lines.push(`${indent}${arrow}${display}${annotation}`);\n });\n }\n return lines.join('\\n');\n}\n"],"mappings":"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"}
@@ -0,0 +1,147 @@
1
+ import { t as isRouteConventionFile } from "./scanner-CQt12vE2.js";
2
+ import { a as isDynamicMetadataExtension, s as isStaticMetadataExtension } from "./metadata-routes-DSDjM_hJ.js";
3
+ import { normalizePath, parseAst } from "vite";
4
+ import { basename, extname } from "node:path";
5
+ import { hasDirective } from "@vitejs/plugin-rsc/transforms";
6
+ //#region src/analyze/classify.ts
7
+ /**
8
+ * classify — environment-graph classification for project modules.
9
+ *
10
+ * The one classification module every frontend shares (design/47): the
11
+ * `timber graph` CLI, the dev-server endpoint, the VS Code extension,
12
+ * and the cold crawl must never disagree about a file's environment.
13
+ * Classification intersects RSC/SSR module-graph membership with
14
+ * first-directive detection to produce the decided taxonomy:
15
+ *
16
+ * - `server` — RSC graph only
17
+ * - `client-boundary` — has `'use client'`
18
+ * - `client-internal` — reachable only behind a boundary, no directive
19
+ * - `shared` — instantiated in both environments; INFO, never a
20
+ * warning — shared-by-design is a legitimate pattern
21
+ * - `server-action` — has `'use server'`
22
+ * - `unreferenced` — in no graph and no provisional signal; files with
23
+ * a directive or filename convention get that
24
+ * provisional kind instead, marked `pending`
25
+ *
26
+ * The browser (`client`) Vite graph stays empty until a real browser
27
+ * requests modules, so membership is RSC × SSR — the SSR graph is a
28
+ * faithful proxy for the client tree (design/47 §8, with the frontier
29
+ * caveat below).
30
+ *
31
+ * Design docs: 47-module-environment-tooling.md (taxonomy, §8),
32
+ * 43-analyze-cli.md (directive detection rules).
33
+ */
34
+ /** File extensions eligible for directive detection. */
35
+ var PARSEABLE_EXTENSIONS = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;
36
+ /**
37
+ * Parse language for a source file, from its extension. Never force
38
+ * `tsx` onto plain `.ts`: JSX-ambiguous TypeScript such as
39
+ * `const identity = <T>(value: T) => value` is valid TS but rejected
40
+ * by a TSX parse, and the resulting parse failure would silently drop
41
+ * the file's directive and poison signals. Plain JS parses as `jsx`
42
+ * (JSX-in-.js is common in Vite projects and JS has no generic-arrow
43
+ * ambiguity for the x-syntax to break).
44
+ */
45
+ function langForFile(file) {
46
+ if (/\.tsx$/.test(file)) return "tsx";
47
+ if (/\.[mc]?ts$/.test(file)) return "ts";
48
+ return "jsx";
49
+ }
50
+ /**
51
+ * Detect the first directive of a module's original source.
52
+ *
53
+ * Never string-match or regex directives (design/43): `hasDirective`
54
+ * from @vitejs/plugin-rsc checks for the `directive` property on
55
+ * ExpressionStatements — the same check the RSC plugin runs to decide
56
+ * boundaries, so classification agrees with what the framework actually
57
+ * does with the file. The directive must be in prologue position (before
58
+ * any import or statement) per the ECMAScript spec. Non-JS sources
59
+ * (CSS, MDX, assets) and sources that fail to parse return null — no
60
+ * directive is a safe default; classification then falls back to graph
61
+ * membership.
62
+ */
63
+ function detectDirective(code, file) {
64
+ if (!PARSEABLE_EXTENSIONS.test(file)) return null;
65
+ let body;
66
+ try {
67
+ body = parseAst(code, { lang: langForFile(file) }).body;
68
+ } catch {
69
+ return null;
70
+ }
71
+ if (hasDirective(body, "use client")) return "use client";
72
+ if (hasDirective(body, "use server")) return "use server";
73
+ return null;
74
+ }
75
+ /**
76
+ * Whether a file is a route-convention source the framework itself
77
+ * imports (page/layout/route/… plus metadata routes). Conventions are
78
+ * derived from the routing scanner and the metadata route table — never
79
+ * a local duplicate list (drift lesson). Each family carries the
80
+ * scanner's own extension rules.
81
+ *
82
+ * Shared by the provisional classifier below and by chains.ts, whose
83
+ * RSC-side walks terminate at the first route-convention file
84
+ * (design/47 §7) — the two must never disagree about what counts.
85
+ *
86
+ * `pageExtensions` mirrors the scanner's parameter: callers that have
87
+ * resolved the app config pass the configured set (a `page.mdx` app);
88
+ * callers without config access fall back to the defaults and tolerate
89
+ * the mismatch.
90
+ */
91
+ function isConventionSourceFile(file, pageExtensions) {
92
+ const ext = extname(file).slice(1);
93
+ const base = basename(file, `.${ext}`);
94
+ return isRouteConventionFile(base, ext, pageExtensions) || isStaticMetadataExtension(base, ext) || isDynamicMetadataExtension(base, ext);
95
+ }
96
+ /**
97
+ * Provisional kind for a file no graph has confirmed (design/47 §2):
98
+ * directive first, then filename convention, else `unreferenced`.
99
+ */
100
+ function provisionalKind(file, directive, ctx) {
101
+ if (directive === "use client") return "client-boundary";
102
+ if (directive === "use server") return "server-action";
103
+ if (ctx?.appDir && !normalizePath(file).startsWith(normalizePath(ctx.appDir) + "/")) return "unreferenced";
104
+ if (isConventionSourceFile(file, ctx?.pageExtensions)) return "server";
105
+ return "unreferenced";
106
+ }
107
+ /**
108
+ * Classify one module from its observation. Pure — all frontends call
109
+ * this with membership data from their own graph source.
110
+ */
111
+ function classifyModule(obs, ctx) {
112
+ const referenced = obs.inRsc || obs.inSsr || obs.inClient === true;
113
+ const subtreeUnknown = obs.inSsr && obs.ssrTransformed === false;
114
+ const envs = {
115
+ rsc: obs.inRsc,
116
+ ssr: obs.inSsr,
117
+ client: obs.inClient === true
118
+ };
119
+ if (!referenced) return {
120
+ file: obs.file,
121
+ kind: provisionalKind(obs.file, obs.directive, ctx),
122
+ envs,
123
+ pending: true,
124
+ subtreeUnknown: false
125
+ };
126
+ let kind;
127
+ if (obs.directive === "use client") kind = "client-boundary";
128
+ else if (obs.directive === "use server") kind = "server-action";
129
+ else if (obs.inRsc && obs.inSsr) kind = "shared";
130
+ else if (obs.inRsc) kind = "server";
131
+ else kind = "client-internal";
132
+ return {
133
+ file: obs.file,
134
+ kind,
135
+ envs,
136
+ pending: false,
137
+ subtreeUnknown
138
+ };
139
+ }
140
+ /** Classify a batch of observations, sorted by file for stable output. */
141
+ function classifyModules(observations, ctx) {
142
+ return [...observations].map((obs) => classifyModule(obs, ctx)).sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
143
+ }
144
+ //#endregion
145
+ export { isConventionSourceFile as a, detectDirective as i, classifyModule as n, langForFile as o, classifyModules as r, PARSEABLE_EXTENSIONS as t };
146
+
147
+ //# sourceMappingURL=classify-BT66U83D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify-BT66U83D.js","names":[],"sources":["../../src/analyze/classify.ts"],"sourcesContent":["/**\n * classify — environment-graph classification for project modules.\n *\n * The one classification module every frontend shares (design/47): the\n * `timber graph` CLI, the dev-server endpoint, the VS Code extension,\n * and the cold crawl must never disagree about a file's environment.\n * Classification intersects RSC/SSR module-graph membership with\n * first-directive detection to produce the decided taxonomy:\n *\n * - `server` — RSC graph only\n * - `client-boundary` — has `'use client'`\n * - `client-internal` — reachable only behind a boundary, no directive\n * - `shared` — instantiated in both environments; INFO, never a\n * warning — shared-by-design is a legitimate pattern\n * - `server-action` — has `'use server'`\n * - `unreferenced` — in no graph and no provisional signal; files with\n * a directive or filename convention get that\n * provisional kind instead, marked `pending`\n *\n * The browser (`client`) Vite graph stays empty until a real browser\n * requests modules, so membership is RSC × SSR — the SSR graph is a\n * faithful proxy for the client tree (design/47 §8, with the frontier\n * caveat below).\n *\n * Design docs: 47-module-environment-tooling.md (taxonomy, §8),\n * 43-analyze-cli.md (directive detection rules).\n */\n\nimport { basename, extname } from 'node:path';\nimport { parseAst, normalizePath } from 'vite';\nimport { hasDirective } from '../rsc-runtime/transforms.ts';\nimport { isRouteConventionFile } from '../routing/scanner.ts';\nimport {\n isDynamicMetadataExtension,\n isStaticMetadataExtension,\n} from '../server/metadata-routes.ts';\n\n/** The statement-list shape hasDirective consumes (estree Program body). */\ntype DirectiveBody = Parameters<typeof hasDirective>[0];\n\nexport type ModuleKind =\n | 'server'\n | 'client-boundary'\n | 'client-internal'\n | 'shared'\n | 'server-action'\n | 'unreferenced';\n\nexport type Directive = 'use client' | 'use server' | null;\n\n/**\n * What a frontend observed about one project module. Graph membership\n * comes from the live dev-server graphs or the cold crawl; the directive\n * comes from the module's original source (never the transformed output,\n * where the RSC plugin has already stripped or proxied it).\n */\nexport interface ModuleObservation {\n /** Absolute path of the source file. */\n file: string;\n /** Present in the RSC environment's module graph. */\n inRsc: boolean;\n /** Present in the SSR environment's module graph. */\n inSsr: boolean;\n /**\n * Present in the browser (`client`) environment's module graph. Only a\n * live dev server with a real connected browser can confirm this — the\n * client graph stays empty otherwise (design/47 §8) — so the cold\n * crawl always observes false. Defaults to false.\n */\n inClient?: boolean;\n /**\n * False when the SSR graph has a node for this file but Vite never\n * transformed it — the §8 signature of a module behind an unexecuted\n * dynamic import (`transformResult == null`). Its own membership is\n * trustworthy; its subtree is not. Defaults to true.\n */\n ssrTransformed?: boolean;\n /** First directive of the original source, or null. */\n directive: Directive;\n}\n\nexport interface ModuleClassification {\n file: string;\n kind: ModuleKind;\n /**\n * Raw per-environment graph membership, in doc 43's classification\n * vocabulary (`{ client, ssr, rsc }`). `kind` is the interpreted\n * answer; this is the evidence. `client` is confirmable only by a\n * live browser graph — static scans always report false.\n */\n envs: { rsc: boolean; ssr: boolean; client: boolean };\n /**\n * True when no graph has confirmed this module — the kind is\n * provisional, derived from directive + filename convention, and\n * frontends must visibly mark it \"pending\" (design/47 §2).\n */\n pending: boolean;\n /**\n * §8 frontier: the module is reachable (it ships to the browser) but\n * was never transformed, so modules behind it may be missing from the\n * graphs entirely. Consumers should transform-on-demand or surface\n * \"reachable, subtree unknown\".\n */\n subtreeUnknown: boolean;\n}\n\n/** File extensions eligible for directive detection. */\nexport const PARSEABLE_EXTENSIONS = /\\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;\n\n/**\n * Parse language for a source file, from its extension. Never force\n * `tsx` onto plain `.ts`: JSX-ambiguous TypeScript such as\n * `const identity = <T>(value: T) => value` is valid TS but rejected\n * by a TSX parse, and the resulting parse failure would silently drop\n * the file's directive and poison signals. Plain JS parses as `jsx`\n * (JSX-in-.js is common in Vite projects and JS has no generic-arrow\n * ambiguity for the x-syntax to break).\n */\nexport function langForFile(file: string): 'ts' | 'tsx' | 'jsx' {\n if (/\\.tsx$/.test(file)) return 'tsx';\n if (/\\.[mc]?ts$/.test(file)) return 'ts';\n return 'jsx';\n}\n\n/**\n * Detect the first directive of a module's original source.\n *\n * Never string-match or regex directives (design/43): `hasDirective`\n * from @vitejs/plugin-rsc checks for the `directive` property on\n * ExpressionStatements — the same check the RSC plugin runs to decide\n * boundaries, so classification agrees with what the framework actually\n * does with the file. The directive must be in prologue position (before\n * any import or statement) per the ECMAScript spec. Non-JS sources\n * (CSS, MDX, assets) and sources that fail to parse return null — no\n * directive is a safe default; classification then falls back to graph\n * membership.\n */\nexport function detectDirective(code: string, file: string): Directive {\n if (!PARSEABLE_EXTENSIONS.test(file)) return null;\n let body: DirectiveBody;\n try {\n // Vite's parseAst returns oxc-typed nodes; hasDirective expects the\n // estree shapes. They are structurally identical for the top-level\n // statement scan — the same bridge the rest of the codebase uses\n // (plugin-context.ts parse memo, routing/export-detect.ts).\n body = parseAst(code, { lang: langForFile(file) }).body as unknown as DirectiveBody;\n } catch {\n return null;\n }\n if (hasDirective(body, 'use client')) return 'use client';\n if (hasDirective(body, 'use server')) return 'use server';\n return null;\n}\n\n/** Optional context for classification. */\nexport interface ClassifyContext {\n /**\n * Absolute path of the app directory. When provided, convention-based\n * provisional classification only applies to files inside it — a\n * `lib/page.tsx` is not a route file, and the scanner would never\n * recognize it there.\n */\n appDir?: string;\n /**\n * Configured pageExtensions, when the caller has resolved the app\n * config. Omitted → scanner defaults.\n */\n pageExtensions?: readonly string[];\n}\n\n/**\n * Whether a file is a route-convention source the framework itself\n * imports (page/layout/route/… plus metadata routes). Conventions are\n * derived from the routing scanner and the metadata route table — never\n * a local duplicate list (drift lesson). Each family carries the\n * scanner's own extension rules.\n *\n * Shared by the provisional classifier below and by chains.ts, whose\n * RSC-side walks terminate at the first route-convention file\n * (design/47 §7) — the two must never disagree about what counts.\n *\n * `pageExtensions` mirrors the scanner's parameter: callers that have\n * resolved the app config pass the configured set (a `page.mdx` app);\n * callers without config access fall back to the defaults and tolerate\n * the mismatch.\n */\nexport function isConventionSourceFile(file: string, pageExtensions?: readonly string[]): boolean {\n const ext = extname(file).slice(1);\n const base = basename(file, `.${ext}`);\n return (\n isRouteConventionFile(base, ext, pageExtensions) ||\n isStaticMetadataExtension(base, ext) ||\n isDynamicMetadataExtension(base, ext)\n );\n}\n\n/**\n * Provisional kind for a file no graph has confirmed (design/47 §2):\n * directive first, then filename convention, else `unreferenced`.\n */\nfunction provisionalKind(file: string, directive: Directive, ctx?: ClassifyContext): ModuleKind {\n if (directive === 'use client') return 'client-boundary';\n if (directive === 'use server') return 'server-action';\n // Normalize both sides before the containment check: on Windows,\n // `file` may arrive slash-normalized (Vite module-node paths are)\n // while appDir comes from node:path with backslashes — comparing raw\n // would misreport every provisional route file as unreferenced.\n if (ctx?.appDir && !normalizePath(file).startsWith(normalizePath(ctx.appDir) + '/')) {\n return 'unreferenced';\n }\n if (isConventionSourceFile(file, ctx?.pageExtensions)) return 'server';\n return 'unreferenced';\n}\n\n/**\n * Classify one module from its observation. Pure — all frontends call\n * this with membership data from their own graph source.\n */\nexport function classifyModule(\n obs: ModuleObservation,\n ctx?: ClassifyContext\n): ModuleClassification {\n const referenced = obs.inRsc || obs.inSsr || obs.inClient === true;\n const subtreeUnknown = obs.inSsr && obs.ssrTransformed === false;\n const envs = { rsc: obs.inRsc, ssr: obs.inSsr, client: obs.inClient === true };\n\n if (!referenced) {\n return {\n file: obs.file,\n kind: provisionalKind(obs.file, obs.directive, ctx),\n envs,\n pending: true,\n subtreeUnknown: false,\n };\n }\n\n // Directive wins over membership: a 'use client' file also has an RSC\n // graph node (the client-reference proxy), and a 'use server' module\n // appears in the SSR graph when referenced from client forms — the\n // directive is what those memberships mean.\n let kind: ModuleKind;\n if (obs.directive === 'use client') kind = 'client-boundary';\n else if (obs.directive === 'use server') kind = 'server-action';\n else if (obs.inRsc && obs.inSsr) kind = 'shared';\n else if (obs.inRsc) kind = 'server';\n else kind = 'client-internal';\n\n return { file: obs.file, kind, envs, pending: false, subtreeUnknown };\n}\n\n/** Classify a batch of observations, sorted by file for stable output. */\nexport function classifyModules(\n observations: Iterable<ModuleObservation>,\n ctx?: ClassifyContext\n): ModuleClassification[] {\n return [...observations]\n .map((obs) => classifyModule(obs, ctx))\n .sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2GA,IAAa,uBAAuB;;;;;;;;;;AAWpC,SAAgB,YAAY,MAAoC;CAC9D,IAAI,SAAS,KAAK,IAAI,GAAG,OAAO;CAChC,IAAI,aAAa,KAAK,IAAI,GAAG,OAAO;CACpC,OAAO;AACT;;;;;;;;;;;;;;AAeA,SAAgB,gBAAgB,MAAc,MAAyB;CACrE,IAAI,CAAC,qBAAqB,KAAK,IAAI,GAAG,OAAO;CAC7C,IAAI;CACJ,IAAI;EAKF,OAAO,SAAS,MAAM,EAAE,MAAM,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC;CACrD,QAAQ;EACN,OAAO;CACT;CACA,IAAI,aAAa,MAAM,YAAY,GAAG,OAAO;CAC7C,IAAI,aAAa,MAAM,YAAY,GAAG,OAAO;CAC7C,OAAO;AACT;;;;;;;;;;;;;;;;;AAkCA,SAAgB,uBAAuB,MAAc,gBAA6C;CAChG,MAAM,MAAM,QAAQ,IAAI,CAAC,CAAC,MAAM,CAAC;CACjC,MAAM,OAAO,SAAS,MAAM,IAAI,KAAK;CACrC,OACE,sBAAsB,MAAM,KAAK,cAAc,KAC/C,0BAA0B,MAAM,GAAG,KACnC,2BAA2B,MAAM,GAAG;AAExC;;;;;AAMA,SAAS,gBAAgB,MAAc,WAAsB,KAAmC;CAC9F,IAAI,cAAc,cAAc,OAAO;CACvC,IAAI,cAAc,cAAc,OAAO;CAKvC,IAAI,KAAK,UAAU,CAAC,cAAc,IAAI,CAAC,CAAC,WAAW,cAAc,IAAI,MAAM,IAAI,GAAG,GAChF,OAAO;CAET,IAAI,uBAAuB,MAAM,KAAK,cAAc,GAAG,OAAO;CAC9D,OAAO;AACT;;;;;AAMA,SAAgB,eACd,KACA,KACsB;CACtB,MAAM,aAAa,IAAI,SAAS,IAAI,SAAS,IAAI,aAAa;CAC9D,MAAM,iBAAiB,IAAI,SAAS,IAAI,mBAAmB;CAC3D,MAAM,OAAO;EAAE,KAAK,IAAI;EAAO,KAAK,IAAI;EAAO,QAAQ,IAAI,aAAa;CAAK;CAE7E,IAAI,CAAC,YACH,OAAO;EACL,MAAM,IAAI;EACV,MAAM,gBAAgB,IAAI,MAAM,IAAI,WAAW,GAAG;EAClD;EACA,SAAS;EACT,gBAAgB;CAClB;CAOF,IAAI;CACJ,IAAI,IAAI,cAAc,cAAc,OAAO;MACtC,IAAI,IAAI,cAAc,cAAc,OAAO;MAC3C,IAAI,IAAI,SAAS,IAAI,OAAO,OAAO;MACnC,IAAI,IAAI,OAAO,OAAO;MACtB,OAAO;CAEZ,OAAO;EAAE,MAAM,IAAI;EAAM;EAAM;EAAM,SAAS;EAAO;CAAe;AACtE;;AAGA,SAAgB,gBACd,cACA,KACwB;CACxB,OAAO,CAAC,GAAG,YAAY,CAAC,CACrB,KAAK,QAAQ,eAAe,KAAK,GAAG,CAAC,CAAC,CACtC,MAAM,GAAG,MAAO,EAAE,OAAO,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,CAAE;AACpE"}
@@ -1,5 +1,5 @@
1
- import { n as scanRoutes } from "./scanner-DmqdxzbW.js";
2
- import { a as ENV_DTS_FILENAME, i as CODEGEN_OUTPUT, n as formatConventionWarnings, r as lintConventions, s as writeRouteCodegen } from "./convention-lint-DLmhGsRS.js";
1
+ import { n as scanRoutes } from "./scanner-CQt12vE2.js";
2
+ import { a as ENV_DTS_FILENAME, i as CODEGEN_OUTPUT, n as formatConventionWarnings, r as lintConventions, s as writeRouteCodegen } from "./convention-lint-BEVW4EID.js";
3
3
  import { resolveTimberContext } from "./cli-context-By7k2xVo.js";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { createRequire } from "node:module";
@@ -249,4 +249,4 @@ async function runTypeCheck(options = {}, deps = {}) {
249
249
  //#endregion
250
250
  export { checkRoutes, runTypeCheck };
251
251
 
252
- //# sourceMappingURL=cli-check-ajNY3B2e.js.map
252
+ //# sourceMappingURL=cli-check-BfQ54-UJ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli-check-ajNY3B2e.js","names":[],"sources":["../../src/cli-check.ts"],"sourcesContent":["/**\n * `timber check` — the implementation behind the CLI command.\n *\n * Two passes, in order:\n *\n * 1. **Routes.** Resolve the project through Vite — so root, `appDir`, and\n * `pageExtensions` are the ones `timber build` uses, including config\n * passed inline to `timber()` — then scan, run the convention linter, and\n * write the route codegen (`.timber/timber-routes.d.ts`,\n * `.timber/segment-types.d.ts`, `timber-env.d.ts`).\n *\n * Codegen runs *before* the type pass on purpose: those\n * files are what give `<Link href>`, `useSegmentParams()` and\n * `MiddlewareContext` their per-route types (design/09-typescript.md\n * §\"Typed Routes\"), and they are build artifacts — a fresh clone that has\n * never run `timber dev` or `timber build` does not have them, so a type\n * pass on its own would report errors about correct code. A clone whose\n * routes changed since the last build has the same problem with stale ones.\n * 2. **Types.** `tsgo --noEmit` over the project.\n *\n * tsgo is spawned as `node <@typescript/native-preview>/lib/tsgo.js`, never as\n * a bare `tsgo` off PATH — the package is an *optional peer dependency*, so\n * there is no reason for its shim to be on a user's PATH, and the shim is a\n * `.cmd` on Windows which cannot be spawned without a shell. `shell: true`\n * would then concatenate the argv into an unescaped command string, so a\n * project path containing a space would break on exactly the platform the\n * shell was there to support. This mirrors `scripts/build-types.js`, whose\n * header comment is the specification for the pattern.\n *\n * Design docs: 11-platform.md §CLI, 09-typescript.md, 28-npm-packaging.md\n */\n\nimport { createRequire } from 'node:module';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { scanRoutes } from './routing/scanner.ts';\nimport { lintConventions, formatConventionWarnings } from './routing/convention-lint.ts';\nimport { writeRouteCodegen, CODEGEN_OUTPUT, ENV_DTS_FILENAME } from './routing/codegen-write.ts';\nimport { resolveTimberContext, type ResolveViteConfig } from './cli-context.ts';\n\n/** The npm package that provides tsgo. Named in the missing-tool message. */\nconst TSGO_PACKAGE = '@typescript/native-preview';\n\n/**\n * The two generated declaration paths, relative to the project root, as they\n * appear in a resolved tsconfig file list. Derived from the writer's own\n * constants — a rename there cannot leave this check hunting for a file nothing\n * writes.\n */\nconst GENERATED_TYPE_PATHS = [ENV_DTS_FILENAME, CODEGEN_OUTPUT];\n\n/**\n * Run the compiler to completion.\n *\n * Resolves with its exit status (`null` when a signal killed it) and rejects\n * only when the process could not be started — the two outcomes `runTypeCheck`\n * has to keep apart. The seam is this narrow on purpose: a test double is a\n * plain async function, with no `ChildProcess` shape to imitate.\n */\nexport type RunTsgo = (command: string, args: string[], cwd: string) => Promise<number | null>;\n\n/**\n * Run the compiler and capture its stdout — used for `--showConfig`, whose\n * output is data we read rather than diagnostics the user reads.\n */\nexport type ReadTsgoConfig = (command: string, args: string[], cwd: string) => Promise<string>;\n\n/** @internal Dependency injection for testing. */\nexport interface CheckDeps {\n /**\n * Resolve tsgo's own JS entry point. Throws when the package is not\n * installed — the caller turns that into an install instruction.\n */\n resolveTsgoEntry?: () => string;\n runTsgo?: RunTsgo;\n readTsgoConfig?: ReadTsgoConfig;\n resolveViteConfig?: ResolveViteConfig;\n}\n\nexport interface CheckOptions {\n /** Project root, for resolving tsgo. Defaults to `process.cwd()`. */\n root?: string;\n /**\n * Path to a **tsconfig**, forwarded to tsgo as `--project` and sourced from\n * the CLI's own `--project`. The route pass takes no part in it: it resolves\n * the app's configuration through Vite, as `timber build` does.\n */\n config?: string;\n}\n\n/**\n * Locate `@typescript/native-preview`'s JS entry.\n *\n * Resolution is attempted from this package first (it is declared an optional\n * peer dependency, so a package manager links it here when the user installs\n * it) and then from the project root (covering a hoisted install, or a user who\n * added it without the peer link). Throws if neither finds it.\n */\nexport function resolveTsgoEntry(\n root: string,\n resolveFrom: (from: string, specifier: string) => string = (from, specifier) =>\n createRequire(from).resolve(specifier)\n): string {\n // `createRequire` takes a filename or a file URL; a plain absolute path is\n // the portable spelling for the project root (a `file://` prefix on a Windows\n // path is not a valid URL). Same form as `checkPeerDependencies`.\n const candidates = [import.meta.url, join(root, 'package.json')];\n let lastError: unknown;\n\n for (const from of candidates) {\n try {\n // The package exports no subpath for its entry, so resolve the manifest\n // and walk to the module its `bin/tsgo` shim imports.\n const manifest = resolveFrom(from, `${TSGO_PACKAGE}/package.json`);\n return join(dirname(manifest), 'lib', 'tsgo.js');\n } catch (err) {\n lastError = err;\n }\n }\n\n throw new Error(\n `[timber] Cannot find ${TSGO_PACKAGE}, which \\`timber check\\` uses to type-check.\\n\\n` +\n ` Install it as a dev dependency:\\n\\n` +\n ` pnpm add -D ${TSGO_PACKAGE}\\n\\n` +\n ` (resolution failed: ${lastError instanceof Error ? lastError.message : String(lastError)})`\n );\n}\n\n/** What the route pass needs from the command line. */\nexport interface RouteOptions {\n /**\n * Path to a **Vite** config file, from `--config`. Undefined means Vite's own\n * discovery, the same as `timber dev`/`build` without the flag. A project\n * that builds with a custom Vite config has to be checkable with it too, or\n * `check` resolves a different app than `build` does (TIM-1332, round 4).\n */\n viteConfigFile?: string;\n}\n\n/** Everything the route scan needs, as the build itself resolves it. */\nexport interface RouteInputs {\n root: string;\n appDir: string;\n pageExtensions: string[] | undefined;\n}\n\n/**\n * Resolve root, appDir and pageExtensions the way `timber build` resolves them\n * — through Vite, never by re-reading `timber.config.ts` (see `cli-context.ts`).\n * Inline `timber({ pageExtensions: [...] })` is the case that bites: a scan\n * that re-read the file would silently use default page extensions and write a\n * route map missing the app's pages. `examples/next-playground-migration` is\n * exactly this shape.\n */\nexport async function resolveRouteInputs(\n options: RouteOptions = {},\n deps: CheckDeps = {}\n): Promise<RouteInputs> {\n const ctx = await resolveTimberContext('check', options.viteConfigFile, deps.resolveViteConfig);\n return { root: ctx.root, appDir: ctx.appDir, pageExtensions: ctx.config.pageExtensions };\n}\n\n/**\n * Resolve the app directory, scan it, fail on proven-fatal route\n * misconfigurations, print warnings, and write the route codegen the type pass\n * depends on.\n *\n * Error-level lint findings throw rather than print, matching what\n * `plugins/routing.ts` does at build time: the tree is not fit to publish, so\n * it must not reach codegen either. Warnings are printed and do not fail the\n * command — same severity contract as dev and build, so `timber check` cannot\n * disagree with them about what a valid app is.\n */\nexport async function checkRoutes(\n options: RouteOptions = {},\n deps: CheckDeps = {}\n): Promise<RouteInputs> {\n const inputs = await resolveRouteInputs(options, deps);\n const { root, appDir, pageExtensions } = inputs;\n\n const tree = scanRoutes(appDir, { pageExtensions });\n\n const warnings = lintConventions(tree, appDir);\n const fatal = warnings.filter((w) => w.level === 'error');\n if (fatal.length > 0) {\n throw new Error(formatConventionWarnings(fatal, { color: false }));\n }\n if (warnings.length > 0) {\n process.stderr.write(`${formatConventionWarnings(warnings)}\\n`);\n }\n\n // Awaited, unlike the dev/build path: the type pass reads these files, so a\n // failed write would be reported as type errors in the user's own code.\n // Reported as a codegen failure rather than surfaced as a bare `EROFS`/\n // `EACCES` from somewhere unnamed — the raw error says which path could not\n // be written but not which pass wanted it.\n try {\n await writeRouteCodegen(tree, appDir, root);\n } catch (err) {\n throw new Error(\n `[timber] Failed to write the generated route types, which the type check reads: ` +\n `${err instanceof Error ? err.message : String(err)}`\n );\n }\n\n verifyCodegenImports(root);\n\n // Returned so the type pass runs against the same root these files landed in.\n return inputs;\n}\n\n/**\n * Verify that the import specifiers in the generated route types resolve on the\n * filesystem. A `.d.ts` whose import fails does NOT produce a TypeScript error —\n * every type that depends on it silently becomes `any`. The generated file\n * contains type-level `_AssertSchemaNotAny` / `_AssertCodecTypeNotAny` guards\n * that catch this at typecheck time, but a diagnostic here fires earlier and\n * names the exact import that is broken rather than leaving the user to decode\n * a constraint error.\n */\nexport function verifyCodegenImports(root: string): void {\n const dtsPath = join(root, CODEGEN_OUTPUT);\n let content: string;\n try {\n content = readFileSync(dtsPath, 'utf-8');\n } catch {\n return;\n }\n\n const dtsDir = dirname(dtsPath);\n\n // Check relative imports (default imports only — the generated codegen uses\n // `import type schema from '...'` for relative paths). Named imports from\n // relative paths are not emitted today; if codegen adds them, extend this.\n const RELATIVE_IMPORT_RE = /^import\\s+type\\s+\\w+\\s+from\\s+'(\\.\\.?\\/[^']+)'/gm;\n const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx'];\n let match;\n while ((match = RELATIVE_IMPORT_RE.exec(content)) !== null) {\n const specifier = match[1];\n const base = join(dtsDir, specifier);\n const resolves = existsSync(base) || EXTENSIONS.some((ext) => existsSync(base + ext));\n if (!resolves) {\n process.stderr.write(\n `[timber] WARNING: the generated ${CODEGEN_OUTPUT} imports '${specifier}',\\n` +\n ` but no file resolves at ${base}{${EXTENSIONS.join(',')}}.\\n` +\n ` Every type that depends on this import will silently become \\`any\\`.\\n\\n` +\n ` Check that your app/ directory is where timber expects it.\\n\\n`\n );\n }\n }\n\n // Bare-specifier imports (`@timber-js/app/params`) are NOT checked here.\n // They are all dependencies of @timber-js/app itself — if timber is installed,\n // they resolve. And createRequire uses CJS resolution which cannot follow\n // ESM-only subpath exports, producing false warnings (TIM-1348 review).\n}\n\n/**\n * The production `RunTsgo`: spawn the compiler with its stdio inherited, so its\n * diagnostics stream to the terminal as they are produced (and are not capped\n * by a buffer, as `execFile` output would be).\n */\nconst spawnTsgo: RunTsgo = async (command, args, cwd) => {\n const { spawn } = await import('node:child_process');\n return new Promise<number | null>((resolve, reject) => {\n // No `shell: true`: Node would concatenate the argv into an unescaped\n // command string, so a project path containing a space would break.\n const child = spawn(command, args, { stdio: 'inherit', cwd });\n child.on('error', reject);\n child.on('close', (code) => resolve(code));\n });\n};\n\n/**\n * The production `ReadTsgoConfig`: `--showConfig`, with stdout captured.\n *\n * `maxBuffer` is raised well past `execFile`'s 1 MB default because the output\n * is one line per file in the program — a large app can exceed that, and\n * overflowing it would make this advisory go quiet on exactly the projects\n * where a misconfigured `include` costs the most. A size-dependent silence is\n * indistinguishable from \"everything is fine\".\n */\nconst execTsgoConfig: ReadTsgoConfig = async (command, args, cwd) => {\n const { execFile } = await import('node:child_process');\n return new Promise<string>((resolve, reject) => {\n execFile(command, args, { cwd, maxBuffer: 64 * 1024 * 1024 }, (err, stdout) =>\n err ? reject(err) : resolve(stdout)\n );\n });\n};\n\n/**\n * Warn when the generated route declarations will not reach the type checker.\n *\n * Writing `.timber/timber-routes.d.ts` and `timber-env.d.ts` is only half the\n * job: TypeScript loads them only if the project's `include`/`files` actually\n * pull `timber-env.d.ts` in (it is what `/// <reference>`s the route map). A\n * tsconfig with an explicit `include` that lists only source directories —\n * `[\"app\", \"*.config.ts\"]`, which is what `create-timber-app` shipped before\n * TIM-1332 — silently omits it, and every `<Link href>` then type-checks\n * against the empty placeholder instead of the generated route union. The\n * check would pass while answering the wrong question.\n *\n * The resolved file list comes from `tsgo --showConfig` rather than from our\n * own reading of the tsconfig: `include`, `exclude`, `files`, and `extends`\n * compose in ways that are the compiler's to decide, and a second\n * implementation of them would be a duplicate that drifts. `--showConfig` only\n * resolves globs, so it costs no type checking.\n *\n * This warns rather than fails. An app that never uses typed routes is\n * correct without those declarations, and a hard error would refuse it — the\n * one outcome a check must not produce for a working project. When the answer\n * cannot be determined (tsgo errored, output was not the expected JSON), the\n * warning is skipped: an advisory that fires on an unknown is noise.\n */\nasync function warnIfRouteTypesUnreachable(\n tsgoEntry: string,\n configArgs: string[],\n cwd: string,\n readConfig: ReadTsgoConfig\n): Promise<void> {\n let files: unknown;\n try {\n const raw = await readConfig(process.execPath, [tsgoEntry, '--showConfig', ...configArgs], cwd);\n files = (JSON.parse(raw) as { files?: unknown }).files;\n } catch {\n return;\n }\n if (!Array.isArray(files)) return;\n\n // Suffix match, not path resolution: `--showConfig` prints paths relative to\n // whichever tsconfig the compiler *found*, which for a bare `timber check`\n // is a directory we never learn. Over-matching only suppresses an advisory.\n //\n // The bare form is matched as well as the `/`-prefixed one. tsgo emits\n // `./timber-env.d.ts` today, but a leading `./` is cosmetic and an emitter\n // that dropped it would turn this into a warning printed at every correctly\n // configured project — the failure direction that trains people to ignore it.\n const reaches = files.some((file) => {\n if (typeof file !== 'string') return false;\n const normalized = file.replace(/\\\\/g, '/');\n return GENERATED_TYPE_PATHS.some(\n (path) => normalized === path || normalized.endsWith(`/${path}`)\n );\n });\n if (reaches) return;\n\n process.stderr.write(\n `[timber] The generated route types are not part of this TypeScript project, so\\n` +\n ` \\`<Link href>\\` and \\`useSegmentParams()\\` are being checked against empty\\n` +\n ` placeholders. Your tsconfig's \"include\" does not pull in timber-env.d.ts.\\n\\n` +\n ` To fix: add it to \"include\":\\n\\n` +\n ` \"include\": [\"app\", \"*.config.ts\", \"timber-env.d.ts\"]\\n\\n`\n );\n}\n\n/**\n * Run `tsgo --noEmit`, streaming its output.\n *\n * Rejects with the real numeric exit status on type errors. A failure to *start*\n * the compiler is reported separately — the historical bug this replaces\n * surfaced ENOENT as \"Type check failed with exit code undefined\", which read\n * as a type error and sent users looking at their own code.\n */\nexport async function runTypeCheck(\n options: CheckOptions = {},\n deps: CheckDeps = {}\n): Promise<void> {\n // The *resolved* root, handed over by the route pass — not `process.cwd()`.\n // The two differ whenever `--config` names a Vite config elsewhere, and the\n // route pass wrote `.timber/*` and `timber-env.d.ts` under the resolved one.\n // Running tsgo from cwd would then discover a different tsconfig entirely and\n // type-check a different project than the one just validated (TIM-1332, round\n // 5) — routes checked for the app, types checked for the monorepo around it.\n const root = options.root ?? process.cwd();\n const tsgoEntry = (deps.resolveTsgoEntry ?? (() => resolveTsgoEntry(root)))();\n const runTsgo = deps.runTsgo ?? spawnTsgo;\n\n // An explicit `--project` is a path the user typed at their own prompt, so it\n // is resolved against *their* cwd before the compiler is moved to the project\n // root. Without `--project`, tsgo discovers `tsconfig.json` from `root`,\n // which is where the declarations it needs were just written.\n const configArgs = options.config ? ['--project', resolve(process.cwd(), options.config)] : [];\n\n // Before checking: say so if the declarations the route pass just wrote will\n // not be part of the program. Otherwise the check passes while `<Link href>`\n // is measured against a placeholder. Same root and same argv as the check\n // itself, or it would inspect a different program than the one tsgo reads.\n await warnIfRouteTypesUnreachable(\n tsgoEntry,\n configArgs,\n root,\n deps.readTsgoConfig ?? execTsgoConfig\n );\n\n let code: number | null;\n try {\n code = await runTsgo(process.execPath, [tsgoEntry, '--noEmit', ...configArgs], root);\n } catch (err) {\n // Could not start the compiler at all. Kept distinct from a nonzero exit:\n // the version this replaced reported ENOENT as \"Type check failed with\n // exit code undefined\", which reads as a verdict on the user's types.\n throw new Error(\n `[timber] Failed to start the type checker: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n\n if (code === 0) return;\n\n // A null code means a signal killed the process rather than it exiting.\n // Say so instead of printing \"exit code null\".\n throw new Error(\n code === null\n ? 'Type check terminated before it finished'\n : `Type check failed with exit code ${code}`\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli-check-BfQ54-UJ.js","names":[],"sources":["../../src/cli-check.ts"],"sourcesContent":["/**\n * `timber check` — the implementation behind the CLI command.\n *\n * Two passes, in order:\n *\n * 1. **Routes.** Resolve the project through Vite — so root, `appDir`, and\n * `pageExtensions` are the ones `timber build` uses, including config\n * passed inline to `timber()` — then scan, run the convention linter, and\n * write the route codegen (`.timber/timber-routes.d.ts`,\n * `.timber/segment-types.d.ts`, `timber-env.d.ts`).\n *\n * Codegen runs *before* the type pass on purpose: those\n * files are what give `<Link href>`, `useSegmentParams()` and\n * `MiddlewareContext` their per-route types (design/09-typescript.md\n * §\"Typed Routes\"), and they are build artifacts — a fresh clone that has\n * never run `timber dev` or `timber build` does not have them, so a type\n * pass on its own would report errors about correct code. A clone whose\n * routes changed since the last build has the same problem with stale ones.\n * 2. **Types.** `tsgo --noEmit` over the project.\n *\n * tsgo is spawned as `node <@typescript/native-preview>/lib/tsgo.js`, never as\n * a bare `tsgo` off PATH — the package is an *optional peer dependency*, so\n * there is no reason for its shim to be on a user's PATH, and the shim is a\n * `.cmd` on Windows which cannot be spawned without a shell. `shell: true`\n * would then concatenate the argv into an unescaped command string, so a\n * project path containing a space would break on exactly the platform the\n * shell was there to support. This mirrors `scripts/build-types.js`, whose\n * header comment is the specification for the pattern.\n *\n * Design docs: 11-platform.md §CLI, 09-typescript.md, 28-npm-packaging.md\n */\n\nimport { createRequire } from 'node:module';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { scanRoutes } from './routing/scanner.ts';\nimport { lintConventions, formatConventionWarnings } from './routing/convention-lint.ts';\nimport { writeRouteCodegen, CODEGEN_OUTPUT, ENV_DTS_FILENAME } from './routing/codegen-write.ts';\nimport { resolveTimberContext, type ResolveViteConfig } from './cli-context.ts';\n\n/** The npm package that provides tsgo. Named in the missing-tool message. */\nconst TSGO_PACKAGE = '@typescript/native-preview';\n\n/**\n * The two generated declaration paths, relative to the project root, as they\n * appear in a resolved tsconfig file list. Derived from the writer's own\n * constants — a rename there cannot leave this check hunting for a file nothing\n * writes.\n */\nconst GENERATED_TYPE_PATHS = [ENV_DTS_FILENAME, CODEGEN_OUTPUT];\n\n/**\n * Run the compiler to completion.\n *\n * Resolves with its exit status (`null` when a signal killed it) and rejects\n * only when the process could not be started — the two outcomes `runTypeCheck`\n * has to keep apart. The seam is this narrow on purpose: a test double is a\n * plain async function, with no `ChildProcess` shape to imitate.\n */\nexport type RunTsgo = (command: string, args: string[], cwd: string) => Promise<number | null>;\n\n/**\n * Run the compiler and capture its stdout — used for `--showConfig`, whose\n * output is data we read rather than diagnostics the user reads.\n */\nexport type ReadTsgoConfig = (command: string, args: string[], cwd: string) => Promise<string>;\n\n/** @internal Dependency injection for testing. */\nexport interface CheckDeps {\n /**\n * Resolve tsgo's own JS entry point. Throws when the package is not\n * installed — the caller turns that into an install instruction.\n */\n resolveTsgoEntry?: () => string;\n runTsgo?: RunTsgo;\n readTsgoConfig?: ReadTsgoConfig;\n resolveViteConfig?: ResolveViteConfig;\n}\n\nexport interface CheckOptions {\n /** Project root, for resolving tsgo. Defaults to `process.cwd()`. */\n root?: string;\n /**\n * Path to a **tsconfig**, forwarded to tsgo as `--project` and sourced from\n * the CLI's own `--project`. The route pass takes no part in it: it resolves\n * the app's configuration through Vite, as `timber build` does.\n */\n config?: string;\n}\n\n/**\n * Locate `@typescript/native-preview`'s JS entry.\n *\n * Resolution is attempted from this package first (it is declared an optional\n * peer dependency, so a package manager links it here when the user installs\n * it) and then from the project root (covering a hoisted install, or a user who\n * added it without the peer link). Throws if neither finds it.\n */\nexport function resolveTsgoEntry(\n root: string,\n resolveFrom: (from: string, specifier: string) => string = (from, specifier) =>\n createRequire(from).resolve(specifier)\n): string {\n // `createRequire` takes a filename or a file URL; a plain absolute path is\n // the portable spelling for the project root (a `file://` prefix on a Windows\n // path is not a valid URL). Same form as `checkPeerDependencies`.\n const candidates = [import.meta.url, join(root, 'package.json')];\n let lastError: unknown;\n\n for (const from of candidates) {\n try {\n // The package exports no subpath for its entry, so resolve the manifest\n // and walk to the module its `bin/tsgo` shim imports.\n const manifest = resolveFrom(from, `${TSGO_PACKAGE}/package.json`);\n return join(dirname(manifest), 'lib', 'tsgo.js');\n } catch (err) {\n lastError = err;\n }\n }\n\n throw new Error(\n `[timber] Cannot find ${TSGO_PACKAGE}, which \\`timber check\\` uses to type-check.\\n\\n` +\n ` Install it as a dev dependency:\\n\\n` +\n ` pnpm add -D ${TSGO_PACKAGE}\\n\\n` +\n ` (resolution failed: ${lastError instanceof Error ? lastError.message : String(lastError)})`\n );\n}\n\n/** What the route pass needs from the command line. */\nexport interface RouteOptions {\n /**\n * Path to a **Vite** config file, from `--config`. Undefined means Vite's own\n * discovery, the same as `timber dev`/`build` without the flag. A project\n * that builds with a custom Vite config has to be checkable with it too, or\n * `check` resolves a different app than `build` does (TIM-1332, round 4).\n */\n viteConfigFile?: string;\n}\n\n/** Everything the route scan needs, as the build itself resolves it. */\nexport interface RouteInputs {\n root: string;\n appDir: string;\n pageExtensions: string[] | undefined;\n}\n\n/**\n * Resolve root, appDir and pageExtensions the way `timber build` resolves them\n * — through Vite, never by re-reading `timber.config.ts` (see `cli-context.ts`).\n * Inline `timber({ pageExtensions: [...] })` is the case that bites: a scan\n * that re-read the file would silently use default page extensions and write a\n * route map missing the app's pages. `examples/next-playground-migration` is\n * exactly this shape.\n */\nexport async function resolveRouteInputs(\n options: RouteOptions = {},\n deps: CheckDeps = {}\n): Promise<RouteInputs> {\n const ctx = await resolveTimberContext('check', options.viteConfigFile, deps.resolveViteConfig);\n return { root: ctx.root, appDir: ctx.appDir, pageExtensions: ctx.config.pageExtensions };\n}\n\n/**\n * Resolve the app directory, scan it, fail on proven-fatal route\n * misconfigurations, print warnings, and write the route codegen the type pass\n * depends on.\n *\n * Error-level lint findings throw rather than print, matching what\n * `plugins/routing.ts` does at build time: the tree is not fit to publish, so\n * it must not reach codegen either. Warnings are printed and do not fail the\n * command — same severity contract as dev and build, so `timber check` cannot\n * disagree with them about what a valid app is.\n */\nexport async function checkRoutes(\n options: RouteOptions = {},\n deps: CheckDeps = {}\n): Promise<RouteInputs> {\n const inputs = await resolveRouteInputs(options, deps);\n const { root, appDir, pageExtensions } = inputs;\n\n const tree = scanRoutes(appDir, { pageExtensions });\n\n const warnings = lintConventions(tree, appDir);\n const fatal = warnings.filter((w) => w.level === 'error');\n if (fatal.length > 0) {\n throw new Error(formatConventionWarnings(fatal, { color: false }));\n }\n if (warnings.length > 0) {\n process.stderr.write(`${formatConventionWarnings(warnings)}\\n`);\n }\n\n // Awaited, unlike the dev/build path: the type pass reads these files, so a\n // failed write would be reported as type errors in the user's own code.\n // Reported as a codegen failure rather than surfaced as a bare `EROFS`/\n // `EACCES` from somewhere unnamed — the raw error says which path could not\n // be written but not which pass wanted it.\n try {\n await writeRouteCodegen(tree, appDir, root);\n } catch (err) {\n throw new Error(\n `[timber] Failed to write the generated route types, which the type check reads: ` +\n `${err instanceof Error ? err.message : String(err)}`\n );\n }\n\n verifyCodegenImports(root);\n\n // Returned so the type pass runs against the same root these files landed in.\n return inputs;\n}\n\n/**\n * Verify that the import specifiers in the generated route types resolve on the\n * filesystem. A `.d.ts` whose import fails does NOT produce a TypeScript error —\n * every type that depends on it silently becomes `any`. The generated file\n * contains type-level `_AssertSchemaNotAny` / `_AssertCodecTypeNotAny` guards\n * that catch this at typecheck time, but a diagnostic here fires earlier and\n * names the exact import that is broken rather than leaving the user to decode\n * a constraint error.\n */\nexport function verifyCodegenImports(root: string): void {\n const dtsPath = join(root, CODEGEN_OUTPUT);\n let content: string;\n try {\n content = readFileSync(dtsPath, 'utf-8');\n } catch {\n return;\n }\n\n const dtsDir = dirname(dtsPath);\n\n // Check relative imports (default imports only — the generated codegen uses\n // `import type schema from '...'` for relative paths). Named imports from\n // relative paths are not emitted today; if codegen adds them, extend this.\n const RELATIVE_IMPORT_RE = /^import\\s+type\\s+\\w+\\s+from\\s+'(\\.\\.?\\/[^']+)'/gm;\n const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx'];\n let match;\n while ((match = RELATIVE_IMPORT_RE.exec(content)) !== null) {\n const specifier = match[1];\n const base = join(dtsDir, specifier);\n const resolves = existsSync(base) || EXTENSIONS.some((ext) => existsSync(base + ext));\n if (!resolves) {\n process.stderr.write(\n `[timber] WARNING: the generated ${CODEGEN_OUTPUT} imports '${specifier}',\\n` +\n ` but no file resolves at ${base}{${EXTENSIONS.join(',')}}.\\n` +\n ` Every type that depends on this import will silently become \\`any\\`.\\n\\n` +\n ` Check that your app/ directory is where timber expects it.\\n\\n`\n );\n }\n }\n\n // Bare-specifier imports (`@timber-js/app/params`) are NOT checked here.\n // They are all dependencies of @timber-js/app itself — if timber is installed,\n // they resolve. And createRequire uses CJS resolution which cannot follow\n // ESM-only subpath exports, producing false warnings (TIM-1348 review).\n}\n\n/**\n * The production `RunTsgo`: spawn the compiler with its stdio inherited, so its\n * diagnostics stream to the terminal as they are produced (and are not capped\n * by a buffer, as `execFile` output would be).\n */\nconst spawnTsgo: RunTsgo = async (command, args, cwd) => {\n const { spawn } = await import('node:child_process');\n return new Promise<number | null>((resolve, reject) => {\n // No `shell: true`: Node would concatenate the argv into an unescaped\n // command string, so a project path containing a space would break.\n const child = spawn(command, args, { stdio: 'inherit', cwd });\n child.on('error', reject);\n child.on('close', (code) => resolve(code));\n });\n};\n\n/**\n * The production `ReadTsgoConfig`: `--showConfig`, with stdout captured.\n *\n * `maxBuffer` is raised well past `execFile`'s 1 MB default because the output\n * is one line per file in the program — a large app can exceed that, and\n * overflowing it would make this advisory go quiet on exactly the projects\n * where a misconfigured `include` costs the most. A size-dependent silence is\n * indistinguishable from \"everything is fine\".\n */\nconst execTsgoConfig: ReadTsgoConfig = async (command, args, cwd) => {\n const { execFile } = await import('node:child_process');\n return new Promise<string>((resolve, reject) => {\n execFile(command, args, { cwd, maxBuffer: 64 * 1024 * 1024 }, (err, stdout) =>\n err ? reject(err) : resolve(stdout)\n );\n });\n};\n\n/**\n * Warn when the generated route declarations will not reach the type checker.\n *\n * Writing `.timber/timber-routes.d.ts` and `timber-env.d.ts` is only half the\n * job: TypeScript loads them only if the project's `include`/`files` actually\n * pull `timber-env.d.ts` in (it is what `/// <reference>`s the route map). A\n * tsconfig with an explicit `include` that lists only source directories —\n * `[\"app\", \"*.config.ts\"]`, which is what `create-timber-app` shipped before\n * TIM-1332 — silently omits it, and every `<Link href>` then type-checks\n * against the empty placeholder instead of the generated route union. The\n * check would pass while answering the wrong question.\n *\n * The resolved file list comes from `tsgo --showConfig` rather than from our\n * own reading of the tsconfig: `include`, `exclude`, `files`, and `extends`\n * compose in ways that are the compiler's to decide, and a second\n * implementation of them would be a duplicate that drifts. `--showConfig` only\n * resolves globs, so it costs no type checking.\n *\n * This warns rather than fails. An app that never uses typed routes is\n * correct without those declarations, and a hard error would refuse it — the\n * one outcome a check must not produce for a working project. When the answer\n * cannot be determined (tsgo errored, output was not the expected JSON), the\n * warning is skipped: an advisory that fires on an unknown is noise.\n */\nasync function warnIfRouteTypesUnreachable(\n tsgoEntry: string,\n configArgs: string[],\n cwd: string,\n readConfig: ReadTsgoConfig\n): Promise<void> {\n let files: unknown;\n try {\n const raw = await readConfig(process.execPath, [tsgoEntry, '--showConfig', ...configArgs], cwd);\n files = (JSON.parse(raw) as { files?: unknown }).files;\n } catch {\n return;\n }\n if (!Array.isArray(files)) return;\n\n // Suffix match, not path resolution: `--showConfig` prints paths relative to\n // whichever tsconfig the compiler *found*, which for a bare `timber check`\n // is a directory we never learn. Over-matching only suppresses an advisory.\n //\n // The bare form is matched as well as the `/`-prefixed one. tsgo emits\n // `./timber-env.d.ts` today, but a leading `./` is cosmetic and an emitter\n // that dropped it would turn this into a warning printed at every correctly\n // configured project — the failure direction that trains people to ignore it.\n const reaches = files.some((file) => {\n if (typeof file !== 'string') return false;\n const normalized = file.replace(/\\\\/g, '/');\n return GENERATED_TYPE_PATHS.some(\n (path) => normalized === path || normalized.endsWith(`/${path}`)\n );\n });\n if (reaches) return;\n\n process.stderr.write(\n `[timber] The generated route types are not part of this TypeScript project, so\\n` +\n ` \\`<Link href>\\` and \\`useSegmentParams()\\` are being checked against empty\\n` +\n ` placeholders. Your tsconfig's \"include\" does not pull in timber-env.d.ts.\\n\\n` +\n ` To fix: add it to \"include\":\\n\\n` +\n ` \"include\": [\"app\", \"*.config.ts\", \"timber-env.d.ts\"]\\n\\n`\n );\n}\n\n/**\n * Run `tsgo --noEmit`, streaming its output.\n *\n * Rejects with the real numeric exit status on type errors. A failure to *start*\n * the compiler is reported separately — the historical bug this replaces\n * surfaced ENOENT as \"Type check failed with exit code undefined\", which read\n * as a type error and sent users looking at their own code.\n */\nexport async function runTypeCheck(\n options: CheckOptions = {},\n deps: CheckDeps = {}\n): Promise<void> {\n // The *resolved* root, handed over by the route pass — not `process.cwd()`.\n // The two differ whenever `--config` names a Vite config elsewhere, and the\n // route pass wrote `.timber/*` and `timber-env.d.ts` under the resolved one.\n // Running tsgo from cwd would then discover a different tsconfig entirely and\n // type-check a different project than the one just validated (TIM-1332, round\n // 5) — routes checked for the app, types checked for the monorepo around it.\n const root = options.root ?? process.cwd();\n const tsgoEntry = (deps.resolveTsgoEntry ?? (() => resolveTsgoEntry(root)))();\n const runTsgo = deps.runTsgo ?? spawnTsgo;\n\n // An explicit `--project` is a path the user typed at their own prompt, so it\n // is resolved against *their* cwd before the compiler is moved to the project\n // root. Without `--project`, tsgo discovers `tsconfig.json` from `root`,\n // which is where the declarations it needs were just written.\n const configArgs = options.config ? ['--project', resolve(process.cwd(), options.config)] : [];\n\n // Before checking: say so if the declarations the route pass just wrote will\n // not be part of the program. Otherwise the check passes while `<Link href>`\n // is measured against a placeholder. Same root and same argv as the check\n // itself, or it would inspect a different program than the one tsgo reads.\n await warnIfRouteTypesUnreachable(\n tsgoEntry,\n configArgs,\n root,\n deps.readTsgoConfig ?? execTsgoConfig\n );\n\n let code: number | null;\n try {\n code = await runTsgo(process.execPath, [tsgoEntry, '--noEmit', ...configArgs], root);\n } catch (err) {\n // Could not start the compiler at all. Kept distinct from a nonzero exit:\n // the version this replaced reported ENOENT as \"Type check failed with\n // exit code undefined\", which reads as a verdict on the user's types.\n throw new Error(\n `[timber] Failed to start the type checker: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n\n if (code === 0) return;\n\n // A null code means a signal killed the process rather than it exiting.\n // Say so instead of printing \"exit code null\".\n throw new Error(\n code === null\n ? 'Type check terminated before it finished'\n : `Type check failed with exit code ${code}`\n );\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { n as scanRoutes } from "./scanner-DmqdxzbW.js";
2
+ import { n as scanRoutes } from "./scanner-CQt12vE2.js";
3
3
  import { c as resolveAppDir } from "./plugin-context-BPBPc3lL.js";
4
4
  import { join } from "node:path";
5
5
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
@@ -296,4 +296,4 @@ function runSchemaSync(projectRoot) {
296
296
  //#endregion
297
297
  export { findSchemaFile as n, parseExistingSchemaKeys as r, cli_schema_sync_exports as t };
298
298
 
299
- //# sourceMappingURL=cli-schema-sync-D2eI8jEg.js.map
299
+ //# sourceMappingURL=cli-schema-sync-czh2dsLs.js.map