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

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 (36) hide show
  1. package/dist/_chunks/{chains-BoO51joc.js → chains-DGX9zmg9.js} +5 -146
  2. package/dist/_chunks/chains-DGX9zmg9.js.map +1 -0
  3. package/dist/_chunks/classify-QwG5rxKI.js +147 -0
  4. package/dist/_chunks/classify-QwG5rxKI.js.map +1 -0
  5. package/dist/_chunks/client-dep-entries-2HCF09no.js +66 -0
  6. package/dist/_chunks/client-dep-entries-2HCF09no.js.map +1 -0
  7. package/dist/_chunks/{dev-server-v97rQH4b.js → dev-server-TFpEwm3H.js} +2 -2
  8. package/dist/_chunks/{dev-server-v97rQH4b.js.map → dev-server-TFpEwm3H.js.map} +1 -1
  9. package/dist/_chunks/{live-graph-9cSnn_h9.js → live-graph-cNuWMYQI.js} +4 -3
  10. package/dist/_chunks/{live-graph-9cSnn_h9.js.map → live-graph-cNuWMYQI.js.map} +1 -1
  11. package/dist/_chunks/{poison-scan-vGV7Re0B.js → poison-scan-CfQ3unZR.js} +2 -4
  12. package/dist/_chunks/poison-scan-CfQ3unZR.js.map +1 -0
  13. package/dist/analyze/classify.d.ts +2 -0
  14. package/dist/analyze/classify.d.ts.map +1 -1
  15. package/dist/analyze/crawl-entry.js +3 -2
  16. package/dist/analyze/crawl-entry.js.map +1 -1
  17. package/dist/analyze/graph-command.js +2 -2
  18. package/dist/analyze/poison-scan.d.ts.map +1 -1
  19. package/dist/cli.js +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +40 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/plugins/client-dep-entries.d.ts +44 -0
  24. package/dist/plugins/client-dep-entries.d.ts.map +1 -0
  25. package/dist/plugins/content.d.ts.map +1 -1
  26. package/dist/plugins/entries.d.ts +6 -0
  27. package/dist/plugins/entries.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/analyze/classify.ts +1 -1
  30. package/src/analyze/poison-scan.ts +1 -4
  31. package/src/index.ts +21 -2
  32. package/src/plugins/client-dep-entries.ts +78 -0
  33. package/src/plugins/content.ts +26 -1
  34. package/src/plugins/entries.ts +7 -0
  35. package/dist/_chunks/chains-BoO51joc.js.map +0 -1
  36. package/dist/_chunks/poison-scan-vGV7Re0B.js.map +0 -1
@@ -1,6 +1,7 @@
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-BoO51joc.js";
3
- import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "./poison-scan-vGV7Re0B.js";
2
+ import { i as detectDirective, n as classifyModule } from "./classify-QwG5rxKI.js";
3
+ import { n as stitchImportChains } from "./chains-DGX9zmg9.js";
4
+ import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "./poison-scan-CfQ3unZR.js";
4
5
  import { normalizePath } from "vite";
5
6
  import { readFileSync } from "node:fs";
6
7
  import { readFile } from "node:fs/promises";
@@ -147,4 +148,4 @@ async function classifyLiveProject(ctx) {
147
148
  //#endregion
148
149
  export { classifyLiveFile, classifyLiveProject };
149
150
 
150
- //# sourceMappingURL=live-graph-9cSnn_h9.js.map
151
+ //# sourceMappingURL=live-graph-cNuWMYQI.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"live-graph-9cSnn_h9.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-cNuWMYQI.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-BoO51joc.js";
2
+ import { o as langForFile, t as PARSEABLE_EXTENSIONS } from "./classify-QwG5rxKI.js";
3
3
  import { parseAst } from "vite";
4
4
  //#region src/analyze/poison-scan.ts
5
5
  /**
@@ -20,8 +20,6 @@ import { parseAst } from "vite";
20
20
  *
21
21
  * Design docs: 47-module-environment-tooling.md §3.
22
22
  */
23
- /** File extensions the specifier scan can parse. */
24
- var PARSEABLE_EXTENSIONS = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;
25
23
  /**
26
24
  * A NON-EMPTY specifier list where every entry is type-only. Empty or
27
25
  * absent lists return false — a bare `import 'pkg'` is a side-effect
@@ -98,4 +96,4 @@ function poisoningsFor(kind, pending, signals) {
98
96
  //#endregion
99
97
  export { importSignals as n, poisoningsFor as r, collectImportSpecifiers as t };
100
98
 
101
- //# sourceMappingURL=poison-scan-vGV7Re0B.js.map
99
+ //# sourceMappingURL=poison-scan-CfQ3unZR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poison-scan-CfQ3unZR.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, PARSEABLE_EXTENSIONS, 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/**\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,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"}
@@ -85,6 +85,8 @@ export interface ModuleClassification {
85
85
  */
86
86
  subtreeUnknown: boolean;
87
87
  }
88
+ /** File extensions eligible for directive detection. */
89
+ export declare const PARSEABLE_EXTENSIONS: RegExp;
88
90
  /**
89
91
  * Parse language for a source file, from its extension. Never force
90
92
  * `tsx` onto plain `.ts`: JSX-ambiguous TypeScript such as
@@ -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;;;;;;;;;;;;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
+ {"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;AAED,wDAAwD;AACxD,eAAO,MAAM,oBAAoB,QAAyC,CAAC;AAE3E;;;;;;;;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,8 @@
1
1
  import { i as scanProjectSourceFiles, n as isScannableSourceFile, t as buildSourceExtensions } from "../_chunks/scan-BoP17W3w.js";
2
2
  import { c as resolveAppDir, i as isTimberPluginApi } from "../_chunks/plugin-context-BPBPc3lL.js";
3
- import { a as detectDirective, i as classifyModules, n as stitchImportChains } from "../_chunks/chains-BoO51joc.js";
4
- import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "../_chunks/poison-scan-vGV7Re0B.js";
3
+ import { i as detectDirective, r as classifyModules } from "../_chunks/classify-QwG5rxKI.js";
4
+ import { n as stitchImportChains } from "../_chunks/chains-DGX9zmg9.js";
5
+ import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "../_chunks/poison-scan-CfQ3unZR.js";
5
6
  import { normalizePath } from "vite";
6
7
  import { join, resolve } from "node:path";
7
8
  import { readFile, realpath, writeFile } from "node:fs/promises";
@@ -1 +1 @@
1
- {"version":3,"file":"crawl-entry.js","names":[],"sources":["../../src/analyze/crawl.ts","../../src/analyze/crawl-entry.ts"],"sourcesContent":["/**\n * crawl — programmatic middleware-mode crawl (the cold path).\n *\n * Boots `createServer({ middlewareMode: true })` — no HTTP port, no route\n * rendering — and BFS-walks `env.transformRequest(url)` → importedModules\n * edges from `virtual:timber-rsc-entry`, then from every discovered\n * `'use client'` boundary through the SSR environment. This reuses Vite's\n * actual resolution (aliases, tsconfig paths, react-server export\n * conditions, virtual modules), so it cannot drift from what dev or the\n * build does; hand-rolled resolution was evaluated and rejected\n * (design/47 §\"Verified Prototype Findings\").\n *\n * Cost model (design/47 §6): 2.5 s on small apps to ~10 s on dep-heavy\n * plugin stacks, with a transient ~1.3 GB RSS — callers MUST run this in\n * a short-lived child process (see crawl-entry.ts) and cache the\n * classification output, not the Vite transforms. The crawl always\n * covers the full project: one crawl per project state, never per\n * question — marginal per-file cost is ~zero once booted.\n *\n * `TIMBER_ANALYZE=1` is set before the server boots so platform dev\n * plugins (wrangler bindings — a measured ~6.8 s remote-bindings\n * handshake on website) skip work the crawl never needs: classification\n * wants import edges, not live Cloudflare connections.\n *\n * Design docs: 47-module-environment-tooling.md §6, §8.\n */\n\nimport { readFile, realpath } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { ViteDevServer } from 'vite';\nimport { normalizePath } from 'vite';\nimport {\n classifyModules,\n detectDirective,\n type Directive,\n type ModuleClassification,\n} from './classify.ts';\nimport { buildSourceExtensions, scanProjectSourceFiles, isScannableSourceFile } from './scan.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 { isTimberPluginApi, resolveAppDir } from '../plugin-context.ts';\n\nconst RSC_ENTRY_URL = 'virtual:timber-rsc-entry';\n\n/**\n * Marker preceding the JSON report on crawl-entry's stdout. Lives here\n * (not in crawl-entry.ts) so consumers can import it without executing\n * the child entry's main().\n */\nexport const CRAWL_RESULT_MARKER = '__TIMBER_CRAWL_RESULT__';\n\nexport interface CrawlOptions {\n /** Project root (the app directory containing vite.config). */\n root: string;\n /** Explicit Vite config file; discovered from root when omitted. */\n configFile?: string;\n /** Overall crawl deadline. Default 120s. */\n timeoutMs?: number;\n}\n\nexport interface CrawlError {\n /** 'scan' covers disk-scan and directive-read failures. */\n environment: 'rsc' | 'ssr' | 'scan';\n id: string;\n message: string;\n}\n\n/**\n * One project file's full graph answer — the classification plus the\n * remaining JSON-contract fields (`boundary`, `chains`, `poisonings`)\n * that need the open graphs / original sources to compute. This is the\n * per-module unit `timber graph` caches and serves.\n */\nexport interface GraphModuleReport extends ModuleClassification {\n /** True when the file itself carries `'use client'`. */\n boundary: boolean;\n /** Boundary-aware import chains (design/47 §7); [] when in no graph. */\n chains: ImportChain[];\n /** Report-only poisoning conflicts (design/47 §3); [] when clean. */\n poisonings: Poisoning[];\n}\n\nexport interface CrawlReport {\n root: string;\n /**\n * True when the crawl deadline expired mid-walk — the report is\n * truncated, not wrong, and the condition is transient (a slow\n * machine, a cold dep-optimizer). Consumers must not cache an\n * incomplete report: with unchanged files it would be served forever,\n * never retrying after the transient cause cleared. Deterministic\n * per-module transform failures do NOT set this — they recur on\n * retry, so caching them is correct.\n */\n incomplete: boolean;\n /** Classification of every project file, sorted by path. */\n modules: GraphModuleReport[];\n /**\n * Transform failures encountered during the BFS. These are classified,\n * not swallowed — CSS/asset URLs and non-transformable modules land\n * here (44 on website in the §6 measurement) and the crawl continues.\n */\n errors: CrawlError[];\n stats: {\n rscGraphModules: number;\n ssrGraphModules: number;\n projectFiles: number;\n durationMs: number;\n };\n}\n\n/** Marker error for deadline expiry — structurally distinguishable\n * from a deterministic transform failure (consumers must not cache a\n * deadline-truncated report; a transform failure recurs on retry and\n * caches fine). */\nclass CrawlDeadlineError extends Error {}\n\n/** Race a promise against the crawl deadline. */\nfunction withDeadline<T>(promise: Promise<T>, remainingMs: number, label: string): Promise<T> {\n return new Promise<T>((resolvePromise, rejectPromise) => {\n const timer = setTimeout(\n () =>\n rejectPromise(\n new CrawlDeadlineError(`crawl deadline exceeded while transforming ${label}`)\n ),\n Math.max(remainingMs, 1)\n );\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolvePromise(value);\n },\n (error) => {\n clearTimeout(timer);\n rejectPromise(error);\n }\n );\n });\n}\n\n/** Duck-typed view of the Vite dev environment surface the crawl uses. */\nexport interface CrawlEnvironment {\n transformRequest(url: string): Promise<unknown>;\n moduleGraph: {\n getModuleByUrl(url: string): Promise<CrawlModuleNode | undefined>;\n };\n}\n\ninterface CrawlModuleNode {\n id: string | null;\n url: string;\n file: string | null;\n importedModules: Set<CrawlModuleNode>;\n transformResult: unknown;\n}\n\ninterface EnvCrawlResult {\n /** Absolute paths of project files reached in this environment. */\n projectFiles: Set<string>;\n /** Project files whose own transform failed (membership unconfirmed subtree). */\n failedFiles: Set<string>;\n /** Total graph modules visited (project + deps + virtual). */\n visited: number;\n /** The deadline expired mid-walk — the result is truncated. */\n deadlineHit: boolean;\n}\n\n/**\n * Whether a graph node's file belongs in the report. `root` must be\n * slash-normalized; module-node paths already are (Vite normalizes\n * them, even on Windows — comparing against a backslashed root would\n * reject every physical module there and misreport the whole app as\n * pending). The rule set is the scan's own (isScannableSourceFile), so\n * report membership, disk-scan coverage, and the cache fingerprint are\n * one set — a graph-reachable file the fingerprint never tracked would\n * let its edits serve a stale cache (codex review, PR #1056).\n */\nfunction isProjectFile(file: string | null, root: string, extensions?: RegExp): file is string {\n if (!file) return false;\n return isScannableSourceFile(normalizePath(file), root, extensions);\n}\n\n/**\n * BFS one environment from the given seed URLs, transforming every\n * discovered module unconditionally. Transforming (rather than reading\n * live graphs) is what closes the §8 blind spot: modules behind\n * unexecuted dynamic imports are transformed here like any other edge.\n *\n * Exported for tests (deadline behavior is exercised with a fake env).\n */\nexport async function crawlEnvironment(\n env: CrawlEnvironment,\n envName: 'rsc' | 'ssr',\n seeds: string[],\n root: string,\n deadlineAt: number,\n errors: CrawlError[],\n extensions?: RegExp\n): Promise<EnvCrawlResult> {\n const queue = [...seeds];\n const enqueued = new Set(queue);\n const projectFiles = new Set<string>();\n const failedFiles = new Set<string>();\n let visited = 0;\n let deadlineHit = false;\n\n // Index-cursor BFS — the queue only grows, so this walks each entry once.\n for (let cursor = 0; cursor < queue.length; cursor++) {\n const url = queue[cursor];\n const remaining = deadlineAt - Date.now();\n if (remaining <= 0) {\n errors.push({ environment: envName, id: url, message: 'crawl deadline exceeded' });\n deadlineHit = true;\n break;\n }\n\n let node: CrawlModuleNode | undefined;\n try {\n await withDeadline(env.transformRequest(url), remaining, url);\n node = await env.moduleGraph.getModuleByUrl(url);\n } catch (error) {\n if (error instanceof CrawlDeadlineError) deadlineHit = true;\n errors.push({\n environment: envName,\n id: url,\n message: error instanceof Error ? error.message : String(error),\n });\n // The URL may still have a graph node with a file — it IS a graph\n // member (reachable), its transform just failed. Record membership\n // AND the failure so classification reports it as reachable with\n // subtreeUnknown, not as pending/unreferenced.\n node = await env.moduleGraph.getModuleByUrl(url).catch(() => undefined);\n if (node && isProjectFile(node.file, root, extensions)) {\n projectFiles.add(node.file);\n failedFiles.add(node.file);\n }\n continue;\n }\n\n if (!node) continue;\n visited++;\n if (isProjectFile(node.file, root, extensions)) projectFiles.add(node.file);\n\n for (const child of node.importedModules) {\n const childUrl = child.url ?? child.id;\n if (!childUrl || enqueued.has(childUrl)) continue;\n enqueued.add(childUrl);\n queue.push(childUrl);\n }\n }\n\n return { projectFiles, failedFiles, visited, deadlineHit };\n}\n\n/** Root-relative Vite URL for an absolute project file path.\n * Both arguments must be slash-normalized (the crawl normalizes at\n * the boundary, so callers always satisfy this). */\nfunction fileToUrl(file: string, root: string): string {\n return file.slice(root.length);\n}\n\n/** What one read of a module's original source yields. */\ninterface SourceFacts {\n directive: Directive;\n /** Import-specifier exclusivity signals (poison-scan). */\n signals: Poisoning[];\n}\n\n/**\n * Read a file's first directive and poison signals from its original\n * source on disk, in one read. A failure is recorded, not swallowed —\n * a lost 'use client' here would silently drop the whole client\n * subtree from the SSR crawl.\n */\nasync function sourceFactsOf(file: string, errors: CrawlError[]): Promise<SourceFacts> {\n try {\n const code = await readFile(file, 'utf8');\n return {\n directive: detectDirective(code, file),\n signals: importSignals(collectImportSpecifiers(code, file)),\n };\n } catch (error) {\n errors.push({\n environment: 'scan',\n id: file,\n message: `directive read failed: ${error instanceof Error ? error.message : String(error)}`,\n });\n return { directive: null, signals: [] };\n }\n}\n\n/**\n * Crawl a project cold and classify every project file.\n *\n * Boots a middleware-mode Vite server with the app's own config (all\n * timber plugins, real resolution), crawls the RSC graph from the entry,\n * detects directives from original sources, crawls the SSR graph from\n * every `'use client'` boundary, and intersects the memberships.\n */\nexport async function crawlProject(options: CrawlOptions): Promise<CrawlReport> {\n const startedAt = Date.now();\n const deadlineAt = startedAt + (options.timeoutMs ?? 120_000);\n // Signal platform dev plugins (wrangler bindings) to skip boot work the\n // crawl never uses. Must be set before createServer runs plugin hooks.\n process.env.TIMBER_ANALYZE = '1';\n\n const { createServer } = await import('vite');\n const server: ViteDevServer = await createServer({\n root: options.root,\n configFile: options.configFile,\n server: { middlewareMode: true, watch: null },\n logLevel: 'error',\n });\n\n try {\n // The root must live in the same path space as the graph nodes.\n // By default Vite resolves through symlinks, so nodes carry\n // realpaths and a symlinked root (pnpm workspaces, ~ links) would\n // string-mismatch every file and misclassify the whole project as\n // unreferenced (design/43 open question 3). Under\n // resolve.preserveSymlinks the graphs keep the SYMLINK paths, and\n // realpathing would create exactly the mismatch it normally\n // prevents — honor the resolved setting. Slash-normalize either\n // way: module-node paths are slash-normalized even on Windows.\n const root = normalizePath(\n server.config.resolve.preserveSymlinks\n ? server.config.root\n : await realpath(server.config.root)\n );\n // Structural assignment, no cast: CrawlEnvironment/CrawlModuleNode\n // are supertypes of Vite's DevEnvironment/EnvironmentModuleNode for\n // the members the BFS reads, so an upstream shape change fails here\n // at compile time instead of inside the walk.\n const rscEnv: CrawlEnvironment | undefined = server.environments.rsc;\n const ssrEnv: CrawlEnvironment | undefined = server.environments.ssr;\n if (!rscEnv?.transformRequest || !ssrEnv?.transformRequest) {\n throw new Error('[timber] crawl requires the rsc and ssr Vite environments');\n }\n\n const errors: CrawlError[] = [];\n\n // appDir and pageExtensions come from the booted server's own\n // resolved timber config (the root-sync plugin api — the single\n // supported way to read it; cf. cli-check.ts). Resolved before\n // Phase 1 so the scan extension set is available to isProjectFile\n // and scanProjectSourceFiles — custom pageExtensions must be\n // admitted throughout, or graph membership disagrees with the\n // disk scan and fingerprint (TIM-1392).\n const rootSyncPlugin = server.config.plugins.find((p) => p.name === 'timber-root-sync');\n let appDir: string;\n let pageExtensions: readonly string[] | undefined;\n if (rootSyncPlugin && isTimberPluginApi(rootSyncPlugin.api)) {\n const pluginCtx = rootSyncPlugin.api.getContext();\n appDir = pluginCtx.appDir;\n pageExtensions = pluginCtx.config.pageExtensions;\n } else {\n try {\n appDir = resolveAppDir(root);\n } catch {\n // No app directory at all — provisional convention hints just\n // never fire; graph-membership classification is unaffected.\n appDir = join(root, 'app');\n }\n }\n // Normalize once at the boundary: appDir comes from node:path\n // join() (backslashes on Windows) but all downstream comparisons\n // — classify, chains, isRouteRoot — expect slash-normalized paths.\n appDir = normalizePath(appDir);\n const extensions = buildSourceExtensions(pageExtensions);\n\n // Phase 1 — RSC graph from the entry.\n const rsc = await crawlEnvironment(\n rscEnv,\n 'rsc',\n [RSC_ENTRY_URL],\n root,\n deadlineAt,\n errors,\n extensions\n );\n\n // Phase 2 — directives + poison signals for everything discovered so\n // far plus files on disk that no graph reached (the provisional/\n // pending set). The disk walk is the same one the graph cache\n // fingerprints (scan.ts) so cache validity covers exactly this set.\n const diskFiles: string[] = [];\n await scanProjectSourceFiles(\n root,\n diskFiles,\n (failure) => errors.push({ environment: 'scan', id: failure.dir, message: failure.message }),\n 0,\n extensions\n );\n // One path space throughout: graph paths are already slash-\n // normalized, disk-walk paths are node-joined (backslashed on\n // Windows) — mixing them in one Set would duplicate every file.\n const allFiles = new Set([...rsc.projectFiles, ...diskFiles.map(normalizePath)]);\n const facts = new Map<string, SourceFacts>();\n for (const file of allFiles) {\n facts.set(file, await sourceFactsOf(file, errors));\n }\n\n // Phase 3 — SSR graph from every RSC-REACHABLE 'use client'\n // boundary. SSR renders exactly the tree the browser hydrates, so\n // this yields the client tree (boundaries plus everything they\n // transitively import). Disk-only boundaries are deliberately NOT\n // seeded: an unreferenced 'use client' file must keep its\n // provisional pending classification, not get promoted to a\n // confirmed boundary (and drag its imports in as client-internal)\n // by the act of analyzing it.\n const boundarySeeds = [...rsc.projectFiles]\n .filter((file) => facts.get(file)?.directive === 'use client')\n .map((file) => fileToUrl(file, root));\n const ssr = await crawlEnvironment(\n ssrEnv,\n 'ssr',\n boundarySeeds,\n root,\n deadlineAt,\n errors,\n extensions\n );\n\n // SSR-only discoveries (client-internal files) need facts too.\n for (const file of ssr.projectFiles) {\n if (!facts.has(file)) {\n facts.set(file, await sourceFactsOf(file, errors));\n allFiles.add(file);\n }\n }\n\n const classifications = classifyModules(\n [...allFiles].map((file) => ({\n file,\n inRsc: rsc.projectFiles.has(file),\n inSsr: ssr.projectFiles.has(file),\n // The crawl transforms unconditionally, so the untransformed\n // frontier only arises where the transform itself failed.\n ssrTransformed: ssr.projectFiles.has(file) ? !ssr.failedFiles.has(file) : undefined,\n directive: facts.get(file)?.directive ?? null,\n })),\n { appDir, pageExtensions }\n );\n\n // Phase 4 — chains, from the live graphs of the still-open server.\n // ChainGraph is a structural supertype of Vite's module graphs\n // (TIM-1382), so the assignment is cast-free and an upstream shape\n // change fails at compile time.\n const rscChainGraph: ChainGraph = server.environments.rsc.moduleGraph;\n const ssrChainGraph: ChainGraph = server.environments.ssr.moduleGraph;\n const chainCtx = {\n rsc: rscChainGraph,\n ssr: ssrChainGraph,\n appDir,\n pageExtensions,\n // facts keys are already slash-normalized (allFiles is built from\n // graph membership + diskFiles.map(normalizePath)), so no\n // per-lookup normalization is needed.\n getDirective: (file: string) => facts.get(file)?.directive ?? null,\n };\n\n const modules: GraphModuleReport[] = classifications.map((classification) => ({\n ...classification,\n boundary: facts.get(classification.file)?.directive === 'use client',\n chains: stitchImportChains(classification.file, chainCtx),\n poisonings: poisoningsFor(\n classification.kind,\n classification.pending,\n facts.get(classification.file)?.signals ?? []\n ),\n }));\n\n return {\n root,\n incomplete: rsc.deadlineHit || ssr.deadlineHit,\n modules,\n errors,\n stats: {\n rscGraphModules: rsc.visited,\n ssrGraphModules: ssr.visited,\n projectFiles: allFiles.size,\n durationMs: Date.now() - startedAt,\n },\n };\n } finally {\n // Bound the cleanup: after a deadline expiry the very transforms\n // that hung are what server.close() waits on — an unbounded await\n // here would hang the child past its own --timeout and the report\n // would never be emitted. The child's process.exit reaps whatever\n // close left behind.\n await Promise.race([\n server.close().catch(() => {}),\n new Promise<void>((resolveTimer) => setTimeout(resolveTimer, 5_000)),\n ]);\n }\n}\n","/**\n * crawl-entry — child-process entry point for the cold crawl.\n *\n * The crawl's memory footprint is transient but large (~1.3 GB RSS on\n * plugin-heavy apps — design/47 §6), so consumers spawn this module as a\n * short-lived child process instead of crawling in-process:\n *\n * node --import tsx/esm crawl-entry.ts --root <dir> [--config <file>]\n * [--out <file>] [--timeout <ms>]\n *\n * With `--out`, the JSON report is written to that file. Otherwise it is\n * printed to stdout after a marker line — plugins in the booted app may\n * write their own stdout noise, so consumers parse everything after the\n * marker, not the whole stream.\n */\n\nimport { writeFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { crawlProject, CRAWL_RESULT_MARKER } from './crawl.ts';\n\nfunction readFlag(argv: string[], name: string): string | undefined {\n const index = argv.indexOf(name);\n return index !== -1 ? argv[index + 1] : undefined;\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n const rootArg = readFlag(argv, '--root');\n if (!rootArg) {\n process.stderr.write('usage: crawl-entry --root <dir> [--config <file>] [--out <file>]\\n');\n process.exitCode = 2;\n return;\n }\n\n const timeoutArg = readFlag(argv, '--timeout');\n const report = await crawlProject({\n root: resolve(rootArg),\n configFile: readFlag(argv, '--config'),\n timeoutMs: timeoutArg ? Number(timeoutArg) : undefined,\n });\n\n const json = JSON.stringify(report);\n const outFile = readFlag(argv, '--out');\n if (outFile) {\n await writeFile(resolve(outFile), json);\n } else {\n // Await the flush: stdout to a pipe is async, and process.exit()\n // below does NOT flush pending pipe writes — a large report would\n // be truncated right after the marker.\n await new Promise<void>((resolveWrite, rejectWrite) => {\n process.stdout.write(`\\n${CRAWL_RESULT_MARKER}\\n${json}\\n`, (error) =>\n error ? rejectWrite(error) : resolveWrite()\n );\n });\n }\n}\n\nmain().then(\n () => {\n // The booted app's plugins may hold handles (watchers, sockets) past\n // server.close(); the crawl is a read-only child, so exit explicitly.\n process.exit(process.exitCode ?? 0);\n },\n (error) => {\n process.stderr.write(`[timber] crawl failed: ${error?.stack ?? String(error)}\\n`);\n process.exit(1);\n }\n);\n"],"mappings":"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"}
1
+ {"version":3,"file":"crawl-entry.js","names":[],"sources":["../../src/analyze/crawl.ts","../../src/analyze/crawl-entry.ts"],"sourcesContent":["/**\n * crawl — programmatic middleware-mode crawl (the cold path).\n *\n * Boots `createServer({ middlewareMode: true })` — no HTTP port, no route\n * rendering — and BFS-walks `env.transformRequest(url)` → importedModules\n * edges from `virtual:timber-rsc-entry`, then from every discovered\n * `'use client'` boundary through the SSR environment. This reuses Vite's\n * actual resolution (aliases, tsconfig paths, react-server export\n * conditions, virtual modules), so it cannot drift from what dev or the\n * build does; hand-rolled resolution was evaluated and rejected\n * (design/47 §\"Verified Prototype Findings\").\n *\n * Cost model (design/47 §6): 2.5 s on small apps to ~10 s on dep-heavy\n * plugin stacks, with a transient ~1.3 GB RSS — callers MUST run this in\n * a short-lived child process (see crawl-entry.ts) and cache the\n * classification output, not the Vite transforms. The crawl always\n * covers the full project: one crawl per project state, never per\n * question — marginal per-file cost is ~zero once booted.\n *\n * `TIMBER_ANALYZE=1` is set before the server boots so platform dev\n * plugins (wrangler bindings — a measured ~6.8 s remote-bindings\n * handshake on website) skip work the crawl never needs: classification\n * wants import edges, not live Cloudflare connections.\n *\n * Design docs: 47-module-environment-tooling.md §6, §8.\n */\n\nimport { readFile, realpath } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { ViteDevServer } from 'vite';\nimport { normalizePath } from 'vite';\nimport {\n classifyModules,\n detectDirective,\n type Directive,\n type ModuleClassification,\n} from './classify.ts';\nimport { buildSourceExtensions, scanProjectSourceFiles, isScannableSourceFile } from './scan.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 { isTimberPluginApi, resolveAppDir } from '../plugin-context.ts';\n\nconst RSC_ENTRY_URL = 'virtual:timber-rsc-entry';\n\n/**\n * Marker preceding the JSON report on crawl-entry's stdout. Lives here\n * (not in crawl-entry.ts) so consumers can import it without executing\n * the child entry's main().\n */\nexport const CRAWL_RESULT_MARKER = '__TIMBER_CRAWL_RESULT__';\n\nexport interface CrawlOptions {\n /** Project root (the app directory containing vite.config). */\n root: string;\n /** Explicit Vite config file; discovered from root when omitted. */\n configFile?: string;\n /** Overall crawl deadline. Default 120s. */\n timeoutMs?: number;\n}\n\nexport interface CrawlError {\n /** 'scan' covers disk-scan and directive-read failures. */\n environment: 'rsc' | 'ssr' | 'scan';\n id: string;\n message: string;\n}\n\n/**\n * One project file's full graph answer — the classification plus the\n * remaining JSON-contract fields (`boundary`, `chains`, `poisonings`)\n * that need the open graphs / original sources to compute. This is the\n * per-module unit `timber graph` caches and serves.\n */\nexport interface GraphModuleReport extends ModuleClassification {\n /** True when the file itself carries `'use client'`. */\n boundary: boolean;\n /** Boundary-aware import chains (design/47 §7); [] when in no graph. */\n chains: ImportChain[];\n /** Report-only poisoning conflicts (design/47 §3); [] when clean. */\n poisonings: Poisoning[];\n}\n\nexport interface CrawlReport {\n root: string;\n /**\n * True when the crawl deadline expired mid-walk — the report is\n * truncated, not wrong, and the condition is transient (a slow\n * machine, a cold dep-optimizer). Consumers must not cache an\n * incomplete report: with unchanged files it would be served forever,\n * never retrying after the transient cause cleared. Deterministic\n * per-module transform failures do NOT set this — they recur on\n * retry, so caching them is correct.\n */\n incomplete: boolean;\n /** Classification of every project file, sorted by path. */\n modules: GraphModuleReport[];\n /**\n * Transform failures encountered during the BFS. These are classified,\n * not swallowed — CSS/asset URLs and non-transformable modules land\n * here (44 on website in the §6 measurement) and the crawl continues.\n */\n errors: CrawlError[];\n stats: {\n rscGraphModules: number;\n ssrGraphModules: number;\n projectFiles: number;\n durationMs: number;\n };\n}\n\n/** Marker error for deadline expiry — structurally distinguishable\n * from a deterministic transform failure (consumers must not cache a\n * deadline-truncated report; a transform failure recurs on retry and\n * caches fine). */\nclass CrawlDeadlineError extends Error {}\n\n/** Race a promise against the crawl deadline. */\nfunction withDeadline<T>(promise: Promise<T>, remainingMs: number, label: string): Promise<T> {\n return new Promise<T>((resolvePromise, rejectPromise) => {\n const timer = setTimeout(\n () =>\n rejectPromise(\n new CrawlDeadlineError(`crawl deadline exceeded while transforming ${label}`)\n ),\n Math.max(remainingMs, 1)\n );\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolvePromise(value);\n },\n (error) => {\n clearTimeout(timer);\n rejectPromise(error);\n }\n );\n });\n}\n\n/** Duck-typed view of the Vite dev environment surface the crawl uses. */\nexport interface CrawlEnvironment {\n transformRequest(url: string): Promise<unknown>;\n moduleGraph: {\n getModuleByUrl(url: string): Promise<CrawlModuleNode | undefined>;\n };\n}\n\ninterface CrawlModuleNode {\n id: string | null;\n url: string;\n file: string | null;\n importedModules: Set<CrawlModuleNode>;\n transformResult: unknown;\n}\n\ninterface EnvCrawlResult {\n /** Absolute paths of project files reached in this environment. */\n projectFiles: Set<string>;\n /** Project files whose own transform failed (membership unconfirmed subtree). */\n failedFiles: Set<string>;\n /** Total graph modules visited (project + deps + virtual). */\n visited: number;\n /** The deadline expired mid-walk — the result is truncated. */\n deadlineHit: boolean;\n}\n\n/**\n * Whether a graph node's file belongs in the report. `root` must be\n * slash-normalized; module-node paths already are (Vite normalizes\n * them, even on Windows — comparing against a backslashed root would\n * reject every physical module there and misreport the whole app as\n * pending). The rule set is the scan's own (isScannableSourceFile), so\n * report membership, disk-scan coverage, and the cache fingerprint are\n * one set — a graph-reachable file the fingerprint never tracked would\n * let its edits serve a stale cache (codex review, PR #1056).\n */\nfunction isProjectFile(file: string | null, root: string, extensions?: RegExp): file is string {\n if (!file) return false;\n return isScannableSourceFile(normalizePath(file), root, extensions);\n}\n\n/**\n * BFS one environment from the given seed URLs, transforming every\n * discovered module unconditionally. Transforming (rather than reading\n * live graphs) is what closes the §8 blind spot: modules behind\n * unexecuted dynamic imports are transformed here like any other edge.\n *\n * Exported for tests (deadline behavior is exercised with a fake env).\n */\nexport async function crawlEnvironment(\n env: CrawlEnvironment,\n envName: 'rsc' | 'ssr',\n seeds: string[],\n root: string,\n deadlineAt: number,\n errors: CrawlError[],\n extensions?: RegExp\n): Promise<EnvCrawlResult> {\n const queue = [...seeds];\n const enqueued = new Set(queue);\n const projectFiles = new Set<string>();\n const failedFiles = new Set<string>();\n let visited = 0;\n let deadlineHit = false;\n\n // Index-cursor BFS — the queue only grows, so this walks each entry once.\n for (let cursor = 0; cursor < queue.length; cursor++) {\n const url = queue[cursor];\n const remaining = deadlineAt - Date.now();\n if (remaining <= 0) {\n errors.push({ environment: envName, id: url, message: 'crawl deadline exceeded' });\n deadlineHit = true;\n break;\n }\n\n let node: CrawlModuleNode | undefined;\n try {\n await withDeadline(env.transformRequest(url), remaining, url);\n node = await env.moduleGraph.getModuleByUrl(url);\n } catch (error) {\n if (error instanceof CrawlDeadlineError) deadlineHit = true;\n errors.push({\n environment: envName,\n id: url,\n message: error instanceof Error ? error.message : String(error),\n });\n // The URL may still have a graph node with a file — it IS a graph\n // member (reachable), its transform just failed. Record membership\n // AND the failure so classification reports it as reachable with\n // subtreeUnknown, not as pending/unreferenced.\n node = await env.moduleGraph.getModuleByUrl(url).catch(() => undefined);\n if (node && isProjectFile(node.file, root, extensions)) {\n projectFiles.add(node.file);\n failedFiles.add(node.file);\n }\n continue;\n }\n\n if (!node) continue;\n visited++;\n if (isProjectFile(node.file, root, extensions)) projectFiles.add(node.file);\n\n for (const child of node.importedModules) {\n const childUrl = child.url ?? child.id;\n if (!childUrl || enqueued.has(childUrl)) continue;\n enqueued.add(childUrl);\n queue.push(childUrl);\n }\n }\n\n return { projectFiles, failedFiles, visited, deadlineHit };\n}\n\n/** Root-relative Vite URL for an absolute project file path.\n * Both arguments must be slash-normalized (the crawl normalizes at\n * the boundary, so callers always satisfy this). */\nfunction fileToUrl(file: string, root: string): string {\n return file.slice(root.length);\n}\n\n/** What one read of a module's original source yields. */\ninterface SourceFacts {\n directive: Directive;\n /** Import-specifier exclusivity signals (poison-scan). */\n signals: Poisoning[];\n}\n\n/**\n * Read a file's first directive and poison signals from its original\n * source on disk, in one read. A failure is recorded, not swallowed —\n * a lost 'use client' here would silently drop the whole client\n * subtree from the SSR crawl.\n */\nasync function sourceFactsOf(file: string, errors: CrawlError[]): Promise<SourceFacts> {\n try {\n const code = await readFile(file, 'utf8');\n return {\n directive: detectDirective(code, file),\n signals: importSignals(collectImportSpecifiers(code, file)),\n };\n } catch (error) {\n errors.push({\n environment: 'scan',\n id: file,\n message: `directive read failed: ${error instanceof Error ? error.message : String(error)}`,\n });\n return { directive: null, signals: [] };\n }\n}\n\n/**\n * Crawl a project cold and classify every project file.\n *\n * Boots a middleware-mode Vite server with the app's own config (all\n * timber plugins, real resolution), crawls the RSC graph from the entry,\n * detects directives from original sources, crawls the SSR graph from\n * every `'use client'` boundary, and intersects the memberships.\n */\nexport async function crawlProject(options: CrawlOptions): Promise<CrawlReport> {\n const startedAt = Date.now();\n const deadlineAt = startedAt + (options.timeoutMs ?? 120_000);\n // Signal platform dev plugins (wrangler bindings) to skip boot work the\n // crawl never uses. Must be set before createServer runs plugin hooks.\n process.env.TIMBER_ANALYZE = '1';\n\n const { createServer } = await import('vite');\n const server: ViteDevServer = await createServer({\n root: options.root,\n configFile: options.configFile,\n server: { middlewareMode: true, watch: null },\n logLevel: 'error',\n });\n\n try {\n // The root must live in the same path space as the graph nodes.\n // By default Vite resolves through symlinks, so nodes carry\n // realpaths and a symlinked root (pnpm workspaces, ~ links) would\n // string-mismatch every file and misclassify the whole project as\n // unreferenced (design/43 open question 3). Under\n // resolve.preserveSymlinks the graphs keep the SYMLINK paths, and\n // realpathing would create exactly the mismatch it normally\n // prevents — honor the resolved setting. Slash-normalize either\n // way: module-node paths are slash-normalized even on Windows.\n const root = normalizePath(\n server.config.resolve.preserveSymlinks\n ? server.config.root\n : await realpath(server.config.root)\n );\n // Structural assignment, no cast: CrawlEnvironment/CrawlModuleNode\n // are supertypes of Vite's DevEnvironment/EnvironmentModuleNode for\n // the members the BFS reads, so an upstream shape change fails here\n // at compile time instead of inside the walk.\n const rscEnv: CrawlEnvironment | undefined = server.environments.rsc;\n const ssrEnv: CrawlEnvironment | undefined = server.environments.ssr;\n if (!rscEnv?.transformRequest || !ssrEnv?.transformRequest) {\n throw new Error('[timber] crawl requires the rsc and ssr Vite environments');\n }\n\n const errors: CrawlError[] = [];\n\n // appDir and pageExtensions come from the booted server's own\n // resolved timber config (the root-sync plugin api — the single\n // supported way to read it; cf. cli-check.ts). Resolved before\n // Phase 1 so the scan extension set is available to isProjectFile\n // and scanProjectSourceFiles — custom pageExtensions must be\n // admitted throughout, or graph membership disagrees with the\n // disk scan and fingerprint (TIM-1392).\n const rootSyncPlugin = server.config.plugins.find((p) => p.name === 'timber-root-sync');\n let appDir: string;\n let pageExtensions: readonly string[] | undefined;\n if (rootSyncPlugin && isTimberPluginApi(rootSyncPlugin.api)) {\n const pluginCtx = rootSyncPlugin.api.getContext();\n appDir = pluginCtx.appDir;\n pageExtensions = pluginCtx.config.pageExtensions;\n } else {\n try {\n appDir = resolveAppDir(root);\n } catch {\n // No app directory at all — provisional convention hints just\n // never fire; graph-membership classification is unaffected.\n appDir = join(root, 'app');\n }\n }\n // Normalize once at the boundary: appDir comes from node:path\n // join() (backslashes on Windows) but all downstream comparisons\n // — classify, chains, isRouteRoot — expect slash-normalized paths.\n appDir = normalizePath(appDir);\n const extensions = buildSourceExtensions(pageExtensions);\n\n // Phase 1 — RSC graph from the entry.\n const rsc = await crawlEnvironment(\n rscEnv,\n 'rsc',\n [RSC_ENTRY_URL],\n root,\n deadlineAt,\n errors,\n extensions\n );\n\n // Phase 2 — directives + poison signals for everything discovered so\n // far plus files on disk that no graph reached (the provisional/\n // pending set). The disk walk is the same one the graph cache\n // fingerprints (scan.ts) so cache validity covers exactly this set.\n const diskFiles: string[] = [];\n await scanProjectSourceFiles(\n root,\n diskFiles,\n (failure) => errors.push({ environment: 'scan', id: failure.dir, message: failure.message }),\n 0,\n extensions\n );\n // One path space throughout: graph paths are already slash-\n // normalized, disk-walk paths are node-joined (backslashed on\n // Windows) — mixing them in one Set would duplicate every file.\n const allFiles = new Set([...rsc.projectFiles, ...diskFiles.map(normalizePath)]);\n const facts = new Map<string, SourceFacts>();\n for (const file of allFiles) {\n facts.set(file, await sourceFactsOf(file, errors));\n }\n\n // Phase 3 — SSR graph from every RSC-REACHABLE 'use client'\n // boundary. SSR renders exactly the tree the browser hydrates, so\n // this yields the client tree (boundaries plus everything they\n // transitively import). Disk-only boundaries are deliberately NOT\n // seeded: an unreferenced 'use client' file must keep its\n // provisional pending classification, not get promoted to a\n // confirmed boundary (and drag its imports in as client-internal)\n // by the act of analyzing it.\n const boundarySeeds = [...rsc.projectFiles]\n .filter((file) => facts.get(file)?.directive === 'use client')\n .map((file) => fileToUrl(file, root));\n const ssr = await crawlEnvironment(\n ssrEnv,\n 'ssr',\n boundarySeeds,\n root,\n deadlineAt,\n errors,\n extensions\n );\n\n // SSR-only discoveries (client-internal files) need facts too.\n for (const file of ssr.projectFiles) {\n if (!facts.has(file)) {\n facts.set(file, await sourceFactsOf(file, errors));\n allFiles.add(file);\n }\n }\n\n const classifications = classifyModules(\n [...allFiles].map((file) => ({\n file,\n inRsc: rsc.projectFiles.has(file),\n inSsr: ssr.projectFiles.has(file),\n // The crawl transforms unconditionally, so the untransformed\n // frontier only arises where the transform itself failed.\n ssrTransformed: ssr.projectFiles.has(file) ? !ssr.failedFiles.has(file) : undefined,\n directive: facts.get(file)?.directive ?? null,\n })),\n { appDir, pageExtensions }\n );\n\n // Phase 4 — chains, from the live graphs of the still-open server.\n // ChainGraph is a structural supertype of Vite's module graphs\n // (TIM-1382), so the assignment is cast-free and an upstream shape\n // change fails at compile time.\n const rscChainGraph: ChainGraph = server.environments.rsc.moduleGraph;\n const ssrChainGraph: ChainGraph = server.environments.ssr.moduleGraph;\n const chainCtx = {\n rsc: rscChainGraph,\n ssr: ssrChainGraph,\n appDir,\n pageExtensions,\n // facts keys are already slash-normalized (allFiles is built from\n // graph membership + diskFiles.map(normalizePath)), so no\n // per-lookup normalization is needed.\n getDirective: (file: string) => facts.get(file)?.directive ?? null,\n };\n\n const modules: GraphModuleReport[] = classifications.map((classification) => ({\n ...classification,\n boundary: facts.get(classification.file)?.directive === 'use client',\n chains: stitchImportChains(classification.file, chainCtx),\n poisonings: poisoningsFor(\n classification.kind,\n classification.pending,\n facts.get(classification.file)?.signals ?? []\n ),\n }));\n\n return {\n root,\n incomplete: rsc.deadlineHit || ssr.deadlineHit,\n modules,\n errors,\n stats: {\n rscGraphModules: rsc.visited,\n ssrGraphModules: ssr.visited,\n projectFiles: allFiles.size,\n durationMs: Date.now() - startedAt,\n },\n };\n } finally {\n // Bound the cleanup: after a deadline expiry the very transforms\n // that hung are what server.close() waits on — an unbounded await\n // here would hang the child past its own --timeout and the report\n // would never be emitted. The child's process.exit reaps whatever\n // close left behind.\n await Promise.race([\n server.close().catch(() => {}),\n new Promise<void>((resolveTimer) => setTimeout(resolveTimer, 5_000)),\n ]);\n }\n}\n","/**\n * crawl-entry — child-process entry point for the cold crawl.\n *\n * The crawl's memory footprint is transient but large (~1.3 GB RSS on\n * plugin-heavy apps — design/47 §6), so consumers spawn this module as a\n * short-lived child process instead of crawling in-process:\n *\n * node --import tsx/esm crawl-entry.ts --root <dir> [--config <file>]\n * [--out <file>] [--timeout <ms>]\n *\n * With `--out`, the JSON report is written to that file. Otherwise it is\n * printed to stdout after a marker line — plugins in the booted app may\n * write their own stdout noise, so consumers parse everything after the\n * marker, not the whole stream.\n */\n\nimport { writeFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { crawlProject, CRAWL_RESULT_MARKER } from './crawl.ts';\n\nfunction readFlag(argv: string[], name: string): string | undefined {\n const index = argv.indexOf(name);\n return index !== -1 ? argv[index + 1] : undefined;\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n const rootArg = readFlag(argv, '--root');\n if (!rootArg) {\n process.stderr.write('usage: crawl-entry --root <dir> [--config <file>] [--out <file>]\\n');\n process.exitCode = 2;\n return;\n }\n\n const timeoutArg = readFlag(argv, '--timeout');\n const report = await crawlProject({\n root: resolve(rootArg),\n configFile: readFlag(argv, '--config'),\n timeoutMs: timeoutArg ? Number(timeoutArg) : undefined,\n });\n\n const json = JSON.stringify(report);\n const outFile = readFlag(argv, '--out');\n if (outFile) {\n await writeFile(resolve(outFile), json);\n } else {\n // Await the flush: stdout to a pipe is async, and process.exit()\n // below does NOT flush pending pipe writes — a large report would\n // be truncated right after the marker.\n await new Promise<void>((resolveWrite, rejectWrite) => {\n process.stdout.write(`\\n${CRAWL_RESULT_MARKER}\\n${json}\\n`, (error) =>\n error ? rejectWrite(error) : resolveWrite()\n );\n });\n }\n}\n\nmain().then(\n () => {\n // The booted app's plugins may hold handles (watchers, sockets) past\n // server.close(); the crawl is a read-only child, so exit explicitly.\n process.exit(process.exitCode ?? 0);\n },\n (error) => {\n process.stderr.write(`[timber] crawl failed: ${error?.stack ?? String(error)}\\n`);\n process.exit(1);\n }\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,IAAM,gBAAgB;;;;;;AAOtB,IAAa,sBAAsB;;;;;AAiEnC,IAAM,qBAAN,cAAiC,MAAM,CAAC;;AAGxC,SAAS,aAAgB,SAAqB,aAAqB,OAA2B;CAC5F,OAAO,IAAI,SAAY,gBAAgB,kBAAkB;EACvD,MAAM,QAAQ,iBAEV,cACE,IAAI,mBAAmB,8CAA8C,OAAO,CAC9E,GACF,KAAK,IAAI,aAAa,CAAC,CACzB;EACA,QAAQ,MACL,UAAU;GACT,aAAa,KAAK;GAClB,eAAe,KAAK;EACtB,IACC,UAAU;GACT,aAAa,KAAK;GAClB,cAAc,KAAK;EACrB,CACF;CACF,CAAC;AACH;;;;;;;;;;;AAuCA,SAAS,cAAc,MAAqB,MAAc,YAAqC;CAC7F,IAAI,CAAC,MAAM,OAAO;CAClB,OAAO,sBAAsB,cAAc,IAAI,GAAG,MAAM,UAAU;AACpE;;;;;;;;;AAUA,eAAsB,iBACpB,KACA,SACA,OACA,MACA,YACA,QACA,YACyB;CACzB,MAAM,QAAQ,CAAC,GAAG,KAAK;CACvB,MAAM,WAAW,IAAI,IAAI,KAAK;CAC9B,MAAM,+BAAe,IAAI,IAAY;CACrC,MAAM,8BAAc,IAAI,IAAY;CACpC,IAAI,UAAU;CACd,IAAI,cAAc;CAGlB,KAAK,IAAI,SAAS,GAAG,SAAS,MAAM,QAAQ,UAAU;EACpD,MAAM,MAAM,MAAM;EAClB,MAAM,YAAY,aAAa,KAAK,IAAI;EACxC,IAAI,aAAa,GAAG;GAClB,OAAO,KAAK;IAAE,aAAa;IAAS,IAAI;IAAK,SAAS;GAA0B,CAAC;GACjF,cAAc;GACd;EACF;EAEA,IAAI;EACJ,IAAI;GACF,MAAM,aAAa,IAAI,iBAAiB,GAAG,GAAG,WAAW,GAAG;GAC5D,OAAO,MAAM,IAAI,YAAY,eAAe,GAAG;EACjD,SAAS,OAAO;GACd,IAAI,iBAAiB,oBAAoB,cAAc;GACvD,OAAO,KAAK;IACV,aAAa;IACb,IAAI;IACJ,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAChE,CAAC;GAKD,OAAO,MAAM,IAAI,YAAY,eAAe,GAAG,CAAC,CAAC,YAAY,KAAA,CAAS;GACtE,IAAI,QAAQ,cAAc,KAAK,MAAM,MAAM,UAAU,GAAG;IACtD,aAAa,IAAI,KAAK,IAAI;IAC1B,YAAY,IAAI,KAAK,IAAI;GAC3B;GACA;EACF;EAEA,IAAI,CAAC,MAAM;EACX;EACA,IAAI,cAAc,KAAK,MAAM,MAAM,UAAU,GAAG,aAAa,IAAI,KAAK,IAAI;EAE1E,KAAK,MAAM,SAAS,KAAK,iBAAiB;GACxC,MAAM,WAAW,MAAM,OAAO,MAAM;GACpC,IAAI,CAAC,YAAY,SAAS,IAAI,QAAQ,GAAG;GACzC,SAAS,IAAI,QAAQ;GACrB,MAAM,KAAK,QAAQ;EACrB;CACF;CAEA,OAAO;EAAE;EAAc;EAAa;EAAS;CAAY;AAC3D;;;;AAKA,SAAS,UAAU,MAAc,MAAsB;CACrD,OAAO,KAAK,MAAM,KAAK,MAAM;AAC/B;;;;;;;AAeA,eAAe,cAAc,MAAc,QAA4C;CACrF,IAAI;EACF,MAAM,OAAO,MAAM,SAAS,MAAM,MAAM;EACxC,OAAO;GACL,WAAW,gBAAgB,MAAM,IAAI;GACrC,SAAS,cAAc,wBAAwB,MAAM,IAAI,CAAC;EAC5D;CACF,SAAS,OAAO;EACd,OAAO,KAAK;GACV,aAAa;GACb,IAAI;GACJ,SAAS,0BAA0B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EAC1F,CAAC;EACD,OAAO;GAAE,WAAW;GAAM,SAAS,CAAC;EAAE;CACxC;AACF;;;;;;;;;AAUA,eAAsB,aAAa,SAA6C;CAC9E,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,aAAa,aAAa,QAAQ,aAAa;CAGrD,QAAQ,IAAI,iBAAiB;CAE7B,MAAM,EAAE,iBAAiB,MAAM,OAAO;CACtC,MAAM,SAAwB,MAAM,aAAa;EAC/C,MAAM,QAAQ;EACd,YAAY,QAAQ;EACpB,QAAQ;GAAE,gBAAgB;GAAM,OAAO;EAAK;EAC5C,UAAU;CACZ,CAAC;CAED,IAAI;EAUF,MAAM,OAAO,cACX,OAAO,OAAO,QAAQ,mBAClB,OAAO,OAAO,OACd,MAAM,SAAS,OAAO,OAAO,IAAI,CACvC;EAKA,MAAM,SAAuC,OAAO,aAAa;EACjE,MAAM,SAAuC,OAAO,aAAa;EACjE,IAAI,CAAC,QAAQ,oBAAoB,CAAC,QAAQ,kBACxC,MAAM,IAAI,MAAM,2DAA2D;EAG7E,MAAM,SAAuB,CAAC;EAS9B,MAAM,iBAAiB,OAAO,OAAO,QAAQ,MAAM,MAAM,EAAE,SAAS,kBAAkB;EACtF,IAAI;EACJ,IAAI;EACJ,IAAI,kBAAkB,kBAAkB,eAAe,GAAG,GAAG;GAC3D,MAAM,YAAY,eAAe,IAAI,WAAW;GAChD,SAAS,UAAU;GACnB,iBAAiB,UAAU,OAAO;EACpC,OACE,IAAI;GACF,SAAS,cAAc,IAAI;EAC7B,QAAQ;GAGN,SAAS,KAAK,MAAM,KAAK;EAC3B;EAKF,SAAS,cAAc,MAAM;EAC7B,MAAM,aAAa,sBAAsB,cAAc;EAGvD,MAAM,MAAM,MAAM,iBAChB,QACA,OACA,CAAC,aAAa,GACd,MACA,YACA,QACA,UACF;EAMA,MAAM,YAAsB,CAAC;EAC7B,MAAM,uBACJ,MACA,YACC,YAAY,OAAO,KAAK;GAAE,aAAa;GAAQ,IAAI,QAAQ;GAAK,SAAS,QAAQ;EAAQ,CAAC,GAC3F,GACA,UACF;EAIA,MAAM,2BAAW,IAAI,IAAI,CAAC,GAAG,IAAI,cAAc,GAAG,UAAU,IAAI,aAAa,CAAC,CAAC;EAC/E,MAAM,wBAAQ,IAAI,IAAyB;EAC3C,KAAK,MAAM,QAAQ,UACjB,MAAM,IAAI,MAAM,MAAM,cAAc,MAAM,MAAM,CAAC;EAcnD,MAAM,MAAM,MAAM,iBAChB,QACA,OALoB,CAAC,GAAG,IAAI,YAAY,CAAC,CACxC,QAAQ,SAAS,MAAM,IAAI,IAAI,CAAC,EAAE,cAAc,YAAY,CAAC,CAC7D,KAAK,SAAS,UAAU,MAAM,IAAI,CAInC,GACA,MACA,YACA,QACA,UACF;EAGA,KAAK,MAAM,QAAQ,IAAI,cACrB,IAAI,CAAC,MAAM,IAAI,IAAI,GAAG;GACpB,MAAM,IAAI,MAAM,MAAM,cAAc,MAAM,MAAM,CAAC;GACjD,SAAS,IAAI,IAAI;EACnB;EAGF,MAAM,kBAAkB,gBACtB,CAAC,GAAG,QAAQ,CAAC,CAAC,KAAK,UAAU;GAC3B;GACA,OAAO,IAAI,aAAa,IAAI,IAAI;GAChC,OAAO,IAAI,aAAa,IAAI,IAAI;GAGhC,gBAAgB,IAAI,aAAa,IAAI,IAAI,IAAI,CAAC,IAAI,YAAY,IAAI,IAAI,IAAI,KAAA;GAC1E,WAAW,MAAM,IAAI,IAAI,CAAC,EAAE,aAAa;EAC3C,EAAE,GACF;GAAE;GAAQ;EAAe,CAC3B;EAQA,MAAM,WAAW;GACf,KAHgC,OAAO,aAAa,IAAI;GAIxD,KAHgC,OAAO,aAAa,IAAI;GAIxD;GACA;GAIA,eAAe,SAAiB,MAAM,IAAI,IAAI,CAAC,EAAE,aAAa;EAChE;EAEA,MAAM,UAA+B,gBAAgB,KAAK,oBAAoB;GAC5E,GAAG;GACH,UAAU,MAAM,IAAI,eAAe,IAAI,CAAC,EAAE,cAAc;GACxD,QAAQ,mBAAmB,eAAe,MAAM,QAAQ;GACxD,YAAY,cACV,eAAe,MACf,eAAe,SACf,MAAM,IAAI,eAAe,IAAI,CAAC,EAAE,WAAW,CAAC,CAC9C;EACF,EAAE;EAEF,OAAO;GACL;GACA,YAAY,IAAI,eAAe,IAAI;GACnC;GACA;GACA,OAAO;IACL,iBAAiB,IAAI;IACrB,iBAAiB,IAAI;IACrB,cAAc,SAAS;IACvB,YAAY,KAAK,IAAI,IAAI;GAC3B;EACF;CACF,UAAU;EAMR,MAAM,QAAQ,KAAK,CACjB,OAAO,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,GAC7B,IAAI,SAAe,iBAAiB,WAAW,cAAc,GAAK,CAAC,CACrE,CAAC;CACH;AACF;;;;;;;;;;;;;;;;;;AC/dA,SAAS,SAAS,MAAgB,MAAkC;CAClE,MAAM,QAAQ,KAAK,QAAQ,IAAI;CAC/B,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,KAAA;AAC1C;AAEA,eAAe,OAAsB;CACnC,MAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;CACjC,MAAM,UAAU,SAAS,MAAM,QAAQ;CACvC,IAAI,CAAC,SAAS;EACZ,QAAQ,OAAO,MAAM,oEAAoE;EACzF,QAAQ,WAAW;EACnB;CACF;CAEA,MAAM,aAAa,SAAS,MAAM,WAAW;CAC7C,MAAM,SAAS,MAAM,aAAa;EAChC,MAAM,QAAQ,OAAO;EACrB,YAAY,SAAS,MAAM,UAAU;EACrC,WAAW,aAAa,OAAO,UAAU,IAAI,KAAA;CAC/C,CAAC;CAED,MAAM,OAAO,KAAK,UAAU,MAAM;CAClC,MAAM,UAAU,SAAS,MAAM,OAAO;CACtC,IAAI,SACF,MAAM,UAAU,QAAQ,OAAO,GAAG,IAAI;MAKtC,MAAM,IAAI,SAAe,cAAc,gBAAgB;EACrD,QAAQ,OAAO,MAAM,KAAK,oBAAoB,IAAI,KAAK,MAAM,UAC3D,QAAQ,YAAY,KAAK,IAAI,aAAa,CAC5C;CACF,CAAC;AAEL;AAEA,KAAK,CAAC,CAAC,WACC;CAGJ,QAAQ,KAAK,QAAQ,YAAY,CAAC;AACpC,IACC,UAAU;CACT,QAAQ,OAAO,MAAM,0BAA0B,OAAO,SAAS,OAAO,KAAK,EAAE,GAAG;CAChF,QAAQ,KAAK,CAAC;AAChB,CACF"}
@@ -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-BoO51joc.js").then((n) => n.t);
381
+ const { formatChains } = await import("../_chunks/chains-DGX9zmg9.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-BoO51joc.js").then((n) => n.t);
400
+ const { formatChains } = await import("../_chunks/chains-DGX9zmg9.js").then((n) => n.t);
401
401
  console.log(" import chains:");
402
402
  console.log(formatChains(data.chains, configDir));
403
403
  }
@@ -1 +1 @@
1
- {"version":3,"file":"poison-scan.d.ts","sourceRoot":"","sources":["../../src/analyze/poison-scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAE7D,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,OAAO,EAAE,aAAa,GAAG,aAAa,CAAC;IACvC,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;CACrB;AAiBD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAsD5E;AAED,8DAA8D;AAC9D,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAyB/D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,SAAS,EAAE,GACnB,SAAS,EAAE,CAUb"}
1
+ {"version":3,"file":"poison-scan.d.ts","sourceRoot":"","sources":["../../src/analyze/poison-scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAEnF,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,OAAO,EAAE,aAAa,GAAG,aAAa,CAAC;IACvC,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;CACrB;AAcD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAsD5E;AAED,8DAA8D;AAC9D,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAyB/D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,SAAS,EAAE,GACnB,SAAS,EAAE,CAUb"}
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-v97rQH4b.js").then((n) => n.t);
93
+ const { RESTART_EXIT_CODE } = await import("./_chunks/dev-server-TFpEwm3H.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":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,MAAM,CAAC;AA0CjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAa1D,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAQ1D,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAElG;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,MAAM;CAAG;AAE1B;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAEvE;AAyGD,wBAAgB,MAAM,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,YAAY,EAAE,CAyXhE;eAEc,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,MAAM,CAAC;AA0CjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAa1D,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAQ1D,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAElG;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,MAAM;CAAG;AAE1B;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAEvE;AAyGD,wBAAgB,MAAM,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,YAAY,EAAE,CA4YhE;eAEc,MAAM"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as reportConfigErrors, i as formatMissingPeers, n as timberDevServer, o as validateConfig, r as checkPeerDependencies } from "./_chunks/dev-server-v97rQH4b.js";
1
+ import { a as reportConfigErrors, i as formatMissingPeers, n as timberDevServer, o as validateConfig, r as checkPeerDependencies } from "./_chunks/dev-server-TFpEwm3H.js";
2
2
  import { i as resolveRenderTimeoutMs } from "./_chunks/render-timeout-DOf_BBcO.js";
3
3
  import { t as writeFileAtomic } from "./_chunks/write-file-atomic-D1qWqHb4.js";
4
4
  import { d as collectInterceptionRewrites, l as getPrerenderExport, n as scanRoutes } from "./_chunks/scanner-DmqdxzbW.js";
@@ -38,6 +38,23 @@ var CONFIG_FILE_NAMES = [
38
38
  function findConfigFile(root) {
39
39
  for (const name of CONFIG_FILE_NAMES) if (existsSync(join(root, name))) return name;
40
40
  }
41
+ /** The name @content-collections/vite gives its plugin. */
42
+ var CONTENT_COLLECTIONS_PLUGIN_NAME = "content-collections";
43
+ /**
44
+ * Whether the user's own config already registers @content-collections/vite.
45
+ * Walks nested arrays and awaits promised plugins, the same PluginOption
46
+ * shapes Vite flattens (and has already resolved by the time config hooks
47
+ * run, so awaiting here costs nothing).
48
+ */
49
+ async function registersContentCollections(plugins) {
50
+ for (const option of plugins ?? []) {
51
+ const plugin = await option;
52
+ if (Array.isArray(plugin)) {
53
+ if (await registersContentCollections(plugin)) return true;
54
+ } else if (plugin && "name" in plugin && plugin.name === CONTENT_COLLECTIONS_PLUGIN_NAME) return true;
55
+ }
56
+ return false;
57
+ }
41
58
  /**
42
59
  * Create the timber-content Vite plugin.
43
60
  *
@@ -72,6 +89,7 @@ function timberContent(ctx) {
72
89
  return {
73
90
  name: "timber-content",
74
91
  async config(config, env) {
92
+ if (await registersContentCollections(config.plugins)) return;
75
93
  const root = config.root ?? ctx.root;
76
94
  ctx.timer.start("content-activate");
77
95
  await activate(root);
@@ -135,6 +153,12 @@ var ENTRY_FILE_MAP = {
135
153
  [VIRTUAL_IDS.ssrEntry]: resolve(SRC_DIR, "server", "ssr-entry.ts"),
136
154
  [VIRTUAL_IDS.browserEntry]: resolve(SRC_DIR, "client", "browser-entry", "index.ts")
137
155
  };
156
+ /**
157
+ * The real file behind virtual:timber-browser-entry. The client dep
158
+ * optimizer scans it explicitly once timber sets optimizeDeps.entries
159
+ * (see plugins/client-dep-entries.ts).
160
+ */
161
+ var BROWSER_ENTRY_FILE = ENTRY_FILE_MAP[VIRTUAL_IDS.browserEntry];
138
162
  /** The \0-prefixed resolved ID for virtual:timber-config */
139
163
  var RESOLVED_CONFIG_ID = `\0${VIRTUAL_IDS.config}`;
140
164
  /** The \0-prefixed resolved ID for virtual:timber-instrumentation */
@@ -9611,9 +9635,23 @@ function timber(config) {
9611
9635
  allowClearScreen: userConfig.clearScreen ?? true,
9612
9636
  console: originalConsole
9613
9637
  });
9638
+ let environments = envOutDirs;
9639
+ if (!isPreview) {
9640
+ ctx.timer.start("client-deps-scan");
9641
+ const { clientOptimizeDepsEntries } = await import("./_chunks/client-dep-entries-2HCF09no.js");
9642
+ const entries = await clientOptimizeDepsEntries(resolvedRoot, BROWSER_ENTRY_FILE, buildDir);
9643
+ ctx.timer.end("client-deps-scan");
9644
+ environments = {
9645
+ ...envOutDirs,
9646
+ client: {
9647
+ ...envOutDirs.client,
9648
+ optimizeDeps: { entries }
9649
+ }
9650
+ };
9651
+ }
9614
9652
  return {
9615
9653
  build: { outDir: buildOutDir },
9616
- environments: envOutDirs,
9654
+ environments,
9617
9655
  optimizeDeps: { exclude: timberOptimizeDepsExclude },
9618
9656
  customLogger,
9619
9657
  server: serverConfig,