@timber-js/app 0.2.0-alpha.200 → 0.2.0-alpha.201
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/bin/timber.mjs +15 -1
- package/dist/_chunks/{actions-d1hCqnU3.js → actions-HUdJADAD.js} +3 -3
- package/dist/_chunks/{actions-d1hCqnU3.js.map → actions-HUdJADAD.js.map} +1 -1
- package/dist/_chunks/{build-manifest-DTmSGLRz.js → build-manifest-DWppEdLB.js} +2 -51
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/{cache-api-ByagcC-J.js → cache-api-CAPbZTga.js} +2 -2
- package/dist/_chunks/{cache-api-ByagcC-J.js.map → cache-api-CAPbZTga.js.map} +1 -1
- package/dist/_chunks/{chains-Bpb0W4ax.js → chains-CBNA0Ozj.js} +3 -3
- package/dist/_chunks/{chains-Bpb0W4ax.js.map → chains-CBNA0Ozj.js.map} +1 -1
- package/dist/_chunks/{cli-check-D6VolrDV.js → cli-check-BzMGuIH6.js} +3 -3
- package/dist/_chunks/{cli-check-D6VolrDV.js.map → cli-check-BzMGuIH6.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js → cli-schema-sync-DnXqcIIj.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js.map → cli-schema-sync-DnXqcIIj.js.map} +1 -1
- package/dist/_chunks/{cloudflare-BFb__LYG.js → cloudflare-DxX1SU0g.js} +3 -3
- package/dist/_chunks/{cloudflare-BFb__LYG.js.map → cloudflare-DxX1SU0g.js.map} +1 -1
- package/dist/_chunks/{convention-lint-fRkwVwEH.js → convention-lint-DHOFvX5s.js} +5 -95
- package/dist/_chunks/convention-lint-DHOFvX5s.js.map +1 -0
- package/dist/_chunks/csp-nonce-hOGniaG4.js +227 -0
- package/dist/_chunks/csp-nonce-hOGniaG4.js.map +1 -0
- package/dist/_chunks/dev-server-DioP7tkQ.js +2288 -0
- package/dist/_chunks/dev-server-DioP7tkQ.js.map +1 -0
- package/dist/_chunks/{error-boundary-BfPHZjm0.js → error-boundary-BQKxl6EX.js} +11 -17
- package/dist/_chunks/{error-boundary-BfPHZjm0.js.map → error-boundary-BQKxl6EX.js.map} +1 -1
- package/dist/_chunks/{graph-cache-CP4GEmf9.js → graph-cache-Cv3njEH8.js} +2 -2
- package/dist/_chunks/{graph-cache-CP4GEmf9.js.map → graph-cache-Cv3njEH8.js.map} +1 -1
- package/dist/_chunks/{live-graph-D_2D32Ad.js → live-graph-VjHFF5EV.js} +3 -3
- package/dist/_chunks/{live-graph-D_2D32Ad.js.map → live-graph-VjHFF5EV.js.map} +1 -1
- package/dist/_chunks/{logger-uLBuGKDI.js → logger-DqJ2VoAY.js} +450 -458
- package/dist/_chunks/logger-DqJ2VoAY.js.map +1 -0
- package/dist/_chunks/{segment-keys-lqtdookO.js → metadata-routes-DSDjM_hJ.js} +2 -61
- package/dist/_chunks/metadata-routes-DSDjM_hJ.js.map +1 -0
- package/dist/_chunks/{poison-scan-Bm9Yyqk9.js → poison-scan-lEbz4pQE.js} +2 -2
- package/dist/_chunks/{poison-scan-Bm9Yyqk9.js.map → poison-scan-lEbz4pQE.js.map} +1 -1
- package/dist/_chunks/{scanner-AiazgH_f.js → scanner-B_tnqFcF.js} +37 -137
- package/dist/_chunks/scanner-B_tnqFcF.js.map +1 -0
- package/dist/_chunks/segment-keys-D5hu1hz4.js +62 -0
- package/dist/_chunks/segment-keys-D5hu1hz4.js.map +1 -0
- package/dist/_chunks/tree-match-CdbvYTBz.js +122 -0
- package/dist/_chunks/tree-match-CdbvYTBz.js.map +1 -0
- package/dist/_chunks/{walkers-B6XUtmqK.js → walkers-Cm3PC5JT.js} +2 -2
- package/dist/_chunks/{walkers-B6XUtmqK.js.map → walkers-Cm3PC5JT.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +3 -3
- package/dist/cache/index.js +1 -1
- package/dist/cli.d.ts +7 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +44 -7
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +20 -1
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -1
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +3 -15
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/error-reconstituter.d.ts +4 -4
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/internal.js +1 -1
- package/dist/dev-tools/debug-channel.d.ts +55 -0
- package/dist/dev-tools/debug-channel.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -2119
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts +7 -0
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/scanner.d.ts +4 -4
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts +2 -2
- package/dist/server/deny-boundary.d.ts +3 -5
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +0 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +5 -12
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +20 -195
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline.d.ts +8 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +15 -13
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +3 -7
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +6 -27
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +1 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +1 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/docs/api/30-api-server.mdx +5 -5
- package/docs/api/34-api-config.mdx +2 -2
- package/docs/api/36-cli.mdx +1 -1
- package/docs/learn/12-error-handling.mdx +7 -8
- package/package.json +1 -2
- package/src/cli.ts +62 -8
- package/src/client/browser-entry/action-dispatch.ts +12 -0
- package/src/client/browser-entry/action-queue.ts +70 -8
- package/src/client/browser-entry/rsc-stream.ts +78 -22
- package/src/client/error-boundary.tsx +13 -39
- package/src/client/error-reconstituter.tsx +5 -5
- package/src/dev-tools/debug-channel.ts +151 -0
- package/src/index.ts +8 -12
- package/src/plugins/dev-server.ts +58 -7
- package/src/plugins/mdx.ts +2 -1
- package/src/routing/convention-lint.ts +4 -111
- package/src/routing/scanner.ts +33 -22
- package/src/server/access-gate.tsx +3 -3
- package/src/server/deny-boundary.ts +5 -23
- package/src/server/deny-renderer.ts +4 -7
- package/src/server/error-boundary-wrapper.ts +11 -35
- package/src/server/pipeline.ts +9 -0
- package/src/server/primitives.ts +20 -26
- package/src/server/rsc-entry/error-renderer.ts +15 -43
- package/src/server/rsc-entry/helpers.ts +10 -67
- package/src/server/rsc-entry/index.ts +1 -0
- package/src/server/rsc-entry/render-route.ts +12 -1
- package/src/server/rsc-entry/rsc-stream.ts +27 -24
- package/src/server/rsc-entry/ssr-renderer.ts +7 -1
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
- package/dist/_chunks/convention-lint-fRkwVwEH.js.map +0 -1
- package/dist/_chunks/logger-uLBuGKDI.js.map +0 -1
- package/dist/_chunks/scanner-AiazgH_f.js.map +0 -1
- package/dist/_chunks/segment-keys-lqtdookO.js.map +0 -1
- package/dist/server/utils/mdx-file.d.ts +0 -17
- package/dist/server/utils/mdx-file.d.ts.map +0 -1
- package/src/server/utils/mdx-file.ts +0 -22
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-cache-CP4GEmf9.js","names":[],"sources":["../../src/plugins/dev-discovery.ts","../../src/analyze/graph-cache.ts"],"sourcesContent":["/**\n * Dev-server discovery records — `.timber/dev/<pid>-<port>-<token>.json`.\n *\n * On startup each dev server writes `{ port, pid, host, protocol }` to its own record\n * file under the app's `.timber/dev/` so the `timber graph` CLI and the\n * VS Code extension can discover a running dev server for the\n * live-graph fast path.\n *\n * One record file per server, one writer per file. This is what makes\n * multi-server roots (Vite auto-increments the port across processes;\n * programmatic embeddings run several servers in one process) safe\n * without any coordination: there is no shared file to displace, no\n * ownership to arbitrate, and no shutdown-order dependency — each\n * server removes exactly its own record.\n *\n * Consumers must list the directory and trust a record only if its pid\n * is alive AND a probe request to the endpoint succeeds; anything else\n * is treated as absent (silent fallback), never an error. Stale records\n * (crashed servers) are therefore the normal case — startup sweeps\n * dead-pid records best-effort, and consumers validate the rest away.\n *\n * The directory layout and file format are implementation details, not\n * a public contract.\n *\n * Design doc: 47-module-environment-tooling.md §5, 21-dev-server.md.\n */\n\nimport type { Server } from 'node:net';\nimport { Server as TlsServer } from 'node:tls';\nimport { randomUUID } from 'node:crypto';\nimport { mkdirSync, writeFileSync, rmSync, readFileSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Relative path of the discovery directory within the project root. */\nexport const DEV_DISCOVERY_DIR = join('.timber', 'dev');\n\nexport interface DevRecord {\n port: number;\n pid: number;\n /**\n * The bound address, as reported by the socket. Consumers probing the\n * endpoint need it for servers bound to non-default interfaces;\n * wildcard addresses (`0.0.0.0`, `::`) should be probed via loopback.\n */\n host: string;\n /**\n * Probe scheme. `server.https` dev servers (including HTTP/2) answer\n * only TLS — an http:// probe of one would fail and wrongly trigger\n * the cold-crawl fallback.\n */\n protocol: 'http' | 'https';\n /**\n * Vite `base` path. When non-root (e.g. `/my-app/`), the WebSocket\n * upgrade path is `base + hmrPath`.\n */\n base?: string;\n /**\n * HMR WebSocket path (from `server.ws.path` or deprecated\n * `server.hmr.path`). Absent when no path is configured (default `/`).\n */\n hmrPath?: string;\n /**\n * Custom WebSocket host override (from `server.ws.host` or deprecated\n * `server.hmr.host`). When set, the WS connection targets this host\n * instead of the HTTP server's bound address.\n */\n hmrHost?: string;\n /**\n * Custom WebSocket port override (from `server.ws.clientPort` ??\n * `server.ws.port`, or deprecated `server.hmr.clientPort` ??\n * `server.hmr.port`). When set, the WS connection targets this port\n * instead of the HTTP server's bound port.\n */\n hmrPort?: number;\n /**\n * Custom WebSocket protocol override (from `server.ws.protocol` or\n * deprecated `server.hmr.protocol`). When set (e.g. `'wss'`), the\n * upgrade request uses the corresponding HTTP scheme.\n */\n hmrProtocol?: string;\n /**\n * Vite 8 WebSocket token (from `config.webSocketToken`). Required for\n * browser WS upgrades; the extension passes it as `?token=` on the\n * WebSocket URL.\n */\n webSocketToken?: string;\n /**\n * Absolute path to the Vite config file this server was started with.\n * When multiple dev servers share a project root but use different\n * configs, the fast path must only accept the record matching the\n * config the CLI resolved (TIM-1393).\n */\n configFile?: string;\n}\n\n/** Best-effort \"is this pid alive\" — signal 0 probes without killing. */\nexport function isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means the process exists but is owned by someone else.\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Remove sibling records whose pid is no longer alive. Purely a\n * tidiness sweep — consumers already ignore records that fail the\n * pid + probe validation, so missing a stale record here is harmless.\n */\nfunction sweepDeadRecords(dir: string): void {\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return;\n }\n for (const entry of entries) {\n if (!entry.endsWith('.json')) continue;\n const filePath = join(dir, entry);\n try {\n const record = JSON.parse(readFileSync(filePath, 'utf8')) as Partial<DevRecord>;\n // Remove only records that parse AND name a dead pid. An\n // unreadable file may be a sibling's record mid-write — leave it;\n // consumers ignore records that fail validation anyway.\n if (typeof record.pid === 'number' && !isPidAlive(record.pid)) {\n rmSync(filePath, { force: true });\n }\n } catch {\n // Unreadable — skip, never delete another writer's in-flight file.\n }\n }\n}\n\n/**\n * Register discovery-record lifecycle handlers on the dev server's HTTP\n * server. Writes `.timber/dev/<pid>-<port>-<token>.json` once the server is\n * listening (the bound port is only known then) and removes exactly\n * that record on clean shutdown.\n *\n * All filesystem work is best-effort: a failure to write or remove a\n * record must never break the dev server. `httpServer` is null in\n * middleware mode — no port exists, so there is nothing to advertise.\n * Typed as `net.Server` — the common base of Vite's `http.Server` and\n * `Http2SecureServer` variants, which carries the events and `address()`.\n */\nexport function registerDevDiscovery(\n httpServer: Server | null,\n root: string,\n config?: {\n configFile?: string | false;\n base?: string;\n server?: {\n hmr?:\n | { path?: string; host?: string; port?: number; clientPort?: number; protocol?: string }\n | boolean;\n ws?:\n | { path?: string; host?: string; port?: number; clientPort?: number; protocol?: string }\n | false;\n };\n webSocketToken?: string;\n }\n): void {\n if (!httpServer) return;\n\n const dir = join(root, DEV_DISCOVERY_DIR);\n let recordPath: string | null = null;\n\n httpServer.on('listening', () => {\n const address = httpServer.address();\n // Unix-socket listeners have a string address — no port to advertise.\n if (address === null || typeof address === 'string') return;\n const hmrOpts = typeof config?.server?.hmr === 'object' ? config.server.hmr : undefined;\n const wsOpts = typeof config?.server?.ws === 'object' ? config.server.ws : undefined;\n const record: DevRecord = {\n port: address.port,\n pid: process.pid,\n host: address.address,\n // Vite's https/h2 servers are tls.Server subclasses; plain HTTP\n // and h2c are not.\n protocol: httpServer instanceof TlsServer ? 'https' : 'http',\n base: config?.base && config.base !== '/' ? config.base : undefined,\n hmrPath: wsOpts?.path ?? hmrOpts?.path,\n hmrHost: wsOpts?.host ?? hmrOpts?.host,\n hmrPort:\n wsOpts?.clientPort || wsOpts?.port || hmrOpts?.clientPort || hmrOpts?.port || undefined,\n hmrProtocol: wsOpts?.protocol ?? hmrOpts?.protocol,\n webSocketToken: config?.webSocketToken,\n configFile: typeof config?.configFile === 'string' ? config.configFile : undefined,\n };\n // pid+port alone is not a unique server identity (same port on two\n // interfaces; worker threads and duplicate module copies share a\n // pid but not module state, so a counter cannot disambiguate\n // either). A random token makes the filename unique regardless of\n // process/isolate topology; pid and port stay in the name only for\n // human debuggability.\n recordPath = join(dir, `${record.pid}-${record.port}-${randomUUID()}.json`);\n try {\n mkdirSync(dir, { recursive: true });\n writeFileSync(recordPath, JSON.stringify(record));\n sweepDeadRecords(dir);\n } catch {\n // Best-effort — consumers fall back to the cold crawl.\n }\n });\n\n httpServer.on('close', () => {\n if (!recordPath) return;\n try {\n // Only ever this server's own record — sibling servers' records\n // are separate files and survive any shutdown order untouched.\n rmSync(recordPath, { force: true });\n } catch {\n // Best-effort — a stale record is validated away by consumers.\n }\n });\n}\n","/**\n * graph-cache — the mtime-keyed output cache for `timber graph`.\n *\n * The cold crawl costs seconds (design/47 §6) and its cost is setup +\n * dependency compilation, not project-file count — so the cacheable\n * unit is the classification OUTPUT, never Vite's transforms. One\n * crawl per project state: the first question after a clone pays for\n * the crawl, every subsequent question is a cache hit until the\n * project changes.\n *\n * Validity is a fingerprint over (path, mtime, size) of every project\n * source file — the same disk walk the crawl itself uses (scan.ts), so\n * cache coverage and crawl coverage cannot drift — plus the config\n * files and the nearest lockfile, since dependency changes change\n * resolution (export conditions can flip a file's environment without\n * touching project code).\n *\n * Deliberately vite-free: a cache hit answers without importing Vite.\n *\n * Design docs: 47-module-environment-tooling.md §\"Cold-path policy\".\n */\n\nimport { createHash } from 'node:crypto';\nimport { createRequire } from 'node:module';\nimport { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport { buildSourceExtensions, scanProjectSourceFiles } from './scan.ts';\nimport type { CrawlReport } from './crawl.ts';\n\n/**\n * JSON contract version (design/47 §9) — one constant for every\n * frontend (CLI and dev-server endpoint), so the two cannot drift.\n * Lives here rather than graph-command.ts because this module is\n * vite-free and both frontends already depend on the analyze core.\n */\nexport const GRAPH_JSON_VERSION = 1;\n\n/** Bump when the report shape or fingerprint inputs change. */\nexport const GRAPH_CACHE_VERSION = 2;\n\n/** Cache location under the app root. */\nconst CACHE_RELATIVE_PATH = join('.timber', 'graph-cache.json');\n\n/**\n * Config/manifest files at the root that participate in the\n * fingerprint. The `.env` family is included because a Vite config\n * using `loadEnv(mode, …)` can select aliases, plugins, the app root,\n * or pageExtensions from it — the disk scan skips dot-prefixed files,\n * so without these entries an `.env.production` edit would never miss\n * the cache (codex review, PR #1056). The build resolves under mode\n * `production`; the development variants are tracked anyway — they are\n * loadable through `loadEnv` with any mode argument, and an absent\n * file contributes nothing.\n */\nconst ROOT_MANIFEST_FILES = [\n 'package.json',\n 'tsconfig.json',\n 'timber.config.ts',\n 'timber.config.js',\n 'timber.config.mjs',\n '.env',\n '.env.local',\n '.env.production',\n '.env.production.local',\n '.env.development',\n '.env.development.local',\n];\n\n/** Lockfile names, searched upward from the root (monorepo workspaces). */\nconst LOCKFILE_NAMES = [\n 'pnpm-lock.yaml',\n 'package-lock.json',\n 'yarn.lock',\n 'bun.lock',\n 'bun.lockb',\n];\n\nexport interface GraphCacheFile {\n version: number;\n fingerprint: string;\n /** The Vite config file the cached crawl booted with. */\n configFile: string;\n /**\n * The resolved pageExtensions from the timber config at crawl time.\n * Stored so the vite-free hit path can reproduce the same scan\n * extension set (custom extensions like 'foo' must be admitted on\n * both the write and the hit path, or the fingerprint disagrees\n * with what was cached).\n */\n pageExtensions?: string[];\n /**\n * Files the fingerprint tracks BEYOND the disk scan, discovered at\n * crawl time: Vite's `configFileDependencies` (a config helper\n * imported from outside the app root can reshape the whole app) and\n * every file the report itself classifies (the graph can reach files\n * the scan skips — `app/.well-known/page.tsx`, a route under a\n * directory named `dist`). The hit path re-stats exactly this list,\n * so an edit to any of them misses the cache.\n */\n trackedExtras: string[];\n report: CrawlReport;\n}\n\n/**\n * Follow a tsconfig/jsconfig `extends` chain, returning every file on\n * it (the start file included). A base config outside the app —\n * `\"extends\": \"../tsconfig.base.json\"` with a `paths` mapping — can\n * reshape Vite's resolution without touching anything else the\n * fingerprint tracks. Parsing is a targeted regex, not JSONC: tsconfig\n * allows comments, and all we need is the `extends` value. Package\n * specifiers resolve through Node resolution (an approximation of\n * TypeScript's — the `.json` append covers the common shapes): a\n * WORKSPACE package like `@repo/tsconfig/base.json` is a link, so its\n * edits change neither the lockfile nor any scanned file and must be\n * fingerprinted directly; registry packages resolve too, harmlessly.\n * Cycles and unresolvable targets just end the walk (bounded at 16).\n */\n/**\n * Strip JSONC comments so a commented-out `\"extends\"` line cannot\n * shadow the real one. String-aware: comment markers inside string\n * literals (a path containing `//`) survive.\n */\nexport function stripJsonComments(raw: string): string {\n let out = '';\n let inString = false;\n for (let i = 0; i < raw.length; i++) {\n const ch = raw[i];\n if (inString) {\n out += ch;\n if (ch === '\\\\') {\n out += raw[i + 1] ?? '';\n i++;\n } else if (ch === '\"') {\n inString = false;\n }\n } else if (ch === '\"') {\n inString = true;\n out += ch;\n } else if (ch === '/' && raw[i + 1] === '/') {\n while (i < raw.length && raw[i] !== '\\n') i++;\n out += '\\n';\n } else if (ch === '/' && raw[i + 1] === '*') {\n i += 2;\n while (i < raw.length && !(raw[i] === '*' && raw[i + 1] === '/')) i++;\n i++;\n } else {\n out += ch;\n }\n }\n return out;\n}\n\nasync function collectTsconfigChain(startFile: string): Promise<string[]> {\n const chain: string[] = [];\n const queue = [startFile];\n const seen = new Set(queue);\n while (queue.length > 0 && chain.length < 16) {\n const current = queue.shift()!;\n let raw: string;\n try {\n raw = stripJsonComments(await readFile(current, 'utf8'));\n } catch {\n continue;\n }\n chain.push(current);\n const match = raw.match(/\"extends\"\\s*:\\s*(\"([^\"]+)\"|\\[([^\\]]*)\\])/);\n if (!match) continue;\n const targets = match[2]\n ? [match[2]]\n : (match[3]?.match(/\"([^\"]+)\"/g) ?? []).map((t) => t.slice(1, -1));\n for (const target of targets) {\n let next: string | null = null;\n if (isAbsolute(target)) {\n // An absolute extends target names the base directly — joining\n // it under the current config's directory would fabricate a\n // path that does not exist and drop the real base.\n next = target;\n if (!/\\.json$/.test(next)) next += '.json';\n } else if (target.startsWith('.')) {\n next = join(dirname(current), target);\n if (!/\\.json$/.test(next)) next += '.json';\n } else {\n const req = createRequire(current);\n for (const candidate of [target, `${target}.json`, `${target}/tsconfig.json`]) {\n try {\n next = req.resolve(candidate);\n break;\n } catch {\n // try the next shape\n }\n }\n }\n if (next && !seen.has(next)) {\n seen.add(next);\n queue.push(next);\n }\n }\n }\n return chain;\n}\n\n/** Nearest lockfile at or above `root`, or null. */\nasync function findLockfile(root: string): Promise<string | null> {\n let dir = root;\n for (;;) {\n for (const name of LOCKFILE_NAMES) {\n const candidate = join(dir, name);\n try {\n await stat(candidate);\n return candidate;\n } catch {\n // keep looking\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/** `path\\0mtime\\0size` entries for a file list; absent files contribute nothing. */\nexport async function fingerprintEntriesForFiles(files: Iterable<string>): Promise<string[]> {\n const entries: string[] = [];\n for (const file of new Set(files)) {\n try {\n const stats = await stat(file);\n entries.push(`${file}\\0${stats.mtimeMs}\\0${stats.size}`);\n } catch {\n // Absent files simply contribute nothing; when one appears\n // later, its entry appears and the fingerprint changes.\n }\n }\n return entries;\n}\n\n/**\n * Entries for the scan-derived base set: project source files, the\n * config, root manifests, and the nearest lockfile. Scan failures are\n * folded into the entry list rather than thrown: an unreadable\n * directory changes the answer's completeness, so it must change the\n * key too.\n */\nexport async function collectBaseFingerprintEntries(\n root: string,\n configFile: string,\n pageExtensions?: readonly string[]\n): Promise<string[]> {\n const extensions = buildSourceExtensions(pageExtensions);\n const sourceFiles: string[] = [];\n const failures: string[] = [];\n await scanProjectSourceFiles(\n root,\n sourceFiles,\n (failure) => failures.push(`${failure.dir}:${failure.message}`),\n 0,\n extensions\n );\n const tracked = [...sourceFiles, configFile, ...ROOT_MANIFEST_FILES.map((f) => join(root, f))];\n // Manifest/.env files beside the CONFIG too, when it lives outside\n // the app root — `loadEnv(mode, process.cwd())` in a monorepo config\n // reads the .env next to the config, not the one under root.\n const configDir = dirname(configFile);\n if (configDir !== root) {\n tracked.push(...ROOT_MANIFEST_FILES.map((f) => join(configDir, f)));\n }\n // ts/jsconfig extends chains: a base config outside the app can carry\n // the `paths` mappings resolution depends on.\n for (const name of ['tsconfig.json', 'jsconfig.json']) {\n tracked.push(...(await collectTsconfigChain(join(root, name))));\n if (configDir !== root) tracked.push(...(await collectTsconfigChain(join(configDir, name))));\n }\n const lockfile = await findLockfile(root);\n if (lockfile) tracked.push(lockfile);\n return [...failures, ...(await fingerprintEntriesForFiles(tracked))];\n}\n\n/** Hash an entry list order-independently. */\nexport function hashFingerprintEntries(entries: string[]): string {\n const unique = [...new Set(entries)].sort();\n return createHash('sha256').update(unique.join('\\n')).digest('hex');\n}\n\n/**\n * Fingerprint the project state `timber graph`'s answers depend on:\n * the scan-derived base set plus `extraFiles` (the cache's\n * trackedExtras on the hit path; config dependencies + report files on\n * the write path).\n */\nexport async function computeProjectFingerprint(\n root: string,\n configFile: string,\n extraFiles: readonly string[] = [],\n pageExtensions?: readonly string[]\n): Promise<string> {\n const entries = await collectBaseFingerprintEntries(root, configFile, pageExtensions);\n entries.push(...(await fingerprintEntriesForFiles(extraFiles)));\n return hashFingerprintEntries(entries);\n}\n\n/** Read the cache file; anything unreadable or version-mismatched is a miss. */\nexport async function readGraphCache(root: string): Promise<GraphCacheFile | null> {\n try {\n const raw = await readFile(join(root, CACHE_RELATIVE_PATH), 'utf8');\n const parsed = JSON.parse(raw) as Partial<GraphCacheFile>;\n if (\n parsed.version !== GRAPH_CACHE_VERSION ||\n typeof parsed.fingerprint !== 'string' ||\n typeof parsed.configFile !== 'string' ||\n !Array.isArray(parsed.trackedExtras) ||\n typeof parsed.report !== 'object' ||\n parsed.report === null\n ) {\n return null;\n }\n return parsed as GraphCacheFile;\n } catch {\n return null;\n }\n}\n\n/**\n * Write the cache atomically (temp file + rename) so a concurrent\n * reader — parallel agent invocations are the expected workload —\n * never sees a torn JSON file.\n */\nexport async function writeGraphCache(root: string, cache: GraphCacheFile): Promise<void> {\n const target = join(root, CACHE_RELATIVE_PATH);\n await mkdir(dirname(target), { recursive: true });\n const temp = `${target}.${process.pid}.tmp`;\n await writeFile(temp, JSON.stringify(cache));\n await rename(temp, target);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"graph-cache-Cv3njEH8.js","names":[],"sources":["../../src/plugins/dev-discovery.ts","../../src/analyze/graph-cache.ts"],"sourcesContent":["/**\n * Dev-server discovery records — `.timber/dev/<pid>-<port>-<token>.json`.\n *\n * On startup each dev server writes `{ port, pid, host, protocol }` to its own record\n * file under the app's `.timber/dev/` so the `timber graph` CLI and the\n * VS Code extension can discover a running dev server for the\n * live-graph fast path.\n *\n * One record file per server, one writer per file. This is what makes\n * multi-server roots (Vite auto-increments the port across processes;\n * programmatic embeddings run several servers in one process) safe\n * without any coordination: there is no shared file to displace, no\n * ownership to arbitrate, and no shutdown-order dependency — each\n * server removes exactly its own record.\n *\n * Consumers must list the directory and trust a record only if its pid\n * is alive AND a probe request to the endpoint succeeds; anything else\n * is treated as absent (silent fallback), never an error. Stale records\n * (crashed servers) are therefore the normal case — startup sweeps\n * dead-pid records best-effort, and consumers validate the rest away.\n *\n * The directory layout and file format are implementation details, not\n * a public contract.\n *\n * Design doc: 47-module-environment-tooling.md §5, 21-dev-server.md.\n */\n\nimport type { Server } from 'node:net';\nimport { Server as TlsServer } from 'node:tls';\nimport { randomUUID } from 'node:crypto';\nimport { mkdirSync, writeFileSync, rmSync, readFileSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Relative path of the discovery directory within the project root. */\nexport const DEV_DISCOVERY_DIR = join('.timber', 'dev');\n\nexport interface DevRecord {\n port: number;\n pid: number;\n /**\n * The bound address, as reported by the socket. Consumers probing the\n * endpoint need it for servers bound to non-default interfaces;\n * wildcard addresses (`0.0.0.0`, `::`) should be probed via loopback.\n */\n host: string;\n /**\n * Probe scheme. `server.https` dev servers (including HTTP/2) answer\n * only TLS — an http:// probe of one would fail and wrongly trigger\n * the cold-crawl fallback.\n */\n protocol: 'http' | 'https';\n /**\n * Vite `base` path. When non-root (e.g. `/my-app/`), the WebSocket\n * upgrade path is `base + hmrPath`.\n */\n base?: string;\n /**\n * HMR WebSocket path (from `server.ws.path` or deprecated\n * `server.hmr.path`). Absent when no path is configured (default `/`).\n */\n hmrPath?: string;\n /**\n * Custom WebSocket host override (from `server.ws.host` or deprecated\n * `server.hmr.host`). When set, the WS connection targets this host\n * instead of the HTTP server's bound address.\n */\n hmrHost?: string;\n /**\n * Custom WebSocket port override (from `server.ws.clientPort` ??\n * `server.ws.port`, or deprecated `server.hmr.clientPort` ??\n * `server.hmr.port`). When set, the WS connection targets this port\n * instead of the HTTP server's bound port.\n */\n hmrPort?: number;\n /**\n * Custom WebSocket protocol override (from `server.ws.protocol` or\n * deprecated `server.hmr.protocol`). When set (e.g. `'wss'`), the\n * upgrade request uses the corresponding HTTP scheme.\n */\n hmrProtocol?: string;\n /**\n * Vite 8 WebSocket token (from `config.webSocketToken`). Required for\n * browser WS upgrades; the extension passes it as `?token=` on the\n * WebSocket URL.\n */\n webSocketToken?: string;\n /**\n * Absolute path to the Vite config file this server was started with.\n * When multiple dev servers share a project root but use different\n * configs, the fast path must only accept the record matching the\n * config the CLI resolved (TIM-1393).\n */\n configFile?: string;\n}\n\n/** Best-effort \"is this pid alive\" — signal 0 probes without killing. */\nexport function isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means the process exists but is owned by someone else.\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Remove sibling records whose pid is no longer alive. Purely a\n * tidiness sweep — consumers already ignore records that fail the\n * pid + probe validation, so missing a stale record here is harmless.\n */\nfunction sweepDeadRecords(dir: string): void {\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return;\n }\n for (const entry of entries) {\n if (!entry.endsWith('.json')) continue;\n const filePath = join(dir, entry);\n try {\n const record = JSON.parse(readFileSync(filePath, 'utf8')) as Partial<DevRecord>;\n // Remove only records that parse AND name a dead pid. An\n // unreadable file may be a sibling's record mid-write — leave it;\n // consumers ignore records that fail validation anyway.\n if (typeof record.pid === 'number' && !isPidAlive(record.pid)) {\n rmSync(filePath, { force: true });\n }\n } catch {\n // Unreadable — skip, never delete another writer's in-flight file.\n }\n }\n}\n\n/**\n * Register discovery-record lifecycle handlers on the dev server's HTTP\n * server. Writes `.timber/dev/<pid>-<port>-<token>.json` once the server is\n * listening (the bound port is only known then) and removes exactly\n * that record on clean shutdown.\n *\n * All filesystem work is best-effort: a failure to write or remove a\n * record must never break the dev server. `httpServer` is null in\n * middleware mode — no port exists, so there is nothing to advertise.\n * Typed as `net.Server` — the common base of Vite's `http.Server` and\n * `Http2SecureServer` variants, which carries the events and `address()`.\n */\nexport function registerDevDiscovery(\n httpServer: Server | null,\n root: string,\n config?: {\n configFile?: string | false;\n base?: string;\n server?: {\n hmr?:\n | { path?: string; host?: string; port?: number; clientPort?: number; protocol?: string }\n | boolean;\n ws?:\n | { path?: string; host?: string; port?: number; clientPort?: number; protocol?: string }\n | false;\n };\n webSocketToken?: string;\n }\n): void {\n if (!httpServer) return;\n\n const dir = join(root, DEV_DISCOVERY_DIR);\n let recordPath: string | null = null;\n\n httpServer.on('listening', () => {\n const address = httpServer.address();\n // Unix-socket listeners have a string address — no port to advertise.\n if (address === null || typeof address === 'string') return;\n const hmrOpts = typeof config?.server?.hmr === 'object' ? config.server.hmr : undefined;\n const wsOpts = typeof config?.server?.ws === 'object' ? config.server.ws : undefined;\n const record: DevRecord = {\n port: address.port,\n pid: process.pid,\n host: address.address,\n // Vite's https/h2 servers are tls.Server subclasses; plain HTTP\n // and h2c are not.\n protocol: httpServer instanceof TlsServer ? 'https' : 'http',\n base: config?.base && config.base !== '/' ? config.base : undefined,\n hmrPath: wsOpts?.path ?? hmrOpts?.path,\n hmrHost: wsOpts?.host ?? hmrOpts?.host,\n hmrPort:\n wsOpts?.clientPort || wsOpts?.port || hmrOpts?.clientPort || hmrOpts?.port || undefined,\n hmrProtocol: wsOpts?.protocol ?? hmrOpts?.protocol,\n webSocketToken: config?.webSocketToken,\n configFile: typeof config?.configFile === 'string' ? config.configFile : undefined,\n };\n // pid+port alone is not a unique server identity (same port on two\n // interfaces; worker threads and duplicate module copies share a\n // pid but not module state, so a counter cannot disambiguate\n // either). A random token makes the filename unique regardless of\n // process/isolate topology; pid and port stay in the name only for\n // human debuggability.\n recordPath = join(dir, `${record.pid}-${record.port}-${randomUUID()}.json`);\n try {\n mkdirSync(dir, { recursive: true });\n writeFileSync(recordPath, JSON.stringify(record));\n sweepDeadRecords(dir);\n } catch {\n // Best-effort — consumers fall back to the cold crawl.\n }\n });\n\n httpServer.on('close', () => {\n if (!recordPath) return;\n try {\n // Only ever this server's own record — sibling servers' records\n // are separate files and survive any shutdown order untouched.\n rmSync(recordPath, { force: true });\n } catch {\n // Best-effort — a stale record is validated away by consumers.\n }\n });\n}\n","/**\n * graph-cache — the mtime-keyed output cache for `timber graph`.\n *\n * The cold crawl costs seconds (design/47 §6) and its cost is setup +\n * dependency compilation, not project-file count — so the cacheable\n * unit is the classification OUTPUT, never Vite's transforms. One\n * crawl per project state: the first question after a clone pays for\n * the crawl, every subsequent question is a cache hit until the\n * project changes.\n *\n * Validity is a fingerprint over (path, mtime, size) of every project\n * source file — the same disk walk the crawl itself uses (scan.ts), so\n * cache coverage and crawl coverage cannot drift — plus the config\n * files and the nearest lockfile, since dependency changes change\n * resolution (export conditions can flip a file's environment without\n * touching project code).\n *\n * Deliberately vite-free: a cache hit answers without importing Vite.\n *\n * Design docs: 47-module-environment-tooling.md §\"Cold-path policy\".\n */\n\nimport { createHash } from 'node:crypto';\nimport { createRequire } from 'node:module';\nimport { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport { buildSourceExtensions, scanProjectSourceFiles } from './scan.ts';\nimport type { CrawlReport } from './crawl.ts';\n\n/**\n * JSON contract version (design/47 §9) — one constant for every\n * frontend (CLI and dev-server endpoint), so the two cannot drift.\n * Lives here rather than graph-command.ts because this module is\n * vite-free and both frontends already depend on the analyze core.\n */\nexport const GRAPH_JSON_VERSION = 1;\n\n/** Bump when the report shape or fingerprint inputs change. */\nexport const GRAPH_CACHE_VERSION = 2;\n\n/** Cache location under the app root. */\nconst CACHE_RELATIVE_PATH = join('.timber', 'graph-cache.json');\n\n/**\n * Config/manifest files at the root that participate in the\n * fingerprint. The `.env` family is included because a Vite config\n * using `loadEnv(mode, …)` can select aliases, plugins, the app root,\n * or pageExtensions from it — the disk scan skips dot-prefixed files,\n * so without these entries an `.env.production` edit would never miss\n * the cache (codex review, PR #1056). The build resolves under mode\n * `production`; the development variants are tracked anyway — they are\n * loadable through `loadEnv` with any mode argument, and an absent\n * file contributes nothing.\n */\nconst ROOT_MANIFEST_FILES = [\n 'package.json',\n 'tsconfig.json',\n 'timber.config.ts',\n 'timber.config.js',\n 'timber.config.mjs',\n '.env',\n '.env.local',\n '.env.production',\n '.env.production.local',\n '.env.development',\n '.env.development.local',\n];\n\n/** Lockfile names, searched upward from the root (monorepo workspaces). */\nconst LOCKFILE_NAMES = [\n 'pnpm-lock.yaml',\n 'package-lock.json',\n 'yarn.lock',\n 'bun.lock',\n 'bun.lockb',\n];\n\nexport interface GraphCacheFile {\n version: number;\n fingerprint: string;\n /** The Vite config file the cached crawl booted with. */\n configFile: string;\n /**\n * The resolved pageExtensions from the timber config at crawl time.\n * Stored so the vite-free hit path can reproduce the same scan\n * extension set (custom extensions like 'foo' must be admitted on\n * both the write and the hit path, or the fingerprint disagrees\n * with what was cached).\n */\n pageExtensions?: string[];\n /**\n * Files the fingerprint tracks BEYOND the disk scan, discovered at\n * crawl time: Vite's `configFileDependencies` (a config helper\n * imported from outside the app root can reshape the whole app) and\n * every file the report itself classifies (the graph can reach files\n * the scan skips — `app/.well-known/page.tsx`, a route under a\n * directory named `dist`). The hit path re-stats exactly this list,\n * so an edit to any of them misses the cache.\n */\n trackedExtras: string[];\n report: CrawlReport;\n}\n\n/**\n * Follow a tsconfig/jsconfig `extends` chain, returning every file on\n * it (the start file included). A base config outside the app —\n * `\"extends\": \"../tsconfig.base.json\"` with a `paths` mapping — can\n * reshape Vite's resolution without touching anything else the\n * fingerprint tracks. Parsing is a targeted regex, not JSONC: tsconfig\n * allows comments, and all we need is the `extends` value. Package\n * specifiers resolve through Node resolution (an approximation of\n * TypeScript's — the `.json` append covers the common shapes): a\n * WORKSPACE package like `@repo/tsconfig/base.json` is a link, so its\n * edits change neither the lockfile nor any scanned file and must be\n * fingerprinted directly; registry packages resolve too, harmlessly.\n * Cycles and unresolvable targets just end the walk (bounded at 16).\n */\n/**\n * Strip JSONC comments so a commented-out `\"extends\"` line cannot\n * shadow the real one. String-aware: comment markers inside string\n * literals (a path containing `//`) survive.\n */\nexport function stripJsonComments(raw: string): string {\n let out = '';\n let inString = false;\n for (let i = 0; i < raw.length; i++) {\n const ch = raw[i];\n if (inString) {\n out += ch;\n if (ch === '\\\\') {\n out += raw[i + 1] ?? '';\n i++;\n } else if (ch === '\"') {\n inString = false;\n }\n } else if (ch === '\"') {\n inString = true;\n out += ch;\n } else if (ch === '/' && raw[i + 1] === '/') {\n while (i < raw.length && raw[i] !== '\\n') i++;\n out += '\\n';\n } else if (ch === '/' && raw[i + 1] === '*') {\n i += 2;\n while (i < raw.length && !(raw[i] === '*' && raw[i + 1] === '/')) i++;\n i++;\n } else {\n out += ch;\n }\n }\n return out;\n}\n\nasync function collectTsconfigChain(startFile: string): Promise<string[]> {\n const chain: string[] = [];\n const queue = [startFile];\n const seen = new Set(queue);\n while (queue.length > 0 && chain.length < 16) {\n const current = queue.shift()!;\n let raw: string;\n try {\n raw = stripJsonComments(await readFile(current, 'utf8'));\n } catch {\n continue;\n }\n chain.push(current);\n const match = raw.match(/\"extends\"\\s*:\\s*(\"([^\"]+)\"|\\[([^\\]]*)\\])/);\n if (!match) continue;\n const targets = match[2]\n ? [match[2]]\n : (match[3]?.match(/\"([^\"]+)\"/g) ?? []).map((t) => t.slice(1, -1));\n for (const target of targets) {\n let next: string | null = null;\n if (isAbsolute(target)) {\n // An absolute extends target names the base directly — joining\n // it under the current config's directory would fabricate a\n // path that does not exist and drop the real base.\n next = target;\n if (!/\\.json$/.test(next)) next += '.json';\n } else if (target.startsWith('.')) {\n next = join(dirname(current), target);\n if (!/\\.json$/.test(next)) next += '.json';\n } else {\n const req = createRequire(current);\n for (const candidate of [target, `${target}.json`, `${target}/tsconfig.json`]) {\n try {\n next = req.resolve(candidate);\n break;\n } catch {\n // try the next shape\n }\n }\n }\n if (next && !seen.has(next)) {\n seen.add(next);\n queue.push(next);\n }\n }\n }\n return chain;\n}\n\n/** Nearest lockfile at or above `root`, or null. */\nasync function findLockfile(root: string): Promise<string | null> {\n let dir = root;\n for (;;) {\n for (const name of LOCKFILE_NAMES) {\n const candidate = join(dir, name);\n try {\n await stat(candidate);\n return candidate;\n } catch {\n // keep looking\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/** `path\\0mtime\\0size` entries for a file list; absent files contribute nothing. */\nexport async function fingerprintEntriesForFiles(files: Iterable<string>): Promise<string[]> {\n const entries: string[] = [];\n for (const file of new Set(files)) {\n try {\n const stats = await stat(file);\n entries.push(`${file}\\0${stats.mtimeMs}\\0${stats.size}`);\n } catch {\n // Absent files simply contribute nothing; when one appears\n // later, its entry appears and the fingerprint changes.\n }\n }\n return entries;\n}\n\n/**\n * Entries for the scan-derived base set: project source files, the\n * config, root manifests, and the nearest lockfile. Scan failures are\n * folded into the entry list rather than thrown: an unreadable\n * directory changes the answer's completeness, so it must change the\n * key too.\n */\nexport async function collectBaseFingerprintEntries(\n root: string,\n configFile: string,\n pageExtensions?: readonly string[]\n): Promise<string[]> {\n const extensions = buildSourceExtensions(pageExtensions);\n const sourceFiles: string[] = [];\n const failures: string[] = [];\n await scanProjectSourceFiles(\n root,\n sourceFiles,\n (failure) => failures.push(`${failure.dir}:${failure.message}`),\n 0,\n extensions\n );\n const tracked = [...sourceFiles, configFile, ...ROOT_MANIFEST_FILES.map((f) => join(root, f))];\n // Manifest/.env files beside the CONFIG too, when it lives outside\n // the app root — `loadEnv(mode, process.cwd())` in a monorepo config\n // reads the .env next to the config, not the one under root.\n const configDir = dirname(configFile);\n if (configDir !== root) {\n tracked.push(...ROOT_MANIFEST_FILES.map((f) => join(configDir, f)));\n }\n // ts/jsconfig extends chains: a base config outside the app can carry\n // the `paths` mappings resolution depends on.\n for (const name of ['tsconfig.json', 'jsconfig.json']) {\n tracked.push(...(await collectTsconfigChain(join(root, name))));\n if (configDir !== root) tracked.push(...(await collectTsconfigChain(join(configDir, name))));\n }\n const lockfile = await findLockfile(root);\n if (lockfile) tracked.push(lockfile);\n return [...failures, ...(await fingerprintEntriesForFiles(tracked))];\n}\n\n/** Hash an entry list order-independently. */\nexport function hashFingerprintEntries(entries: string[]): string {\n const unique = [...new Set(entries)].sort();\n return createHash('sha256').update(unique.join('\\n')).digest('hex');\n}\n\n/**\n * Fingerprint the project state `timber graph`'s answers depend on:\n * the scan-derived base set plus `extraFiles` (the cache's\n * trackedExtras on the hit path; config dependencies + report files on\n * the write path).\n */\nexport async function computeProjectFingerprint(\n root: string,\n configFile: string,\n extraFiles: readonly string[] = [],\n pageExtensions?: readonly string[]\n): Promise<string> {\n const entries = await collectBaseFingerprintEntries(root, configFile, pageExtensions);\n entries.push(...(await fingerprintEntriesForFiles(extraFiles)));\n return hashFingerprintEntries(entries);\n}\n\n/** Read the cache file; anything unreadable or version-mismatched is a miss. */\nexport async function readGraphCache(root: string): Promise<GraphCacheFile | null> {\n try {\n const raw = await readFile(join(root, CACHE_RELATIVE_PATH), 'utf8');\n const parsed = JSON.parse(raw) as Partial<GraphCacheFile>;\n if (\n parsed.version !== GRAPH_CACHE_VERSION ||\n typeof parsed.fingerprint !== 'string' ||\n typeof parsed.configFile !== 'string' ||\n !Array.isArray(parsed.trackedExtras) ||\n typeof parsed.report !== 'object' ||\n parsed.report === null\n ) {\n return null;\n }\n return parsed as GraphCacheFile;\n } catch {\n return null;\n }\n}\n\n/**\n * Write the cache atomically (temp file + rename) so a concurrent\n * reader — parallel agent invocations are the expected workload —\n * never sees a torn JSON file.\n */\nexport async function writeGraphCache(root: string, cache: GraphCacheFile): Promise<void> {\n const target = join(root, CACHE_RELATIVE_PATH);\n await mkdir(dirname(target), { recursive: true });\n const temp = `${target}.${process.pid}.tmp`;\n await writeFile(temp, JSON.stringify(cache));\n await rename(temp, target);\n}\n"],"mappings":"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"}
|
|
@@ -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-CBNA0Ozj.js";
|
|
3
|
+
import { n as importSignals, r as poisoningsFor, t as collectImportSpecifiers } from "./poison-scan-lEbz4pQE.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-VjHFF5EV.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-graph-D_2D32Ad.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-VjHFF5EV.js","names":[],"sources":["../../src/analyze/live-graph.ts"],"sourcesContent":["/**\n * live-graph — classification from a running dev server's environment\n * module graphs (the fast path).\n *\n * When a dev server runs, the env module graphs are already resident —\n * classification is a lookup, not a crawl. This module adapts those\n * graphs to the SAME classification pipeline the cold crawl uses\n * (classify.ts + chains.ts + poison-scan.ts — design/47's \"one\n * classification module, thin frontends\" requirement), so the\n * dev-server endpoint and the CLI can never disagree about a file.\n *\n * Differences from the crawl, both inherent to the live source:\n *\n * - The browser (`client`) graph can be non-empty here (a real browser\n * has requested modules), so `envs.client` is confirmable.\n * - Rendering populates graphs lazily — only visited routes' code is\n * present, and modules behind unexecuted dynamic imports carry\n * `transformResult == null` (the §8 frontier). Those are reported as\n * `subtreeUnknown` rather than transformed on demand: a hint must\n * never mutate the dev server's graphs as a side effect of asking.\n *\n * Design docs: 47-module-environment-tooling.md §8, §\"Dev-server\n * endpoint\".\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { readFileSync } from 'node:fs';\nimport { normalizePath } from 'vite';\nimport {\n classifyModule,\n detectDirective,\n type ClassifyContext,\n type Directive,\n} from './classify.ts';\nimport type { GraphModuleReport } from './crawl.ts';\nimport { stitchImportChains, type ChainGraph, type ImportChain } from './chains.ts';\nimport {\n collectImportSpecifiers,\n importSignals,\n poisoningsFor,\n type Poisoning,\n} from './poison-scan.ts';\nimport { buildSourceExtensions, scanProjectSourceFiles, type ScanFailure } from './scan.ts';\n\n/**\n * Duck-typed node view for liveness checks — the `transformResult`\n * member ChainModuleNode deliberately omits (§8 detection is a\n * classification concern, not a chain concern).\n */\ninterface LiveModuleNode {\n transformResult: unknown;\n}\n\nimport type { ChainModuleNode } from './chains.ts';\n\n/** Duck-typed module graph — structural supertype of Vite's. */\nexport interface LiveGraph extends ChainGraph {\n getModulesByFile(file: string): Set<ChainModuleNode & LiveModuleNode> | undefined;\n}\n\nexport interface LiveGraphContext {\n rsc: LiveGraph;\n ssr: LiveGraph;\n client: LiveGraph;\n /** Realpath'd project root. */\n root: string;\n appDir: string;\n pageExtensions?: readonly string[];\n}\n\n/**\n * Per-module answer — the crawl's shape, aliased rather than restated\n * so a field added to one frontend cannot silently miss the other.\n */\nexport type LiveModuleReport = GraphModuleReport;\n\n/** Non-empty node set for a file in one graph. */\nfunction memberOf(graph: LiveGraph, file: string): boolean {\n return (graph.getModulesByFile(file)?.size ?? 0) > 0;\n}\n\n/**\n * §8: an SSR-graph member whose every node was never transformed is a\n * module behind an unexecuted dynamic import — reachable (it ships to\n * the browser) but its subtree may be missing from the graphs.\n *\n * Live-source caveat: Vite also clears `transformResult` when a watched\n * edit invalidates a node, so on the live graphs `subtreeUnknown`\n * means \"not currently transformed\" — a freshly-edited module reads\n * the same as a dormant dynamic-import payload until the next render\n * re-transforms it. The two states are indistinguishable from the\n * graph alone, and both honestly mean the subtree is not currently\n * known; consumers refreshing on `timber:graph-update` events see the\n * flag clear once the module is re-transformed.\n */\nfunction ssrTransformed(graph: LiveGraph, file: string): boolean | undefined {\n const nodes = graph.getModulesByFile(file);\n if (!nodes || nodes.size === 0) return undefined;\n for (const node of nodes) {\n if (node.transformResult != null) return true;\n }\n return false;\n}\n\n/**\n * Classify one file from the live graphs. `file` must be an absolute\n * realpath (graph nodes carry realpaths); it is slash-normalized here\n * to match Vite's node keying.\n */\nexport async function classifyLiveFile(\n rawFile: string,\n ctx: LiveGraphContext,\n // Shared across a ?all=1 sweep (classifyLiveProject) — a per-call\n // cache would re-read every chain hop's source per target file,\n // quadratic sync IO on well-connected graphs.\n getDirective: (file: string) => Directive = makeDirectiveReader(),\n // Failures are recorded, not swallowed (crawl parity): a lost source\n // read means the directive/poisoning half of the answer is missing,\n // and consumers deserve to see that beside the classification.\n errors?: LiveGraphError[]\n): Promise<LiveModuleReport> {\n const file = normalizePath(rawFile);\n let code: string | null = null;\n try {\n code = await readFile(file, 'utf8');\n } catch (error) {\n // Unreadable (deleted mid-query): classification proceeds on graph\n // membership alone; directive/poisonings default to none.\n errors?.push({\n environment: 'scan',\n id: file,\n message: `source read failed: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n const directive: Directive = code === null ? null : detectDirective(code, file);\n const signals = code === null ? [] : importSignals(collectImportSpecifiers(code, file));\n\n const classifyCtx: ClassifyContext = {\n appDir: ctx.appDir,\n pageExtensions: ctx.pageExtensions,\n };\n const classification = classifyModule(\n {\n file,\n inRsc: memberOf(ctx.rsc, file),\n inSsr: memberOf(ctx.ssr, file),\n inClient: memberOf(ctx.client, file),\n ssrTransformed: ssrTransformed(ctx.ssr, file),\n directive,\n },\n classifyCtx\n );\n\n const chains = classification.pending\n ? []\n : stitchImportChains(file, {\n rsc: ctx.rsc,\n ssr: ctx.ssr,\n appDir: ctx.appDir,\n pageExtensions: ctx.pageExtensions,\n getDirective,\n });\n\n return {\n ...classification,\n boundary: directive === 'use client',\n chains,\n poisonings: poisoningsFor(classification.kind, classification.pending, signals),\n };\n}\n\n/**\n * Synchronous directive reader with a per-call cache. chains.ts wants a\n * sync callback; node:fs readFileSync per unique hop file is acceptable\n * for a walk bounded by chain length.\n */\nfunction makeDirectiveReader(): (file: string) => Directive {\n const cache = new Map<string, Directive>();\n return (file) => {\n const hit = cache.get(file);\n if (hit !== undefined) return hit;\n let directive: Directive = null;\n try {\n directive = detectDirective(readFileSync(file, 'utf8'), file);\n } catch {\n directive = null;\n }\n cache.set(file, directive);\n return directive;\n };\n}\n\n/** Live-source error entry — the crawl's error vocabulary. */\nexport interface LiveGraphError {\n environment: 'scan';\n id: string;\n message: string;\n}\n\nexport interface LiveProjectReport {\n root: string;\n modules: LiveModuleReport[];\n errors: LiveGraphError[];\n}\n\n/**\n * Classify every project source file from the live graphs. Files on\n * disk that no graph reached get the §2 provisional/pending\n * classification — on a lazily-warmed dev server that includes routes\n * nobody has visited yet, which is honest: the graphs genuinely have\n * not confirmed them.\n */\nexport async function classifyLiveProject(ctx: LiveGraphContext): Promise<LiveProjectReport> {\n const files: string[] = [];\n const errors: LiveProjectReport['errors'] = [];\n const extensions = buildSourceExtensions(ctx.pageExtensions);\n await scanProjectSourceFiles(\n ctx.root,\n files,\n (failure: ScanFailure) =>\n errors.push({ environment: 'scan', id: failure.dir, message: failure.message }),\n 0,\n extensions\n );\n const modules: LiveModuleReport[] = [];\n const getDirective = makeDirectiveReader();\n for (const file of files) {\n modules.push(await classifyLiveFile(file, ctx, getDirective, errors));\n }\n modules.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));\n return { root: ctx.root, modules, errors };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA,SAAS,SAAS,OAAkB,MAAuB;CACzD,QAAQ,MAAM,iBAAiB,IAAI,CAAC,EAAE,QAAQ,KAAK;AACrD;;;;;;;;;;;;;;;AAgBA,SAAS,eAAe,OAAkB,MAAmC;CAC3E,MAAM,QAAQ,MAAM,iBAAiB,IAAI;CACzC,IAAI,CAAC,SAAS,MAAM,SAAS,GAAG,OAAO,KAAA;CACvC,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,mBAAmB,MAAM,OAAO;CAE3C,OAAO;AACT;;;;;;AAOA,eAAsB,iBACpB,SACA,KAIA,eAA4C,oBAAoB,GAIhE,QAC2B;CAC3B,MAAM,OAAO,cAAc,OAAO;CAClC,IAAI,OAAsB;CAC1B,IAAI;EACF,OAAO,MAAM,SAAS,MAAM,MAAM;CACpC,SAAS,OAAO;EAGd,QAAQ,KAAK;GACX,aAAa;GACb,IAAI;GACJ,SAAS,uBAAuB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACvF,CAAC;CACH;CACA,MAAM,YAAuB,SAAS,OAAO,OAAO,gBAAgB,MAAM,IAAI;CAC9E,MAAM,UAAU,SAAS,OAAO,CAAC,IAAI,cAAc,wBAAwB,MAAM,IAAI,CAAC;CAEtF,MAAM,cAA+B;EACnC,QAAQ,IAAI;EACZ,gBAAgB,IAAI;CACtB;CACA,MAAM,iBAAiB,eACrB;EACE;EACA,OAAO,SAAS,IAAI,KAAK,IAAI;EAC7B,OAAO,SAAS,IAAI,KAAK,IAAI;EAC7B,UAAU,SAAS,IAAI,QAAQ,IAAI;EACnC,gBAAgB,eAAe,IAAI,KAAK,IAAI;EAC5C;CACF,GACA,WACF;CAEA,MAAM,SAAS,eAAe,UAC1B,CAAC,IACD,mBAAmB,MAAM;EACvB,KAAK,IAAI;EACT,KAAK,IAAI;EACT,QAAQ,IAAI;EACZ,gBAAgB,IAAI;EACpB;CACF,CAAC;CAEL,OAAO;EACL,GAAG;EACH,UAAU,cAAc;EACxB;EACA,YAAY,cAAc,eAAe,MAAM,eAAe,SAAS,OAAO;CAChF;AACF;;;;;;AAOA,SAAS,sBAAmD;CAC1D,MAAM,wBAAQ,IAAI,IAAuB;CACzC,QAAQ,SAAS;EACf,MAAM,MAAM,MAAM,IAAI,IAAI;EAC1B,IAAI,QAAQ,KAAA,GAAW,OAAO;EAC9B,IAAI,YAAuB;EAC3B,IAAI;GACF,YAAY,gBAAgB,aAAa,MAAM,MAAM,GAAG,IAAI;EAC9D,QAAQ;GACN,YAAY;EACd;EACA,MAAM,IAAI,MAAM,SAAS;EACzB,OAAO;CACT;AACF;;;;;;;;AAsBA,eAAsB,oBAAoB,KAAmD;CAC3F,MAAM,QAAkB,CAAC;CACzB,MAAM,SAAsC,CAAC;CAC7C,MAAM,aAAa,sBAAsB,IAAI,cAAc;CAC3D,MAAM,uBACJ,IAAI,MACJ,QACC,YACC,OAAO,KAAK;EAAE,aAAa;EAAQ,IAAI,QAAQ;EAAK,SAAS,QAAQ;CAAQ,CAAC,GAChF,GACA,UACF;CACA,MAAM,UAA8B,CAAC;CACrC,MAAM,eAAe,oBAAoB;CACzC,KAAK,MAAM,QAAQ,OACjB,QAAQ,KAAK,MAAM,iBAAiB,MAAM,KAAK,cAAc,MAAM,CAAC;CAEtE,QAAQ,MAAM,GAAG,MAAO,EAAE,OAAO,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,CAAE;CACvE,OAAO;EAAE,MAAM,IAAI;EAAM;EAAS;CAAO;AAC3C"}
|