@timber-js/app 0.2.0-alpha.205 → 0.2.0-alpha.207
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{chains-BjGKq9Je.js → chains-B4bgOl2g.js} +2 -2
- package/dist/_chunks/{chains-BjGKq9Je.js.map → chains-B4bgOl2g.js.map} +1 -1
- package/dist/_chunks/{cli-check-3WE_GyoL.js → cli-check-G8o30ldg.js} +3 -3
- package/dist/_chunks/{cli-check-3WE_GyoL.js.map → cli-check-G8o30ldg.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-D3iIZjku.js → cli-schema-sync--LZKs8Zn.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-D3iIZjku.js.map → cli-schema-sync--LZKs8Zn.js.map} +1 -1
- package/dist/_chunks/{convention-lint-BnO5TyHD.js → convention-lint-5KzoMg_A.js} +35 -1
- package/dist/_chunks/convention-lint-5KzoMg_A.js.map +1 -0
- package/dist/_chunks/{dev-server-Dbdgx2dD.js → dev-server-BzBxgYAV.js} +3 -3
- package/dist/_chunks/{dev-server-Dbdgx2dD.js.map → dev-server-BzBxgYAV.js.map} +1 -1
- package/dist/_chunks/{live-graph-BM2UbbNx.js → live-graph-Bm5WTvcr.js} +3 -3
- package/dist/_chunks/{live-graph-BM2UbbNx.js.map → live-graph-Bm5WTvcr.js.map} +1 -1
- package/dist/_chunks/{poison-scan-g83aAXhs.js → poison-scan-DG5eI2_z.js} +2 -2
- package/dist/_chunks/{poison-scan-g83aAXhs.js.map → poison-scan-DG5eI2_z.js.map} +1 -1
- package/dist/_chunks/{scanner-BhNruzv0.js → scanner-BQ5sPIJ7.js} +2 -2
- package/dist/_chunks/{scanner-BhNruzv0.js.map → scanner-BQ5sPIJ7.js.map} +1 -1
- package/dist/_chunks/schema-bridge-C83xa9lT.js.map +1 -1
- package/dist/_chunks/{tree-match-CdbvYTBz.js → tree-match-6yihorhk.js} +49 -2
- package/dist/_chunks/tree-match-6yihorhk.js.map +1 -0
- package/dist/_chunks/{walkers-BNsswm-h.js → walkers-CN2N_bA-.js} +2 -2
- package/dist/_chunks/{walkers-BNsswm-h.js.map → walkers-CN2N_bA-.js.map} +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cli.js +3 -3
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/schema-bridge.d.ts +2 -2
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/server/action-client.d.ts +2 -2
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/tree-match.d.ts +10 -0
- package/dist/server/tree-match.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/browser-entry/action-dispatch.ts +8 -2
- package/src/plugins/routing.ts +1 -1
- package/src/routing/convention-lint.ts +60 -0
- package/src/schema-bridge.ts +4 -2
- package/src/server/action-client.ts +4 -2
- package/src/server/route-matcher.ts +73 -20
- package/src/server/tree-match.ts +250 -18
- package/dist/_chunks/convention-lint-BnO5TyHD.js.map +0 -1
- package/dist/_chunks/tree-match-CdbvYTBz.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-B4bgOl2g.js";
|
|
3
|
+
import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "./poison-scan-DG5eI2_z.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-Bm5WTvcr.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-graph-BM2UbbNx.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-Bm5WTvcr.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-B4bgOl2g.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-DG5eI2_z.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poison-scan-
|
|
1
|
+
{"version":3,"file":"poison-scan-DG5eI2_z.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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as isDynamicMetadataExtension, n as classifyMetadataRoute } from "./metadata-routes-DSDjM_hJ.js";
|
|
2
2
|
import { n as NULL_BYTE_RE, t as ENCODED_SEPARATOR_RE } from "./canonicalize-CgHoscYO.js";
|
|
3
|
-
import { t as matchUrlParts } from "./tree-match-
|
|
3
|
+
import { t as matchUrlParts } from "./tree-match-6yihorhk.js";
|
|
4
4
|
import { i as DEFAULT_PAGE_EXTENSIONS, n as classifyUrlSegment, r as effectiveUrlSegment, t as classifySegment } from "./segment-classify-C539Pa2O.js";
|
|
5
5
|
import { n as treePathDepth, t as computeSegmentTreePaths } from "./segment-keys-D5hu1hz4.js";
|
|
6
6
|
import { h as swallow } from "./logger-k2DQ4EUf.js";
|
|
@@ -1668,4 +1668,4 @@ function findScriptExtFile(dirPath, name) {
|
|
|
1668
1668
|
//#endregion
|
|
1669
1669
|
export { scanRoutes as n, collectInterceptionRewrites as r, isRouteConventionFile as t };
|
|
1670
1670
|
|
|
1671
|
-
//# sourceMappingURL=scanner-
|
|
1671
|
+
//# sourceMappingURL=scanner-BQ5sPIJ7.js.map
|