@warlock.js/core 5.5.0 → 5.7.0
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/CHANGELOG.md +32 -0
- package/esm/application/app.d.mts.map +1 -1
- package/esm/application/app.mjs +7 -1
- package/esm/application/app.mjs.map +1 -1
- package/esm/benchmark/profiler.d.mts.map +1 -1
- package/esm/benchmark/profiler.mjs +4 -3
- package/esm/benchmark/profiler.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs +10 -7
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/cli-commands.utils.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/routes/format-routes-table.mjs +3 -3
- package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -1
- package/esm/cli/commands/routes/route-row.mjs.map +1 -1
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/cli/parse-cli-args.mjs +1 -0
- package/esm/cli/parse-cli-args.mjs.map +1 -1
- package/esm/cli/string-similarity.mjs +13 -4
- package/esm/cli/string-similarity.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs +5 -2
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/config/config-loader.mjs +3 -1
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/locale-configuration.mjs +11 -1
- package/esm/config/locale-configuration.mjs.map +1 -1
- package/esm/container/container-instance-registry.mjs +40 -0
- package/esm/container/container-instance-registry.mjs.map +1 -0
- package/esm/container/index.d.mts +11 -0
- package/esm/container/index.d.mts.map +1 -1
- package/esm/container/index.mjs +11 -0
- package/esm/container/index.mjs.map +1 -1
- package/esm/database/drop-tables-action.mjs.map +1 -1
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/dependency-graph.d.mts.map +1 -1
- package/esm/dev-server/dependency-graph.mjs +2 -1
- package/esm/dev-server/dependency-graph.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs +7 -5
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +1 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/dev-server/loader/build-transpile-init.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs.map +1 -1
- package/esm/dev-server/loader/register-loader.mjs.map +1 -1
- package/esm/dev-server/loader/resolve-hook.mjs.map +1 -1
- package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +22 -8
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/dev-server/types.d.mts.map +1 -1
- package/esm/dev-server/utils.mjs.map +1 -1
- package/esm/encryption/encrypt.mjs +1 -0
- package/esm/encryption/encrypt.mjs.map +1 -1
- package/esm/errors/container-key-missing-error.mjs +28 -0
- package/esm/errors/container-key-missing-error.mjs.map +1 -0
- package/esm/errors/unknown-locale-error.mjs +28 -0
- package/esm/errors/unknown-locale-error.mjs.map +1 -0
- package/esm/generations/add-command.action.mjs +8 -2
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs.map +1 -1
- package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
- package/esm/generations/features/ai-google.feature.mjs.map +1 -1
- package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
- package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
- package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
- package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
- package/esm/generations/features/ai.feature.mjs.map +1 -1
- package/esm/generations/features/herald.feature.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/scheduler.feature.mjs.map +1 -1
- package/esm/generations/features/shared/patch-tsconfig-include.mjs.map +1 -1
- package/esm/generations/features/shared/relocate-conflicting-home-route.mjs +6 -1
- package/esm/generations/features/shared/relocate-conflicting-home-route.mjs.map +1 -1
- package/esm/generations/features/tailwind.feature.mjs.map +1 -1
- package/esm/generations/features/types.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/http/context/request-memo.mjs.map +1 -1
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/idempotency.middleware.d.mts.map +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +1 -2
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
- package/esm/http/middleware/utils/cidr-match.mjs +1 -0
- package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
- package/esm/http/port-preflight.d.mts.map +1 -1
- package/esm/http/port-preflight.mjs.map +1 -1
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +16 -9
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +38 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +47 -0
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server.d.mts.map +1 -1
- package/esm/http/server.mjs.map +1 -1
- package/esm/http/types.d.mts.map +1 -1
- package/esm/image/image.d.mts.map +1 -1
- package/esm/image/image.mjs.map +1 -1
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/production/build-contributions.mjs.map +1 -1
- package/esm/production/bundle-framework-dependencies.mjs +152 -0
- package/esm/production/bundle-framework-dependencies.mjs.map +1 -0
- package/esm/production/production-builder.mjs +30 -3
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/production/tsconfig-path-aliases.mjs +99 -0
- package/esm/production/tsconfig-path-aliases.mjs.map +1 -0
- package/esm/react/index.d.mts.map +1 -1
- package/esm/react/index.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +4 -2
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/repositories/contracts/query-builder.contract.d.mts.map +1 -1
- package/esm/repositories/contracts/types.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/types.d.mts.map +1 -1
- package/esm/router/positional-handler-diagnostics.d.mts.map +1 -1
- package/esm/router/positional-handler-diagnostics.mjs +9 -3
- package/esm/router/positional-handler-diagnostics.mjs.map +1 -1
- package/esm/router/route-registry.mjs +1 -1
- package/esm/router/route-registry.mjs.map +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +1 -2
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +1 -0
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +6 -2
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/types.d.mts.map +1 -1
- package/esm/storage/utils/contain-path.mjs.map +1 -1
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +6 -1
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-lifecycle-state.mjs.map +1 -1
- package/esm/tests/test-setup-timeout.mjs.map +1 -1
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/use-cases/use-cases-registry.d.mts.map +1 -1
- package/esm/use-cases/use-cases-registry.mjs.map +1 -1
- package/esm/utils/app-log.mjs.map +1 -1
- package/esm/utils/queue.mjs.map +1 -1
- package/esm/utils/sleep.mjs.map +1 -1
- package/esm/utils/version-compare.d.mts.map +1 -1
- package/esm/utils/version-compare.mjs +12 -4
- package/esm/utils/version-compare.mjs.map +1 -1
- package/esm/validation/plugins/database-plugin.mjs.map +1 -1
- package/esm/vite/lower-stage3-decorators.mjs +1 -0
- package/esm/vite/lower-stage3-decorators.mjs.map +1 -1
- package/package.json +12 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register-loader.mjs","names":[],"sources":["../../../../../../../../core/src/dev-server/loader/register-loader.ts"],"sourcesContent":["import { putFileAsync } from \"@warlock.js/fs\";\nimport { build, type Plugin } from \"esbuild\";\nimport { register } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { type MessagePort, MessageChannel } from \"node:worker_threads\";\nimport type { TranspileInit } from \"./load-hook.js\";\n\n/**\n * Rewrite the hook bundle's remaining npm imports to absolute paths resolved\n * from **core's own** location.\n *\n * The bundle is written into the consuming app's `.warlock/` directory, so a\n * bare `import \"esbuild\"` in it resolves starting from the *app*, walking up\n * the app's `node_modules`. `esbuild` and `get-tsconfig` are core's\n * dependencies, not the app's — under npm/yarn's flat hoisting they happen to\n * be reachable anyway, but under pnpm's strict layout they are not, and the\n * dev server dies with `ERR_MODULE_NOT_FOUND` for a package the app never\n * imported. That is a phantom dependency baked into generated code.\n *\n * Bundling them in instead is not an option for `esbuild`: its JS API is a\n * thin wrapper that spawns a platform-specific native binary, so inlining the\n * JavaScript would not remove the need for the package on disk. Resolving to\n * an absolute `file://` URL at generation time does — the generated file then\n * points straight at the copy core itself is using, whatever the installer's\n * layout.\n */\nexport const resolveExternalsFromCore: Plugin = {\n name: \"warlock-resolve-externals-from-core\",\n setup(build) {\n // The filter is deliberately loose — anything not starting `.` or `/` —\n // so the guards below carry the real logic.\n build.onResolve({ filter: /^[^./]/ }, args => {\n // The entry point comes through here too, and on Windows its absolute\n // path (\"D:\\\\…\") is not `.` or `/`. Marking it external fails the build.\n if (args.kind === \"entry-point\" || path.isAbsolute(args.path)) {\n return null;\n }\n\n if (args.path.startsWith(\"node:\")) {\n return { external: true };\n }\n\n try {\n return { path: import.meta.resolve(args.path), external: true };\n } catch {\n // Unresolvable from here — leave it bare and let Node try, which is\n // exactly the previous behaviour rather than a hard failure.\n return { external: true };\n }\n });\n },\n};\n\n/**\n * Bundle, write, and register the ESM loader hook.\n *\n * **Why bundle?**\n * `module.register()` runs the hook file in a fresh Node.js worker thread that\n * has no tsx hook of its own. The hook source is TypeScript, so we must produce\n * a plain ESM bundle before registering. esbuild inline-bundles all three hook\n * modules (`hook-thread`, `resolve-hook`, `load-hook`, `version-registry`)\n * into a single `.mjs` file written to `.warlock/`. External npm packages\n * (`esbuild`, `node:*`) are kept external — the hook thread can resolve those\n * from `node_modules` normally.\n *\n * **Why a file and not a `data:` URL?**\n * Some Node versions have issues resolving `import.meta.url` inside `data:`\n * modules. A real file under `.warlock/` is simpler and debuggable.\n *\n * **Timing**\n * Called from `FilesOrchestrator.init()` before any user `src/` module is\n * dynamically imported. `module.register()` takes effect for all subsequent\n * `import()` calls, which is exactly the window we need.\n *\n * @param transpile - Transpile-cache config to ship into the hook worker,\n * or `null` to keep the hook in tsx-passthrough mode.\n *\n * @returns The main-thread side of the MessageChannel. Callers post\n * `{ type: \"bump\", absolutePath }` messages on it to invalidate modules.\n *\n * @example\n * const port = await registerLoader(transpileInit);\n * // Later, when a file changes:\n * port.postMessage({ type: \"bump\", absolutePath: \"/abs/path/to/user.model.ts\" });\n */\nexport async function registerLoader(
|
|
1
|
+
{"version":3,"file":"register-loader.mjs","names":[],"sources":["../../../../../../../../core/src/dev-server/loader/register-loader.ts"],"sourcesContent":["import { putFileAsync } from \"@warlock.js/fs\";\nimport { build, type Plugin } from \"esbuild\";\nimport { register } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { type MessagePort, MessageChannel } from \"node:worker_threads\";\nimport type { TranspileInit } from \"./load-hook.js\";\n\n/**\n * Rewrite the hook bundle's remaining npm imports to absolute paths resolved\n * from **core's own** location.\n *\n * The bundle is written into the consuming app's `.warlock/` directory, so a\n * bare `import \"esbuild\"` in it resolves starting from the *app*, walking up\n * the app's `node_modules`. `esbuild` and `get-tsconfig` are core's\n * dependencies, not the app's — under npm/yarn's flat hoisting they happen to\n * be reachable anyway, but under pnpm's strict layout they are not, and the\n * dev server dies with `ERR_MODULE_NOT_FOUND` for a package the app never\n * imported. That is a phantom dependency baked into generated code.\n *\n * Bundling them in instead is not an option for `esbuild`: its JS API is a\n * thin wrapper that spawns a platform-specific native binary, so inlining the\n * JavaScript would not remove the need for the package on disk. Resolving to\n * an absolute `file://` URL at generation time does — the generated file then\n * points straight at the copy core itself is using, whatever the installer's\n * layout.\n */\nexport const resolveExternalsFromCore: Plugin = {\n name: \"warlock-resolve-externals-from-core\",\n setup(build) {\n // The filter is deliberately loose — anything not starting `.` or `/` —\n // so the guards below carry the real logic.\n build.onResolve({ filter: /^[^./]/ }, (args) => {\n // The entry point comes through here too, and on Windows its absolute\n // path (\"D:\\\\…\") is not `.` or `/`. Marking it external fails the build.\n if (args.kind === \"entry-point\" || path.isAbsolute(args.path)) {\n return null;\n }\n\n if (args.path.startsWith(\"node:\")) {\n return { external: true };\n }\n\n try {\n return { path: import.meta.resolve(args.path), external: true };\n } catch {\n // Unresolvable from here — leave it bare and let Node try, which is\n // exactly the previous behaviour rather than a hard failure.\n return { external: true };\n }\n });\n },\n};\n\n/**\n * Bundle, write, and register the ESM loader hook.\n *\n * **Why bundle?**\n * `module.register()` runs the hook file in a fresh Node.js worker thread that\n * has no tsx hook of its own. The hook source is TypeScript, so we must produce\n * a plain ESM bundle before registering. esbuild inline-bundles all three hook\n * modules (`hook-thread`, `resolve-hook`, `load-hook`, `version-registry`)\n * into a single `.mjs` file written to `.warlock/`. External npm packages\n * (`esbuild`, `node:*`) are kept external — the hook thread can resolve those\n * from `node_modules` normally.\n *\n * **Why a file and not a `data:` URL?**\n * Some Node versions have issues resolving `import.meta.url` inside `data:`\n * modules. A real file under `.warlock/` is simpler and debuggable.\n *\n * **Timing**\n * Called from `FilesOrchestrator.init()` before any user `src/` module is\n * dynamically imported. `module.register()` takes effect for all subsequent\n * `import()` calls, which is exactly the window we need.\n *\n * @param transpile - Transpile-cache config to ship into the hook worker,\n * or `null` to keep the hook in tsx-passthrough mode.\n *\n * @returns The main-thread side of the MessageChannel. Callers post\n * `{ type: \"bump\", absolutePath }` messages on it to invalidate modules.\n *\n * @example\n * const port = await registerLoader(transpileInit);\n * // Later, when a file changes:\n * port.postMessage({ type: \"bump\", absolutePath: \"/abs/path/to/user.model.ts\" });\n */\nexport async function registerLoader(transpile: TranspileInit): Promise<MessagePort> {\n const { port1, port2 } = new MessageChannel();\n\n // hook-thread is a sibling module, so it shares THIS file's extension:\n // `.ts` when core runs from source (tsx), `.mjs` when published.\n const selfPath = fileURLToPath(import.meta.url);\n const hookThreadPath = path.join(path.dirname(selfPath), `hook-thread${path.extname(selfPath)}`);\n\n const bundleResult = await build({\n entryPoints: [hookThreadPath],\n bundle: true,\n format: \"esm\",\n write: false,\n platform: \"node\",\n target: \"node20\",\n // npm packages and Node built-ins stay external, but the plugin rewrites\n // each remaining npm specifier to an absolute path resolved from core's\n // own install — see `resolveExternalsFromCore` for why bare imports break\n // under pnpm's strict layout.\n packages: \"external\",\n plugins: [resolveExternalsFromCore],\n });\n\n const bundledCode = bundleResult.outputFiles[0].text;\n // Caller (filesOrchestrator.init) guarantees .warlock/ exists before this runs.\n const hookBundlePath = path.join(process.cwd(), \".warlock\", \"loader-hook.mjs\");\n await putFileAsync(hookBundlePath, bundledCode);\n\n const srcRoot = path.join(process.cwd(), \"src\");\n\n // No tsx registration: our hook owns resolution (own-resolver) and the\n // transpile of every `.ts`/`.tsx` (esbuild, in the load hook). The chain\n // is simply [our hook] → [Node default] for non-TS (npm/.js, node:).\n // In this monorepo tsx is still the *launcher* (`tsx start.ts`) so its\n // loader is present anyway, but it is never consulted for TypeScript —\n // our hook short-circuits first. A released `node bin/warlock.js` has no\n // tsx at all and relies entirely on this hook.\n\n register(pathToFileURL(hookBundlePath).href, import.meta.url, {\n data: { port: port2, srcRoot, transpile },\n transferList: [port2],\n });\n\n return port1;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,2BAAmC;CAC9C,MAAM;CACN,MAAM,OAAO;EAGX,MAAM,UAAU,EAAE,QAAQ,SAAS,IAAI,SAAS;GAG9C,IAAI,KAAK,SAAS,iBAAiB,KAAK,WAAW,KAAK,IAAI,GAC1D,OAAO;GAGT,IAAI,KAAK,KAAK,WAAW,OAAO,GAC9B,OAAO,EAAE,UAAU,KAAK;GAG1B,IAAI;IACF,OAAO;KAAE,MAAM,OAAO,KAAK,QAAQ,KAAK,IAAI;KAAG,UAAU;IAAK;GAChE,QAAQ;IAGN,OAAO,EAAE,UAAU,KAAK;GAC1B;EACF,CAAC;CACH;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,eAAsB,eAAe,WAAgD;CACnF,MAAM,EAAE,OAAO,UAAU,IAAI,eAAe;CAI5C,MAAM,WAAW,cAAc,OAAO,KAAK,GAAG;CAkB9C,MAAM,eAAc,MAfO,MAAM;EAC/B,aAAa,CAHQ,KAAK,KAAK,KAAK,QAAQ,QAAQ,GAAG,cAAc,KAAK,QAAQ,QAAQ,GAG/D,CAAC;EAC5B,QAAQ;EACR,QAAQ;EACR,OAAO;EACP,UAAU;EACV,QAAQ;EAKR,UAAU;EACV,SAAS,CAAC,wBAAwB;CACpC,CAAC,EAE+B,CAAC,YAAY,EAAE,CAAC;CAEhD,MAAM,iBAAiB,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY,iBAAiB;CAC7E,MAAM,aAAa,gBAAgB,WAAW;CAE9C,MAAM,UAAU,KAAK,KAAK,QAAQ,IAAI,GAAG,KAAK;CAU9C,SAAS,cAAc,cAAc,CAAC,CAAC,MAAM,OAAO,KAAK,KAAK;EAC5D,MAAM;GAAE,MAAM;GAAO;GAAS;EAAU;EACxC,cAAc,CAAC,KAAK;CACtB,CAAC;CAED,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-hook.mjs","names":[],"sources":["../../../../../../../../core/src/dev-server/loader/resolve-hook.ts"],"sourcesContent":["import { getTsconfig, resolvePathAlias } from \"get-tsconfig\";\nimport { existsSync } from \"node:fs\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { ownResolve, type PathsMatcher } from \"./own-resolver.js\";\nimport { captureResolution } from \"./resolve-capture.js\";\nimport { getVersion } from \"./version-registry.js\";\n\n/**\n * The `file://` URL prefix for the project's `src/` root, set once during\n * `initialize()`. Used to identify which resolved URLs belong to user code.\n */\nlet srcRootUrl = \"\";\n\n/**\n * Configure the source root used by the resolve hook.\n * Must be called from `initialize()` before any imports are resolved.\n *\n * @param srcRoot - Absolute filesystem path to the project `src/` directory.\n */\nexport function setSrcRoot(srcRoot: string): void {\n srcRootUrl = pathToFileURL(srcRoot).href + \"/\";\n}\n\n/** Lazily-built tsconfig `paths` matcher (worker has no startup hook). */\nlet pathsMatcher: PathsMatcher | null = null;\nlet pathsMatcherBuilt = false;\n\nfunction getPathsMatcher(): PathsMatcher | null {\n if (pathsMatcherBuilt) return pathsMatcher;\n pathsMatcherBuilt = true;\n const tsconfig = getTsconfig(process.cwd());\n // get-tsconfig v5 renamed the curried `createPathsMatcher(t)(spec)` to a direct\n // `resolvePathAlias(t, spec)`. Wrap it to keep the PathsMatcher closure shape.\n pathsMatcher = tsconfig ? (specifier: string) => resolvePathAlias(tsconfig, specifier) : null;\n return pathsMatcher;\n}\n\ntype ResolveContext = {\n parentURL?: string;\n conditions?: string[];\n importAttributes?: Record<string, string>;\n};\n\ntype ResolveResult = {\n url: string;\n format?: string;\n shortCircuit?: boolean;\n};\n\ntype NextResolve = (specifier: string, context?: ResolveContext) => Promise<ResolveResult>;\n\n/**\n * ESM loader `resolve()` hook.\n *\n * The framework's own resolver (`ownResolve`) handles tsconfig `paths`,\n * aliases, and TS extension/`index` probing. Specifiers it doesn't own\n * (bare npm, `node:`, `file:`) fall through to `nextResolve` (Node\n * default). After resolution, stamps a `?v=<N>` version token onto any\n * `.ts` / `.tsx` file that lives under the configured `src/` root.\n *\n * The version token is the cache-bust mechanism: when a file changes the main\n * thread bumps its counter, the next `import()` produces a new URL, Node sees\n * a cache miss, and loads fresh content — native ESM cycles are preserved\n * because static imports remain static throughout.\n *\n * **parentURL cleaning**: the resolved URL carries `?v=N` as a suffix. If a\n * module loaded this way statically imports a relative sibling, Node uses that\n * `?v=N` URL as the `parentURL` for the next `resolve()` call. Passing it\n * unmodified to `nextResolve()` causes tsx to produce paths like\n * `some-sibling.ts?v=0` (the query leaks into URL joining). We strip `?v=N`\n * from `parentURL` before forwarding to prevent this.\n *\n * @example\n * // First import of user.model.ts → ?v=0\n * // File watcher bumps → next import → ?v=1 → fresh module\n */\nexport async function resolve(\n specifier: string,\n context: ResolveContext,\n nextResolve: NextResolve,\n): Promise<ResolveResult> {\n const cleanContext
|
|
1
|
+
{"version":3,"file":"resolve-hook.mjs","names":[],"sources":["../../../../../../../../core/src/dev-server/loader/resolve-hook.ts"],"sourcesContent":["import { getTsconfig, resolvePathAlias } from \"get-tsconfig\";\nimport { existsSync } from \"node:fs\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { ownResolve, type PathsMatcher } from \"./own-resolver.js\";\nimport { captureResolution } from \"./resolve-capture.js\";\nimport { getVersion } from \"./version-registry.js\";\n\n/**\n * The `file://` URL prefix for the project's `src/` root, set once during\n * `initialize()`. Used to identify which resolved URLs belong to user code.\n */\nlet srcRootUrl = \"\";\n\n/**\n * Configure the source root used by the resolve hook.\n * Must be called from `initialize()` before any imports are resolved.\n *\n * @param srcRoot - Absolute filesystem path to the project `src/` directory.\n */\nexport function setSrcRoot(srcRoot: string): void {\n srcRootUrl = pathToFileURL(srcRoot).href + \"/\";\n}\n\n/** Lazily-built tsconfig `paths` matcher (worker has no startup hook). */\nlet pathsMatcher: PathsMatcher | null = null;\nlet pathsMatcherBuilt = false;\n\nfunction getPathsMatcher(): PathsMatcher | null {\n if (pathsMatcherBuilt) return pathsMatcher;\n pathsMatcherBuilt = true;\n const tsconfig = getTsconfig(process.cwd());\n // get-tsconfig v5 renamed the curried `createPathsMatcher(t)(spec)` to a direct\n // `resolvePathAlias(t, spec)`. Wrap it to keep the PathsMatcher closure shape.\n pathsMatcher = tsconfig ? (specifier: string) => resolvePathAlias(tsconfig, specifier) : null;\n return pathsMatcher;\n}\n\ntype ResolveContext = {\n parentURL?: string;\n conditions?: string[];\n importAttributes?: Record<string, string>;\n};\n\ntype ResolveResult = {\n url: string;\n format?: string;\n shortCircuit?: boolean;\n};\n\ntype NextResolve = (specifier: string, context?: ResolveContext) => Promise<ResolveResult>;\n\n/**\n * ESM loader `resolve()` hook.\n *\n * The framework's own resolver (`ownResolve`) handles tsconfig `paths`,\n * aliases, and TS extension/`index` probing. Specifiers it doesn't own\n * (bare npm, `node:`, `file:`) fall through to `nextResolve` (Node\n * default). After resolution, stamps a `?v=<N>` version token onto any\n * `.ts` / `.tsx` file that lives under the configured `src/` root.\n *\n * The version token is the cache-bust mechanism: when a file changes the main\n * thread bumps its counter, the next `import()` produces a new URL, Node sees\n * a cache miss, and loads fresh content — native ESM cycles are preserved\n * because static imports remain static throughout.\n *\n * **parentURL cleaning**: the resolved URL carries `?v=N` as a suffix. If a\n * module loaded this way statically imports a relative sibling, Node uses that\n * `?v=N` URL as the `parentURL` for the next `resolve()` call. Passing it\n * unmodified to `nextResolve()` causes tsx to produce paths like\n * `some-sibling.ts?v=0` (the query leaks into URL joining). We strip `?v=N`\n * from `parentURL` before forwarding to prevent this.\n *\n * @example\n * // First import of user.model.ts → ?v=0\n * // File watcher bumps → next import → ?v=1 → fresh module\n */\nexport async function resolve(\n specifier: string,\n context: ResolveContext,\n nextResolve: NextResolve,\n): Promise<ResolveResult> {\n const cleanContext = context.parentURL?.includes(\"?v=\")\n ? { ...context, parentURL: context.parentURL.replace(/\\?v=\\d+$/, \"\") }\n : context;\n\n // The framework's own resolver owns tsconfig `paths` + TS\n // extension/index probing. Anything it returns `null` for (bare npm,\n // `node:`, `file:`) falls through to Node default via `nextResolve`.\n const owned = ownResolve(specifier, cleanContext.parentURL, getPathsMatcher(), existsSync);\n // When we produced the URL ourselves we did NOT call nextResolve, so the\n // result must short-circuit the loader chain or Node throws\n // ERR_LOADER_CHAIN_INCOMPLETE (e.g. for @warlock.js/* .ts that resolve\n // outside the project src root and hit the early return below).\n const result: ResolveResult = owned\n ? { url: owned, shortCircuit: true }\n : await nextResolve(specifier, cleanContext);\n\n const { url } = result;\n\n // Ground-truth recorder (no-op unless WARLOCK_RESOLVE_CAPTURE is set):\n // logs the answer for this (specifier, parentURL) so the Phase B\n // resolver can be proven to reproduce tsx exactly.\n captureResolution({\n specifier,\n parentURL: cleanContext.parentURL,\n url,\n format: result.format,\n });\n\n const cleanUrl = url.replace(/\\?v=\\d+$/, \"\");\n\n if (\n !cleanUrl.startsWith(srcRootUrl) ||\n (!cleanUrl.endsWith(\".ts\") && !cleanUrl.endsWith(\".tsx\"))\n ) {\n return result;\n }\n\n const absolutePath = fileURLToPath(cleanUrl);\n const version = getVersion(absolutePath);\n\n // Spread the full result from nextResolve (preserves `format` and any other\n // fields tsx sets) and only override `url`. Dropping `format` would cause\n // tsx's load hook to see an undefined format and fall through to Node's\n // default loader, which rejects unknown extensions like .tsx.\n return { ...result, url: `${cleanUrl}?v=${version}`, shortCircuit: true };\n}\n"],"mappings":";;;;;;;;;;;;AAWA,IAAI,aAAa;;;;;;;AAQjB,SAAgB,WAAW,SAAuB;CAChD,aAAa,cAAc,OAAO,CAAC,CAAC,OAAO;AAC7C;;AAGA,IAAI,eAAoC;AACxC,IAAI,oBAAoB;AAExB,SAAS,kBAAuC;CAC9C,IAAI,mBAAmB,OAAO;CAC9B,oBAAoB;CACpB,MAAM,WAAW,YAAY,QAAQ,IAAI,CAAC;CAG1C,eAAe,YAAY,cAAsB,iBAAiB,UAAU,SAAS,IAAI;CACzF,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,eAAsB,QACpB,WACA,SACA,aACwB;CACxB,MAAM,eAAe,QAAQ,WAAW,SAAS,KAAK,IAClD;EAAE,GAAG;EAAS,WAAW,QAAQ,UAAU,QAAQ,YAAY,EAAE;CAAE,IACnE;CAKJ,MAAM,QAAQ,WAAW,WAAW,aAAa,WAAW,gBAAgB,GAAG,UAAU;CAKzF,MAAM,SAAwB,QAC1B;EAAE,KAAK;EAAO,cAAc;CAAK,IACjC,MAAM,YAAY,WAAW,YAAY;CAE7C,MAAM,EAAE,QAAQ;CAKhB,kBAAkB;EAChB;EACA,WAAW,aAAa;EACxB;EACA,QAAQ,OAAO;CACjB,CAAC;CAED,MAAM,WAAW,IAAI,QAAQ,YAAY,EAAE;CAE3C,IACE,CAAC,SAAS,WAAW,UAAU,KAC9B,CAAC,SAAS,SAAS,KAAK,KAAK,CAAC,SAAS,SAAS,MAAM,GAEvD,OAAO;CAIT,MAAM,UAAU,WADK,cAAc,QACG,CAAC;CAMvC,OAAO;EAAE,GAAG;EAAQ,KAAK,GAAG,SAAS,KAAK;EAAW,cAAc;CAAK;AAC1E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transpile-cache.mjs","names":[],"sources":["../../../../../../../../core/src/dev-server/loader/transpile-cache.ts"],"sourcesContent":["/**\n * Persisted transpile cache.\n *\n * Keyed by a hash of *source content* plus a transform-options fingerprint.\n * Same source + same options → same key → same output, always. Changed\n * content → different key → fresh transpile. There is no path-keyed entry\n * anywhere, so the old \"stack trace points at a mangled cache filename\" bug\n * class is structurally impossible — the on-disk filename is opaque and the\n * source identity lives only inside the source map.\n *\n * This module is pure and hook-agnostic on purpose: it knows how to read,\n * write and evict cache entries given a key, nothing about the ESM loader,\n * `?v=N` versioning, or esbuild. Phase 2 wires it into the load hook.\n *\n * @example\n * const fp = computeFingerprint({\n * esbuildVersion: esbuild.version,\n * cacheEpoch: CACHE_EPOCH,\n * compilerOptions,\n * });\n * const key = cacheKey(sourceText, fp);\n * const hit = cache.get(key);\n * if (!hit) cache.put(key, { code, map });\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport {\n mkdirSync,\n readdirSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * A cached transpile result: the emitted JS and its source map (raw JSON\n * string, exactly as esbuild produced it). Either may be empty for code\n * that produced no map.\n */\nexport type TranspileEntry = {\n code: string;\n map: string;\n};\n\n/**\n * Inputs that change transpiled output for identical source text. Bumping\n * any of these must invalidate every entry — that is achieved structurally\n * by folding them into the key, not by sweeping the cache.\n */\nexport type FingerprintParts = {\n /** `esbuild.version` — a transform-engine upgrade can change output. */\n esbuildVersion: string;\n /**\n * Monotonic cache-format epoch owned by the framework. Bump it whenever\n * the cache contract (entry shape, map handling, key derivation) changes\n * so old entries are guaranteed to miss after a framework upgrade.\n */\n cacheEpoch: number;\n /**\n * The resolved tsconfig `compilerOptions` blob, hashed wholesale rather\n * than cherry-picking fields — safer against tsconfig drift (a new option\n * that affects output can't silently serve stale code).\n */\n compilerOptions: unknown;\n};\n\n/**\n * Current cache-format epoch. Bump on any change to {@link TranspileEntry}\n * shape, the storage layout, or how the loader consumes entries.\n */\nexport const CACHE_EPOCH = 1;\n\n/**\n * Stable JSON stringify — object keys sorted recursively so semantically\n * equal option blobs always produce the same fingerprint regardless of key\n * insertion order.\n */\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(\",\")}]`;\n }\n\n const entries = Object.keys(value as Record<string, unknown>)\n .sort()\n .map(key => {\n const child = stableStringify((value as Record<string, unknown>)[key]);\n return `${JSON.stringify(key)}:${child}`;\n });\n\n return `{${entries.join(\",\")}}`;\n}\n\n/**\n * Fold every output-affecting input into one short hex fingerprint that\n * becomes part of every cache key.\n */\nexport function computeFingerprint(parts: FingerprintParts): string {\n const canonical = stableStringify({\n esbuildVersion: parts.esbuildVersion,\n cacheEpoch: parts.cacheEpoch,\n compilerOptions: parts.compilerOptions,\n });\n\n return createHash(\"sha256\").update(canonical).digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Derive the cache key for a source file. The NUL separator makes\n * `sourceText` and `fingerprint` unambiguous (no concatenation collision).\n */\nexport function cacheKey(sourceText: string, fingerprint: string): string {\n return createHash(\"sha256\")\n .update(sourceText)\n .update(\"\\0\")\n .update(fingerprint)\n .digest(\"hex\");\n}\n\n/** Options for {@link TranspileCache.gc}. */\nexport type GcOptions = {\n /** Evict least-recently-modified entries once total size exceeds this. */\n maxBytes?: number;\n /** Evict entries whose mtime is older than this many milliseconds. */\n maxAgeMs?: number;\n};\n\ntype CacheFileInfo = {\n codePath: string;\n mapPath: string;\n size: number;\n mtimeMs: number;\n};\n\n/**\n * Content-hash-addressed transpile cache stored on disk.\n *\n * Layout: `<cacheDir>/<first2-of-key>/<key>.js` plus a sibling `.js.map`.\n * Sharding by the first two hex chars keeps any single directory small on\n * large projects. GC metadata is derived from filesystem mtime so there is\n * no second source of truth (no sidecar index to keep consistent).\n */\nexport class TranspileCache {\n public constructor(private readonly cacheDir: string) {}\n\n private shardDir(key: string): string {\n return path.join(this.cacheDir, key.slice(0, 2));\n }\n\n /**\n * `label` is an optional human-readable prefix (e.g. a path slug). It is\n * cosmetic only — the content `key` is still what makes the name unique,\n * so the same `(key, label)` pair must be passed to `get` and `put`.\n */\n private fileName(key: string, label: string | undefined, ext: string): string {\n return label ? `${label}.${key}${ext}` : `${key}${ext}`;\n }\n\n private codePath(key: string, label?: string): string {\n return path.join(this.shardDir(key), this.fileName(key, label, \".js\"));\n }\n\n private mapPath(key: string, label?: string): string {\n return path.join(this.shardDir(key), this.fileName(key, label, \".js.map\"));\n }\n\n /**\n * Return the cached entry for `key`, or `null` on a miss. Synchronous on\n * purpose: this sits on the module-load hot path and must not pay an\n * event-loop hop per import.\n */\n public get(key: string, label?: string): TranspileEntry | null {\n try {\n const code = readFileSync(this.codePath(key, label), \"utf8\");\n let map = \"\";\n try {\n map = readFileSync(this.mapPath(key, label), \"utf8\");\n } catch {\n // A code entry with no map is valid (e.g. map-less transform).\n }\n return { code, map };\n } catch {\n return null;\n }\n }\n\n /**\n * Store an entry for `key`. Written atomically (temp file + rename) so a\n * concurrent {@link get} never observes a half-written module.\n */\n public put(key: string, entry: TranspileEntry, label?: string): void {\n const shard = this.shardDir(key);\n mkdirSync(shard, { recursive: true });\n\n this.atomicWrite(this.codePath(key, label), entry.code);\n if (entry.map) {\n this.atomicWrite(this.mapPath(key, label), entry.map);\n }\n }\n\n private atomicWrite(filePath: string, content: string): void {\n const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n try {\n writeFileSync(tempPath, content);\n renameSync(tempPath, filePath);\n } catch (error) {\n // Windows can report EPERM/EACCES when another loader process wins the\n // rename to this content-addressed destination. The winner's file is a\n // valid commit only when it contains the exact bytes we meant to store;\n // otherwise preserve the original error instead of accepting corruption.\n const code = (error as NodeJS.ErrnoException).code;\n const mayBeConcurrentCommit =\n code === \"EEXIST\" || code === \"EPERM\" || code === \"EACCES\";\n\n if (!mayBeConcurrentCommit || !this.hasCommittedContent(filePath, content)) {\n throw error;\n }\n } finally {\n try {\n rmSync(tempPath, { force: true });\n } catch {\n // Best effort: a cleanup failure must not mask the rename result.\n }\n }\n }\n\n private hasCommittedContent(filePath: string, content: string): boolean {\n try {\n return readFileSync(filePath, \"utf8\") === content;\n } catch {\n return false;\n }\n }\n\n /**\n * Opportunistic eviction. Cheap, runs at boot (not on the hot path).\n * Removes age-expired entries first, then trims to the size budget by\n * evicting least-recently-modified entries. Both bounds are optional;\n * with neither set this is a no-op.\n */\n public gc(options: GcOptions = {}): void {\n const { maxBytes, maxAgeMs } = options;\n if (maxBytes === undefined && maxAgeMs === undefined) return;\n\n const files = this.scan();\n const now = Date.now();\n\n let live = files;\n\n if (maxAgeMs !== undefined) {\n live = [];\n for (const file of files) {\n if (now - file.mtimeMs > maxAgeMs) {\n this.evict(file);\n } else {\n live.push(file);\n }\n }\n }\n\n if (maxBytes !== undefined) {\n let total = live.reduce((sum, file) => sum + file.size, 0);\n if (total > maxBytes) {\n // Oldest first — least-recently-modified is the eviction order.\n live.sort((a, b) => a.mtimeMs - b.mtimeMs);\n for (const file of live) {\n if (total <= maxBytes) break;\n total -= file.size;\n this.evict(file);\n }\n }\n }\n }\n\n private scan(): CacheFileInfo[] {\n const result: CacheFileInfo[] = [];\n\n let shards: string[];\n try {\n shards = readdirSync(this.cacheDir);\n } catch {\n return result;\n }\n\n for (const shard of shards) {\n const shardPath = path.join(this.cacheDir, shard);\n let entries: string[];\n try {\n entries = readdirSync(shardPath);\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(\".js\")) continue;\n const codePath = path.join(shardPath, entry);\n try {\n const stat = statSync(codePath);\n const mapPath = `${codePath}.map`;\n let mapSize = 0;\n try {\n mapSize = statSync(mapPath).size;\n } catch {\n // no map sidecar — fine\n }\n result.push({\n codePath,\n mapPath,\n size: stat.size + mapSize,\n mtimeMs: stat.mtimeMs,\n });\n } catch {\n // entry vanished mid-scan — skip\n }\n }\n }\n\n return result;\n }\n\n private evict(file: CacheFileInfo): void {\n try {\n rmSync(file.codePath, { force: true });\n } catch {\n // best effort\n }\n try {\n rmSync(file.mapPath, { force: true });\n } catch {\n // best effort\n }\n }\n\n /** Remove the entire cache directory (used by `--fresh`). */\n public clear(): void {\n try {\n rmSync(this.cacheDir, { recursive: true, force: true });\n } catch {\n // best effort\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transpile-cache.mjs","names":[],"sources":["../../../../../../../../core/src/dev-server/loader/transpile-cache.ts"],"sourcesContent":["/**\n * Persisted transpile cache.\n *\n * Keyed by a hash of *source content* plus a transform-options fingerprint.\n * Same source + same options → same key → same output, always. Changed\n * content → different key → fresh transpile. There is no path-keyed entry\n * anywhere, so the old \"stack trace points at a mangled cache filename\" bug\n * class is structurally impossible — the on-disk filename is opaque and the\n * source identity lives only inside the source map.\n *\n * This module is pure and hook-agnostic on purpose: it knows how to read,\n * write and evict cache entries given a key, nothing about the ESM loader,\n * `?v=N` versioning, or esbuild. Phase 2 wires it into the load hook.\n *\n * @example\n * const fp = computeFingerprint({\n * esbuildVersion: esbuild.version,\n * cacheEpoch: CACHE_EPOCH,\n * compilerOptions,\n * });\n * const key = cacheKey(sourceText, fp);\n * const hit = cache.get(key);\n * if (!hit) cache.put(key, { code, map });\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport {\n mkdirSync,\n readdirSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * A cached transpile result: the emitted JS and its source map (raw JSON\n * string, exactly as esbuild produced it). Either may be empty for code\n * that produced no map.\n */\nexport type TranspileEntry = {\n code: string;\n map: string;\n};\n\n/**\n * Inputs that change transpiled output for identical source text. Bumping\n * any of these must invalidate every entry — that is achieved structurally\n * by folding them into the key, not by sweeping the cache.\n */\nexport type FingerprintParts = {\n /** `esbuild.version` — a transform-engine upgrade can change output. */\n esbuildVersion: string;\n /**\n * Monotonic cache-format epoch owned by the framework. Bump it whenever\n * the cache contract (entry shape, map handling, key derivation) changes\n * so old entries are guaranteed to miss after a framework upgrade.\n */\n cacheEpoch: number;\n /**\n * The resolved tsconfig `compilerOptions` blob, hashed wholesale rather\n * than cherry-picking fields — safer against tsconfig drift (a new option\n * that affects output can't silently serve stale code).\n */\n compilerOptions: unknown;\n};\n\n/**\n * Current cache-format epoch. Bump on any change to {@link TranspileEntry}\n * shape, the storage layout, or how the loader consumes entries.\n */\nexport const CACHE_EPOCH = 1;\n\n/**\n * Stable JSON stringify — object keys sorted recursively so semantically\n * equal option blobs always produce the same fingerprint regardless of key\n * insertion order.\n */\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(\",\")}]`;\n }\n\n const entries = Object.keys(value as Record<string, unknown>)\n .sort()\n .map((key) => {\n const child = stableStringify((value as Record<string, unknown>)[key]);\n return `${JSON.stringify(key)}:${child}`;\n });\n\n return `{${entries.join(\",\")}}`;\n}\n\n/**\n * Fold every output-affecting input into one short hex fingerprint that\n * becomes part of every cache key.\n */\nexport function computeFingerprint(parts: FingerprintParts): string {\n const canonical = stableStringify({\n esbuildVersion: parts.esbuildVersion,\n cacheEpoch: parts.cacheEpoch,\n compilerOptions: parts.compilerOptions,\n });\n\n return createHash(\"sha256\").update(canonical).digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Derive the cache key for a source file. The NUL separator makes\n * `sourceText` and `fingerprint` unambiguous (no concatenation collision).\n */\nexport function cacheKey(sourceText: string, fingerprint: string): string {\n return createHash(\"sha256\").update(sourceText).update(\"\\0\").update(fingerprint).digest(\"hex\");\n}\n\n/** Options for {@link TranspileCache.gc}. */\nexport type GcOptions = {\n /** Evict least-recently-modified entries once total size exceeds this. */\n maxBytes?: number;\n /** Evict entries whose mtime is older than this many milliseconds. */\n maxAgeMs?: number;\n};\n\ntype CacheFileInfo = {\n codePath: string;\n mapPath: string;\n size: number;\n mtimeMs: number;\n};\n\n/**\n * Content-hash-addressed transpile cache stored on disk.\n *\n * Layout: `<cacheDir>/<first2-of-key>/<key>.js` plus a sibling `.js.map`.\n * Sharding by the first two hex chars keeps any single directory small on\n * large projects. GC metadata is derived from filesystem mtime so there is\n * no second source of truth (no sidecar index to keep consistent).\n */\nexport class TranspileCache {\n public constructor(private readonly cacheDir: string) {}\n\n private shardDir(key: string): string {\n return path.join(this.cacheDir, key.slice(0, 2));\n }\n\n /**\n * `label` is an optional human-readable prefix (e.g. a path slug). It is\n * cosmetic only — the content `key` is still what makes the name unique,\n * so the same `(key, label)` pair must be passed to `get` and `put`.\n */\n private fileName(key: string, label: string | undefined, ext: string): string {\n return label ? `${label}.${key}${ext}` : `${key}${ext}`;\n }\n\n private codePath(key: string, label?: string): string {\n return path.join(this.shardDir(key), this.fileName(key, label, \".js\"));\n }\n\n private mapPath(key: string, label?: string): string {\n return path.join(this.shardDir(key), this.fileName(key, label, \".js.map\"));\n }\n\n /**\n * Return the cached entry for `key`, or `null` on a miss. Synchronous on\n * purpose: this sits on the module-load hot path and must not pay an\n * event-loop hop per import.\n */\n public get(key: string, label?: string): TranspileEntry | null {\n try {\n const code = readFileSync(this.codePath(key, label), \"utf8\");\n let map = \"\";\n try {\n map = readFileSync(this.mapPath(key, label), \"utf8\");\n } catch {\n // A code entry with no map is valid (e.g. map-less transform).\n }\n return { code, map };\n } catch {\n return null;\n }\n }\n\n /**\n * Store an entry for `key`. Written atomically (temp file + rename) so a\n * concurrent {@link get} never observes a half-written module.\n */\n public put(key: string, entry: TranspileEntry, label?: string): void {\n const shard = this.shardDir(key);\n mkdirSync(shard, { recursive: true });\n\n this.atomicWrite(this.codePath(key, label), entry.code);\n if (entry.map) {\n this.atomicWrite(this.mapPath(key, label), entry.map);\n }\n }\n\n private atomicWrite(filePath: string, content: string): void {\n const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n try {\n writeFileSync(tempPath, content);\n renameSync(tempPath, filePath);\n } catch (error) {\n // Windows can report EPERM/EACCES when another loader process wins the\n // rename to this content-addressed destination. The winner's file is a\n // valid commit only when it contains the exact bytes we meant to store;\n // otherwise preserve the original error instead of accepting corruption.\n const code = (error as NodeJS.ErrnoException).code;\n const mayBeConcurrentCommit = code === \"EEXIST\" || code === \"EPERM\" || code === \"EACCES\";\n\n if (!mayBeConcurrentCommit || !this.hasCommittedContent(filePath, content)) {\n throw error;\n }\n } finally {\n try {\n rmSync(tempPath, { force: true });\n } catch {\n // Best effort: a cleanup failure must not mask the rename result.\n }\n }\n }\n\n private hasCommittedContent(filePath: string, content: string): boolean {\n try {\n return readFileSync(filePath, \"utf8\") === content;\n } catch {\n return false;\n }\n }\n\n /**\n * Opportunistic eviction. Cheap, runs at boot (not on the hot path).\n * Removes age-expired entries first, then trims to the size budget by\n * evicting least-recently-modified entries. Both bounds are optional;\n * with neither set this is a no-op.\n */\n public gc(options: GcOptions = {}): void {\n const { maxBytes, maxAgeMs } = options;\n if (maxBytes === undefined && maxAgeMs === undefined) return;\n\n const files = this.scan();\n const now = Date.now();\n\n let live = files;\n\n if (maxAgeMs !== undefined) {\n live = [];\n for (const file of files) {\n if (now - file.mtimeMs > maxAgeMs) {\n this.evict(file);\n } else {\n live.push(file);\n }\n }\n }\n\n if (maxBytes !== undefined) {\n let total = live.reduce((sum, file) => sum + file.size, 0);\n if (total > maxBytes) {\n // Oldest first — least-recently-modified is the eviction order.\n live.sort((a, b) => a.mtimeMs - b.mtimeMs);\n for (const file of live) {\n if (total <= maxBytes) break;\n total -= file.size;\n this.evict(file);\n }\n }\n }\n }\n\n private scan(): CacheFileInfo[] {\n const result: CacheFileInfo[] = [];\n\n let shards: string[];\n try {\n shards = readdirSync(this.cacheDir);\n } catch {\n return result;\n }\n\n for (const shard of shards) {\n const shardPath = path.join(this.cacheDir, shard);\n let entries: string[];\n try {\n entries = readdirSync(shardPath);\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(\".js\")) continue;\n const codePath = path.join(shardPath, entry);\n try {\n const stat = statSync(codePath);\n const mapPath = `${codePath}.map`;\n let mapSize = 0;\n try {\n mapSize = statSync(mapPath).size;\n } catch {\n // no map sidecar — fine\n }\n result.push({\n codePath,\n mapPath,\n size: stat.size + mapSize,\n mtimeMs: stat.mtimeMs,\n });\n } catch {\n // entry vanished mid-scan — skip\n }\n }\n }\n\n return result;\n }\n\n private evict(file: CacheFileInfo): void {\n try {\n rmSync(file.codePath, { force: true });\n } catch {\n // best effort\n }\n try {\n rmSync(file.mapPath, { force: true });\n } catch {\n // best effort\n }\n }\n\n /** Remove the entire cache directory (used by `--fresh`). */\n public clear(): void {\n try {\n rmSync(this.cacheDir, { recursive: true, force: true });\n } catch {\n // best effort\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-loader.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/module-loader.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { router } from \"../router/router\";\nimport { devLogError, formatModuleNotFoundError } from \"./dev-logger\";\nimport type { CleanupFunction, FileManager } from \"./file-manager\";\nimport type { SpecialFileType, SpecialFilesCollector } from \"./special-files-collector\";\n\ndeclare global {\n var __currentModuleFile: FileManager | undefined;\n}\n\n/**\n * Thrown when a special file (route/event/main/locale) fails to import or\n * register. Carries the file that failed and the original cause so a broken\n * module surfaces with its origin instead of vanishing into a 404.\n */\nexport class ModuleLoadError extends Error {\n public constructor(\n public readonly type: string,\n public readonly file: FileManager,\n public readonly cause: unknown,\n ) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n super(`Failed to load ${type}: ${file.relativePath} - ${reason}`);\n this.name = \"ModuleLoadError\";\n }\n}\n\n/**\n * The four \"special\" file kinds that the dev server eagerly imports at boot.\n * Order matters: locales first (used by everything), events before main so\n * listeners are registered when main runs, routes last so handlers are bound.\n */\nconst SPECIAL_TYPES: readonly SpecialFileType[] = [\"locale\", \"event\", \"main\", \"route\"] as const;\n\n/**\n * Loads application modules through the ESM loader hook. The hook stamps\n * `?v=N` on every import, so each reload is a fresh module without any\n * userland cache-busting plumbing.\n */\nexport class ModuleLoader {\n private readonly loadedModules = new Map<string, unknown>();\n\n constructor(private readonly specialFilesCollector: SpecialFilesCollector) {}\n\n /**\n * Eagerly load every special file at boot, in the canonical order so\n * later phases (e.g. routes) see state earlier phases registered.\n *\n * A failing module no longer vanishes silently: each failure is collected\n * (so one broken file doesn't hide the others) and, once every file has been\n * attempted, an aggregate error is thrown. The boot callers\n * (`DevelopmentServer.start`, the HTTP test bootstrap) already wrap this in a\n * try/catch that logs and aborts the boot — so a broken route module aborts\n * boot loudly rather than booting into a surface that 404s.\n */\n public async loadAll(): Promise<void> {\n const failures: ModuleLoadError[] = [];\n\n for (const type of SPECIAL_TYPES) {\n for (const file of this.specialFilesCollector.getFilesByType(type)) {\n try {\n await this.loadModule(file, type);\n } catch (error) {\n failures.push(\n error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error),\n );\n }\n }\n }\n\n if (failures.length > 0) {\n throw new AggregateError(\n failures,\n `${failures.length} module(s) failed to load during boot:\\n` +\n failures.map((failure) => ` - ${failure.message}`).join(\"\\n\"),\n );\n }\n }\n\n /**\n * Import a file through the loader hook. For routes, scopes the import in\n * `router.withSourceFile()` so the added routes carry their origin.\n */\n public async loadModule<T = unknown>(
|
|
1
|
+
{"version":3,"file":"module-loader.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/module-loader.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { router } from \"../router/router\";\nimport { devLogError, formatModuleNotFoundError } from \"./dev-logger\";\nimport type { CleanupFunction, FileManager } from \"./file-manager\";\nimport type { SpecialFileType, SpecialFilesCollector } from \"./special-files-collector\";\n\ndeclare global {\n var __currentModuleFile: FileManager | undefined;\n}\n\n/**\n * Thrown when a special file (route/event/main/locale) fails to import or\n * register. Carries the file that failed and the original cause so a broken\n * module surfaces with its origin instead of vanishing into a 404.\n */\nexport class ModuleLoadError extends Error {\n public constructor(\n public readonly type: string,\n public readonly file: FileManager,\n public readonly cause: unknown,\n ) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n super(`Failed to load ${type}: ${file.relativePath} - ${reason}`);\n this.name = \"ModuleLoadError\";\n }\n}\n\n/**\n * The four \"special\" file kinds that the dev server eagerly imports at boot.\n * Order matters: locales first (used by everything), events before main so\n * listeners are registered when main runs, routes last so handlers are bound.\n */\nconst SPECIAL_TYPES: readonly SpecialFileType[] = [\"locale\", \"event\", \"main\", \"route\"] as const;\n\n/**\n * Loads application modules through the ESM loader hook. The hook stamps\n * `?v=N` on every import, so each reload is a fresh module without any\n * userland cache-busting plumbing.\n */\nexport class ModuleLoader {\n private readonly loadedModules = new Map<string, unknown>();\n\n constructor(private readonly specialFilesCollector: SpecialFilesCollector) {}\n\n /**\n * Eagerly load every special file at boot, in the canonical order so\n * later phases (e.g. routes) see state earlier phases registered.\n *\n * A failing module no longer vanishes silently: each failure is collected\n * (so one broken file doesn't hide the others) and, once every file has been\n * attempted, an aggregate error is thrown. The boot callers\n * (`DevelopmentServer.start`, the HTTP test bootstrap) already wrap this in a\n * try/catch that logs and aborts the boot — so a broken route module aborts\n * boot loudly rather than booting into a surface that 404s.\n */\n public async loadAll(): Promise<void> {\n const failures: ModuleLoadError[] = [];\n\n for (const type of SPECIAL_TYPES) {\n for (const file of this.specialFilesCollector.getFilesByType(type)) {\n try {\n await this.loadModule(file, type);\n } catch (error) {\n failures.push(\n error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error),\n );\n }\n }\n }\n\n if (failures.length > 0) {\n throw new AggregateError(\n failures,\n `${failures.length} module(s) failed to load during boot:\\n` +\n failures.map((failure) => ` - ${failure.message}`).join(\"\\n\"),\n );\n }\n }\n\n /**\n * Import a file through the loader hook. For routes, scopes the import in\n * `router.withSourceFile()` so the added routes carry their origin.\n */\n public async loadModule<T = unknown>(file: FileManager, type: string): Promise<T | undefined> {\n if (file.relativePath.endsWith(\".env\")) return undefined;\n\n globalThis.__currentModuleFile = file;\n\n try {\n const fileUrl = pathToFileURL(file.absolutePath).href;\n\n const load = async () => {\n const module = (await import(fileUrl)) as Record<string, unknown>;\n this.loadedModules.set(file.absolutePath, module);\n this.registerCleanup(file, module);\n return module as T;\n };\n\n return type === \"route\" ? await router.withSourceFile(file.relativePath, load) : await load();\n } catch (error: any) {\n if (error.code === \"ERR_MODULE_NOT_FOUND\") {\n devLogError(formatModuleNotFoundError(error));\n } else {\n // Pass the error through so devLogError prints the stack. With dev\n // source maps enabled, frames remap to the original `.ts` line/col\n // (e.g. the actual call site of an undefined symbol) instead of\n // vanishing — a one-line \"x is not defined\" is undebuggable.\n devLogError(\n `Failed to load ${type}: ${file.relativePath} - ${error?.message || error}`,\n error,\n );\n }\n\n // RETHROW after logging. Previously this returned undefined, so a route\n // file that threw on import or registration disappeared with no boot\n // error — the whole surface 404'd silently. Boot callers abort on the\n // throw; the HMR batch-reload handler catches it, keeps the server\n // alive, and shows it loudly.\n throw error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error);\n } finally {\n globalThis.__currentModuleFile = undefined;\n }\n }\n\n /**\n * Reload a previously-loaded special file. The version-bump that makes the\n * import fresh is done by the caller (layer-executor) before invoking this.\n * Routes are removed from the registry first so re-registration is clean.\n */\n public async reloadModule(file: FileManager): Promise<void> {\n const moduleType = this.specialFilesCollector.getFileType(file.relativePath);\n if (!moduleType) return;\n\n this.runCleanup(file);\n\n if (moduleType === \"route\") {\n router.removeRoutesBySourceFile(file.relativePath);\n }\n\n this.loadedModules.delete(file.absolutePath);\n await this.loadModule(file, moduleType);\n }\n\n /**\n * Run every cleanup hook the file registered (or `$cleanup` on its exports)\n * and reset the list. Called once per HMR reload.\n */\n public runCleanup(file: FileManager): void {\n for (const hook of file.cleanup) {\n try {\n if (typeof hook === \"function\") {\n hook();\n } else if (\n hook &&\n typeof (hook as { unsubscribe?: () => void }).unsubscribe === \"function\"\n ) {\n (hook as { unsubscribe: () => void }).unsubscribe();\n }\n } catch {\n // Cleanup errors shouldn't break HMR.\n }\n }\n file.resetCleanup();\n }\n\n /**\n * Clean up after a file whose source was deleted from disk.\n */\n public cleanupDeletedModule(file: FileManager): void {\n this.loadedModules.delete(file.absolutePath);\n\n if (file.type === \"route\") {\n router.removeRoutesBySourceFile(file.relativePath);\n }\n\n this.runCleanup(file);\n }\n\n /**\n * Scan a freshly-loaded module for cleanup handlers and register them on\n * the FileManager so they run before the next reload. Priority: explicit\n * `export function cleanup()` → `.$cleanup` on any exported value.\n */\n private registerCleanup(file: FileManager, module: Record<string, unknown>): void {\n if (typeof module.cleanup === \"function\") {\n file.addCleanup(module.cleanup as CleanupFunction);\n return;\n }\n\n const cleanups: CleanupFunction[] = [];\n\n for (const exportedValue of Object.values(module)) {\n const cleanup = (exportedValue as { $cleanup?: unknown })?.$cleanup;\n if (typeof cleanup === \"function\") {\n cleanups.push(cleanup.bind(exportedValue) as CleanupFunction);\n }\n }\n\n if (cleanups.length > 0) {\n file.addCleanup(cleanups);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -54,7 +54,7 @@ function isTypeOnlyFile(source) {
|
|
|
54
54
|
if (/^export\s+type\s+/.test(match)) continue;
|
|
55
55
|
const specifiersMatch = match.match(/export\s+\{([^}]*)\}/);
|
|
56
56
|
if (specifiersMatch) {
|
|
57
|
-
if (specifiersMatch[1].split(",").map((s) => s.trim()).filter(Boolean).some((item) => !item.startsWith("type "))) return false;
|
|
57
|
+
if ((specifiersMatch[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).some((item) => !item.startsWith("type "))) return false;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
continue;
|
|
@@ -85,7 +85,7 @@ function hasRuntimeImports(line) {
|
|
|
85
85
|
if (isTypeOnlyImport(trimmed)) return false;
|
|
86
86
|
const specifiersMatch = trimmed.match(/import\s+(?:type\s+)?\{([^}]+)\}/);
|
|
87
87
|
if (!specifiersMatch) return true;
|
|
88
|
-
return !specifiersMatch[1].split(",").map((s) => s.trim()).filter(Boolean).every((item) => /^type\s+\w+/.test(item));
|
|
88
|
+
return !(specifiersMatch[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).every((item) => /^type\s+\w+/.test(item));
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* Decide whether an `export ... from "m"` statement re-exports only types.
|
|
@@ -97,7 +97,7 @@ function isExportTypeOnlyStatement(line) {
|
|
|
97
97
|
if (/^export\s+type\s+/.test(trimmed)) return true;
|
|
98
98
|
const specifiersMatch = trimmed.match(/export\s+\{([^}]+)\}/);
|
|
99
99
|
if (!specifiersMatch) return false;
|
|
100
|
-
return specifiersMatch[1].split(",").map((s) => s.trim()).filter(Boolean).every((item) => /^type\s+\w+/.test(item));
|
|
100
|
+
return (specifiersMatch[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).every((item) => /^type\s+\w+/.test(item));
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
103
103
|
* Extract import paths using regex (more reliable for TypeScript).
|
|
@@ -125,23 +125,37 @@ function extractImportPathsWithRegex(source) {
|
|
|
125
125
|
});
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
|
-
if (!isTypeOnly)
|
|
128
|
+
if (!isTypeOnly) {
|
|
129
|
+
const existing = imports[existingIndex];
|
|
130
|
+
if (existing) existing.isTypeOnly = false;
|
|
131
|
+
}
|
|
129
132
|
};
|
|
130
133
|
const importRegex = /import\s+(?:type\s+)?(\{[\s\S]*?\}|\*\s+as\s+\w+|\w+(?:\s*,\s*\{[\s\S]*?\})?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
131
134
|
let match;
|
|
132
135
|
while ((match = importRegex.exec(source)) !== null) {
|
|
133
136
|
const fullMatch = match[0];
|
|
134
137
|
const importPath = match[2];
|
|
138
|
+
if (importPath === void 0) continue;
|
|
135
139
|
record(importPath, fullMatch, !hasRuntimeImports(fullMatch));
|
|
136
140
|
}
|
|
137
141
|
const sideEffectRegex = /import\s+['"]([^'"]+)['"]/g;
|
|
138
|
-
while ((match = sideEffectRegex.exec(source)) !== null)
|
|
142
|
+
while ((match = sideEffectRegex.exec(source)) !== null) {
|
|
143
|
+
const sideEffectPath = match[1];
|
|
144
|
+
if (sideEffectPath === void 0) continue;
|
|
145
|
+
record(sideEffectPath, match[0], false);
|
|
146
|
+
}
|
|
139
147
|
const dynamicImportPattern = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
140
|
-
while ((match = dynamicImportPattern.exec(source)) !== null)
|
|
148
|
+
while ((match = dynamicImportPattern.exec(source)) !== null) {
|
|
149
|
+
const dynamicPath = match[1];
|
|
150
|
+
if (dynamicPath === void 0) continue;
|
|
151
|
+
record(dynamicPath, match[0], false);
|
|
152
|
+
}
|
|
141
153
|
const exportFromPattern = /export\s+(?:\{[^}]*\}|\*|\w+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
142
154
|
while ((match = exportFromPattern.exec(source)) !== null) {
|
|
143
155
|
const fullMatch = match[0];
|
|
144
|
-
|
|
156
|
+
const exportPath = match[1];
|
|
157
|
+
if (exportPath === void 0) continue;
|
|
158
|
+
record(exportPath, fullMatch, isExportTypeOnlyStatement(fullMatch));
|
|
145
159
|
}
|
|
146
160
|
return imports;
|
|
147
161
|
}
|
|
@@ -339,7 +353,7 @@ function isNodeBuiltin(importPath) {
|
|
|
339
353
|
"v8"
|
|
340
354
|
];
|
|
341
355
|
if (importPath.startsWith("node:")) return true;
|
|
342
|
-
const moduleName = importPath.split("/")[0];
|
|
356
|
+
const moduleName = importPath.split("/")[0] ?? importPath;
|
|
343
357
|
return builtins.includes(moduleName);
|
|
344
358
|
}
|
|
345
359
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-imports.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/parse-imports.ts"],"sourcesContent":["import { fileExistsAsync, directoryExistsAsync } from \"@warlock.js/fs\";\nimport { type ImportSpecifier, parse } from \"es-module-lexer\";\nimport path from \"node:path\";\nimport { Path } from \"../utils/normalized-path\";\nimport { tsconfigManager } from \"./tsconfig-manager\";\n\n/**\n * Detect if a file contains only type definitions (no runtime code)\n *\n * A file is considered type-only if ALL its exports are:\n * - interface declarations\n * - type alias declarations\n * - export type { ... } statements\n * - export type { ... } from \"...\" statements\n *\n * Files with any of these are NOT type-only:\n * - export const/let/var\n * - export function\n * - export class\n * - export default (non-type)\n * - export { ... } (without type keyword)\n * - export * from (without type keyword)\n *\n * @param source - The source code to analyze\n * @returns true if the file exports only types\n */\nexport function isTypeOnlyFile(source: string): boolean {\n // Remove comments to avoid false positives\n const withoutComments = source\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\") // block comments\n .replace(/\\/\\/.*$/gm, \"\"); // line comments\n\n // Remove string literals to avoid false positives\n const withoutStrings = withoutComments\n .replace(/'(?:[^'\\\\]|\\\\.)*'/g, \"''\")\n .replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, '\"\"')\n .replace(/`(?:[^`\\\\]|\\\\.)*`/g, \"``\");\n\n // Patterns for type-only exports (these are safe)\n const typeOnlyPatterns = [\n /export\\s+type\\s+\\{[^}]*\\}/g, // export type { Foo, Bar }\n /export\\s+type\\s+\\{[^}]*\\}\\s+from\\s+['\"]/g, // export type { Foo } from \"...\"\n /export\\s+interface\\s+\\w+/g, // export interface Foo\n /export\\s+type\\s+\\w+\\s*=/g, // export type Foo =\n ];\n\n // Remove all type-only exports from consideration\n let remaining = withoutStrings;\n for (const pattern of typeOnlyPatterns) {\n remaining = remaining.replace(pattern, \"\");\n }\n\n // Patterns for runtime exports (these make the file NOT type-only)\n const runtimeExportPatterns = [\n /export\\s+(?:const|let|var)\\s+\\w+/g, // export const/let/var foo\n /export\\s+function\\s+\\w+/g, // export function foo\n /export\\s+async\\s+function\\s+\\w+/g, // export async function foo\n /export\\s+class\\s+\\w+/g, // export class Foo\n /export\\s+enum\\s+\\w+/g, // export enum Foo (enums have runtime value)\n /export\\s+default\\s+(?!type\\s)/g, // export default (not type)\n /export\\s+\\{[^}]*\\}(?!\\s+from)/g, // export { foo } (local re-export without type)\n /export\\s+\\{[^}]*\\}\\s+from\\s+['\"][^'\"]+['\"]/g, // export { foo } from (without type)\n /export\\s+\\*\\s+from\\s+['\"][^'\"]+['\"]/g, // export * from (re-exports everything)\n /export\\s+\\*\\s+as\\s+\\w+/g, // export * as namespace\n ];\n\n for (const pattern of runtimeExportPatterns) {\n if (pattern.test(remaining)) {\n // Reset regex lastIndex for subsequent tests\n pattern.lastIndex = 0;\n\n // Special case: check if export { } contains only type exports\n if (pattern.source.includes(\"export\\\\s+\\\\{\")) {\n const matches = remaining.match(/export\\s+\\{([^}]*)\\}(?:\\s+from\\s+['\"][^'\"]+['\"])?/g);\n if (matches) {\n for (const match of matches) {\n // Skip if it's already a type export\n if (/^export\\s+type\\s+/.test(match)) continue;\n\n // Extract the specifiers\n const specifiersMatch = match.match(/export\\s+\\{([^}]*)\\}/);\n if (specifiersMatch) {\n const specifiers = specifiersMatch[1];\n const items = specifiers\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n // If any specifier is NOT prefixed with \"type \", this is a runtime export\n const hasRuntimeSpecifier = items.some((item) => !item.startsWith(\"type \"));\n if (hasRuntimeSpecifier) {\n return false;\n }\n }\n }\n }\n continue;\n }\n\n return false;\n }\n }\n\n // If we get here, no runtime exports were found\n // But we should also verify the file has at least some type exports\n // (an empty file or file with only imports is not really \"type-only\")\n const hasTypeExports = typeOnlyPatterns.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(withoutStrings);\n });\n\n return hasTypeExports;\n}\n\n/**\n * Check if an import statement is a pure `import type` (the keyword sits\n * immediately after `import`, covering `import type { X }`, `import type Foo`,\n * and `import type * as Ns`).\n */\nfunction isTypeOnlyImport(line: string): boolean {\n const trimmed = line.trim();\n\n return trimmed.startsWith(\"import type \") || !!trimmed.match(/^import\\s+type\\s+[\\{\\*]/);\n}\n\n/**\n * Decide whether an `import ... from \"m\"` statement contributes any runtime\n * binding. Returns false for pure `import type` and for destructured imports\n * where every specifier is prefixed with the `type` keyword.\n */\nfunction hasRuntimeImports(line: string): boolean {\n const trimmed = line.trim();\n\n if (isTypeOnlyImport(trimmed)) {\n return false;\n }\n\n const specifiersMatch = trimmed.match(/import\\s+(?:type\\s+)?\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return true;\n }\n\n const items = specifiersMatch[1].split(\",\").map((s) => s.trim()).filter(Boolean);\n\n return !items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Decide whether an `export ... from \"m\"` statement re-exports only types.\n * Covers `export type { X } from`, `export type * from`, and\n * `export { type X, type Y } from`.\n */\nfunction isExportTypeOnlyStatement(line: string): boolean {\n const trimmed = line.trim();\n\n if (/^export\\s+type\\s+/.test(trimmed)) {\n return true;\n }\n\n const specifiersMatch = trimmed.match(/export\\s+\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return false;\n }\n\n const items = specifiersMatch[1].split(\",\").map((s) => s.trim()).filter(Boolean);\n\n return items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Extract import paths using regex (more reliable for TypeScript).\n * Each entry carries an `isTypeOnly` flag: true when every occurrence of the\n * path in the source is a type-only statement (no runtime binding).\n * A single runtime occurrence makes the path runtime.\n */\nfunction extractImportPathsWithRegex(\n source: string,\n): Array<{ path: string; originalLine: string; isTypeOnly: boolean }> {\n const imports: Array<{ path: string; originalLine: string; isTypeOnly: boolean }> = [];\n const seenPaths = new Map<string, number>();\n\n /**\n * Record an import path with its type-only flag.\n * If the same path appears in multiple statements, the edge is type-only\n * iff every occurrence is type-only — a single runtime statement makes it runtime.\n */\n const record = (importPath: string, originalLine: string, isTypeOnly: boolean): void => {\n if (!importPath) {\n return;\n }\n\n const existingIndex = seenPaths.get(importPath);\n\n if (existingIndex === undefined) {\n seenPaths.set(importPath, imports.length);\n imports.push({ path: importPath, originalLine, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n imports[existingIndex].isTypeOnly = false;\n }\n };\n\n // Pattern 1: Standard ES module imports (handles multiline)\n // Matches: import { ... } from \"path\", import Foo from \"path\", import Foo, { ... } from \"path\"\n const importRegex =\n /import\\s+(?:type\\s+)?(\\{[\\s\\S]*?\\}|\\*\\s+as\\s+\\w+|\\w+(?:\\s*,\\s*\\{[\\s\\S]*?\\})?)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n let match;\n\n while ((match = importRegex.exec(source)) !== null) {\n const fullMatch = match[0];\n const importPath = match[2];\n const isTypeOnly = !hasRuntimeImports(fullMatch);\n\n record(importPath, fullMatch, isTypeOnly);\n }\n\n // Pattern 1b: Side-effect imports - import \"path\" — always runtime\n const sideEffectRegex = /import\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = sideEffectRegex.exec(source)) !== null) {\n record(match[1], match[0], false);\n }\n\n // Pattern 2: Dynamic imports - import(\"path\") — always runtime\n const dynamicImportPattern = /import\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n\n while ((match = dynamicImportPattern.exec(source)) !== null) {\n record(match[1], match[0], false);\n }\n\n // Pattern 3: Export from - export ... from \"path\"\n const exportFromPattern = /export\\s+(?:\\{[^}]*\\}|\\*|\\w+)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = exportFromPattern.exec(source)) !== null) {\n const fullMatch = match[0];\n\n record(match[1], fullMatch, isExportTypeOnlyStatement(fullMatch));\n }\n\n return imports;\n}\n\n/**\n * Metadata for a single resolved import edge.\n * `isTypeOnly` is true iff every import/export statement that references this\n * path is type-only (pure `import type`, `export type`, or destructured lists\n * where every specifier carries the `type` keyword). A single runtime\n * occurrence flips the flag to false — that's what matters for cycle detection.\n */\nexport type ResolvedImport = {\n absolutePath: string;\n isTypeOnly: boolean;\n};\n\n/**\n * Parse import/export statements in a TS/JS source file and return the\n * resolved dependency edges keyed by the original import path. Includes\n * type-only edges (flagged) so downstream consumers can distinguish runtime\n * cycles from type-only ones.\n *\n * @example\n * const importMap = await parseImports(source, \"/abs/path/to/file.ts\");\n * for (const [importPath, { absolutePath, isTypeOnly }] of importMap) {\n * // ...\n * }\n */\nexport async function parseImports(\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n try {\n // Skip .d.ts files - they're type declarations, not runtime code\n if (filePath.endsWith(\".d.ts\")) {\n return new Map();\n }\n\n // Try es-module-lexer first (faster and more accurate for simple cases)\n try {\n const [imports] = await parse(source);\n\n if (imports && imports.length > 0) {\n return await resolveImports(imports as ImportSpecifier[], source, filePath);\n }\n } catch (lexerError) {\n // es-module-lexer failed, fall back to regex-based extraction\n // This is common with TypeScript files that have complex syntax\n }\n\n // Fallback: Use regex-based extraction (more forgiving with TypeScript)\n const regexImports = extractImportPathsWithRegex(source);\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const { path: importPath, isTypeOnly } of regexImports) {\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (resolvedPath) {\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n }\n\n return resolvedImports;\n } catch (error) {\n console.error(`Error parsing imports for ${filePath}:`, error);\n\n return new Map();\n }\n}\n\n/**\n * Merge a resolved import into the output map, preserving the type-only rule:\n * a path is type-only iff every statement that references it is type-only.\n */\nfunction mergeResolvedImport(\n target: Map<string, ResolvedImport>,\n importPath: string,\n absolutePath: string,\n isTypeOnly: boolean,\n): void {\n const existing = target.get(importPath);\n\n if (!existing) {\n target.set(importPath, { absolutePath, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n existing.isTypeOnly = false;\n }\n}\n\n/**\n * Resolve imports from `es-module-lexer` output. For each specifier, slice the\n * raw statement (`ss..se`) and re-run the type-only heuristic so we can flag\n * the edge correctly — the lexer itself does not distinguish `import type`.\n */\nasync function resolveImports(\n imports: ImportSpecifier[],\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const imp of imports) {\n const importPath = imp.n;\n\n if (!importPath) {\n continue;\n }\n\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (!resolvedPath) {\n continue;\n }\n\n const isTypeOnly = isLexerStatementTypeOnly(imp, source);\n\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n\n return resolvedImports;\n}\n\n/**\n * Classify a single lexer-reported statement as type-only or runtime.\n * Dynamic imports (`import(...)`) are always runtime. For static\n * imports/export-from we slice the raw source and apply the same heuristic\n * the regex fallback uses.\n */\nfunction isLexerStatementTypeOnly(imp: ImportSpecifier, source: string): boolean {\n // Dynamic imports always execute — they cannot be type-only.\n // es-module-lexer reports dynamic with a.n === undefined typically, but\n // we guard by checking the statement prefix too.\n const statementStart = imp.ss;\n const statementEnd = imp.se;\n\n if (\n typeof statementStart !== \"number\" ||\n typeof statementEnd !== \"number\" ||\n statementEnd <= statementStart\n ) {\n return false;\n }\n\n const statement = source.slice(statementStart, statementEnd);\n const trimmed = statement.trim();\n\n if (trimmed.startsWith(\"import(\") || /^\\bimport\\s*\\(/.test(trimmed)) {\n return false;\n }\n\n if (trimmed.startsWith(\"export\")) {\n return isExportTypeOnlyStatement(statement);\n }\n\n return !hasRuntimeImports(statement);\n}\n\n/**\n * Resolve alias imports to actual file paths with extensions\n * Example: app/users/services/get-users.service -> /absolute/path/to/src/app/users/services/get-users.service.ts\n */\nasync function resolveAliasImport(importPath: string): Promise<string | null> {\n // Use tsconfig manager to resolve the alias to an absolute path\n const resolvedBase = tsconfigManager.resolveAliasToAbsolute(importPath);\n\n if (!resolvedBase) return null;\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Resolve relative imports to actual file paths\n * Example: ./../services/get-user.service -> /absolute/path/to/services/get-user.service.ts\n */\nasync function resolveRelativeImport(\n importPath: string,\n currentFilePath: string,\n): Promise<string | null> {\n const dir = path.dirname(currentFilePath);\n // Use path.resolve to handle .. and . properly, then normalize to forward slashes\n const resolvedBase = Path.normalize(path.resolve(dir, importPath));\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Try to resolve a file path by checking different extensions\n * TypeScript/JavaScript files can be imported without extensions\n *\n * @TODO: For better performance, we need to check the files in files orchestrator\n * instead of using the file system as we will be fetching all project files anyway.\n */\n// Cache for file existence checks to avoid redundant filesystem calls\nconst fileExistsCache = new Map<string, boolean>();\n\n/**\n * Clear the file exists cache\n * Should be called when new files are created to ensure fresh lookups\n */\nexport function clearFileExistsCache(): void {\n fileExistsCache.clear();\n}\n\nasync function cachedFileExists(filePath: string): Promise<boolean> {\n if (fileExistsCache.has(filePath)) {\n return fileExistsCache.get(filePath)!;\n }\n const exists = (await fileExistsAsync(filePath)) as boolean;\n fileExistsCache.set(filePath, exists);\n return exists;\n}\n\nasync function tryResolveWithExtensions(basePath: string): Promise<string | null> {\n // Normalize the base path first (handle Windows paths)\n const normalizedBase = Path.normalize(basePath);\n\n // List of extensions to try, in order of preference\n const extensions = [\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\", \".cjs\"];\n const validExtensions = new Set(extensions);\n\n // If the path already has a VALID code file extension, check if it exists\n const ext = path.extname(normalizedBase);\n if (ext && validExtensions.has(ext)) {\n if (await cachedFileExists(normalizedBase)) {\n return normalizedBase;\n }\n // If explicit extension doesn't exist, return null\n return null;\n }\n\n // Try all extensions in parallel for better performance\n const pathsToCheck = extensions.map((extension) => normalizedBase + extension);\n const results = await Promise.all(\n pathsToCheck.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n // Return the first one that exists (in order of preference)\n for (const result of results) {\n if (result.exists) {\n return result.path;\n }\n }\n\n // Try index files in directory\n if (await directoryExistsAsync(normalizedBase)) {\n const indexPaths = extensions.map((extension) =>\n Path.join(normalizedBase, `index${extension}`),\n );\n const indexResults = await Promise.all(\n indexPaths.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n for (const result of indexResults) {\n if (result.exists) {\n return result.path;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Check if import is a Node.js built-in module\n */\nfunction isNodeBuiltin(importPath: string): boolean {\n const builtins = [\n \"fs\",\n \"path\",\n \"http\",\n \"https\",\n \"crypto\",\n \"stream\",\n \"util\",\n \"events\",\n \"buffer\",\n \"child_process\",\n \"os\",\n \"url\",\n \"querystring\",\n \"zlib\",\n \"net\",\n \"tls\",\n \"dns\",\n \"dgram\",\n \"cluster\",\n \"worker_threads\",\n \"perf_hooks\",\n \"async_hooks\",\n \"timers\",\n \"readline\",\n \"repl\",\n \"vm\",\n \"assert\",\n \"console\",\n \"process\",\n \"v8\",\n ];\n\n // Check for node: prefix or direct builtin name\n if (importPath.startsWith(\"node:\")) return true;\n\n const moduleName = importPath.split(\"/\")[0];\n return builtins.includes(moduleName);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"parse-imports.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/parse-imports.ts"],"sourcesContent":["import { fileExistsAsync, directoryExistsAsync } from \"@warlock.js/fs\";\nimport { type ImportSpecifier, parse } from \"es-module-lexer\";\nimport path from \"node:path\";\nimport { Path } from \"../utils/normalized-path\";\nimport { tsconfigManager } from \"./tsconfig-manager\";\n\n/**\n * Detect if a file contains only type definitions (no runtime code)\n *\n * A file is considered type-only if ALL its exports are:\n * - interface declarations\n * - type alias declarations\n * - export type { ... } statements\n * - export type { ... } from \"...\" statements\n *\n * Files with any of these are NOT type-only:\n * - export const/let/var\n * - export function\n * - export class\n * - export default (non-type)\n * - export { ... } (without type keyword)\n * - export * from (without type keyword)\n *\n * @param source - The source code to analyze\n * @returns true if the file exports only types\n */\nexport function isTypeOnlyFile(source: string): boolean {\n // Remove comments to avoid false positives\n const withoutComments = source\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\") // block comments\n .replace(/\\/\\/.*$/gm, \"\"); // line comments\n\n // Remove string literals to avoid false positives\n const withoutStrings = withoutComments\n .replace(/'(?:[^'\\\\]|\\\\.)*'/g, \"''\")\n .replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, '\"\"')\n .replace(/`(?:[^`\\\\]|\\\\.)*`/g, \"``\");\n\n // Patterns for type-only exports (these are safe)\n const typeOnlyPatterns = [\n /export\\s+type\\s+\\{[^}]*\\}/g, // export type { Foo, Bar }\n /export\\s+type\\s+\\{[^}]*\\}\\s+from\\s+['\"]/g, // export type { Foo } from \"...\"\n /export\\s+interface\\s+\\w+/g, // export interface Foo\n /export\\s+type\\s+\\w+\\s*=/g, // export type Foo =\n ];\n\n // Remove all type-only exports from consideration\n let remaining = withoutStrings;\n for (const pattern of typeOnlyPatterns) {\n remaining = remaining.replace(pattern, \"\");\n }\n\n // Patterns for runtime exports (these make the file NOT type-only)\n const runtimeExportPatterns = [\n /export\\s+(?:const|let|var)\\s+\\w+/g, // export const/let/var foo\n /export\\s+function\\s+\\w+/g, // export function foo\n /export\\s+async\\s+function\\s+\\w+/g, // export async function foo\n /export\\s+class\\s+\\w+/g, // export class Foo\n /export\\s+enum\\s+\\w+/g, // export enum Foo (enums have runtime value)\n /export\\s+default\\s+(?!type\\s)/g, // export default (not type)\n /export\\s+\\{[^}]*\\}(?!\\s+from)/g, // export { foo } (local re-export without type)\n /export\\s+\\{[^}]*\\}\\s+from\\s+['\"][^'\"]+['\"]/g, // export { foo } from (without type)\n /export\\s+\\*\\s+from\\s+['\"][^'\"]+['\"]/g, // export * from (re-exports everything)\n /export\\s+\\*\\s+as\\s+\\w+/g, // export * as namespace\n ];\n\n for (const pattern of runtimeExportPatterns) {\n if (pattern.test(remaining)) {\n // Reset regex lastIndex for subsequent tests\n pattern.lastIndex = 0;\n\n // Special case: check if export { } contains only type exports\n if (pattern.source.includes(\"export\\\\s+\\\\{\")) {\n const matches = remaining.match(/export\\s+\\{([^}]*)\\}(?:\\s+from\\s+['\"][^'\"]+['\"])?/g);\n if (matches) {\n for (const match of matches) {\n // Skip if it's already a type export\n if (/^export\\s+type\\s+/.test(match)) continue;\n\n // Extract the specifiers\n const specifiersMatch = match.match(/export\\s+\\{([^}]*)\\}/);\n if (specifiersMatch) {\n // Capture group 1 exists whenever the pattern matched. `?? \"\"`\n // yields an empty specifier list, which `some()` below reads as\n // \"no runtime specifier\" — the same answer an empty `{}` gives.\n const specifiers = specifiersMatch[1] ?? \"\";\n const items = specifiers\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n // If any specifier is NOT prefixed with \"type \", this is a runtime export\n const hasRuntimeSpecifier = items.some((item) => !item.startsWith(\"type \"));\n if (hasRuntimeSpecifier) {\n return false;\n }\n }\n }\n }\n continue;\n }\n\n return false;\n }\n }\n\n // If we get here, no runtime exports were found\n // But we should also verify the file has at least some type exports\n // (an empty file or file with only imports is not really \"type-only\")\n const hasTypeExports = typeOnlyPatterns.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(withoutStrings);\n });\n\n return hasTypeExports;\n}\n\n/**\n * Check if an import statement is a pure `import type` (the keyword sits\n * immediately after `import`, covering `import type { X }`, `import type Foo`,\n * and `import type * as Ns`).\n */\nfunction isTypeOnlyImport(line: string): boolean {\n const trimmed = line.trim();\n\n return trimmed.startsWith(\"import type \") || !!trimmed.match(/^import\\s+type\\s+[\\{\\*]/);\n}\n\n/**\n * Decide whether an `import ... from \"m\"` statement contributes any runtime\n * binding. Returns false for pure `import type` and for destructured imports\n * where every specifier is prefixed with the `type` keyword.\n */\nfunction hasRuntimeImports(line: string): boolean {\n const trimmed = line.trim();\n\n if (isTypeOnlyImport(trimmed)) {\n return false;\n }\n\n const specifiersMatch = trimmed.match(/import\\s+(?:type\\s+)?\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return true;\n }\n\n const items = (specifiersMatch[1] ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n return !items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Decide whether an `export ... from \"m\"` statement re-exports only types.\n * Covers `export type { X } from`, `export type * from`, and\n * `export { type X, type Y } from`.\n */\nfunction isExportTypeOnlyStatement(line: string): boolean {\n const trimmed = line.trim();\n\n if (/^export\\s+type\\s+/.test(trimmed)) {\n return true;\n }\n\n const specifiersMatch = trimmed.match(/export\\s+\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return false;\n }\n\n const items = (specifiersMatch[1] ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n return items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Extract import paths using regex (more reliable for TypeScript).\n * Each entry carries an `isTypeOnly` flag: true when every occurrence of the\n * path in the source is a type-only statement (no runtime binding).\n * A single runtime occurrence makes the path runtime.\n */\nfunction extractImportPathsWithRegex(\n source: string,\n): Array<{ path: string; originalLine: string; isTypeOnly: boolean }> {\n const imports: Array<{ path: string; originalLine: string; isTypeOnly: boolean }> = [];\n const seenPaths = new Map<string, number>();\n\n /**\n * Record an import path with its type-only flag.\n * If the same path appears in multiple statements, the edge is type-only\n * iff every occurrence is type-only — a single runtime statement makes it runtime.\n */\n const record = (importPath: string, originalLine: string, isTypeOnly: boolean): void => {\n if (!importPath) {\n return;\n }\n\n const existingIndex = seenPaths.get(importPath);\n\n if (existingIndex === undefined) {\n seenPaths.set(importPath, imports.length);\n imports.push({ path: importPath, originalLine, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n const existing = imports[existingIndex];\n\n // `existingIndex` came from findIndex on this same array, so it is in\n // range; the compiler does not carry that here.\n if (existing) existing.isTypeOnly = false;\n }\n };\n\n // Pattern 1: Standard ES module imports (handles multiline)\n // Matches: import { ... } from \"path\", import Foo from \"path\", import Foo, { ... } from \"path\"\n const importRegex =\n /import\\s+(?:type\\s+)?(\\{[\\s\\S]*?\\}|\\*\\s+as\\s+\\w+|\\w+(?:\\s*,\\s*\\{[\\s\\S]*?\\})?)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n let match;\n\n while ((match = importRegex.exec(source)) !== null) {\n const fullMatch = match[0];\n const importPath = match[2];\n\n // Each pattern here captures its path in a group, so a match always carries\n // one. Skipping rather than recording keeps a malformed match out of the\n // dependency graph entirely — recording `undefined` would add an import\n // edge to a module named \"undefined\", which the dev server would then\n // watch, fail to resolve, and report against the wrong file.\n if (importPath === undefined) continue;\n\n const isTypeOnly = !hasRuntimeImports(fullMatch);\n\n record(importPath, fullMatch, isTypeOnly);\n }\n\n // Pattern 1b: Side-effect imports - import \"path\" — always runtime\n const sideEffectRegex = /import\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = sideEffectRegex.exec(source)) !== null) {\n const sideEffectPath = match[1];\n\n if (sideEffectPath === undefined) continue;\n\n record(sideEffectPath, match[0], false);\n }\n\n // Pattern 2: Dynamic imports - import(\"path\") — always runtime\n const dynamicImportPattern = /import\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n\n while ((match = dynamicImportPattern.exec(source)) !== null) {\n const dynamicPath = match[1];\n\n if (dynamicPath === undefined) continue;\n\n record(dynamicPath, match[0], false);\n }\n\n // Pattern 3: Export from - export ... from \"path\"\n const exportFromPattern = /export\\s+(?:\\{[^}]*\\}|\\*|\\w+)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = exportFromPattern.exec(source)) !== null) {\n const fullMatch = match[0];\n const exportPath = match[1];\n\n if (exportPath === undefined) continue;\n\n record(exportPath, fullMatch, isExportTypeOnlyStatement(fullMatch));\n }\n\n return imports;\n}\n\n/**\n * Metadata for a single resolved import edge.\n * `isTypeOnly` is true iff every import/export statement that references this\n * path is type-only (pure `import type`, `export type`, or destructured lists\n * where every specifier carries the `type` keyword). A single runtime\n * occurrence flips the flag to false — that's what matters for cycle detection.\n */\nexport type ResolvedImport = {\n absolutePath: string;\n isTypeOnly: boolean;\n};\n\n/**\n * Parse import/export statements in a TS/JS source file and return the\n * resolved dependency edges keyed by the original import path. Includes\n * type-only edges (flagged) so downstream consumers can distinguish runtime\n * cycles from type-only ones.\n *\n * @example\n * const importMap = await parseImports(source, \"/abs/path/to/file.ts\");\n * for (const [importPath, { absolutePath, isTypeOnly }] of importMap) {\n * // ...\n * }\n */\nexport async function parseImports(\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n try {\n // Skip .d.ts files - they're type declarations, not runtime code\n if (filePath.endsWith(\".d.ts\")) {\n return new Map();\n }\n\n // Try es-module-lexer first (faster and more accurate for simple cases)\n try {\n const [imports] = await parse(source);\n\n if (imports && imports.length > 0) {\n return await resolveImports(imports as ImportSpecifier[], source, filePath);\n }\n } catch (lexerError) {\n // es-module-lexer failed, fall back to regex-based extraction\n // This is common with TypeScript files that have complex syntax\n }\n\n // Fallback: Use regex-based extraction (more forgiving with TypeScript)\n const regexImports = extractImportPathsWithRegex(source);\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const { path: importPath, isTypeOnly } of regexImports) {\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (resolvedPath) {\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n }\n\n return resolvedImports;\n } catch (error) {\n console.error(`Error parsing imports for ${filePath}:`, error);\n\n return new Map();\n }\n}\n\n/**\n * Merge a resolved import into the output map, preserving the type-only rule:\n * a path is type-only iff every statement that references it is type-only.\n */\nfunction mergeResolvedImport(\n target: Map<string, ResolvedImport>,\n importPath: string,\n absolutePath: string,\n isTypeOnly: boolean,\n): void {\n const existing = target.get(importPath);\n\n if (!existing) {\n target.set(importPath, { absolutePath, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n existing.isTypeOnly = false;\n }\n}\n\n/**\n * Resolve imports from `es-module-lexer` output. For each specifier, slice the\n * raw statement (`ss..se`) and re-run the type-only heuristic so we can flag\n * the edge correctly — the lexer itself does not distinguish `import type`.\n */\nasync function resolveImports(\n imports: ImportSpecifier[],\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const imp of imports) {\n const importPath = imp.n;\n\n if (!importPath) {\n continue;\n }\n\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (!resolvedPath) {\n continue;\n }\n\n const isTypeOnly = isLexerStatementTypeOnly(imp, source);\n\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n\n return resolvedImports;\n}\n\n/**\n * Classify a single lexer-reported statement as type-only or runtime.\n * Dynamic imports (`import(...)`) are always runtime. For static\n * imports/export-from we slice the raw source and apply the same heuristic\n * the regex fallback uses.\n */\nfunction isLexerStatementTypeOnly(imp: ImportSpecifier, source: string): boolean {\n // Dynamic imports always execute — they cannot be type-only.\n // es-module-lexer reports dynamic with a.n === undefined typically, but\n // we guard by checking the statement prefix too.\n const statementStart = imp.ss;\n const statementEnd = imp.se;\n\n if (\n typeof statementStart !== \"number\" ||\n typeof statementEnd !== \"number\" ||\n statementEnd <= statementStart\n ) {\n return false;\n }\n\n const statement = source.slice(statementStart, statementEnd);\n const trimmed = statement.trim();\n\n if (trimmed.startsWith(\"import(\") || /^\\bimport\\s*\\(/.test(trimmed)) {\n return false;\n }\n\n if (trimmed.startsWith(\"export\")) {\n return isExportTypeOnlyStatement(statement);\n }\n\n return !hasRuntimeImports(statement);\n}\n\n/**\n * Resolve alias imports to actual file paths with extensions\n * Example: app/users/services/get-users.service -> /absolute/path/to/src/app/users/services/get-users.service.ts\n */\nasync function resolveAliasImport(importPath: string): Promise<string | null> {\n // Use tsconfig manager to resolve the alias to an absolute path\n const resolvedBase = tsconfigManager.resolveAliasToAbsolute(importPath);\n\n if (!resolvedBase) return null;\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Resolve relative imports to actual file paths\n * Example: ./../services/get-user.service -> /absolute/path/to/services/get-user.service.ts\n */\nasync function resolveRelativeImport(\n importPath: string,\n currentFilePath: string,\n): Promise<string | null> {\n const dir = path.dirname(currentFilePath);\n // Use path.resolve to handle .. and . properly, then normalize to forward slashes\n const resolvedBase = Path.normalize(path.resolve(dir, importPath));\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Try to resolve a file path by checking different extensions\n * TypeScript/JavaScript files can be imported without extensions\n *\n * @TODO: For better performance, we need to check the files in files orchestrator\n * instead of using the file system as we will be fetching all project files anyway.\n */\n// Cache for file existence checks to avoid redundant filesystem calls\nconst fileExistsCache = new Map<string, boolean>();\n\n/**\n * Clear the file exists cache\n * Should be called when new files are created to ensure fresh lookups\n */\nexport function clearFileExistsCache(): void {\n fileExistsCache.clear();\n}\n\nasync function cachedFileExists(filePath: string): Promise<boolean> {\n if (fileExistsCache.has(filePath)) {\n return fileExistsCache.get(filePath)!;\n }\n const exists = (await fileExistsAsync(filePath)) as boolean;\n fileExistsCache.set(filePath, exists);\n return exists;\n}\n\nasync function tryResolveWithExtensions(basePath: string): Promise<string | null> {\n // Normalize the base path first (handle Windows paths)\n const normalizedBase = Path.normalize(basePath);\n\n // List of extensions to try, in order of preference\n const extensions = [\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\", \".cjs\"];\n const validExtensions = new Set(extensions);\n\n // If the path already has a VALID code file extension, check if it exists\n const ext = path.extname(normalizedBase);\n if (ext && validExtensions.has(ext)) {\n if (await cachedFileExists(normalizedBase)) {\n return normalizedBase;\n }\n // If explicit extension doesn't exist, return null\n return null;\n }\n\n // Try all extensions in parallel for better performance\n const pathsToCheck = extensions.map((extension) => normalizedBase + extension);\n const results = await Promise.all(\n pathsToCheck.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n // Return the first one that exists (in order of preference)\n for (const result of results) {\n if (result.exists) {\n return result.path;\n }\n }\n\n // Try index files in directory\n if (await directoryExistsAsync(normalizedBase)) {\n const indexPaths = extensions.map((extension) =>\n Path.join(normalizedBase, `index${extension}`),\n );\n const indexResults = await Promise.all(\n indexPaths.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n for (const result of indexResults) {\n if (result.exists) {\n return result.path;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Check if import is a Node.js built-in module\n */\nfunction isNodeBuiltin(importPath: string): boolean {\n const builtins = [\n \"fs\",\n \"path\",\n \"http\",\n \"https\",\n \"crypto\",\n \"stream\",\n \"util\",\n \"events\",\n \"buffer\",\n \"child_process\",\n \"os\",\n \"url\",\n \"querystring\",\n \"zlib\",\n \"net\",\n \"tls\",\n \"dns\",\n \"dgram\",\n \"cluster\",\n \"worker_threads\",\n \"perf_hooks\",\n \"async_hooks\",\n \"timers\",\n \"readline\",\n \"repl\",\n \"vm\",\n \"assert\",\n \"console\",\n \"process\",\n \"v8\",\n ];\n\n // Check for node: prefix or direct builtin name\n if (importPath.startsWith(\"node:\")) return true;\n\n // `split` always yields a first element. Falling back to the whole path\n // keeps the builtin check answering about a real name rather than undefined.\n const moduleName = importPath.split(\"/\")[0] ?? importPath;\n\n return builtins.includes(moduleName);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type-generator.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/type-generator.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\nimport { constants } from \"fs\";\nimport { access, readFile, writeFile } from \"fs/promises\";\nimport { join, resolve } from \"path\";\nimport ts from \"typescript\";\nimport { warlockPath } from \"../utils\";\nimport { devLogError, devLogInfo, devLogSuccess, devServeLog } from \"./dev-logger\";\nimport { readConfigAst } from \"./read-config-ast\";\nimport { runTypingsGeneration, type TypingsGenerationPorts } from \"./run-typings-generation\";\nimport { filesOrchestrator } from \"./files-orchestrator\";\nimport { Path } from \"../utils/normalized-path\";\n\n/**\n * Typings manifest structure for tracking file hashes\n */\ntype TypingsManifest = {\n version: string;\n lastBuildTime: number;\n storage: {\n sourceHash: string;\n drivers: string[];\n } | null;\n config: Record<\n string,\n {\n sourceHash: string;\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }\n >;\n};\n\n/**\n * TypeGenerator - Generates TypeScript type definitions from config files\n *\n * Parses source config files using the TypeScript Compiler API to extract\n * keys and generate module augmentation types for IDE autocomplete.\n *\n * Uses manifest-based reconciliation to only regenerate when source files change.\n */\nexport class TypeGenerator {\n /**\n * Output directory for generated typings\n */\n private outputDir = warlockPath(\"typings\");\n\n /**\n * Path to typings manifest file\n */\n private manifestPath = join(this.outputDir, \"typings-manifest.json\");\n\n /**\n * Cached manifest data\n */\n private manifest: TypingsManifest | null = null;\n\n /**\n * Cache for config type info and keys\n */\n private configCache = new Map<\n string,\n {\n sourceHash: string;\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }\n >();\n\n /**\n * Generate all framework type definitions\n *\n * Uses manifest-based reconciliation:\n * - If output files don't exist: full regeneration\n * - If files exist: only regenerate changed configs\n */\n public async generateAll(): Promise<void> {\n await this.ensureOutputDir();\n\n const storageFile = join(this.outputDir, \"storage.d.ts\");\n const configFile = join(this.outputDir, \"config.d.ts\");\n\n const [manifestExists, storageExists, configExists] = await Promise.all([\n this.exists(this.manifestPath),\n this.exists(storageFile),\n this.exists(configFile),\n ]);\n\n if (!manifestExists || !storageExists || !configExists) {\n // Full regeneration (first run or files deleted)\n await this.fullGeneration();\n } else {\n // Load manifest for hash comparison\n await this.loadManifest();\n // Reconciliation: only regenerate changed files\n await this.reconcile();\n }\n\n await this.saveManifest();\n }\n\n /**\n * Full regeneration of all type files\n */\n private async fullGeneration(): Promise<void> {\n // Generate storage types\n const storageConfigPath = await this.findConfigFile(\"storage\");\n if (storageConfigPath) {\n this.generateStorageTypes(storageConfigPath);\n }\n\n // Generate config types\n await this.generateConfigTypes();\n }\n\n /**\n * Reconcile: only regenerate changed files\n */\n private async reconcile(): Promise<void> {\n const files = filesOrchestrator.getFiles();\n let storageChanged = false;\n let configChanged = false;\n let unchangedCount = 0;\n\n // Check storage config\n for (const [path, fileManager] of files) {\n if (path.startsWith(\"src/config/storage\")) {\n const manifestEntry = this.manifest?.storage;\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\n storageChanged = true;\n await this.generateStorageTypes(path);\n }\n break;\n }\n }\n\n // Check config files\n for (const [path, fileManager] of files) {\n if (!path.startsWith(\"src/config/\")) continue;\n if (path.includes(\"index\")) continue;\n\n // Extract config name (remove dir prefix and extension)\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\n\n const manifestEntry = this.manifest?.config[configName];\n\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\n // File changed or new - regenerate\n configChanged = true;\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\n\n this.configCache.set(configName, {\n sourceHash: fileManager.hash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n } else {\n // Unchanged - load from manifest\n unchangedCount++;\n this.configCache.set(configName, manifestEntry);\n }\n }\n\n if (configChanged) {\n await this.writeConfigTypesFromCache();\n } else {\n devLogInfo(`Config types unchanged (${unchangedCount} configs cached)`);\n }\n }\n\n /**\n * Generate storage driver name types\n */\n public async generateStorageTypes(configPath: string): Promise<void> {\n try {\n const driverKeys = await this.extractStorageDriverKeys(configPath);\n\n if (driverKeys.length === 0) {\n devServeLog(\"âš ï¸ No storage drivers found in config\");\n return;\n }\n\n // Get file hash from filesOrchestrator\n const fileManager = filesOrchestrator.getFiles().get(configPath);\n const sourceHash = fileManager?.hash || \"\";\n\n // Update manifest\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.storage = {\n sourceHash,\n drivers: driverKeys,\n };\n\n const interfaceContent = driverKeys.map((k) => ` ${this.toInterfaceKey(k)}: true;`).join(\"\\n\");\n\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\n// Generated from: ${configPath}\n// Regenerates on dev-server start and when storage config changes\n\nimport \"@warlock.js/core\";\n\ndeclare module \"@warlock.js/core\" {\n interface StorageDriverRegistry {\n${interfaceContent}\n }\n}\n`;\n\n const outputPath = join(this.outputDir, \"storage.d.ts\");\n await writeFile(outputPath, content, \"utf-8\");\n\n devLogSuccess(`Generated storage types: ${driverKeys.join(\", \")}`);\n } catch (error) {\n devServeLog(`âš ï¸ Failed to generate storage types: ${error}`);\n }\n }\n\n /**\n * Check if a file change should trigger type regeneration\n */\n public shouldRegenerateTypes(changedPath: string): boolean {\n return changedPath.includes(\"src/config/\") || changedPath.includes(\"config/\");\n }\n\n /**\n * Handle file change - uses incremental update via cache\n */\n public async handleFileChange(changedPath: string): Promise<void> {\n if (!this.shouldRegenerateTypes(changedPath)) {\n return;\n }\n\n // Regenerate storage types if storage config changed\n if (changedPath.includes(\"config/storage\")) {\n await this.generateStorageTypes(changedPath);\n await this.saveManifest();\n return;\n }\n\n // Extract config name from path\n const match = changedPath.match(/config\\/([^/]+)\\.[^.]+$/);\n if (!match) {\n return;\n }\n\n const configName = match[1];\n if (configName === \"index\") return;\n\n devLogInfo(`Config changed: ${configName}, updating...`);\n\n // Get file manager for hash\n const fileManager = filesOrchestrator.getFiles().get(changedPath);\n const sourceHash = fileManager?.hash || Date.now().toString();\n\n // Update only the changed config in cache (use optimized combined extraction)\n const configDir = join(process.cwd(), \"src/config\");\n const configPath = join(configDir, `${configName}.ts`);\n\n const info = await this.extractConfigInfo(configPath, configName);\n\n this.configCache.set(configName, {\n sourceHash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n\n // Regenerate config.d.ts from cache\n await this.writeConfigTypesFromCache();\n await this.saveManifest();\n }\n\n /**\n * Generate config types - populates cache and writes file\n */\n public async generateConfigTypes(): Promise<void> {\n try {\n const files = filesOrchestrator.getFiles();\n\n // Clear and repopulate cache\n this.configCache.clear();\n\n for (const [path, fileManager] of files) {\n if (!path.startsWith(\"src/config/\")) continue;\n if (path.includes(\"index\")) continue;\n\n // Extract config name\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\n\n // One parse per config, both extractions off it — see `read-config-ast.ts`.\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\n\n this.configCache.set(configName, {\n sourceHash: fileManager.hash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n }\n\n await this.writeConfigTypesFromCache();\n } catch (error) {\n devServeLog(`âš ï¸ Failed to generate config types: ${error}`);\n }\n }\n\n /**\n * Quote an interface key that isn't a valid JS identifier (e.g. a config\n * named `use-cases` or a storage driver `do-spaces`) so the generated\n * `.d.ts` stays syntactically valid. Identifier-safe names are left bare\n * to keep the output clean.\n */\n private toInterfaceKey(name: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);\n }\n\n /**\n * Write config.d.ts from cached data\n */\n private async writeConfigTypesFromCache(): Promise<void> {\n const configTypeInfos: Array<{\n name: string;\n typeName: string | null;\n importSource: string | null;\n }> = [];\n const allKeys: string[] = [];\n\n // Update manifest config section\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.config = {};\n\n for (const [name, data] of this.configCache) {\n configTypeInfos.push({\n name,\n typeName: data.typeName,\n importSource: data.importSource,\n });\n allKeys.push(...data.keys);\n\n // Store in manifest\n this.manifest.config[name] = data;\n }\n\n // Group imports by source\n const imports = new Map<string, Set<string>>();\n for (const info of configTypeInfos) {\n if (info.typeName && info.importSource) {\n if (!imports.has(info.importSource)) {\n imports.set(info.importSource, new Set());\n }\n imports.get(info.importSource)!.add(info.typeName);\n }\n }\n\n const importStatements = Array.from(imports.entries())\n .map(([source, types]) => `import type { ${Array.from(types).join(\", \")} } from \"${source}\";`)\n .join(\"\\n\");\n\n const configEntries = configTypeInfos\n .map((info) => ` ${this.toInterfaceKey(info.name)}: ${info.typeName || \"unknown\"};`)\n .join(\"\\n\");\n\n const keyEntries = allKeys.map((key) => ` \"${key}\": true;`).join(\"\\n\");\n\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\n// Regenerates on dev-server start and when config files change\n\n${importStatements}\nimport \"@warlock.js/core\";\n\ndeclare module \"@warlock.js/core\" {\n interface ConfigRegistry {\n${configEntries}\n }\n\n interface ConfigKeyRegistry {\n${keyEntries}\n }\n}\n`;\n\n const outputPath = join(this.outputDir, \"config.d.ts\");\n await writeFile(outputPath, content, \"utf-8\");\n\n devLogSuccess(\n `Generated config types: ${this.configCache.size} configs, ${allKeys.length} keys`,\n );\n }\n\n // ============================================================\n // Manifest Management\n // ============================================================\n\n /**\n * Load manifest from disk\n */\n private async loadManifest(): Promise<boolean> {\n try {\n if (await this.exists(this.manifestPath)) {\n const content = await readFile(this.manifestPath, \"utf-8\");\n this.manifest = JSON.parse(content);\n return true;\n }\n } catch {\n // Manifest corrupted or missing\n }\n this.manifest = null;\n return false;\n }\n\n /**\n * Save manifest to disk\n */\n private async saveManifest(): Promise<void> {\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.lastBuildTime = Date.now();\n\n await writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2), \"utf-8\");\n }\n\n /**\n * Create empty manifest structure\n */\n private createEmptyManifest(): TypingsManifest {\n return {\n version: \"1.0.0\",\n lastBuildTime: Date.now(),\n storage: null,\n config: {},\n };\n }\n\n // ============================================================\n // Type Extraction Methods\n // ============================================================\n\n /**\n * Extract BOTH type info AND keys in a single pass\n *\n * One parse serves both extractions. It used to be one whole TypeScript\n * Program per file — halved from two by an earlier pass, which optimised\n * inside a premise that did not need to hold. `read-config-ast.ts` has the\n * numbers.\n *\n * @param configPath Absolute path to the config file\n * @param configName Config name (e.g., \"auth\", \"notifications\")\n * @returns Combined result with type info and keys\n */\n private async extractConfigInfo(\n configPath: string,\n configName: string,\n ): Promise<{\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }> {\n if (!(await this.exists(configPath))) {\n return { typeName: null, importSource: null, keys: [] };\n }\n\n // Parse, do not resolve — `read-config-ast.ts` explains why, with numbers.\n const sourceFile = await readConfigAst(configPath);\n\n if (!sourceFile) {\n return { typeName: null, importSource: null, keys: [] };\n }\n\n // === Type Info Extraction ===\n const importedTypes = new Map<string, string>();\n const localExportedTypes = new Set<string>();\n let foundTypeName: string | null = null;\n\n // === Keys Extraction ===\n const keys: string[] = [];\n\n const visitForTypes = (node: ts.Node): void => {\n // Collect imported types\n if (ts.isImportDeclaration(node)) {\n const moduleSpecifier = node.moduleSpecifier;\n if (ts.isStringLiteral(moduleSpecifier)) {\n const source = moduleSpecifier.text;\n const importClause = node.importClause;\n if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) {\n for (const element of importClause.namedBindings.elements) {\n importedTypes.set(element.name.text, source);\n }\n }\n }\n }\n\n // Collect locally exported types\n if (ts.isTypeAliasDeclaration(node)) {\n const modifiers = ts.getModifiers(node);\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\n localExportedTypes.add(node.name.text);\n }\n }\n\n // Collect locally exported interfaces\n if (ts.isInterfaceDeclaration(node)) {\n const modifiers = ts.getModifiers(node);\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\n localExportedTypes.add(node.name.text);\n }\n }\n\n // Find type used on config variable + extract keys\n if (ts.isVariableDeclaration(node)) {\n // Type info\n if (node.type && ts.isTypeReferenceNode(node.type)) {\n foundTypeName = node.type.typeName.getText(sourceFile);\n }\n\n // Keys extraction\n if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) {\n const visitKeys = (objNode: ts.ObjectLiteralExpression, prefix: string): void => {\n for (const prop of objNode.properties) {\n if (ts.isPropertyAssignment(prop) && prop.name) {\n const keyName = prop.name.getText(sourceFile);\n const fullKey = prefix ? `${prefix}.${keyName}` : keyName;\n keys.push(fullKey);\n if (ts.isObjectLiteralExpression(prop.initializer)) {\n visitKeys(prop.initializer, fullKey);\n }\n }\n }\n };\n visitKeys(node.initializer, configName);\n }\n }\n\n ts.forEachChild(node, visitForTypes);\n };\n\n ts.forEachChild(sourceFile, visitForTypes);\n\n // Resolve type info\n let typeName: string | null = null;\n let importSource: string | null = null;\n\n if (foundTypeName) {\n if (importedTypes.has(foundTypeName)) {\n typeName = foundTypeName;\n importSource = importedTypes.get(foundTypeName)!;\n } else if (localExportedTypes.has(foundTypeName)) {\n typeName = foundTypeName;\n const relativePath = Path.toRelative(configPath).replace(/\\.(ts|tsx)$/, \"\");\n importSource = `../../${relativePath}`;\n }\n }\n\n return { typeName, importSource, keys };\n }\n\n /**\n * Extract driver keys from storage config\n */\n private async extractStorageDriverKeys(configPath: string): Promise<string[]> {\n const absolutePath = resolve(configPath);\n\n if (!(await this.exists(absolutePath))) {\n devServeLog(`âš ï¸ Storage config not found: ${absolutePath}`);\n return [];\n }\n\n const sourceFile = await readConfigAst(absolutePath);\n\n if (!sourceFile) {\n devServeLog(`âš ï¸ Could not parse storage config: ${absolutePath}`);\n return [];\n }\n\n const keys: string[] = [];\n\n const visit = (node: ts.Node): void => {\n if (ts.isPropertyAssignment(node)) {\n const propName = node.name.getText(sourceFile);\n\n if (propName === \"drivers\" && ts.isObjectLiteralExpression(node.initializer)) {\n for (const prop of node.initializer.properties) {\n if (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) {\n const keyName = prop.name?.getText(sourceFile);\n\n if (keyName) {\n keys.push(keyName);\n }\n }\n }\n }\n }\n\n ts.forEachChild(node, visit);\n };\n\n ts.forEachChild(sourceFile, visit);\n\n return keys;\n }\n\n /**\n * Find a config file by name\n */\n private async findConfigFile(configName: string): Promise<string | undefined> {\n const possiblePaths = [`src/config/${configName}.ts`, `config/${configName}.ts`];\n\n for (const path of possiblePaths) {\n const fullPath = join(process.cwd(), path);\n\n if (await this.exists(fullPath)) {\n return path;\n }\n }\n\n try {\n const files = filesOrchestrator.getFiles();\n\n for (const [filePath] of files) {\n if (filePath.includes(`config/${configName}`)) {\n return filePath;\n }\n }\n } catch {\n // Files orchestrator not initialized yet\n }\n\n return undefined;\n }\n\n /**\n * Ensure output directory exists\n */\n private async ensureOutputDir(): Promise<void> {\n await ensureDirectoryAsync(this.outputDir);\n }\n\n /**\n * Check if a path exists (async wrapper)\n */\n private async exists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * The ports `runTypingsGeneration` needs, bound to this instance and this\n * package's dev logger.\n */\n private get typingsGenerationPorts(): TypingsGenerationPorts {\n return {\n generate: () => this.generateAll(),\n info: (message) => devLogInfo(message),\n success: (message) => devLogSuccess(message),\n error: (message) => devLogError(message),\n };\n }\n\n /**\n * Generate every config's typings.\n *\n * Runs IN THIS PROCESS. It used to spawn `npx warlock generate.typings`,\n * which could not resolve in a source checkout — see\n * `run-typings-generation.ts` for the whole story.\n */\n public async executeGenerateAllCommand(): Promise<void> {\n await runTypingsGeneration(this.typingsGenerationPorts);\n }\n\n /**\n * Regenerate typings after a batch reload — but only when the batch actually\n * touched a config file, since nothing else contributes to them.\n */\n public async executeTypingsGenerator(upcomingFiles: string[]): Promise<void> {\n const touchedAConfig = Array.from(new Set(upcomingFiles)).some((file) =>\n Path.normalize(file).includes(\"src/config/\"),\n );\n\n if (!touchedAConfig) return;\n\n /*\n The changed paths are deliberately NOT passed along. The previous version\n built a `files` array here and then threw it away, spawning the same\n whole-project command as the branch above — so \"incremental\" was a name,\n not a behaviour. `generateAll()` reads the orchestrator, which the batch\n reload has already updated, so the full pass is both correct and the only\n pass that ever actually ran.\n */\n await runTypingsGeneration(this.typingsGenerationPorts);\n }\n}\n\n/**\n * Singleton instance for use throughout dev-server\n */\nexport const typeGenerator = new TypeGenerator();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAa,gBAAb,MAA2B;;mBAIL,YAAY,SAAS;sBAKlB,KAAK,KAAK,WAAW,uBAAuB;kBAKxB;qCAKrB,IAAI,IAQxB;;;;;;;;;CASF,MAAa,cAA6B;EACxC,MAAM,KAAK,gBAAgB;EAE3B,MAAM,cAAc,KAAK,KAAK,WAAW,cAAc;EACvD,MAAM,aAAa,KAAK,KAAK,WAAW,aAAa;EAErD,MAAM,CAAC,gBAAgB,eAAe,gBAAgB,MAAM,QAAQ,IAAI;GACtE,KAAK,OAAO,KAAK,YAAY;GAC7B,KAAK,OAAO,WAAW;GACvB,KAAK,OAAO,UAAU;EACxB,CAAC;EAED,IAAI,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,cAExC,MAAM,KAAK,eAAe;OACrB;GAEL,MAAM,KAAK,aAAa;GAExB,MAAM,KAAK,UAAU;EACvB;EAEA,MAAM,KAAK,aAAa;CAC1B;;;;CAKA,MAAc,iBAAgC;EAE5C,MAAM,oBAAoB,MAAM,KAAK,eAAe,SAAS;EAC7D,IAAI,mBACF,KAAK,qBAAqB,iBAAiB;EAI7C,MAAM,KAAK,oBAAoB;CACjC;;;;CAKA,MAAc,YAA2B;EACvC,MAAM,QAAQ,kBAAkB,SAAS;EAEzC,IAAI,gBAAgB;EACpB,IAAI,iBAAiB;EAGrB,KAAK,MAAM,CAAC,MAAM,gBAAgB,OAChC,IAAI,KAAK,WAAW,oBAAoB,GAAG;GACzC,MAAM,gBAAgB,KAAK,UAAU;GACrC,IAAI,CAAC,iBAAiB,cAAc,eAAe,YAAY,MAE7D,MAAM,KAAK,qBAAqB,IAAI;GAEtC;EACF;EAIF,KAAK,MAAM,CAAC,MAAM,gBAAgB,OAAO;GACvC,IAAI,CAAC,KAAK,WAAW,aAAa,GAAG;GACrC,IAAI,KAAK,SAAS,OAAO,GAAG;GAG5B,MAAM,aAAa,KAAK,QAAQ,eAAe,EAAE,CAAC,CAAC,QAAQ,YAAY,EAAE;GAEzE,MAAM,gBAAgB,KAAK,UAAU,OAAO;GAE5C,IAAI,CAAC,iBAAiB,cAAc,eAAe,YAAY,MAAM;IAEnE,gBAAgB;IAChB,MAAM,OAAO,MAAM,KAAK,kBAAkB,YAAY,cAAc,UAAU;IAE9E,KAAK,YAAY,IAAI,YAAY;KAC/B,YAAY,YAAY;KACxB,UAAU,KAAK;KACf,cAAc,KAAK;KACnB,MAAM,KAAK;IACb,CAAC;GACH,OAAO;IAEL;IACA,KAAK,YAAY,IAAI,YAAY,aAAa;GAChD;EACF;EAEA,IAAI,eACF,MAAM,KAAK,0BAA0B;OAErC,WAAW,2BAA2B,eAAe,iBAAiB;CAE1E;;;;CAKA,MAAa,qBAAqB,YAAmC;EACnE,IAAI;GACF,MAAM,aAAa,MAAM,KAAK,yBAAyB,UAAU;GAEjE,IAAI,WAAW,WAAW,GAAG;IAC3B,YAAY,8CAA2C;IACvD;GACF;GAIA,MAAM,aADc,kBAAkB,SAAS,CAAC,CAAC,IAAI,UACxB,CAAC,EAAE,QAAQ;GAGxC,IAAI,CAAC,KAAK,UACR,KAAK,WAAW,KAAK,oBAAoB;GAE3C,KAAK,SAAS,UAAU;IACtB;IACA,SAAS;GACX;GAIA,MAAM,UAAU;qBACD,WAAW;;;;;;;EAHD,WAAW,KAAK,MAAM,OAAO,KAAK,eAAe,CAAC,EAAE,QAAQ,CAAC,CAAC,KAAK,IAUjF,EAAE;;;;GAMb,MAAM,UADa,KAAK,KAAK,WAAW,cACf,GAAG,SAAS,OAAO;GAE5C,cAAc,4BAA4B,WAAW,KAAK,IAAI,GAAG;EACnE,SAAS,OAAO;GACd,YAAY,4CAA4C,OAAO;EACjE;CACF;;;;CAKA,AAAO,sBAAsB,aAA8B;EACzD,OAAO,YAAY,SAAS,aAAa,KAAK,YAAY,SAAS,SAAS;CAC9E;;;;CAKA,MAAa,iBAAiB,aAAoC;EAChE,IAAI,CAAC,KAAK,sBAAsB,WAAW,GACzC;EAIF,IAAI,YAAY,SAAS,gBAAgB,GAAG;GAC1C,MAAM,KAAK,qBAAqB,WAAW;GAC3C,MAAM,KAAK,aAAa;GACxB;EACF;EAGA,MAAM,QAAQ,YAAY,MAAM,yBAAyB;EACzD,IAAI,CAAC,OACH;EAGF,MAAM,aAAa,MAAM;EACzB,IAAI,eAAe,SAAS;EAE5B,WAAW,mBAAmB,WAAW,cAAc;EAIvD,MAAM,aADc,kBAAkB,SAAS,CAAC,CAAC,IAAI,WACxB,CAAC,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,SAAS;EAI5D,MAAM,aAAa,KADD,KAAK,QAAQ,IAAI,GAAG,YACN,GAAG,GAAG,WAAW,IAAI;EAErD,MAAM,OAAO,MAAM,KAAK,kBAAkB,YAAY,UAAU;EAEhE,KAAK,YAAY,IAAI,YAAY;GAC/B;GACA,UAAU,KAAK;GACf,cAAc,KAAK;GACnB,MAAM,KAAK;EACb,CAAC;EAGD,MAAM,KAAK,0BAA0B;EACrC,MAAM,KAAK,aAAa;CAC1B;;;;CAKA,MAAa,sBAAqC;EAChD,IAAI;GACF,MAAM,QAAQ,kBAAkB,SAAS;GAGzC,KAAK,YAAY,MAAM;GAEvB,KAAK,MAAM,CAAC,MAAM,gBAAgB,OAAO;IACvC,IAAI,CAAC,KAAK,WAAW,aAAa,GAAG;IACrC,IAAI,KAAK,SAAS,OAAO,GAAG;IAG5B,MAAM,aAAa,KAAK,QAAQ,eAAe,EAAE,CAAC,CAAC,QAAQ,YAAY,EAAE;IAGzE,MAAM,OAAO,MAAM,KAAK,kBAAkB,YAAY,cAAc,UAAU;IAE9E,KAAK,YAAY,IAAI,YAAY;KAC/B,YAAY,YAAY;KACxB,UAAU,KAAK;KACf,cAAc,KAAK;KACnB,MAAM,KAAK;IACb,CAAC;GACH;GAEA,MAAM,KAAK,0BAA0B;EACvC,SAAS,OAAO;GACd,YAAY,2CAA2C,OAAO;EAChE;CACF;;;;;;;CAQA,AAAQ,eAAe,MAAsB;EAC3C,OAAO,6BAA6B,KAAK,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI;CAC7E;;;;CAKA,MAAc,4BAA2C;EACvD,MAAM,kBAID,CAAC;EACN,MAAM,UAAoB,CAAC;EAG3B,IAAI,CAAC,KAAK,UACR,KAAK,WAAW,KAAK,oBAAoB;EAE3C,KAAK,SAAS,SAAS,CAAC;EAExB,KAAK,MAAM,CAAC,MAAM,SAAS,KAAK,aAAa;GAC3C,gBAAgB,KAAK;IACnB;IACA,UAAU,KAAK;IACf,cAAc,KAAK;GACrB,CAAC;GACD,QAAQ,KAAK,GAAG,KAAK,IAAI;GAGzB,KAAK,SAAS,OAAO,QAAQ;EAC/B;EAGA,MAAM,0BAAU,IAAI,IAAyB;EAC7C,KAAK,MAAM,QAAQ,iBACjB,IAAI,KAAK,YAAY,KAAK,cAAc;GACtC,IAAI,CAAC,QAAQ,IAAI,KAAK,YAAY,GAChC,QAAQ,IAAI,KAAK,8BAAc,IAAI,IAAI,CAAC;GAE1C,QAAQ,IAAI,KAAK,YAAY,CAAC,CAAE,IAAI,KAAK,QAAQ;EACnD;EAaF,MAAM,UAAU;;;EAVS,MAAM,KAAK,QAAQ,QAAQ,CAAC,CAAC,CACnD,KAAK,CAAC,QAAQ,WAAW,iBAAiB,MAAM,KAAK,KAAK,CAAC,CAAC,KAAK,IAAI,EAAE,WAAW,OAAO,GAAG,CAAC,CAC7F,KAAK,IAWK,EAAE;;;;;EATO,gBACnB,KAAK,SAAS,OAAO,KAAK,eAAe,KAAK,IAAI,EAAE,IAAI,KAAK,YAAY,UAAU,EAAE,CAAC,CACtF,KAAK,IAYE,EAAE;;;;EAVO,QAAQ,KAAK,QAAQ,QAAQ,IAAI,SAAS,CAAC,CAAC,KAAK,IAc7D,EAAE;;;;EAMT,MAAM,UADa,KAAK,KAAK,WAAW,aACf,GAAG,SAAS,OAAO;EAE5C,cACE,2BAA2B,KAAK,YAAY,KAAK,YAAY,QAAQ,OAAO,MAC9E;CACF;;;;CASA,MAAc,eAAiC;EAC7C,IAAI;GACF,IAAI,MAAM,KAAK,OAAO,KAAK,YAAY,GAAG;IACxC,MAAM,UAAU,MAAM,SAAS,KAAK,cAAc,OAAO;IACzD,KAAK,WAAW,KAAK,MAAM,OAAO;IAClC,OAAO;GACT;EACF,QAAQ,CAER;EACA,KAAK,WAAW;EAChB,OAAO;CACT;;;;CAKA,MAAc,eAA8B;EAC1C,IAAI,CAAC,KAAK,UACR,KAAK,WAAW,KAAK,oBAAoB;EAE3C,KAAK,SAAS,gBAAgB,KAAK,IAAI;EAEvC,MAAM,UAAU,KAAK,cAAc,KAAK,UAAU,KAAK,UAAU,MAAM,CAAC,GAAG,OAAO;CACpF;;;;CAKA,AAAQ,sBAAuC;EAC7C,OAAO;GACL,SAAS;GACT,eAAe,KAAK,IAAI;GACxB,SAAS;GACT,QAAQ,CAAC;EACX;CACF;;;;;;;;;;;;;CAkBA,MAAc,kBACZ,YACA,YAKC;EACD,IAAI,CAAE,MAAM,KAAK,OAAO,UAAU,GAChC,OAAO;GAAE,UAAU;GAAM,cAAc;GAAM,MAAM,CAAC;EAAE;EAIxD,MAAM,aAAa,MAAM,cAAc,UAAU;EAEjD,IAAI,CAAC,YACH,OAAO;GAAE,UAAU;GAAM,cAAc;GAAM,MAAM,CAAC;EAAE;EAIxD,MAAM,gCAAgB,IAAI,IAAoB;EAC9C,MAAM,qCAAqB,IAAI,IAAY;EAC3C,IAAI,gBAA+B;EAGnC,MAAM,OAAiB,CAAC;EAExB,MAAM,iBAAiB,SAAwB;GAE7C,IAAI,GAAG,oBAAoB,IAAI,GAAG;IAChC,MAAM,kBAAkB,KAAK;IAC7B,IAAI,GAAG,gBAAgB,eAAe,GAAG;KACvC,MAAM,SAAS,gBAAgB;KAC/B,MAAM,eAAe,KAAK;KAC1B,IAAI,cAAc,iBAAiB,GAAG,eAAe,aAAa,aAAa,GAC7E,KAAK,MAAM,WAAW,aAAa,cAAc,UAC/C,cAAc,IAAI,QAAQ,KAAK,MAAM,MAAM;IAGjD;GACF;GAGA,IAAI,GAAG,uBAAuB,IAAI,GAEhC;QADkB,GAAG,aAAa,IACtB,CAAC,EAAE,MAAM,MAAM,EAAE,SAAS,GAAG,WAAW,aAAa,GAC/D,mBAAmB,IAAI,KAAK,KAAK,IAAI;GACvC;GAIF,IAAI,GAAG,uBAAuB,IAAI,GAEhC;QADkB,GAAG,aAAa,IACtB,CAAC,EAAE,MAAM,MAAM,EAAE,SAAS,GAAG,WAAW,aAAa,GAC/D,mBAAmB,IAAI,KAAK,KAAK,IAAI;GACvC;GAIF,IAAI,GAAG,sBAAsB,IAAI,GAAG;IAElC,IAAI,KAAK,QAAQ,GAAG,oBAAoB,KAAK,IAAI,GAC/C,gBAAgB,KAAK,KAAK,SAAS,QAAQ,UAAU;IAIvD,IAAI,KAAK,eAAe,GAAG,0BAA0B,KAAK,WAAW,GAAG;KACtE,MAAM,aAAa,SAAqC,WAAyB;MAC/E,KAAK,MAAM,QAAQ,QAAQ,YACzB,IAAI,GAAG,qBAAqB,IAAI,KAAK,KAAK,MAAM;OAC9C,MAAM,UAAU,KAAK,KAAK,QAAQ,UAAU;OAC5C,MAAM,UAAU,SAAS,GAAG,OAAO,GAAG,YAAY;OAClD,KAAK,KAAK,OAAO;OACjB,IAAI,GAAG,0BAA0B,KAAK,WAAW,GAC/C,UAAU,KAAK,aAAa,OAAO;MAEvC;KAEJ;KACA,UAAU,KAAK,aAAa,UAAU;IACxC;GACF;GAEA,GAAG,aAAa,MAAM,aAAa;EACrC;EAEA,GAAG,aAAa,YAAY,aAAa;EAGzC,IAAI,WAA0B;EAC9B,IAAI,eAA8B;EAElC,IAAI,eACF;OAAI,cAAc,IAAI,aAAa,GAAG;IACpC,WAAW;IACX,eAAe,cAAc,IAAI,aAAa;GAChD,OAAO,IAAI,mBAAmB,IAAI,aAAa,GAAG;IAChD,WAAW;IAEX,eAAe,SADM,KAAK,WAAW,UAAU,CAAC,CAAC,QAAQ,eAAe,EACrC;GACrC;;EAGF,OAAO;GAAE;GAAU;GAAc;EAAK;CACxC;;;;CAKA,MAAc,yBAAyB,YAAuC;EAC5E,MAAM,eAAe,QAAQ,UAAU;EAEvC,IAAI,CAAE,MAAM,KAAK,OAAO,YAAY,GAAI;GACtC,YAAY,oCAAoC,cAAc;GAC9D,OAAO,CAAC;EACV;EAEA,MAAM,aAAa,MAAM,cAAc,YAAY;EAEnD,IAAI,CAAC,YAAY;GACf,YAAY,0CAA0C,cAAc;GACpE,OAAO,CAAC;EACV;EAEA,MAAM,OAAiB,CAAC;EAExB,MAAM,SAAS,SAAwB;GACrC,IAAI,GAAG,qBAAqB,IAAI,GAG9B;QAFiB,KAAK,KAAK,QAAQ,UAExB,MAAM,aAAa,GAAG,0BAA0B,KAAK,WAAW,GACzE;UAAK,MAAM,QAAQ,KAAK,YAAY,YAClC,IAAI,GAAG,qBAAqB,IAAI,KAAK,GAAG,8BAA8B,IAAI,GAAG;MAC3E,MAAM,UAAU,KAAK,MAAM,QAAQ,UAAU;MAE7C,IAAI,SACF,KAAK,KAAK,OAAO;KAErB;IACF;GACF;GAGF,GAAG,aAAa,MAAM,KAAK;EAC7B;EAEA,GAAG,aAAa,YAAY,KAAK;EAEjC,OAAO;CACT;;;;CAKA,MAAc,eAAe,YAAiD;EAC5E,MAAM,gBAAgB,CAAC,cAAc,WAAW,MAAM,UAAU,WAAW,IAAI;EAE/E,KAAK,MAAM,QAAQ,eAAe;GAChC,MAAM,WAAW,KAAK,QAAQ,IAAI,GAAG,IAAI;GAEzC,IAAI,MAAM,KAAK,OAAO,QAAQ,GAC5B,OAAO;EAEX;EAEA,IAAI;GACF,MAAM,QAAQ,kBAAkB,SAAS;GAEzC,KAAK,MAAM,CAAC,aAAa,OACvB,IAAI,SAAS,SAAS,UAAU,YAAY,GAC1C,OAAO;EAGb,QAAQ,CAER;CAGF;;;;CAKA,MAAc,kBAAiC;EAC7C,MAAM,qBAAqB,KAAK,SAAS;CAC3C;;;;CAKA,MAAc,OAAO,MAAgC;EACnD,IAAI;GACF,MAAM,OAAO,MAAM,UAAU,IAAI;GACjC,OAAO;EACT,QAAQ;GACN,OAAO;EACT;CACF;;;;;CAMA,IAAY,yBAAiD;EAC3D,OAAO;GACL,gBAAgB,KAAK,YAAY;GACjC,OAAO,YAAY,WAAW,OAAO;GACrC,UAAU,YAAY,cAAc,OAAO;GAC3C,QAAQ,YAAY,YAAY,OAAO;EACzC;CACF;;;;;;;;CASA,MAAa,4BAA2C;EACtD,MAAM,qBAAqB,KAAK,sBAAsB;CACxD;;;;;CAMA,MAAa,wBAAwB,eAAwC;EAK3E,IAAI,CAJmB,MAAM,KAAK,IAAI,IAAI,aAAa,CAAC,CAAC,CAAC,MAAM,SAC9D,KAAK,UAAU,IAAI,CAAC,CAAC,SAAS,aAAa,CAG3B,GAAG;EAUrB,MAAM,qBAAqB,KAAK,sBAAsB;CACxD;AACF;;;;AAKA,MAAa,gBAAgB,IAAI,cAAc"}
|
|
1
|
+
{"version":3,"file":"type-generator.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/type-generator.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\nimport { constants } from \"fs\";\nimport { access, readFile, writeFile } from \"fs/promises\";\nimport { join, resolve } from \"path\";\nimport ts from \"typescript\";\nimport { warlockPath } from \"../utils\";\nimport { devLogError, devLogInfo, devLogSuccess, devServeLog } from \"./dev-logger\";\nimport { readConfigAst } from \"./read-config-ast\";\nimport { runTypingsGeneration, type TypingsGenerationPorts } from \"./run-typings-generation\";\nimport { filesOrchestrator } from \"./files-orchestrator\";\nimport { Path } from \"../utils/normalized-path\";\n\n/**\n * Typings manifest structure for tracking file hashes\n */\ntype TypingsManifest = {\n version: string;\n lastBuildTime: number;\n storage: {\n sourceHash: string;\n drivers: string[];\n } | null;\n config: Record<\n string,\n {\n sourceHash: string;\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }\n >;\n};\n\n/**\n * TypeGenerator - Generates TypeScript type definitions from config files\n *\n * Parses source config files using the TypeScript Compiler API to extract\n * keys and generate module augmentation types for IDE autocomplete.\n *\n * Uses manifest-based reconciliation to only regenerate when source files change.\n */\nexport class TypeGenerator {\n /**\n * Output directory for generated typings\n */\n private outputDir = warlockPath(\"typings\");\n\n /**\n * Path to typings manifest file\n */\n private manifestPath = join(this.outputDir, \"typings-manifest.json\");\n\n /**\n * Cached manifest data\n */\n private manifest: TypingsManifest | null = null;\n\n /**\n * Cache for config type info and keys\n */\n private configCache = new Map<\n string,\n {\n sourceHash: string;\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }\n >();\n\n /**\n * Generate all framework type definitions\n *\n * Uses manifest-based reconciliation:\n * - If output files don't exist: full regeneration\n * - If files exist: only regenerate changed configs\n */\n public async generateAll(): Promise<void> {\n await this.ensureOutputDir();\n\n const storageFile = join(this.outputDir, \"storage.d.ts\");\n const configFile = join(this.outputDir, \"config.d.ts\");\n\n const [manifestExists, storageExists, configExists] = await Promise.all([\n this.exists(this.manifestPath),\n this.exists(storageFile),\n this.exists(configFile),\n ]);\n\n if (!manifestExists || !storageExists || !configExists) {\n // Full regeneration (first run or files deleted)\n await this.fullGeneration();\n } else {\n // Load manifest for hash comparison\n await this.loadManifest();\n // Reconciliation: only regenerate changed files\n await this.reconcile();\n }\n\n await this.saveManifest();\n }\n\n /**\n * Full regeneration of all type files\n */\n private async fullGeneration(): Promise<void> {\n // Generate storage types\n const storageConfigPath = await this.findConfigFile(\"storage\");\n if (storageConfigPath) {\n this.generateStorageTypes(storageConfigPath);\n }\n\n // Generate config types\n await this.generateConfigTypes();\n }\n\n /**\n * Reconcile: only regenerate changed files\n */\n private async reconcile(): Promise<void> {\n const files = filesOrchestrator.getFiles();\n let storageChanged = false;\n let configChanged = false;\n let unchangedCount = 0;\n\n // Check storage config\n for (const [path, fileManager] of files) {\n if (path.startsWith(\"src/config/storage\")) {\n const manifestEntry = this.manifest?.storage;\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\n storageChanged = true;\n await this.generateStorageTypes(path);\n }\n break;\n }\n }\n\n // Check config files\n for (const [path, fileManager] of files) {\n if (!path.startsWith(\"src/config/\")) continue;\n if (path.includes(\"index\")) continue;\n\n // Extract config name (remove dir prefix and extension)\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\n\n const manifestEntry = this.manifest?.config[configName];\n\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\n // File changed or new - regenerate\n configChanged = true;\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\n\n this.configCache.set(configName, {\n sourceHash: fileManager.hash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n } else {\n // Unchanged - load from manifest\n unchangedCount++;\n this.configCache.set(configName, manifestEntry);\n }\n }\n\n if (configChanged) {\n await this.writeConfigTypesFromCache();\n } else {\n devLogInfo(`Config types unchanged (${unchangedCount} configs cached)`);\n }\n }\n\n /**\n * Generate storage driver name types\n */\n public async generateStorageTypes(configPath: string): Promise<void> {\n try {\n const driverKeys = await this.extractStorageDriverKeys(configPath);\n\n if (driverKeys.length === 0) {\n devServeLog(\"âš ï¸ No storage drivers found in config\");\n return;\n }\n\n // Get file hash from filesOrchestrator\n const fileManager = filesOrchestrator.getFiles().get(configPath);\n const sourceHash = fileManager?.hash || \"\";\n\n // Update manifest\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.storage = {\n sourceHash,\n drivers: driverKeys,\n };\n\n const interfaceContent = driverKeys\n .map((k) => ` ${this.toInterfaceKey(k)}: true;`)\n .join(\"\\n\");\n\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\n// Generated from: ${configPath}\n// Regenerates on dev-server start and when storage config changes\n\nimport \"@warlock.js/core\";\n\ndeclare module \"@warlock.js/core\" {\n interface StorageDriverRegistry {\n${interfaceContent}\n }\n}\n`;\n\n const outputPath = join(this.outputDir, \"storage.d.ts\");\n await writeFile(outputPath, content, \"utf-8\");\n\n devLogSuccess(`Generated storage types: ${driverKeys.join(\", \")}`);\n } catch (error) {\n devServeLog(`âš ï¸ Failed to generate storage types: ${error}`);\n }\n }\n\n /**\n * Check if a file change should trigger type regeneration\n */\n public shouldRegenerateTypes(changedPath: string): boolean {\n return changedPath.includes(\"src/config/\") || changedPath.includes(\"config/\");\n }\n\n /**\n * Handle file change - uses incremental update via cache\n */\n public async handleFileChange(changedPath: string): Promise<void> {\n if (!this.shouldRegenerateTypes(changedPath)) {\n return;\n }\n\n // Regenerate storage types if storage config changed\n if (changedPath.includes(\"config/storage\")) {\n await this.generateStorageTypes(changedPath);\n await this.saveManifest();\n return;\n }\n\n // Extract config name from path\n const match = changedPath.match(/config\\/([^/]+)\\.[^.]+$/);\n if (!match) {\n return;\n }\n\n const configName = match[1];\n if (configName === \"index\") return;\n\n devLogInfo(`Config changed: ${configName}, updating...`);\n\n // Get file manager for hash\n const fileManager = filesOrchestrator.getFiles().get(changedPath);\n const sourceHash = fileManager?.hash || Date.now().toString();\n\n // Update only the changed config in cache (use optimized combined extraction)\n const configDir = join(process.cwd(), \"src/config\");\n const configPath = join(configDir, `${configName}.ts`);\n\n const info = await this.extractConfigInfo(configPath, configName);\n\n this.configCache.set(configName, {\n sourceHash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n\n // Regenerate config.d.ts from cache\n await this.writeConfigTypesFromCache();\n await this.saveManifest();\n }\n\n /**\n * Generate config types - populates cache and writes file\n */\n public async generateConfigTypes(): Promise<void> {\n try {\n const files = filesOrchestrator.getFiles();\n\n // Clear and repopulate cache\n this.configCache.clear();\n\n for (const [path, fileManager] of files) {\n if (!path.startsWith(\"src/config/\")) continue;\n if (path.includes(\"index\")) continue;\n\n // Extract config name\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\n\n // One parse per config, both extractions off it — see `read-config-ast.ts`.\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\n\n this.configCache.set(configName, {\n sourceHash: fileManager.hash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n }\n\n await this.writeConfigTypesFromCache();\n } catch (error) {\n devServeLog(`âš ï¸ Failed to generate config types: ${error}`);\n }\n }\n\n /**\n * Quote an interface key that isn't a valid JS identifier (e.g. a config\n * named `use-cases` or a storage driver `do-spaces`) so the generated\n * `.d.ts` stays syntactically valid. Identifier-safe names are left bare\n * to keep the output clean.\n */\n private toInterfaceKey(name: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);\n }\n\n /**\n * Write config.d.ts from cached data\n */\n private async writeConfigTypesFromCache(): Promise<void> {\n const configTypeInfos: Array<{\n name: string;\n typeName: string | null;\n importSource: string | null;\n }> = [];\n const allKeys: string[] = [];\n\n // Update manifest config section\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.config = {};\n\n for (const [name, data] of this.configCache) {\n configTypeInfos.push({\n name,\n typeName: data.typeName,\n importSource: data.importSource,\n });\n allKeys.push(...data.keys);\n\n // Store in manifest\n this.manifest.config[name] = data;\n }\n\n // Group imports by source\n const imports = new Map<string, Set<string>>();\n for (const info of configTypeInfos) {\n if (info.typeName && info.importSource) {\n if (!imports.has(info.importSource)) {\n imports.set(info.importSource, new Set());\n }\n imports.get(info.importSource)!.add(info.typeName);\n }\n }\n\n const importStatements = Array.from(imports.entries())\n .map(([source, types]) => `import type { ${Array.from(types).join(\", \")} } from \"${source}\";`)\n .join(\"\\n\");\n\n const configEntries = configTypeInfos\n .map((info) => ` ${this.toInterfaceKey(info.name)}: ${info.typeName || \"unknown\"};`)\n .join(\"\\n\");\n\n const keyEntries = allKeys.map((key) => ` \"${key}\": true;`).join(\"\\n\");\n\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\n// Regenerates on dev-server start and when config files change\n\n${importStatements}\nimport \"@warlock.js/core\";\n\ndeclare module \"@warlock.js/core\" {\n interface ConfigRegistry {\n${configEntries}\n }\n\n interface ConfigKeyRegistry {\n${keyEntries}\n }\n}\n`;\n\n const outputPath = join(this.outputDir, \"config.d.ts\");\n await writeFile(outputPath, content, \"utf-8\");\n\n devLogSuccess(\n `Generated config types: ${this.configCache.size} configs, ${allKeys.length} keys`,\n );\n }\n\n // ============================================================\n // Manifest Management\n // ============================================================\n\n /**\n * Load manifest from disk\n */\n private async loadManifest(): Promise<boolean> {\n try {\n if (await this.exists(this.manifestPath)) {\n const content = await readFile(this.manifestPath, \"utf-8\");\n this.manifest = JSON.parse(content);\n return true;\n }\n } catch {\n // Manifest corrupted or missing\n }\n this.manifest = null;\n return false;\n }\n\n /**\n * Save manifest to disk\n */\n private async saveManifest(): Promise<void> {\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.lastBuildTime = Date.now();\n\n await writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2), \"utf-8\");\n }\n\n /**\n * Create empty manifest structure\n */\n private createEmptyManifest(): TypingsManifest {\n return {\n version: \"1.0.0\",\n lastBuildTime: Date.now(),\n storage: null,\n config: {},\n };\n }\n\n // ============================================================\n // Type Extraction Methods\n // ============================================================\n\n /**\n * Extract BOTH type info AND keys in a single pass\n *\n * One parse serves both extractions. It used to be one whole TypeScript\n * Program per file — halved from two by an earlier pass, which optimised\n * inside a premise that did not need to hold. `read-config-ast.ts` has the\n * numbers.\n *\n * @param configPath Absolute path to the config file\n * @param configName Config name (e.g., \"auth\", \"notifications\")\n * @returns Combined result with type info and keys\n */\n private async extractConfigInfo(\n configPath: string,\n configName: string,\n ): Promise<{\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }> {\n if (!(await this.exists(configPath))) {\n return { typeName: null, importSource: null, keys: [] };\n }\n\n // Parse, do not resolve — `read-config-ast.ts` explains why, with numbers.\n const sourceFile = await readConfigAst(configPath);\n\n if (!sourceFile) {\n return { typeName: null, importSource: null, keys: [] };\n }\n\n // === Type Info Extraction ===\n const importedTypes = new Map<string, string>();\n const localExportedTypes = new Set<string>();\n let foundTypeName: string | null = null;\n\n // === Keys Extraction ===\n const keys: string[] = [];\n\n const visitForTypes = (node: ts.Node): void => {\n // Collect imported types\n if (ts.isImportDeclaration(node)) {\n const moduleSpecifier = node.moduleSpecifier;\n if (ts.isStringLiteral(moduleSpecifier)) {\n const source = moduleSpecifier.text;\n const importClause = node.importClause;\n if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) {\n for (const element of importClause.namedBindings.elements) {\n importedTypes.set(element.name.text, source);\n }\n }\n }\n }\n\n // Collect locally exported types\n if (ts.isTypeAliasDeclaration(node)) {\n const modifiers = ts.getModifiers(node);\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\n localExportedTypes.add(node.name.text);\n }\n }\n\n // Collect locally exported interfaces\n if (ts.isInterfaceDeclaration(node)) {\n const modifiers = ts.getModifiers(node);\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\n localExportedTypes.add(node.name.text);\n }\n }\n\n // Find type used on config variable + extract keys\n if (ts.isVariableDeclaration(node)) {\n // Type info\n if (node.type && ts.isTypeReferenceNode(node.type)) {\n foundTypeName = node.type.typeName.getText(sourceFile);\n }\n\n // Keys extraction\n if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) {\n const visitKeys = (objNode: ts.ObjectLiteralExpression, prefix: string): void => {\n for (const prop of objNode.properties) {\n if (ts.isPropertyAssignment(prop) && prop.name) {\n const keyName = prop.name.getText(sourceFile);\n const fullKey = prefix ? `${prefix}.${keyName}` : keyName;\n keys.push(fullKey);\n if (ts.isObjectLiteralExpression(prop.initializer)) {\n visitKeys(prop.initializer, fullKey);\n }\n }\n }\n };\n visitKeys(node.initializer, configName);\n }\n }\n\n ts.forEachChild(node, visitForTypes);\n };\n\n ts.forEachChild(sourceFile, visitForTypes);\n\n // Resolve type info\n let typeName: string | null = null;\n let importSource: string | null = null;\n\n if (foundTypeName) {\n if (importedTypes.has(foundTypeName)) {\n typeName = foundTypeName;\n importSource = importedTypes.get(foundTypeName)!;\n } else if (localExportedTypes.has(foundTypeName)) {\n typeName = foundTypeName;\n const relativePath = Path.toRelative(configPath).replace(/\\.(ts|tsx)$/, \"\");\n importSource = `../../${relativePath}`;\n }\n }\n\n return { typeName, importSource, keys };\n }\n\n /**\n * Extract driver keys from storage config\n */\n private async extractStorageDriverKeys(configPath: string): Promise<string[]> {\n const absolutePath = resolve(configPath);\n\n if (!(await this.exists(absolutePath))) {\n devServeLog(`âš ï¸ Storage config not found: ${absolutePath}`);\n return [];\n }\n\n const sourceFile = await readConfigAst(absolutePath);\n\n if (!sourceFile) {\n devServeLog(`âš ï¸ Could not parse storage config: ${absolutePath}`);\n return [];\n }\n\n const keys: string[] = [];\n\n const visit = (node: ts.Node): void => {\n if (ts.isPropertyAssignment(node)) {\n const propName = node.name.getText(sourceFile);\n\n if (propName === \"drivers\" && ts.isObjectLiteralExpression(node.initializer)) {\n for (const prop of node.initializer.properties) {\n if (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) {\n const keyName = prop.name?.getText(sourceFile);\n\n if (keyName) {\n keys.push(keyName);\n }\n }\n }\n }\n }\n\n ts.forEachChild(node, visit);\n };\n\n ts.forEachChild(sourceFile, visit);\n\n return keys;\n }\n\n /**\n * Find a config file by name\n */\n private async findConfigFile(configName: string): Promise<string | undefined> {\n const possiblePaths = [`src/config/${configName}.ts`, `config/${configName}.ts`];\n\n for (const path of possiblePaths) {\n const fullPath = join(process.cwd(), path);\n\n if (await this.exists(fullPath)) {\n return path;\n }\n }\n\n try {\n const files = filesOrchestrator.getFiles();\n\n for (const [filePath] of files) {\n if (filePath.includes(`config/${configName}`)) {\n return filePath;\n }\n }\n } catch {\n // Files orchestrator not initialized yet\n }\n\n return undefined;\n }\n\n /**\n * Ensure output directory exists\n */\n private async ensureOutputDir(): Promise<void> {\n await ensureDirectoryAsync(this.outputDir);\n }\n\n /**\n * Check if a path exists (async wrapper)\n */\n private async exists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * The ports `runTypingsGeneration` needs, bound to this instance and this\n * package's dev logger.\n */\n private get typingsGenerationPorts(): TypingsGenerationPorts {\n return {\n generate: () => this.generateAll(),\n info: (message) => devLogInfo(message),\n success: (message) => devLogSuccess(message),\n error: (message) => devLogError(message),\n };\n }\n\n /**\n * Generate every config's typings.\n *\n * Runs IN THIS PROCESS. It used to spawn `npx warlock generate.typings`,\n * which could not resolve in a source checkout — see\n * `run-typings-generation.ts` for the whole story.\n */\n public async executeGenerateAllCommand(): Promise<void> {\n await runTypingsGeneration(this.typingsGenerationPorts);\n }\n\n /**\n * Regenerate typings after a batch reload — but only when the batch actually\n * touched a config file, since nothing else contributes to them.\n */\n public async executeTypingsGenerator(upcomingFiles: string[]): Promise<void> {\n const touchedAConfig = Array.from(new Set(upcomingFiles)).some((file) =>\n Path.normalize(file).includes(\"src/config/\"),\n );\n\n if (!touchedAConfig) return;\n\n /*\n The changed paths are deliberately NOT passed along. The previous version\n built a `files` array here and then threw it away, spawning the same\n whole-project command as the branch above — so \"incremental\" was a name,\n not a behaviour. `generateAll()` reads the orchestrator, which the batch\n reload has already updated, so the full pass is both correct and the only\n pass that ever actually ran.\n */\n await runTypingsGeneration(this.typingsGenerationPorts);\n }\n}\n\n/**\n * Singleton instance for use throughout dev-server\n */\nexport const typeGenerator = new TypeGenerator();\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/types.ts"],"mappings":";KAAY,QAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/types.ts"],"mappings":";KAAY,QAAA;AAAA,KAGA,YAAA;EACV,YAAA;EACA,YAAA;EACA,YAAA;EACA,IAAA;EACA,YAAA;EALsB;;;;;;;EAatB,oBAAA;EACA,UAAA;EACA,IAAA,EAAM,QAAQ;AAAA;;;AAAA;AAWhB;;;;AAAqB;KAAT,SAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/utils.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\nimport glob from \"fast-glob\";\nimport { srcPath, warlockPath } from \"../utils\";\nimport { Path } from \"../utils/normalized-path\";\n\n/**\n * Ensure the .warlock/ directory exists. Used by the loader-hook\n * registration which writes its bundled hook file into this directory.\n */\nexport async function ensureWarlockDirectory() {\n await ensureDirectoryAsync(warlockPath());\n}\n\n/**\n * Glob the project's `src/` directory for `.ts`/`.tsx` files. Returns\n * normalised absolute paths.\n */\nexport async function getFilesFromDirectory(directoryPath = srcPath(), pattern = \"**/*.{ts,tsx}\") {\n const files = await glob(`${Path.normalize(directoryPath)}/${pattern}`, { absolute: true });\n return files.map(file => Path.normalize(file));\n}\n\nexport async function getCertainFilesFromDirectory(directoryPath: string, filesNames: string[]) {\n const pattern = filesNames.length === 1 ? filesNames[0] : `(${filesNames.join(\"|\")})`;\n return getFilesFromDirectory(directoryPath, pattern + \".{ts,tsx}\");\n}\n\nexport function areSetsEqual<T>(set1: Set<T>, set2: Set<T>): boolean {\n if (set1.size !== set2.size) return false;\n for (const item of set1) {\n if (!set2.has(item)) return false;\n }\n return true;\n}\n"],"mappings":";;;;;;;;;;;AASA,eAAsB,yBAAyB;CAC7C,MAAM,qBAAqB,YAAY,CAAC;AAC1C;;;;;AAMA,eAAsB,sBAAsB,gBAAgB,QAAQ,GAAG,UAAU,iBAAiB;CAEhG,QAAO,MADa,KAAK,GAAG,KAAK,UAAU,aAAa,EAAE,GAAG,WAAW,EAAE,UAAU,KAAK,CAAC,EAC9E,CAAC,
|
|
1
|
+
{"version":3,"file":"utils.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/utils.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\nimport glob from \"fast-glob\";\nimport { srcPath, warlockPath } from \"../utils\";\nimport { Path } from \"../utils/normalized-path\";\n\n/**\n * Ensure the .warlock/ directory exists. Used by the loader-hook\n * registration which writes its bundled hook file into this directory.\n */\nexport async function ensureWarlockDirectory() {\n await ensureDirectoryAsync(warlockPath());\n}\n\n/**\n * Glob the project's `src/` directory for `.ts`/`.tsx` files. Returns\n * normalised absolute paths.\n */\nexport async function getFilesFromDirectory(directoryPath = srcPath(), pattern = \"**/*.{ts,tsx}\") {\n const files = await glob(`${Path.normalize(directoryPath)}/${pattern}`, { absolute: true });\n return files.map((file) => Path.normalize(file));\n}\n\nexport async function getCertainFilesFromDirectory(directoryPath: string, filesNames: string[]) {\n const pattern = filesNames.length === 1 ? filesNames[0] : `(${filesNames.join(\"|\")})`;\n return getFilesFromDirectory(directoryPath, pattern + \".{ts,tsx}\");\n}\n\nexport function areSetsEqual<T>(set1: Set<T>, set2: Set<T>): boolean {\n if (set1.size !== set2.size) return false;\n for (const item of set1) {\n if (!set2.has(item)) return false;\n }\n return true;\n}\n"],"mappings":";;;;;;;;;;;AASA,eAAsB,yBAAyB;CAC7C,MAAM,qBAAqB,YAAY,CAAC;AAC1C;;;;;AAMA,eAAsB,sBAAsB,gBAAgB,QAAQ,GAAG,UAAU,iBAAiB;CAEhG,QAAO,MADa,KAAK,GAAG,KAAK,UAAU,aAAa,EAAE,GAAG,WAAW,EAAE,UAAU,KAAK,CAAC,EAC9E,CAAC,KAAK,SAAS,KAAK,UAAU,IAAI,CAAC;AACjD;AAEA,eAAsB,6BAA6B,eAAuB,YAAsB;CAE9F,OAAO,sBAAsB,gBADb,WAAW,WAAW,IAAI,WAAW,KAAK,IAAI,WAAW,KAAK,GAAG,EAAE,MAC7B,WAAW;AACnE;AAEA,SAAgB,aAAgB,MAAc,MAAuB;CACnE,IAAI,KAAK,SAAS,KAAK,MAAM,OAAO;CACpC,KAAK,MAAM,QAAQ,MACjB,IAAI,CAAC,KAAK,IAAI,IAAI,GAAG,OAAO;CAE9B,OAAO;AACT"}
|
|
@@ -66,6 +66,7 @@ function decrypt(cipherText) {
|
|
|
66
66
|
const parts = cipherText.split(":");
|
|
67
67
|
if (parts.length !== 3) throw new Error("Invalid encrypted format. Expected iv:ciphertext:authTag");
|
|
68
68
|
const [ivHex, encryptedHex, authTagHex] = parts;
|
|
69
|
+
if (ivHex === void 0 || encryptedHex === void 0 || authTagHex === void 0) throw new Error("Invalid encrypted format. Expected iv:ciphertext:authTag");
|
|
69
70
|
const decipher = crypto.createDecipheriv(algorithm, keyBuffer, Buffer.from(ivHex, "hex"));
|
|
70
71
|
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
71
72
|
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|