@timber-js/app 0.2.0-alpha.202 → 0.2.0-alpha.204
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{chains-CBNA0Ozj.js → chains-C9Y7Oyno.js} +6 -5
- package/dist/_chunks/chains-C9Y7Oyno.js.map +1 -0
- package/dist/_chunks/{dev-server-DioP7tkQ.js → dev-server-BV01iyUs.js} +2 -2
- package/dist/_chunks/{dev-server-DioP7tkQ.js.map → dev-server-BV01iyUs.js.map} +1 -1
- package/dist/_chunks/{live-graph-VjHFF5EV.js → live-graph-DCLQzV6m.js} +3 -3
- package/dist/_chunks/{live-graph-VjHFF5EV.js.map → live-graph-DCLQzV6m.js.map} +1 -1
- package/dist/_chunks/{poison-scan-lEbz4pQE.js → poison-scan-CX_NaNDi.js} +2 -2
- package/dist/_chunks/{poison-scan-lEbz4pQE.js.map → poison-scan-CX_NaNDi.js.map} +1 -1
- package/dist/analyze/classify.d.ts +5 -4
- package/dist/analyze/classify.d.ts.map +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cli.js +1 -1
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/server/deny-renderer.d.ts +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +8 -10
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/analyze/classify.ts +5 -4
- package/src/client/browser-entry/rsc-stream.ts +5 -1
- package/src/server/deny-renderer.ts +1 -3
- package/src/server/rsc-entry/helpers.ts +9 -14
- package/src/server/rsc-entry/rsc-stream.ts +10 -15
- package/dist/_chunks/chains-CBNA0Ozj.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as scanProjectSourceFiles, t as buildSourceExtensions } from "./scan-BoP17W3w.js";
|
|
2
|
-
import { a as detectDirective, n as stitchImportChains, r as classifyModule } from "./chains-
|
|
3
|
-
import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "./poison-scan-
|
|
2
|
+
import { a as detectDirective, n as stitchImportChains, r as classifyModule } from "./chains-C9Y7Oyno.js";
|
|
3
|
+
import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "./poison-scan-CX_NaNDi.js";
|
|
4
4
|
import { normalizePath } from "vite";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { readFile } from "node:fs/promises";
|
|
@@ -147,4 +147,4 @@ async function classifyLiveProject(ctx) {
|
|
|
147
147
|
//#endregion
|
|
148
148
|
export { classifyLiveFile, classifyLiveProject };
|
|
149
149
|
|
|
150
|
-
//# sourceMappingURL=live-graph-
|
|
150
|
+
//# sourceMappingURL=live-graph-DCLQzV6m.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-graph-VjHFF5EV.js","names":[],"sources":["../../src/analyze/live-graph.ts"],"sourcesContent":["/**\n * live-graph — classification from a running dev server's environment\n * module graphs (the fast path).\n *\n * When a dev server runs, the env module graphs are already resident —\n * classification is a lookup, not a crawl. This module adapts those\n * graphs to the SAME classification pipeline the cold crawl uses\n * (classify.ts + chains.ts + poison-scan.ts — design/47's \"one\n * classification module, thin frontends\" requirement), so the\n * dev-server endpoint and the CLI can never disagree about a file.\n *\n * Differences from the crawl, both inherent to the live source:\n *\n * - The browser (`client`) graph can be non-empty here (a real browser\n * has requested modules), so `envs.client` is confirmable.\n * - Rendering populates graphs lazily — only visited routes' code is\n * present, and modules behind unexecuted dynamic imports carry\n * `transformResult == null` (the §8 frontier). Those are reported as\n * `subtreeUnknown` rather than transformed on demand: a hint must\n * never mutate the dev server's graphs as a side effect of asking.\n *\n * Design docs: 47-module-environment-tooling.md §8, §\"Dev-server\n * endpoint\".\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { readFileSync } from 'node:fs';\nimport { normalizePath } from 'vite';\nimport {\n classifyModule,\n detectDirective,\n type ClassifyContext,\n type Directive,\n} from './classify.ts';\nimport type { GraphModuleReport } from './crawl.ts';\nimport { stitchImportChains, type ChainGraph, type ImportChain } from './chains.ts';\nimport {\n collectImportSpecifiers,\n importSignals,\n poisoningsFor,\n type Poisoning,\n} from './poison-scan.ts';\nimport { buildSourceExtensions, scanProjectSourceFiles, type ScanFailure } from './scan.ts';\n\n/**\n * Duck-typed node view for liveness checks — the `transformResult`\n * member ChainModuleNode deliberately omits (§8 detection is a\n * classification concern, not a chain concern).\n */\ninterface LiveModuleNode {\n transformResult: unknown;\n}\n\nimport type { ChainModuleNode } from './chains.ts';\n\n/** Duck-typed module graph — structural supertype of Vite's. */\nexport interface LiveGraph extends ChainGraph {\n getModulesByFile(file: string): Set<ChainModuleNode & LiveModuleNode> | undefined;\n}\n\nexport interface LiveGraphContext {\n rsc: LiveGraph;\n ssr: LiveGraph;\n client: LiveGraph;\n /** Realpath'd project root. */\n root: string;\n appDir: string;\n pageExtensions?: readonly string[];\n}\n\n/**\n * Per-module answer — the crawl's shape, aliased rather than restated\n * so a field added to one frontend cannot silently miss the other.\n */\nexport type LiveModuleReport = GraphModuleReport;\n\n/** Non-empty node set for a file in one graph. */\nfunction memberOf(graph: LiveGraph, file: string): boolean {\n return (graph.getModulesByFile(file)?.size ?? 0) > 0;\n}\n\n/**\n * §8: an SSR-graph member whose every node was never transformed is a\n * module behind an unexecuted dynamic import — reachable (it ships to\n * the browser) but its subtree may be missing from the graphs.\n *\n * Live-source caveat: Vite also clears `transformResult` when a watched\n * edit invalidates a node, so on the live graphs `subtreeUnknown`\n * means \"not currently transformed\" — a freshly-edited module reads\n * the same as a dormant dynamic-import payload until the next render\n * re-transforms it. The two states are indistinguishable from the\n * graph alone, and both honestly mean the subtree is not currently\n * known; consumers refreshing on `timber:graph-update` events see the\n * flag clear once the module is re-transformed.\n */\nfunction ssrTransformed(graph: LiveGraph, file: string): boolean | undefined {\n const nodes = graph.getModulesByFile(file);\n if (!nodes || nodes.size === 0) return undefined;\n for (const node of nodes) {\n if (node.transformResult != null) return true;\n }\n return false;\n}\n\n/**\n * Classify one file from the live graphs. `file` must be an absolute\n * realpath (graph nodes carry realpaths); it is slash-normalized here\n * to match Vite's node keying.\n */\nexport async function classifyLiveFile(\n rawFile: string,\n ctx: LiveGraphContext,\n // Shared across a ?all=1 sweep (classifyLiveProject) — a per-call\n // cache would re-read every chain hop's source per target file,\n // quadratic sync IO on well-connected graphs.\n getDirective: (file: string) => Directive = makeDirectiveReader(),\n // Failures are recorded, not swallowed (crawl parity): a lost source\n // read means the directive/poisoning half of the answer is missing,\n // and consumers deserve to see that beside the classification.\n errors?: LiveGraphError[]\n): Promise<LiveModuleReport> {\n const file = normalizePath(rawFile);\n let code: string | null = null;\n try {\n code = await readFile(file, 'utf8');\n } catch (error) {\n // Unreadable (deleted mid-query): classification proceeds on graph\n // membership alone; directive/poisonings default to none.\n errors?.push({\n environment: 'scan',\n id: file,\n message: `source read failed: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n const directive: Directive = code === null ? null : detectDirective(code, file);\n const signals = code === null ? [] : importSignals(collectImportSpecifiers(code, file));\n\n const classifyCtx: ClassifyContext = {\n appDir: ctx.appDir,\n pageExtensions: ctx.pageExtensions,\n };\n const classification = classifyModule(\n {\n file,\n inRsc: memberOf(ctx.rsc, file),\n inSsr: memberOf(ctx.ssr, file),\n inClient: memberOf(ctx.client, file),\n ssrTransformed: ssrTransformed(ctx.ssr, file),\n directive,\n },\n classifyCtx\n );\n\n const chains = classification.pending\n ? []\n : stitchImportChains(file, {\n rsc: ctx.rsc,\n ssr: ctx.ssr,\n appDir: ctx.appDir,\n pageExtensions: ctx.pageExtensions,\n getDirective,\n });\n\n return {\n ...classification,\n boundary: directive === 'use client',\n chains,\n poisonings: poisoningsFor(classification.kind, classification.pending, signals),\n };\n}\n\n/**\n * Synchronous directive reader with a per-call cache. chains.ts wants a\n * sync callback; node:fs readFileSync per unique hop file is acceptable\n * for a walk bounded by chain length.\n */\nfunction makeDirectiveReader(): (file: string) => Directive {\n const cache = new Map<string, Directive>();\n return (file) => {\n const hit = cache.get(file);\n if (hit !== undefined) return hit;\n let directive: Directive = null;\n try {\n directive = detectDirective(readFileSync(file, 'utf8'), file);\n } catch {\n directive = null;\n }\n cache.set(file, directive);\n return directive;\n };\n}\n\n/** Live-source error entry — the crawl's error vocabulary. */\nexport interface LiveGraphError {\n environment: 'scan';\n id: string;\n message: string;\n}\n\nexport interface LiveProjectReport {\n root: string;\n modules: LiveModuleReport[];\n errors: LiveGraphError[];\n}\n\n/**\n * Classify every project source file from the live graphs. Files on\n * disk that no graph reached get the §2 provisional/pending\n * classification — on a lazily-warmed dev server that includes routes\n * nobody has visited yet, which is honest: the graphs genuinely have\n * not confirmed them.\n */\nexport async function classifyLiveProject(ctx: LiveGraphContext): Promise<LiveProjectReport> {\n const files: string[] = [];\n const errors: LiveProjectReport['errors'] = [];\n const extensions = buildSourceExtensions(ctx.pageExtensions);\n await scanProjectSourceFiles(\n ctx.root,\n files,\n (failure: ScanFailure) =>\n errors.push({ environment: 'scan', id: failure.dir, message: failure.message }),\n 0,\n extensions\n );\n const modules: LiveModuleReport[] = [];\n const getDirective = makeDirectiveReader();\n for (const file of files) {\n modules.push(await classifyLiveFile(file, ctx, getDirective, errors));\n }\n modules.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));\n return { root: ctx.root, modules, errors };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA,SAAS,SAAS,OAAkB,MAAuB;CACzD,QAAQ,MAAM,iBAAiB,IAAI,CAAC,EAAE,QAAQ,KAAK;AACrD;;;;;;;;;;;;;;;AAgBA,SAAS,eAAe,OAAkB,MAAmC;CAC3E,MAAM,QAAQ,MAAM,iBAAiB,IAAI;CACzC,IAAI,CAAC,SAAS,MAAM,SAAS,GAAG,OAAO,KAAA;CACvC,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,mBAAmB,MAAM,OAAO;CAE3C,OAAO;AACT;;;;;;AAOA,eAAsB,iBACpB,SACA,KAIA,eAA4C,oBAAoB,GAIhE,QAC2B;CAC3B,MAAM,OAAO,cAAc,OAAO;CAClC,IAAI,OAAsB;CAC1B,IAAI;EACF,OAAO,MAAM,SAAS,MAAM,MAAM;CACpC,SAAS,OAAO;EAGd,QAAQ,KAAK;GACX,aAAa;GACb,IAAI;GACJ,SAAS,uBAAuB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACvF,CAAC;CACH;CACA,MAAM,YAAuB,SAAS,OAAO,OAAO,gBAAgB,MAAM,IAAI;CAC9E,MAAM,UAAU,SAAS,OAAO,CAAC,IAAI,cAAc,wBAAwB,MAAM,IAAI,CAAC;CAEtF,MAAM,cAA+B;EACnC,QAAQ,IAAI;EACZ,gBAAgB,IAAI;CACtB;CACA,MAAM,iBAAiB,eACrB;EACE;EACA,OAAO,SAAS,IAAI,KAAK,IAAI;EAC7B,OAAO,SAAS,IAAI,KAAK,IAAI;EAC7B,UAAU,SAAS,IAAI,QAAQ,IAAI;EACnC,gBAAgB,eAAe,IAAI,KAAK,IAAI;EAC5C;CACF,GACA,WACF;CAEA,MAAM,SAAS,eAAe,UAC1B,CAAC,IACD,mBAAmB,MAAM;EACvB,KAAK,IAAI;EACT,KAAK,IAAI;EACT,QAAQ,IAAI;EACZ,gBAAgB,IAAI;EACpB;CACF,CAAC;CAEL,OAAO;EACL,GAAG;EACH,UAAU,cAAc;EACxB;EACA,YAAY,cAAc,eAAe,MAAM,eAAe,SAAS,OAAO;CAChF;AACF;;;;;;AAOA,SAAS,sBAAmD;CAC1D,MAAM,wBAAQ,IAAI,IAAuB;CACzC,QAAQ,SAAS;EACf,MAAM,MAAM,MAAM,IAAI,IAAI;EAC1B,IAAI,QAAQ,KAAA,GAAW,OAAO;EAC9B,IAAI,YAAuB;EAC3B,IAAI;GACF,YAAY,gBAAgB,aAAa,MAAM,MAAM,GAAG,IAAI;EAC9D,QAAQ;GACN,YAAY;EACd;EACA,MAAM,IAAI,MAAM,SAAS;EACzB,OAAO;CACT;AACF;;;;;;;;AAsBA,eAAsB,oBAAoB,KAAmD;CAC3F,MAAM,QAAkB,CAAC;CACzB,MAAM,SAAsC,CAAC;CAC7C,MAAM,aAAa,sBAAsB,IAAI,cAAc;CAC3D,MAAM,uBACJ,IAAI,MACJ,QACC,YACC,OAAO,KAAK;EAAE,aAAa;EAAQ,IAAI,QAAQ;EAAK,SAAS,QAAQ;CAAQ,CAAC,GAChF,GACA,UACF;CACA,MAAM,UAA8B,CAAC;CACrC,MAAM,eAAe,oBAAoB;CACzC,KAAK,MAAM,QAAQ,OACjB,QAAQ,KAAK,MAAM,iBAAiB,MAAM,KAAK,cAAc,MAAM,CAAC;CAEtE,QAAQ,MAAM,GAAG,MAAO,EAAE,OAAO,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,CAAE;CACvE,OAAO;EAAE,MAAM,IAAI;EAAM;EAAS;CAAO;AAC3C"}
|
|
1
|
+
{"version":3,"file":"live-graph-DCLQzV6m.js","names":[],"sources":["../../src/analyze/live-graph.ts"],"sourcesContent":["/**\n * live-graph — classification from a running dev server's environment\n * module graphs (the fast path).\n *\n * When a dev server runs, the env module graphs are already resident —\n * classification is a lookup, not a crawl. This module adapts those\n * graphs to the SAME classification pipeline the cold crawl uses\n * (classify.ts + chains.ts + poison-scan.ts — design/47's \"one\n * classification module, thin frontends\" requirement), so the\n * dev-server endpoint and the CLI can never disagree about a file.\n *\n * Differences from the crawl, both inherent to the live source:\n *\n * - The browser (`client`) graph can be non-empty here (a real browser\n * has requested modules), so `envs.client` is confirmable.\n * - Rendering populates graphs lazily — only visited routes' code is\n * present, and modules behind unexecuted dynamic imports carry\n * `transformResult == null` (the §8 frontier). Those are reported as\n * `subtreeUnknown` rather than transformed on demand: a hint must\n * never mutate the dev server's graphs as a side effect of asking.\n *\n * Design docs: 47-module-environment-tooling.md §8, §\"Dev-server\n * endpoint\".\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { readFileSync } from 'node:fs';\nimport { normalizePath } from 'vite';\nimport {\n classifyModule,\n detectDirective,\n type ClassifyContext,\n type Directive,\n} from './classify.ts';\nimport type { GraphModuleReport } from './crawl.ts';\nimport { stitchImportChains, type ChainGraph, type ImportChain } from './chains.ts';\nimport {\n collectImportSpecifiers,\n importSignals,\n poisoningsFor,\n type Poisoning,\n} from './poison-scan.ts';\nimport { buildSourceExtensions, scanProjectSourceFiles, type ScanFailure } from './scan.ts';\n\n/**\n * Duck-typed node view for liveness checks — the `transformResult`\n * member ChainModuleNode deliberately omits (§8 detection is a\n * classification concern, not a chain concern).\n */\ninterface LiveModuleNode {\n transformResult: unknown;\n}\n\nimport type { ChainModuleNode } from './chains.ts';\n\n/** Duck-typed module graph — structural supertype of Vite's. */\nexport interface LiveGraph extends ChainGraph {\n getModulesByFile(file: string): Set<ChainModuleNode & LiveModuleNode> | undefined;\n}\n\nexport interface LiveGraphContext {\n rsc: LiveGraph;\n ssr: LiveGraph;\n client: LiveGraph;\n /** Realpath'd project root. */\n root: string;\n appDir: string;\n pageExtensions?: readonly string[];\n}\n\n/**\n * Per-module answer — the crawl's shape, aliased rather than restated\n * so a field added to one frontend cannot silently miss the other.\n */\nexport type LiveModuleReport = GraphModuleReport;\n\n/** Non-empty node set for a file in one graph. */\nfunction memberOf(graph: LiveGraph, file: string): boolean {\n return (graph.getModulesByFile(file)?.size ?? 0) > 0;\n}\n\n/**\n * §8: an SSR-graph member whose every node was never transformed is a\n * module behind an unexecuted dynamic import — reachable (it ships to\n * the browser) but its subtree may be missing from the graphs.\n *\n * Live-source caveat: Vite also clears `transformResult` when a watched\n * edit invalidates a node, so on the live graphs `subtreeUnknown`\n * means \"not currently transformed\" — a freshly-edited module reads\n * the same as a dormant dynamic-import payload until the next render\n * re-transforms it. The two states are indistinguishable from the\n * graph alone, and both honestly mean the subtree is not currently\n * known; consumers refreshing on `timber:graph-update` events see the\n * flag clear once the module is re-transformed.\n */\nfunction ssrTransformed(graph: LiveGraph, file: string): boolean | undefined {\n const nodes = graph.getModulesByFile(file);\n if (!nodes || nodes.size === 0) return undefined;\n for (const node of nodes) {\n if (node.transformResult != null) return true;\n }\n return false;\n}\n\n/**\n * Classify one file from the live graphs. `file` must be an absolute\n * realpath (graph nodes carry realpaths); it is slash-normalized here\n * to match Vite's node keying.\n */\nexport async function classifyLiveFile(\n rawFile: string,\n ctx: LiveGraphContext,\n // Shared across a ?all=1 sweep (classifyLiveProject) — a per-call\n // cache would re-read every chain hop's source per target file,\n // quadratic sync IO on well-connected graphs.\n getDirective: (file: string) => Directive = makeDirectiveReader(),\n // Failures are recorded, not swallowed (crawl parity): a lost source\n // read means the directive/poisoning half of the answer is missing,\n // and consumers deserve to see that beside the classification.\n errors?: LiveGraphError[]\n): Promise<LiveModuleReport> {\n const file = normalizePath(rawFile);\n let code: string | null = null;\n try {\n code = await readFile(file, 'utf8');\n } catch (error) {\n // Unreadable (deleted mid-query): classification proceeds on graph\n // membership alone; directive/poisonings default to none.\n errors?.push({\n environment: 'scan',\n id: file,\n message: `source read failed: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n const directive: Directive = code === null ? null : detectDirective(code, file);\n const signals = code === null ? [] : importSignals(collectImportSpecifiers(code, file));\n\n const classifyCtx: ClassifyContext = {\n appDir: ctx.appDir,\n pageExtensions: ctx.pageExtensions,\n };\n const classification = classifyModule(\n {\n file,\n inRsc: memberOf(ctx.rsc, file),\n inSsr: memberOf(ctx.ssr, file),\n inClient: memberOf(ctx.client, file),\n ssrTransformed: ssrTransformed(ctx.ssr, file),\n directive,\n },\n classifyCtx\n );\n\n const chains = classification.pending\n ? []\n : stitchImportChains(file, {\n rsc: ctx.rsc,\n ssr: ctx.ssr,\n appDir: ctx.appDir,\n pageExtensions: ctx.pageExtensions,\n getDirective,\n });\n\n return {\n ...classification,\n boundary: directive === 'use client',\n chains,\n poisonings: poisoningsFor(classification.kind, classification.pending, signals),\n };\n}\n\n/**\n * Synchronous directive reader with a per-call cache. chains.ts wants a\n * sync callback; node:fs readFileSync per unique hop file is acceptable\n * for a walk bounded by chain length.\n */\nfunction makeDirectiveReader(): (file: string) => Directive {\n const cache = new Map<string, Directive>();\n return (file) => {\n const hit = cache.get(file);\n if (hit !== undefined) return hit;\n let directive: Directive = null;\n try {\n directive = detectDirective(readFileSync(file, 'utf8'), file);\n } catch {\n directive = null;\n }\n cache.set(file, directive);\n return directive;\n };\n}\n\n/** Live-source error entry — the crawl's error vocabulary. */\nexport interface LiveGraphError {\n environment: 'scan';\n id: string;\n message: string;\n}\n\nexport interface LiveProjectReport {\n root: string;\n modules: LiveModuleReport[];\n errors: LiveGraphError[];\n}\n\n/**\n * Classify every project source file from the live graphs. Files on\n * disk that no graph reached get the §2 provisional/pending\n * classification — on a lazily-warmed dev server that includes routes\n * nobody has visited yet, which is honest: the graphs genuinely have\n * not confirmed them.\n */\nexport async function classifyLiveProject(ctx: LiveGraphContext): Promise<LiveProjectReport> {\n const files: string[] = [];\n const errors: LiveProjectReport['errors'] = [];\n const extensions = buildSourceExtensions(ctx.pageExtensions);\n await scanProjectSourceFiles(\n ctx.root,\n files,\n (failure: ScanFailure) =>\n errors.push({ environment: 'scan', id: failure.dir, message: failure.message }),\n 0,\n extensions\n );\n const modules: LiveModuleReport[] = [];\n const getDirective = makeDirectiveReader();\n for (const file of files) {\n modules.push(await classifyLiveFile(file, ctx, getDirective, errors));\n }\n modules.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));\n return { root: ctx.root, modules, errors };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA,SAAS,SAAS,OAAkB,MAAuB;CACzD,QAAQ,MAAM,iBAAiB,IAAI,CAAC,EAAE,QAAQ,KAAK;AACrD;;;;;;;;;;;;;;;AAgBA,SAAS,eAAe,OAAkB,MAAmC;CAC3E,MAAM,QAAQ,MAAM,iBAAiB,IAAI;CACzC,IAAI,CAAC,SAAS,MAAM,SAAS,GAAG,OAAO,KAAA;CACvC,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,mBAAmB,MAAM,OAAO;CAE3C,OAAO;AACT;;;;;;AAOA,eAAsB,iBACpB,SACA,KAIA,eAA4C,oBAAoB,GAIhE,QAC2B;CAC3B,MAAM,OAAO,cAAc,OAAO;CAClC,IAAI,OAAsB;CAC1B,IAAI;EACF,OAAO,MAAM,SAAS,MAAM,MAAM;CACpC,SAAS,OAAO;EAGd,QAAQ,KAAK;GACX,aAAa;GACb,IAAI;GACJ,SAAS,uBAAuB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACvF,CAAC;CACH;CACA,MAAM,YAAuB,SAAS,OAAO,OAAO,gBAAgB,MAAM,IAAI;CAC9E,MAAM,UAAU,SAAS,OAAO,CAAC,IAAI,cAAc,wBAAwB,MAAM,IAAI,CAAC;CAEtF,MAAM,cAA+B;EACnC,QAAQ,IAAI;EACZ,gBAAgB,IAAI;CACtB;CACA,MAAM,iBAAiB,eACrB;EACE;EACA,OAAO,SAAS,IAAI,KAAK,IAAI;EAC7B,OAAO,SAAS,IAAI,KAAK,IAAI;EAC7B,UAAU,SAAS,IAAI,QAAQ,IAAI;EACnC,gBAAgB,eAAe,IAAI,KAAK,IAAI;EAC5C;CACF,GACA,WACF;CAEA,MAAM,SAAS,eAAe,UAC1B,CAAC,IACD,mBAAmB,MAAM;EACvB,KAAK,IAAI;EACT,KAAK,IAAI;EACT,QAAQ,IAAI;EACZ,gBAAgB,IAAI;EACpB;CACF,CAAC;CAEL,OAAO;EACL,GAAG;EACH,UAAU,cAAc;EACxB;EACA,YAAY,cAAc,eAAe,MAAM,eAAe,SAAS,OAAO;CAChF;AACF;;;;;;AAOA,SAAS,sBAAmD;CAC1D,MAAM,wBAAQ,IAAI,IAAuB;CACzC,QAAQ,SAAS;EACf,MAAM,MAAM,MAAM,IAAI,IAAI;EAC1B,IAAI,QAAQ,KAAA,GAAW,OAAO;EAC9B,IAAI,YAAuB;EAC3B,IAAI;GACF,YAAY,gBAAgB,aAAa,MAAM,MAAM,GAAG,IAAI;EAC9D,QAAQ;GACN,YAAY;EACd;EACA,MAAM,IAAI,MAAM,SAAS;EACzB,OAAO;CACT;AACF;;;;;;;;AAsBA,eAAsB,oBAAoB,KAAmD;CAC3F,MAAM,QAAkB,CAAC;CACzB,MAAM,SAAsC,CAAC;CAC7C,MAAM,aAAa,sBAAsB,IAAI,cAAc;CAC3D,MAAM,uBACJ,IAAI,MACJ,QACC,YACC,OAAO,KAAK;EAAE,aAAa;EAAQ,IAAI,QAAQ;EAAK,SAAS,QAAQ;CAAQ,CAAC,GAChF,GACA,UACF;CACA,MAAM,UAA8B,CAAC;CACrC,MAAM,eAAe,oBAAoB;CACzC,KAAK,MAAM,QAAQ,OACjB,QAAQ,KAAK,MAAM,iBAAiB,MAAM,KAAK,cAAc,MAAM,CAAC;CAEtE,QAAQ,MAAM,GAAG,MAAO,EAAE,OAAO,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,CAAE;CACvE,OAAO;EAAE,MAAM,IAAI;EAAM;EAAS;CAAO;AAC3C"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as INFERRED_RULES, t as EXPLICIT_MARKERS } from "./poison-rules-DoEhbqaY.js";
|
|
2
|
-
import { o as langForFile } from "./chains-
|
|
2
|
+
import { o as langForFile } from "./chains-C9Y7Oyno.js";
|
|
3
3
|
import { parseAst } from "vite";
|
|
4
4
|
//#region src/analyze/poison-scan.ts
|
|
5
5
|
/**
|
|
@@ -98,4 +98,4 @@ function poisoningsFor(kind, pending, signals) {
|
|
|
98
98
|
//#endregion
|
|
99
99
|
export { importSignals as n, poisoningsFor as r, collectImportSpecifiers as t };
|
|
100
100
|
|
|
101
|
-
//# sourceMappingURL=poison-scan-
|
|
101
|
+
//# sourceMappingURL=poison-scan-CX_NaNDi.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poison-scan-
|
|
1
|
+
{"version":3,"file":"poison-scan-CX_NaNDi.js","names":[],"sources":["../../src/analyze/poison-scan.ts"],"sourcesContent":["/**\n * poison-scan — report-only poisoning detection for `timber graph`.\n *\n * Matches a module's static import specifiers against the shared rule\n * lists in poison-rules.ts (the single source of truth — design/47 §3)\n * and reports a poisoning only when the implied exclusivity CONFLICTS\n * with where the module actually runs: a server-only signal in a\n * client-reachable module, or a client-only signal in a server-reachable\n * one. A `node:fs` import in a file only the RSC environment loads is\n * working code, not a finding.\n *\n * Scope: import-specifier signals only (explicit markers + the inferred\n * rules' `matchesImport`). The inferred browser-global identifier rules\n * need shadow-safe allowlisted-position AST matching and are a separate\n * ticket — reporting them from a text scan would flag legitimate code.\n *\n * Design docs: 47-module-environment-tooling.md §3.\n */\n\nimport { parseAst } from 'vite';\nimport { EXPLICIT_MARKERS, INFERRED_RULES } from './poison-rules.ts';\nimport { langForFile, type ModuleKind } from './classify.ts';\n\n/** One poisoning finding on a module, in the JSON-contract shape. */\nexport interface Poisoning {\n /** Rule id — an inferred rule's id, or the explicit marker specifier. */\n rule: string;\n /** The import specifier that matched. */\n specifier: string;\n /** Which exclusivity the signal implies for the containing module. */\n implies: 'server-only' | 'client-only';\n /** Human-readable description for reporter output. */\n description: string;\n}\n\n/** File extensions the specifier scan can parse. */\nconst PARSEABLE_EXTENSIONS = /\\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;\n\n/**\n * A NON-EMPTY specifier list where every entry is type-only. Empty or\n * absent lists return false — a bare `import 'pkg'` is a side-effect\n * import and very much runs.\n */\nfunction isAllSpecifiersTypeOnly(\n specifiers: Array<{ importKind?: string; exportKind?: string }> | undefined\n): boolean {\n if (!specifiers || specifiers.length === 0) return false;\n return specifiers.every((s) => s.importKind === 'type' || s.exportKind === 'type');\n}\n\n/**\n * Collect every static import/export source and string-literal dynamic\n * import from a module's original source. Sources that fail to parse\n * (or are not JS/TS at all) return [] — no specifiers is a safe\n * default for a report-only scan.\n */\nexport function collectImportSpecifiers(code: string, file: string): string[] {\n if (!PARSEABLE_EXTENSIONS.test(file)) return [];\n let program;\n try {\n // Language from the extension, never forced tsx — see langForFile.\n program = parseAst(code, { lang: langForFile(file) });\n } catch {\n return [];\n }\n const specifiers: string[] = [];\n const visit = (node: unknown): void => {\n if (Array.isArray(node)) {\n for (const child of node) visit(child);\n return;\n }\n if (typeof node !== 'object' || node === null) return;\n const record = node as Record<string, unknown> & {\n type?: string;\n importKind?: string;\n exportKind?: string;\n specifiers?: Array<{ importKind?: string; exportKind?: string }>;\n };\n if (\n (record.type === 'ImportDeclaration' ||\n record.type === 'ExportNamedDeclaration' ||\n record.type === 'ExportAllDeclaration') &&\n // Type-only imports/exports are erased at runtime — `import type\n // { Stats } from 'node:fs'` in client code is working code, and\n // flagging it would be a false positive in a report-only scan.\n // Covers both the declaration-level form (`import type {…}`,\n // importKind on the declaration) and the inline form\n // (`import { type Stats }`, where the declaration stays 'value'\n // and each SPECIFIER carries the kind) — a declaration whose\n // specifiers are all type-only is erased just the same.\n record.importKind !== 'type' &&\n record.exportKind !== 'type' &&\n !isAllSpecifiersTypeOnly(record.specifiers) &&\n typeof (record.source as { value?: unknown } | null)?.value === 'string'\n ) {\n specifiers.push((record.source as { value: string }).value);\n } else if (\n record.type === 'ImportExpression' &&\n (record.source as { type?: string; value?: unknown })?.type === 'Literal' &&\n typeof (record.source as { value?: unknown }).value === 'string'\n ) {\n specifiers.push((record.source as { value: string }).value);\n }\n for (const key of Object.keys(record)) {\n if (key === 'type') continue;\n visit(record[key]);\n }\n };\n visit(program.body);\n return specifiers;\n}\n\n/** Exclusivity signals a module's import specifiers carry. */\nexport function importSignals(specifiers: string[]): Poisoning[] {\n const signals: Poisoning[] = [];\n for (const specifier of specifiers) {\n for (const marker of EXPLICIT_MARKERS) {\n if (specifier === marker.specifier) {\n signals.push({\n rule: marker.specifier,\n specifier,\n implies: marker.specifier === 'server-only' ? 'server-only' : 'client-only',\n description: `imports the ${marker.specifier} poison-pill marker`,\n });\n }\n }\n for (const rule of INFERRED_RULES) {\n if (rule.matchesImport?.(specifier)) {\n signals.push({\n rule: rule.id,\n specifier,\n implies: rule.implies,\n description: rule.description,\n });\n }\n }\n }\n return signals;\n}\n\n/**\n * Filter a module's signals down to actual conflicts with its\n * classification. Environment reach follows the taxonomy: boundary /\n * client-internal / shared modules run client-side; server / shared /\n * server-action modules run server-side. Pending (provisional) modules\n * run nowhere confirmed, so nothing conflicts yet.\n */\nexport function poisoningsFor(\n kind: ModuleKind,\n pending: boolean,\n signals: Poisoning[]\n): Poisoning[] {\n if (pending || signals.length === 0) return [];\n const clientReachable =\n kind === 'client-boundary' || kind === 'client-internal' || kind === 'shared';\n const serverReachable = kind === 'server' || kind === 'shared' || kind === 'server-action';\n return signals.filter(\n (signal) =>\n (signal.implies === 'server-only' && clientReachable) ||\n (signal.implies === 'client-only' && serverReachable)\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAM,uBAAuB;;;;;;AAO7B,SAAS,wBACP,YACS;CACT,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG,OAAO;CACnD,OAAO,WAAW,OAAO,MAAM,EAAE,eAAe,UAAU,EAAE,eAAe,MAAM;AACnF;;;;;;;AAQA,SAAgB,wBAAwB,MAAc,MAAwB;CAC5E,IAAI,CAAC,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC;CAC9C,IAAI;CACJ,IAAI;EAEF,UAAU,SAAS,MAAM,EAAE,MAAM,YAAY,IAAI,EAAE,CAAC;CACtD,QAAQ;EACN,OAAO,CAAC;CACV;CACA,MAAM,aAAuB,CAAC;CAC9B,MAAM,SAAS,SAAwB;EACrC,IAAI,MAAM,QAAQ,IAAI,GAAG;GACvB,KAAK,MAAM,SAAS,MAAM,MAAM,KAAK;GACrC;EACF;EACA,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;EAC/C,MAAM,SAAS;EAMf,KACG,OAAO,SAAS,uBACf,OAAO,SAAS,4BAChB,OAAO,SAAS,2BASlB,OAAO,eAAe,UACtB,OAAO,eAAe,UACtB,CAAC,wBAAwB,OAAO,UAAU,KAC1C,OAAQ,OAAO,QAAuC,UAAU,UAEhE,WAAW,KAAM,OAAO,OAA6B,KAAK;OACrD,IACL,OAAO,SAAS,sBACf,OAAO,QAA+C,SAAS,aAChE,OAAQ,OAAO,OAA+B,UAAU,UAExD,WAAW,KAAM,OAAO,OAA6B,KAAK;EAE5D,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG;GACrC,IAAI,QAAQ,QAAQ;GACpB,MAAM,OAAO,IAAI;EACnB;CACF;CACA,MAAM,QAAQ,IAAI;CAClB,OAAO;AACT;;AAGA,SAAgB,cAAc,YAAmC;CAC/D,MAAM,UAAuB,CAAC;CAC9B,KAAK,MAAM,aAAa,YAAY;EAClC,KAAK,MAAM,UAAU,kBACnB,IAAI,cAAc,OAAO,WACvB,QAAQ,KAAK;GACX,MAAM,OAAO;GACb;GACA,SAAS,OAAO,cAAc,gBAAgB,gBAAgB;GAC9D,aAAa,eAAe,OAAO,UAAU;EAC/C,CAAC;EAGL,KAAK,MAAM,QAAQ,gBACjB,IAAI,KAAK,gBAAgB,SAAS,GAChC,QAAQ,KAAK;GACX,MAAM,KAAK;GACX;GACA,SAAS,KAAK;GACd,aAAa,KAAK;EACpB,CAAC;CAGP;CACA,OAAO;AACT;;;;;;;;AASA,SAAgB,cACd,MACA,SACA,SACa;CACb,IAAI,WAAW,QAAQ,WAAW,GAAG,OAAO,CAAC;CAC7C,MAAM,kBACJ,SAAS,qBAAqB,SAAS,qBAAqB,SAAS;CACvE,MAAM,kBAAkB,SAAS,YAAY,SAAS,YAAY,SAAS;CAC3E,OAAO,QAAQ,QACZ,WACE,OAAO,YAAY,iBAAiB,mBACpC,OAAO,YAAY,iBAAiB,eACzC;AACF"}
|
|
@@ -99,10 +99,11 @@ export declare function langForFile(file: string): 'ts' | 'tsx' | 'jsx';
|
|
|
99
99
|
* Detect the first directive of a module's original source.
|
|
100
100
|
*
|
|
101
101
|
* Never string-match or regex directives (design/43): `hasDirective`
|
|
102
|
-
* from @vitejs/plugin-rsc
|
|
103
|
-
* check the RSC plugin runs to decide
|
|
104
|
-
* agrees with what the framework actually
|
|
105
|
-
*
|
|
102
|
+
* from @vitejs/plugin-rsc checks for the `directive` property on
|
|
103
|
+
* ExpressionStatements — the same check the RSC plugin runs to decide
|
|
104
|
+
* boundaries, so classification agrees with what the framework actually
|
|
105
|
+
* does with the file. The directive must be in prologue position (before
|
|
106
|
+
* any import or statement) per the ECMAScript spec. Non-JS sources
|
|
106
107
|
* (CSS, MDX, assets) and sources that fail to parse return null — no
|
|
107
108
|
* directive is a safe default; classification then falls back to graph
|
|
108
109
|
* membership.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../src/analyze/classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAcH,MAAM,MAAM,UAAU,GAClB,QAAQ,GACR,iBAAiB,GACjB,iBAAiB,GACjB,QAAQ,GACR,eAAe,GACf,cAAc,CAAC;AAEnB,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,YAAY,GAAG,IAAI,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,KAAK,EAAE,OAAO,CAAC;IACf,qDAAqD;IACrD,KAAK,EAAE,OAAO,CAAC;IACf;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,uDAAuD;IACvD,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,IAAI,EAAE;QAAE,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;IACtD;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAKD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,CAI9D;AAED
|
|
1
|
+
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../src/analyze/classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAcH,MAAM,MAAM,UAAU,GAClB,QAAQ,GACR,iBAAiB,GACjB,iBAAiB,GACjB,QAAQ,GACR,eAAe,GACf,cAAc,CAAC;AAEnB,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,YAAY,GAAG,IAAI,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,KAAK,EAAE,OAAO,CAAC;IACf,qDAAqD;IACrD,KAAK,EAAE,OAAO,CAAC;IACf;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,uDAAuD;IACvD,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,IAAI,EAAE;QAAE,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;IACtD;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAKD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,CAI9D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAerE;AAED,2CAA2C;AAC3C,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAQhG;AAoBD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,iBAAiB,EACtB,GAAG,CAAC,EAAE,eAAe,GACpB,oBAAoB,CA2BtB;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAC7B,YAAY,EAAE,QAAQ,CAAC,iBAAiB,CAAC,EACzC,GAAG,CAAC,EAAE,eAAe,GACpB,oBAAoB,EAAE,CAIxB"}
|
|
@@ -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-
|
|
4
|
-
import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "../_chunks/poison-scan-
|
|
3
|
+
import { a as detectDirective, i as classifyModules, n as stitchImportChains } from "../_chunks/chains-C9Y7Oyno.js";
|
|
4
|
+
import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "../_chunks/poison-scan-CX_NaNDi.js";
|
|
5
5
|
import { normalizePath } from "vite";
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import { readFile, realpath, writeFile } from "node:fs/promises";
|
|
@@ -378,7 +378,7 @@ async function printSingleFile(input, target, module, answer, json) {
|
|
|
378
378
|
console.log(` environments: ${envs.length > 0 ? envs.join(", ") : "none"}`);
|
|
379
379
|
if (module.subtreeUnknown) console.log(" note: reachable, but its subtree is unknown (never transformed)");
|
|
380
380
|
if (module.chains.length > 0) {
|
|
381
|
-
const { formatChains } = await import("../_chunks/chains-
|
|
381
|
+
const { formatChains } = await import("../_chunks/chains-C9Y7Oyno.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-
|
|
400
|
+
const { formatChains } = await import("../_chunks/chains-C9Y7Oyno.js").then((n) => n.t);
|
|
401
401
|
console.log(" import chains:");
|
|
402
402
|
console.log(formatChains(data.chains, configDir));
|
|
403
403
|
}
|
package/dist/cli.js
CHANGED
|
@@ -90,7 +90,7 @@ async function runDev(options, _deps) {
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
const { fork } = await import("node:child_process");
|
|
93
|
-
const { RESTART_EXIT_CODE } = await import("./_chunks/dev-server-
|
|
93
|
+
const { RESTART_EXIT_CODE } = await import("./_chunks/dev-server-BV01iyUs.js").then((n) => n.t);
|
|
94
94
|
while (true) {
|
|
95
95
|
const result = await new Promise((resolve) => {
|
|
96
96
|
const child = fork(process.argv[1], process.argv.slice(2), {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rsc-stream.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/rsc-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAItF,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,MAAM,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"rsc-stream.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/rsc-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAItF,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,MAAM,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,GAAG,IAAI,CAmH/D"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as formatMissingPeers, i as formatConfigErrors, n as timberDevServer, o as validateConfig, r as checkPeerDependencies } from "./_chunks/dev-server-
|
|
1
|
+
import { a as formatMissingPeers, i as formatConfigErrors, n as timberDevServer, o as validateConfig, r as checkPeerDependencies } from "./_chunks/dev-server-BV01iyUs.js";
|
|
2
2
|
import { h as swallow } from "./_chunks/logger-DqJ2VoAY.js";
|
|
3
3
|
import "./_chunks/csp-nonce-hOGniaG4.js";
|
|
4
4
|
import { a as isDynamicMetadataExtension } from "./_chunks/metadata-routes-DSDjM_hJ.js";
|
|
@@ -27,7 +27,7 @@ export interface LayoutEntry {
|
|
|
27
27
|
/** Callback to create a debug channel sink for RSC rendering. */
|
|
28
28
|
export type DebugChannelFactory = () => {
|
|
29
29
|
writable: WritableStream;
|
|
30
|
-
};
|
|
30
|
+
} | undefined;
|
|
31
31
|
/** Callback to pass RSC stream to SSR for HTML rendering. */
|
|
32
32
|
export type CallSsrFn = (rscStream: ReadableStream<Uint8Array>, navContext: NavContext) => Promise<Response>;
|
|
33
33
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deny-renderer.d.ts","sourceRoot":"","sources":["../../src/server/deny-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAc7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAKjE,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,iEAAiE;AACjE,MAAM,MAAM,mBAAmB,GAAG,MAAM;
|
|
1
|
+
{"version":3,"file":"deny-renderer.d.ts","sourceRoot":"","sources":["../../src/server/deny-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAc7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAKjE,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,iEAAiE;AACjE,MAAM,MAAM,mBAAmB,GAAG,MAAM;IAAE,QAAQ,EAAE,cAAc,CAAA;CAAE,GAAG,SAAS,CAAC;AAEjF,6DAA6D;AAC7D,MAAM,MAAM,SAAS,GAAG,CACtB,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,KACnB,OAAO,CAAC,QAAQ,CAAC,CAAC;AA+BvB;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,KAAK,QAAQ,GAAG,IAAI,CAAC;AAEhG;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,WAAW,EAAE,EAC/B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,EAAE,qBAAqB,EACtC,sBAAsB,EAAE,mBAAmB,EAC3C,OAAO,EAAE,SAAS,EAClB,WAAW,CAAC,EAAE,mBAAmB,EACjC,gBAAgB,SAAW,GAC1B,OAAO,CAAC,QAAQ,CAAC,CAsInB;AAgBD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,WAAW,EAAE,EAC/B,GAAG,EAAE,OAAO,EACZ,eAAe,EAAE,OAAO,EACxB,sBAAsB,EAAE,mBAAmB,EAC3C,gBAAgB,SAAW,GAC1B,OAAO,CAAC,QAAQ,CAAC,CAiDnB"}
|
|
@@ -7,19 +7,17 @@ import type { ManifestSegmentNode } from '../route-matcher.ts';
|
|
|
7
7
|
import type { SegmentInfo } from '../../shared/segment-info.ts';
|
|
8
8
|
import type { SlotSkipEntry } from '../slot-resolver.ts';
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* No-op — returns undefined so no debugChannel is passed to Flight.
|
|
11
|
+
* Debug rows ride the main stream inline (React's default behavior).
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* is omitted entirely so debug rows (owner stacks) ride the main stream
|
|
15
|
-
* and reach the browser for console warning resolution.
|
|
13
|
+
* Production Flight emits no debug rows. In dev, inline debug rows
|
|
14
|
+
* are handled by the Flight client for owner stack resolution.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* A writable-only debugChannel triggers a react-server-dom-webpack
|
|
17
|
+
* >=19.3.0 Flight client regression (owner/stack refs parsed after
|
|
18
|
+
* stream close produce "Connection closed" errors).
|
|
19
19
|
*/
|
|
20
|
-
export declare function createDebugChannelSink():
|
|
21
|
-
writable: WritableStream;
|
|
22
|
-
};
|
|
20
|
+
export declare function createDebugChannelSink(): undefined;
|
|
23
21
|
/**
|
|
24
22
|
* Parsed component debug info extracted from the Flight debug channel.
|
|
25
23
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,IAAI
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,IAAI,SAAS,CAElD;AAID;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,EAAE,CA8ClE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,KAAK,CAAC;IACtB,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B,CAAC,EACF,YAAY,CAAC,EAAE,aAAa,EAAE,EAC9B,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAC7B,gBAAgB,SAAW,GAC1B,WAAW,EAAE,CAsEf;AAMD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -13,7 +13,7 @@ import type { RscPayloadRoot } from '../../shared/payload-root.ts';
|
|
|
13
13
|
import { type DenyPageEntry } from '../deny-boundary.ts';
|
|
14
14
|
import { DenySignal, RedirectSignal } from '../primitives.ts';
|
|
15
15
|
import { type DebugComponentEntry } from './helpers.ts';
|
|
16
|
-
import {
|
|
16
|
+
import type { DebugChannelRegistry } from '../../dev-tools/debug-channel.ts';
|
|
17
17
|
/**
|
|
18
18
|
* Mutable signal state captured during RSC rendering.
|
|
19
19
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rsc-stream.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAInE,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE3E,OAAO,EAIL,UAAU,EACV,cAAc,EAEf,MAAM,kBAAkB,CAAC;AAG1B,OAAO,
|
|
1
|
+
{"version":3,"file":"rsc-stream.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAInE,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE3E,OAAO,EAIL,UAAU,EACV,cAAc,EAEf,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAI7E;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B;;;;;;;;;;OAUG;IACH,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;IACtC,WAAW,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACvD;;;;;;;;OAQG;IACH,kBAAkB,EAAE,OAAO,GAAG,IAAI,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB;gEAC4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;IAClD,OAAO,EAAE,aAAa,CAAC;IACvB,2EAA2E;IAC3E,kBAAkB,CAAC,EAAE,MAAM,mBAAmB,EAAE,CAAC;IACjD,gEAAgE;IAChE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,OAAO,EACZ,aAAa,CAAC,EAAE,aAAa,EAAE,EAC/B,gBAAgB,CAAC,EAAE,oBAAoB,GACtC,eAAe,CAwLjB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.204",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"@content-collections/vite": "^0.2.0 || ^0.3.0",
|
|
164
164
|
"@typescript/native-preview": "^7.0.0-dev.0",
|
|
165
165
|
"@vitejs/plugin-react": "^6.1.0",
|
|
166
|
-
"@vitejs/plugin-rsc": ">=0.5.
|
|
166
|
+
"@vitejs/plugin-rsc": ">=0.5.35",
|
|
167
167
|
"nuqs": "^2.0.0",
|
|
168
168
|
"react": "19.3.0",
|
|
169
169
|
"react-dom": "19.3.0",
|
package/src/analyze/classify.ts
CHANGED
|
@@ -126,10 +126,11 @@ export function langForFile(file: string): 'ts' | 'tsx' | 'jsx' {
|
|
|
126
126
|
* Detect the first directive of a module's original source.
|
|
127
127
|
*
|
|
128
128
|
* Never string-match or regex directives (design/43): `hasDirective`
|
|
129
|
-
* from @vitejs/plugin-rsc
|
|
130
|
-
* check the RSC plugin runs to decide
|
|
131
|
-
* agrees with what the framework actually
|
|
132
|
-
*
|
|
129
|
+
* from @vitejs/plugin-rsc checks for the `directive` property on
|
|
130
|
+
* ExpressionStatements — the same check the RSC plugin runs to decide
|
|
131
|
+
* boundaries, so classification agrees with what the framework actually
|
|
132
|
+
* does with the file. The directive must be in prologue position (before
|
|
133
|
+
* any import or statement) per the ECMAScript spec. Non-JS sources
|
|
133
134
|
* (CSS, MDX, assets) and sources that fail to parse return null — no
|
|
134
135
|
* directive is a safe default; classification then falls back to graph
|
|
135
136
|
* membership.
|
|
@@ -95,7 +95,11 @@ export function createRscPayloadStream(): RscStreamResult | null {
|
|
|
95
95
|
},
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
// No debug channel — debug rows ride the main stream inline.
|
|
99
|
+
// A readable debug channel causes "Connection closed" when HMR
|
|
100
|
+
// messages are lost or sessions expire. React handles inline
|
|
101
|
+
// debug rows natively for owner stack resolution.
|
|
102
|
+
const debugChannel = null;
|
|
99
103
|
|
|
100
104
|
// Close the stream when the document finishes loading.
|
|
101
105
|
// DOMContentLoaded fires after the HTML parser has processed all
|
|
@@ -49,9 +49,7 @@ export interface LayoutEntry {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/** Callback to create a debug channel sink for RSC rendering. */
|
|
52
|
-
export type DebugChannelFactory = () => {
|
|
53
|
-
writable: WritableStream;
|
|
54
|
-
};
|
|
52
|
+
export type DebugChannelFactory = () => { writable: WritableStream } | undefined;
|
|
55
53
|
|
|
56
54
|
/** Callback to pass RSC stream to SSR for HTML rendering. */
|
|
57
55
|
export type CallSsrFn = (
|
|
@@ -13,23 +13,18 @@ import { isClientReference } from '../utils/element-type.ts';
|
|
|
13
13
|
import { isStaticRequestDependent } from '../request-dep.ts';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* No-op — returns undefined so no debugChannel is passed to Flight.
|
|
17
|
+
* Debug rows ride the main stream inline (React's default behavior).
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* is omitted entirely so debug rows (owner stacks) ride the main stream
|
|
21
|
-
* and reach the browser for console warning resolution.
|
|
19
|
+
* Production Flight emits no debug rows. In dev, inline debug rows
|
|
20
|
+
* are handled by the Flight client for owner stack resolution.
|
|
22
21
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* A writable-only debugChannel triggers a react-server-dom-webpack
|
|
23
|
+
* >=19.3.0 Flight client regression (owner/stack refs parsed after
|
|
24
|
+
* stream close produce "Connection closed" errors).
|
|
25
25
|
*/
|
|
26
|
-
export function createDebugChannelSink():
|
|
27
|
-
|
|
28
|
-
// Drain the readable side so the writable never back-pressures.
|
|
29
|
-
sink.readable.pipeTo(new WritableStream()).catch(() => {});
|
|
30
|
-
// Only writable — omitting `readable` avoids Flight's keepDebugAlive
|
|
31
|
-
// mode which holds deferred debug refs open per request.
|
|
32
|
-
return { writable: sink.writable };
|
|
26
|
+
export function createDebugChannelSink(): undefined {
|
|
27
|
+
return undefined;
|
|
33
28
|
}
|
|
34
29
|
|
|
35
30
|
// ─── Debug Channel Collector (dev mode only) ────────────────────────────
|
|
@@ -27,8 +27,8 @@ import {
|
|
|
27
27
|
} from '../primitives.ts';
|
|
28
28
|
import { checkAndWarnRscPropError } from '../rsc-prop-warnings.ts';
|
|
29
29
|
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
30
|
+
import { isAbortError, type DebugComponentEntry } from './helpers.ts';
|
|
31
|
+
import type { DebugChannelRegistry } from '../../dev-tools/debug-channel.ts';
|
|
32
32
|
import { isDebug } from '../debug.ts';
|
|
33
33
|
import { isDevMode } from '../debug.ts';
|
|
34
34
|
|
|
@@ -111,18 +111,13 @@ export function renderRscStream(
|
|
|
111
111
|
|
|
112
112
|
let rscStream: ReadableStream<Uint8Array> | undefined;
|
|
113
113
|
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const debugChannel = devDebugRegistry
|
|
122
|
-
? createDevDebugChannel(devDebugRegistry)
|
|
123
|
-
: createDebugChannelSink();
|
|
124
|
-
const debugRequestId =
|
|
125
|
-
'requestId' in debugChannel ? (debugChannel as { requestId: string }).requestId : undefined;
|
|
114
|
+
// No debugChannel — debug rows ride the main RSC stream inline.
|
|
115
|
+
// Production Flight emits none. In dev, the browser Flight client
|
|
116
|
+
// handles inline debug rows for owner stack resolution (React's
|
|
117
|
+
// default behavior). A writable-only debugChannel triggers a
|
|
118
|
+
// react-server-dom-webpack >=19.3.0 Flight client regression.
|
|
119
|
+
const debugChannel = undefined;
|
|
120
|
+
const debugRequestId = undefined;
|
|
126
121
|
|
|
127
122
|
try {
|
|
128
123
|
rscStream = renderToReadableStream(
|
|
@@ -284,7 +279,7 @@ export function renderRscStream(
|
|
|
284
279
|
// In dev mode, debug rows ride the main stream (no separate channel), so
|
|
285
280
|
// this is undefined — the error overlay uses stack traces directly.
|
|
286
281
|
getDebugComponents:
|
|
287
|
-
'getComponents' in debugChannel
|
|
282
|
+
debugChannel && 'getComponents' in debugChannel
|
|
288
283
|
? (debugChannel as { getComponents: () => DebugComponentEntry[] }).getComponents
|
|
289
284
|
: undefined,
|
|
290
285
|
debugRequestId,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chains-CBNA0Ozj.js","names":[],"sources":["../../src/analyze/classify.ts","../../src/analyze/chains.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. */\nconst 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 scans the top-level statement list — the same\n * check the RSC plugin runs to decide boundaries, so classification\n * agrees with what the framework actually does with the file (a\n * directive after imports or statements still counts). 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","/**\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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"}
|