@warlock.js/core 5.10.0 → 5.12.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 +33 -0
- package/esm/application/app.d.mts +3 -3
- package/esm/application/app.mjs +6 -6
- package/esm/application/app.mjs.map +1 -1
- package/esm/cli/cli-commands.utils.mjs +7 -3
- package/esm/cli/cli-commands.utils.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/health.check.mjs +1 -1
- package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
- package/esm/connectors/socket-connector.mjs +13 -16
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/container/index.d.mts +20 -7
- package/esm/container/index.d.mts.map +1 -1
- package/esm/container/index.mjs +8 -5
- package/esm/container/index.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs +15 -3
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +1 -1
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +50 -9
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts +8 -0
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs +26 -4
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +3 -3
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +16 -2
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/flags.mjs +16 -1
- package/esm/dev-server/flags.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs +27 -3
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/errors/container-key-missing-error.mjs +18 -6
- package/esm/errors/container-key-missing-error.mjs.map +1 -1
- package/esm/generations/stubs.mjs +10 -10
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/context/request-context.d.mts +14 -11
- package/esm/http/context/request-context.d.mts.map +1 -1
- package/esm/http/context/request-context.mjs +13 -9
- package/esm/http/context/request-context.mjs.map +1 -1
- package/esm/http/createHttpApplication.d.mts.map +1 -1
- package/esm/http/createHttpApplication.mjs +2 -0
- package/esm/http/createHttpApplication.mjs.map +1 -1
- package/esm/http/csp.d.mts +139 -0
- package/esm/http/csp.d.mts.map +1 -0
- package/esm/http/csp.mjs +134 -0
- package/esm/http/csp.mjs.map +1 -0
- package/esm/http/errors/errors.d.mts +17 -1
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +20 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +8 -3
- package/esm/http/index.mjs +6 -1
- package/esm/http/middleware/concurrency-limit.middleware.d.mts +2 -2
- package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.d.mts +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +10 -0
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.d.mts +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.d.mts.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs +13 -13
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/request-controller.d.mts +1 -1
- package/esm/http/request.d.mts +32 -44
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +52 -44
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +18 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +31 -0
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/stream-react-response.d.mts +65 -0
- package/esm/http/stream-react-response.d.mts.map +1 -0
- package/esm/http/stream-react-response.mjs +46 -0
- package/esm/http/stream-react-response.mjs.map +1 -0
- package/esm/http/tracing/index.mjs +4 -0
- package/esm/http/tracing/trace-id.d.mts +14 -0
- package/esm/http/tracing/trace-id.d.mts.map +1 -0
- package/esm/http/tracing/trace-id.mjs +37 -0
- package/esm/http/tracing/trace-id.mjs.map +1 -0
- package/esm/http/tracing/tracing-dispatcher.d.mts +42 -0
- package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -0
- package/esm/http/tracing/tracing-dispatcher.mjs +94 -0
- package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -0
- package/esm/http/tracing/tracing.type.d.mts +66 -0
- package/esm/http/tracing/tracing.type.d.mts.map +1 -0
- package/esm/http/types.d.mts +27 -21
- package/esm/http/types.d.mts.map +1 -1
- package/esm/index.d.mts +9 -4
- package/esm/index.mjs +6 -2
- package/esm/production/esbuild-preflight.mjs +47 -0
- package/esm/production/esbuild-preflight.mjs.map +1 -0
- package/esm/production/production-builder.mjs +19 -15
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/restful/restful.d.mts +1 -1
- package/esm/router/log-request-lifecycle.mjs +12 -2
- package/esm/router/log-request-lifecycle.mjs.map +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +10 -0
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +1 -1
- package/esm/socket/utils.mjs +1 -2
- package/esm/socket/utils.mjs.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs +2 -5
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +12 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +315 -25
- package/llms.txt +1 -0
- package/package.json +12 -12
- package/skills/add-connector/SKILL.md +1 -1
- package/skills/build-restful/SKILL.md +2 -2
- package/skills/configure-app/SKILL.md +31 -0
- package/skills/create-controller/SKILL.md +4 -4
- package/skills/request-tracing/SKILL.md +208 -0
- package/skills/send-response/SKILL.md +16 -0
- package/skills/store-file/SKILL.md +1 -1
- package/skills/upload-file/SKILL.md +14 -2
- package/skills/use-app-context/SKILL.md +2 -2
- package/skills/use-middleware/SKILL.md +2 -2
- package/skills/use-repository/SKILL.md +1 -1
- package/skills/use-request-locals/SKILL.md +1 -1
- package/skills/validate-input/SKILL.md +13 -0
- package/skills/wire-socket/SKILL.md +3 -3
- package/skills/write-middleware/SKILL.md +11 -5
- package/skills/write-use-case/SKILL.md +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"development-server.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/development-server.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport events from \"@mongez/events\";\r\nimport { fileExistsAsync, unlinkAsync } from \"@warlock.js/fs\";\r\nimport { Application } from \"../application\";\r\nimport { connectorsManager } from \"../connectors/connectors-manager\";\r\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\r\nimport { warlockConfigManager } from \"../warlock-config\";\r\nimport { BootPreconditionError } from \"./boot-precondition-error\";\r\nimport { devLogInfo, devLogSection, devLogWarn, devServeLog } from \"./dev-logger\";\r\nimport { filesOrchestrator } from \"./files-orchestrator\";\r\nimport { MANIFEST_PATH } from \"./flags\";\r\nimport { LayerExecutor } from \"./layer-executor\";\r\nimport { printReadyBlock } from \"./ready-block\";\r\nimport { restartDevServer } from \"./restart-dev-server\";\r\nimport { devServerShortcuts } from \"./shortcuts\";\r\nimport type { StartDevServerOptions } from \"./start-development-server\";\r\nimport { typeGenerator } from \"./type-generator\";\r\n\r\ntype Batch = { added: string[]; changed: string[]; deleted: string[] };\r\n\r\n/**\r\n * Top-level coordinator for `warlock dev`. Wires the file orchestrator, the\r\n * connectors, and the layer executor together, and listens for the watcher's\r\n * batched events to drive HMR.\r\n */\r\nexport class DevelopmentServer {\r\n private layerExecutor?: LayerExecutor;\r\n private running = false;\r\n private readonly options: StartDevServerOptions;\r\n\r\n public constructor(options: StartDevServerOptions = {}) {\r\n this.options = options;\r\n devLogSection(\"Starting Development Server...\");\r\n }\r\n\r\n public async start(): Promise<void> {\r\n try {\r\n const startedAt = performance.now();\r\n\r\n // --fresh deletes the manifest so reconciliation re-parses every file\r\n // from disk. Transpile caching is owned by the loader hook (in-memory).\r\n if (this.options.fresh && (await fileExistsAsync(MANIFEST_PATH))) {\r\n await unlinkAsync(MANIFEST_PATH);\r\n devServeLog(colors.cyanBright(\"Cleared manifest (--fresh)\"));\r\n }\r\n\r\n await filesOrchestrator.init();\r\n await filesOrchestrator.initializeAll();\r\n await filesOrchestrator.watchFiles();\r\n\r\n filesOrchestrator.specialFilesCollector.collect(filesOrchestrator.getFiles());\r\n\r\n this.setupEventListeners();\r\n\r\n // Decorator-driven registries (models, etc.) must be populated before\r\n // routes/services can resolve symbols by name.\r\n await this.autoDiscoverFiles();\r\n\r\n await filesOrchestrator.moduleLoader.loadAll();\r\n\r\n // A rejecting validator (Application.onValidateBoot) must abort boot\r\n // before late-phase connectors bind a port — this is what makes the\r\n // hook actually prevent serving instead of merely being defined.\r\n //\r\n // Wrapped and re-thrown as a BootPreconditionError: a rejected\r\n // validator is, by definition, a precondition whose cause cannot\r\n // change because the worker tried again — the supervisor must not\r\n // restart into the same rejection. See boot-precondition-error.ts.\r\n try {\r\n await Application.runStartupValidators();\r\n } catch (error) {\r\n throw new BootPreconditionError((error as Error).message, { cause: error });\r\n }\r\n\r\n // Late-phase connectors (http, socket) bind after app code has\r\n // registered routes/listeners.\r\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\r\n\r\n this.layerExecutor = new LayerExecutor(\r\n filesOrchestrator.getDependencyGraph(),\r\n filesOrchestrator.specialFilesCollector,\r\n filesOrchestrator.moduleLoader,\r\n (absolutePath) => filesOrchestrator.bumpVersion(absolutePath),\r\n () => filesOrchestrator.flushVersionBumps(),\r\n );\r\n\r\n this.running = true;\r\n\r\n const duration = performance.now() - startedAt;\r\n\r\n // App modules are loaded and both connector phases are active — signal a\r\n // complete boot so `Application.onceBooted(...)` listeners fire.\r\n Application.markBooted({\r\n environment: Application.environment,\r\n runtimeStrategy: Application.runtimeStrategy,\r\n bootDurationMs: duration,\r\n });\r\n\r\n // ONE block, AFTER the boot it summarises, replacing the scattered\r\n // \"N route(s) registered\" / \"Server ready at …\" / \"ready in …\" lines that\r\n // used to arrive in completion order. Warnings logged during boot sit\r\n // above it and stay there — see `ready-block.ts`.\r\n printReadyBlock(duration);\r\n\r\n // Precedence: explicit CLI option > devServer.* config > default.\r\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\r\n const generateTypings =\r\n this.options.generateTypings ?? devServerConfig?.generateTypings ?? true;\r\n const healthCheckers = this.options.healthCheckers ?? devServerConfig?.healthCheckers ?? true;\r\n\r\n if (generateTypings) typeGenerator.executeGenerateAllCommand();\r\n\r\n if (healthCheckers) {\r\n filesOrchestrator.startCheckingHealth(healthCheckers === true ? undefined : healthCheckers);\r\n }\r\n } catch (error) {\r\n devServeLog(colors.redBright(`Failed to start Development Server: ${error}`));\r\n await this.shutdown();\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Eagerly import files whose decorators populate global registries so any\r\n * symbol-by-name resolution later in boot finds them.\r\n */\r\n private async autoDiscoverFiles(): Promise<void> {\r\n const discoveryTypes = [\"model\"] as const;\r\n for (const file of filesOrchestrator.files.values()) {\r\n if (file.type && (discoveryTypes as readonly string[]).includes(file.type)) {\r\n await filesOrchestrator.moduleLoader.loadModule(file, file.type);\r\n }\r\n }\r\n }\r\n\r\n private setupEventListeners(): void {\r\n events.on(\"dev-server:batch-complete\", (batch: Batch) => this.handleBatchComplete(batch));\r\n }\r\n\r\n private async handleBatchComplete(batch: Batch): Promise<void> {\r\n if (!this.running || !this.layerExecutor) return;\r\n\r\n // warlock.config.ts holds settings read at boot (CLI commands, build\r\n // options, watch patterns, scheduled jobs) and .env feeds every config\r\n // that reads it. Neither can be hot-reloaded without leaving running\r\n // services configured with stale values, so the only honest response is\r\n // a full restart.\r\n const restartTrigger = this.findRestartTrigger(batch);\r\n\r\n if (restartTrigger) {\r\n // On success this never comes back — the process exits and the\r\n // supervisor replaces it. Reaching the next line means the restart was\r\n // declined by config or wasn't possible, so we fall through and still\r\n // hot-reload whatever ordinary code shared the batch.\r\n await this.restartForConfigChange(restartTrigger);\r\n }\r\n\r\n // No-op saves (an editor that fsyncs without a content write) are already\r\n // filtered upstream by the file hash in FileEventHandler, so batch.changed\r\n // only contains genuinely-changed paths by the time it reaches here.\r\n\r\n const total = batch.added.length + batch.changed.length + batch.deleted.length;\r\n if (total === 0) return;\r\n\r\n // Boot-time files still ride along in `changed` when a restart was\r\n // declined (`restartOnConfigChange: false`) or wasn't possible — they are\r\n // never hot-reloaded, so keep them out of the reload set either way.\r\n const codeFiles = [...batch.added, ...batch.changed].filter(\r\n (p) => !isEnvPath(p) && p !== \"warlock.config.ts\",\r\n );\r\n\r\n try {\r\n await this.layerExecutor.executeBatchReload(\r\n codeFiles,\r\n filesOrchestrator.getFiles(),\r\n batch.deleted,\r\n batch.changed,\r\n );\r\n\r\n typeGenerator.executeTypingsGenerator([...batch.added, ...batch.changed]);\r\n\r\n filesOrchestrator.checkHealth(batch);\r\n } catch (error) {\r\n devServeLog(colors.redBright(`Failed to execute batch reload: ${error}`));\r\n }\r\n }\r\n\r\n /**\r\n * The boot-time file in this batch that can only be applied by restarting,\r\n * or `undefined` when the batch is ordinary application code.\r\n */\r\n private findRestartTrigger(batch: Batch): string | undefined {\r\n if (batch.changed.includes(\"warlock.config.ts\")) {\r\n return \"warlock.config.ts\";\r\n }\r\n\r\n return [...batch.added, ...batch.changed].find(isEnvPath);\r\n }\r\n\r\n /**\r\n * Restart so a changed boot-time file actually takes effect.\r\n *\r\n * Opt out with `devServer.restartOnConfigChange: false` and the old\r\n * behaviour returns: a warning telling you to restart yourself. If the\r\n * restart can't be performed at all (no supervisor, a shutdown that threw),\r\n * `restartDevServer` reports it and we fall back to the same warning rather\r\n * than leaving the server running against config it isn't using.\r\n */\r\n private async restartForConfigChange(file: string): Promise<void> {\r\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\r\n\r\n if (devServerConfig?.restartOnConfigChange === false) {\r\n devLogWarn(`${file} changed — restart the dev server to apply.`);\r\n return;\r\n }\r\n\r\n devLogInfo(`${file} changed — restarting to apply.`);\r\n\r\n const restarted = await restartDevServer(this);\r\n\r\n if (!restarted) {\r\n devLogWarn(`${file} changed — restart the dev server to apply.`);\r\n }\r\n }\r\n\r\n public async shutdown(): Promise<void> {\r\n // Always hand the terminal back, even on a repeat/no-op shutdown — a\r\n // process that exits while stdin is still in raw mode leaves the user's\r\n // shell without line editing or Ctrl+C.\r\n devServerShortcuts.release();\r\n\r\n if (!this.running) return;\r\n devServeLog(colors.redBright(\"Shutting down Development Server...\"));\r\n this.running = false;\r\n await connectorsManager.shutdown();\r\n devServeLog(colors.greenBright(\"Development Server stopped\"));\r\n }\r\n\r\n public isRunning(): boolean {\r\n return this.running;\r\n }\r\n}\r\n\r\nfunction isEnvPath(path: string): boolean {\r\n const basename = path.split(\"/\").pop() ?? path;\r\n return basename === \".env\" || basename.startsWith(\".env.\");\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"development-server.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/development-server.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport events from \"@mongez/events\";\r\nimport { fileExistsAsync, unlinkAsync } from \"@warlock.js/fs\";\r\nimport { Application } from \"../application\";\r\nimport { connectorsManager } from \"../connectors/connectors-manager\";\r\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\r\nimport { warlockConfigManager } from \"../warlock-config\";\r\nimport { BootPreconditionError } from \"./boot-precondition-error\";\r\nimport { devLogInfo, devLogSection, devLogWarn, devServeLog } from \"./dev-logger\";\r\nimport { filesOrchestrator } from \"./files-orchestrator\";\r\nimport { MANIFEST_PATH } from \"./flags\";\r\nimport type { IncomingReloadTimings } from \"./layer-executor\";\r\nimport { LayerExecutor } from \"./layer-executor\";\r\nimport { printReadyBlock } from \"./ready-block\";\r\nimport { restartDevServer } from \"./restart-dev-server\";\r\nimport { devServerShortcuts } from \"./shortcuts\";\r\nimport type { StartDevServerOptions } from \"./start-development-server\";\r\nimport { typeGenerator } from \"./type-generator\";\r\n\r\ntype Batch = {\r\n added: string[];\r\n changed: string[];\r\n deleted: string[];\r\n timings?: IncomingReloadTimings;\r\n};\r\n\r\n/**\r\n * Top-level coordinator for `warlock dev`. Wires the file orchestrator, the\r\n * connectors, and the layer executor together, and listens for the watcher's\r\n * batched events to drive HMR.\r\n */\r\nexport class DevelopmentServer {\r\n private layerExecutor?: LayerExecutor;\r\n private running = false;\r\n private readonly options: StartDevServerOptions;\r\n\r\n public constructor(options: StartDevServerOptions = {}) {\r\n this.options = options;\r\n devLogSection(\"Starting Development Server...\");\r\n }\r\n\r\n public async start(): Promise<void> {\r\n try {\r\n const startedAt = performance.now();\r\n\r\n // --fresh deletes the manifest so reconciliation re-parses every file\r\n // from disk. Transpile caching is owned by the loader hook (in-memory).\r\n if (this.options.fresh && (await fileExistsAsync(MANIFEST_PATH))) {\r\n await unlinkAsync(MANIFEST_PATH);\r\n devServeLog(colors.cyanBright(\"Cleared manifest (--fresh)\"));\r\n }\r\n\r\n await filesOrchestrator.init();\r\n await filesOrchestrator.initializeAll();\r\n await filesOrchestrator.watchFiles();\r\n\r\n filesOrchestrator.specialFilesCollector.collect(filesOrchestrator.getFiles());\r\n\r\n this.setupEventListeners();\r\n\r\n // Decorator-driven registries (models, etc.) must be populated before\r\n // routes/services can resolve symbols by name.\r\n await this.autoDiscoverFiles();\r\n\r\n await filesOrchestrator.moduleLoader.loadAll();\r\n\r\n // A rejecting validator (Application.onValidateBoot) must abort boot\r\n // before late-phase connectors bind a port — this is what makes the\r\n // hook actually prevent serving instead of merely being defined.\r\n //\r\n // Wrapped and re-thrown as a BootPreconditionError: a rejected\r\n // validator is, by definition, a precondition whose cause cannot\r\n // change because the worker tried again — the supervisor must not\r\n // restart into the same rejection. See boot-precondition-error.ts.\r\n try {\r\n await Application.runStartupValidators();\r\n } catch (error) {\r\n throw new BootPreconditionError((error as Error).message, { cause: error });\r\n }\r\n\r\n // Late-phase connectors (http, socket) bind after app code has\r\n // registered routes/listeners.\r\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\r\n\r\n this.layerExecutor = new LayerExecutor(\r\n filesOrchestrator.getDependencyGraph(),\r\n filesOrchestrator.specialFilesCollector,\r\n filesOrchestrator.moduleLoader,\r\n (absolutePath) => filesOrchestrator.bumpVersion(absolutePath),\r\n () => filesOrchestrator.flushVersionBumps(),\r\n );\r\n\r\n this.running = true;\r\n\r\n const duration = performance.now() - startedAt;\r\n\r\n // App modules are loaded and both connector phases are active — signal a\r\n // complete boot so `Application.onceBooted(...)` listeners fire.\r\n Application.markBooted({\r\n environment: Application.environment,\r\n runtimeStrategy: Application.runtimeStrategy,\r\n bootDurationMs: duration,\r\n });\r\n\r\n // ONE block, AFTER the boot it summarises, replacing the scattered\r\n // \"N route(s) registered\" / \"Server ready at …\" / \"ready in …\" lines that\r\n // used to arrive in completion order. Warnings logged during boot sit\r\n // above it and stay there — see `ready-block.ts`.\r\n printReadyBlock(duration);\r\n\r\n // Precedence: explicit CLI option > devServer.* config > default.\r\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\r\n const generateTypings =\r\n this.options.generateTypings ?? devServerConfig?.generateTypings ?? true;\r\n const healthCheckers = this.options.healthCheckers ?? devServerConfig?.healthCheckers ?? true;\r\n\r\n if (generateTypings) typeGenerator.executeGenerateAllCommand();\r\n\r\n if (healthCheckers) {\r\n filesOrchestrator.startCheckingHealth(healthCheckers === true ? undefined : healthCheckers);\r\n }\r\n } catch (error) {\r\n devServeLog(colors.redBright(`Failed to start Development Server: ${error}`));\r\n await this.shutdown();\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Eagerly import files whose decorators populate global registries so any\r\n * symbol-by-name resolution later in boot finds them.\r\n */\r\n private async autoDiscoverFiles(): Promise<void> {\r\n const discoveryTypes = [\"model\"] as const;\r\n for (const file of filesOrchestrator.files.values()) {\r\n if (file.type && (discoveryTypes as readonly string[]).includes(file.type)) {\r\n await filesOrchestrator.moduleLoader.loadModule(file, file.type);\r\n }\r\n }\r\n }\r\n\r\n private setupEventListeners(): void {\r\n events.on(\"dev-server:batch-complete\", (batch: Batch) => this.handleBatchComplete(batch));\r\n }\r\n\r\n private async handleBatchComplete(batch: Batch): Promise<void> {\r\n if (!this.running || !this.layerExecutor) return;\r\n\r\n // warlock.config.ts holds settings read at boot (CLI commands, build\r\n // options, watch patterns, scheduled jobs) and .env feeds every config\r\n // that reads it. Neither can be hot-reloaded without leaving running\r\n // services configured with stale values, so the only honest response is\r\n // a full restart.\r\n const restartTrigger = this.findRestartTrigger(batch);\r\n\r\n if (restartTrigger) {\r\n // On success this never comes back — the process exits and the\r\n // supervisor replaces it. Reaching the next line means the restart was\r\n // declined by config or wasn't possible, so we fall through and still\r\n // hot-reload whatever ordinary code shared the batch.\r\n await this.restartForConfigChange(restartTrigger);\r\n }\r\n\r\n // No-op saves (an editor that fsyncs without a content write) are already\r\n // filtered upstream by the file hash in FileEventHandler, so batch.changed\r\n // only contains genuinely-changed paths by the time it reaches here.\r\n\r\n const total = batch.added.length + batch.changed.length + batch.deleted.length;\r\n if (total === 0) return;\r\n\r\n // Boot-time files still ride along in `changed` when a restart was\r\n // declined (`restartOnConfigChange: false`) or wasn't possible — they are\r\n // never hot-reloaded, so keep them out of the reload set either way.\r\n const codeFiles = [...batch.added, ...batch.changed].filter(\r\n (p) => !isEnvPath(p) && p !== \"warlock.config.ts\",\r\n );\r\n\r\n try {\r\n await this.layerExecutor.executeBatchReload(\r\n codeFiles,\r\n filesOrchestrator.getFiles(),\r\n batch.deleted,\r\n batch.changed,\r\n batch.timings,\r\n );\r\n\r\n typeGenerator.executeTypingsGenerator([...batch.added, ...batch.changed]);\r\n\r\n filesOrchestrator.checkHealth(batch);\r\n } catch (error) {\r\n devServeLog(colors.redBright(`Failed to execute batch reload: ${error}`));\r\n }\r\n }\r\n\r\n /**\r\n * The boot-time file in this batch that can only be applied by restarting,\r\n * or `undefined` when the batch is ordinary application code.\r\n */\r\n private findRestartTrigger(batch: Batch): string | undefined {\r\n if (batch.changed.includes(\"warlock.config.ts\")) {\r\n return \"warlock.config.ts\";\r\n }\r\n\r\n return [...batch.added, ...batch.changed].find(isEnvPath);\r\n }\r\n\r\n /**\r\n * Restart so a changed boot-time file actually takes effect.\r\n *\r\n * Opt out with `devServer.restartOnConfigChange: false` and the old\r\n * behaviour returns: a warning telling you to restart yourself. If the\r\n * restart can't be performed at all (no supervisor, a shutdown that threw),\r\n * `restartDevServer` reports it and we fall back to the same warning rather\r\n * than leaving the server running against config it isn't using.\r\n */\r\n private async restartForConfigChange(file: string): Promise<void> {\r\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\r\n\r\n if (devServerConfig?.restartOnConfigChange === false) {\r\n devLogWarn(`${file} changed — restart the dev server to apply.`);\r\n return;\r\n }\r\n\r\n devLogInfo(`${file} changed — restarting to apply.`);\r\n\r\n const restarted = await restartDevServer(this);\r\n\r\n if (!restarted) {\r\n devLogWarn(`${file} changed — restart the dev server to apply.`);\r\n }\r\n }\r\n\r\n public async shutdown(): Promise<void> {\r\n // Always hand the terminal back, even on a repeat/no-op shutdown — a\r\n // process that exits while stdin is still in raw mode leaves the user's\r\n // shell without line editing or Ctrl+C.\r\n devServerShortcuts.release();\r\n\r\n if (!this.running) return;\r\n devServeLog(colors.redBright(\"Shutting down Development Server...\"));\r\n this.running = false;\r\n await connectorsManager.shutdown();\r\n devServeLog(colors.greenBright(\"Development Server stopped\"));\r\n }\r\n\r\n public isRunning(): boolean {\r\n return this.running;\r\n }\r\n}\r\n\r\nfunction isEnvPath(path: string): boolean {\r\n const basename = path.split(\"/\").pop() ?? path;\r\n return basename === \".env\" || basename.startsWith(\".env.\");\r\n}\r\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Path } from "../utils/normalized-path.mjs";
|
|
2
2
|
import { devLogSuccess } from "./dev-logger.mjs";
|
|
3
|
-
import "./flags.mjs";
|
|
3
|
+
import { isTimingsEnabled } from "./flags.mjs";
|
|
4
4
|
import { clearFileExistsCache } from "./parse-imports.mjs";
|
|
5
5
|
import { debounce } from "@mongez/reinforcements";
|
|
6
6
|
import events from "@mongez/events";
|
|
@@ -20,20 +20,34 @@ var FileEventHandler = class {
|
|
|
20
20
|
this.pendingChanges = /* @__PURE__ */ new Set();
|
|
21
21
|
this.pendingAdds = /* @__PURE__ */ new Set();
|
|
22
22
|
this.pendingDeletes = /* @__PURE__ */ new Set();
|
|
23
|
-
this.processPendingEvents = debounce(() => this.processBatch(),
|
|
23
|
+
this.processPendingEvents = debounce(() => this.processBatch(), 50);
|
|
24
24
|
}
|
|
25
|
-
handleFileChange(absolutePath) {
|
|
25
|
+
handleFileChange(absolutePath, settleMs) {
|
|
26
|
+
this.recordTimingStart(settleMs);
|
|
26
27
|
this.pendingChanges.add(Path.toRelative(absolutePath));
|
|
27
28
|
this.processPendingEvents();
|
|
28
29
|
}
|
|
29
|
-
handleFileAdd(absolutePath) {
|
|
30
|
+
handleFileAdd(absolutePath, settleMs) {
|
|
31
|
+
this.recordTimingStart(settleMs);
|
|
30
32
|
this.pendingAdds.add(Path.toRelative(absolutePath));
|
|
31
33
|
this.processPendingEvents();
|
|
32
34
|
}
|
|
33
|
-
handleFileDelete(absolutePath) {
|
|
35
|
+
handleFileDelete(absolutePath, settleMs) {
|
|
36
|
+
this.recordTimingStart(settleMs);
|
|
34
37
|
this.pendingDeletes.add(Path.toRelative(absolutePath));
|
|
35
38
|
this.processPendingEvents();
|
|
36
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Mark the debounce-wait phase start on the first event of a batch, and
|
|
42
|
+
* track the slowest watcher-settle duration seen this batch. `settleMs`
|
|
43
|
+
* only arrives when `devServer.timings` is on (see `FilesWatcher`), so
|
|
44
|
+
* this is a no-op past the cheap `performance.now()` call when it's off.
|
|
45
|
+
*/
|
|
46
|
+
recordTimingStart(settleMs) {
|
|
47
|
+
if (this.batchStartedAt === void 0) this.batchStartedAt = performance.now();
|
|
48
|
+
if (settleMs === void 0) return;
|
|
49
|
+
this.watcherSettleMs = this.watcherSettleMs === void 0 ? settleMs : Math.max(this.watcherSettleMs, settleMs);
|
|
50
|
+
}
|
|
37
51
|
async processBatch() {
|
|
38
52
|
const changes = Array.from(this.pendingChanges);
|
|
39
53
|
const adds = Array.from(this.pendingAdds);
|
|
@@ -41,24 +55,33 @@ var FileEventHandler = class {
|
|
|
41
55
|
this.pendingChanges.clear();
|
|
42
56
|
this.pendingAdds.clear();
|
|
43
57
|
this.pendingDeletes.clear();
|
|
58
|
+
const debounceWaitMs = this.batchStartedAt !== void 0 ? performance.now() - this.batchStartedAt : void 0;
|
|
59
|
+
const watcherSettleMs = this.watcherSettleMs;
|
|
60
|
+
this.batchStartedAt = void 0;
|
|
61
|
+
this.watcherSettleMs = void 0;
|
|
44
62
|
if (changes.length === 0 && adds.length === 0 && deletes.length === 0) return;
|
|
45
63
|
const externalChanges = changes.filter(isExternalPath);
|
|
64
|
+
const externalAdds = adds.filter(isExternalPath);
|
|
46
65
|
const codeChanges = changes.filter((p) => !isExternalPath(p));
|
|
47
66
|
const codeAdds = adds.filter((p) => !isExternalPath(p));
|
|
48
67
|
if (codeAdds.length + codeChanges.length > 1) {
|
|
49
68
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
50
69
|
clearFileExistsCache();
|
|
51
70
|
}
|
|
52
|
-
await this.processBatchAdds(codeAdds);
|
|
71
|
+
const { added: addedCodePaths, vanished } = await this.processBatchAdds(codeAdds);
|
|
53
72
|
const changedCodePaths = await this.processBatchChanges(codeChanges);
|
|
54
73
|
await this.processBatchDeletes(deletes);
|
|
55
74
|
this.fileOperations.updateFileDependents();
|
|
56
75
|
this.fileOperations.syncFilesToManifest();
|
|
57
76
|
await this.manifest.save();
|
|
58
77
|
events.trigger("dev-server:batch-complete", {
|
|
59
|
-
added:
|
|
78
|
+
added: [...externalAdds, ...addedCodePaths],
|
|
60
79
|
changed: [...externalChanges, ...changedCodePaths],
|
|
61
|
-
deleted: deletes
|
|
80
|
+
deleted: [...deletes, ...vanished],
|
|
81
|
+
timings: isTimingsEnabled() ? {
|
|
82
|
+
watcherSettleMs: watcherSettleMs ?? 0,
|
|
83
|
+
debounceWaitMs: debounceWaitMs ?? 0
|
|
84
|
+
} : void 0
|
|
62
85
|
});
|
|
63
86
|
}
|
|
64
87
|
/**
|
|
@@ -75,15 +98,33 @@ var FileEventHandler = class {
|
|
|
75
98
|
});
|
|
76
99
|
return changed;
|
|
77
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Process pending add events. Returns the paths that genuinely became
|
|
103
|
+
* available (`added`) separately from paths that no longer existed by the
|
|
104
|
+
* time they were read/stat'd (`vanished`) — the latter is a rename or move
|
|
105
|
+
* racing the filesystem, not a failure, so it is unwound as a deletion
|
|
106
|
+
* instead of being logged as an error.
|
|
107
|
+
*/
|
|
78
108
|
async processBatchAdds(relativePaths) {
|
|
109
|
+
const added = [];
|
|
110
|
+
const vanished = [];
|
|
79
111
|
await runInBatches(relativePaths, 500, async (path) => {
|
|
80
112
|
try {
|
|
81
|
-
await this.fileOperations.addFile(path)
|
|
113
|
+
if ((await this.fileOperations.addFile(path)).state === "deleted") {
|
|
114
|
+
await this.fileOperations.deleteFile(path);
|
|
115
|
+
vanished.push(path);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
added.push(path);
|
|
82
119
|
devLogSuccess(`Added file: ${path}`);
|
|
83
120
|
} catch (error) {
|
|
84
121
|
console.error(`Failed to add file ${path}:`, error);
|
|
85
122
|
}
|
|
86
123
|
});
|
|
124
|
+
return {
|
|
125
|
+
added,
|
|
126
|
+
vanished
|
|
127
|
+
};
|
|
87
128
|
}
|
|
88
129
|
async processBatchDeletes(relativePaths) {
|
|
89
130
|
for (const relativePath of relativePaths) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-event-handler.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/file-event-handler.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { debounce } from \"@mongez/reinforcements\";\nimport type { DependencyGraph } from \"./dependency-graph\";\nimport { devLogSuccess } from \"./dev-logger\";\nimport type { FileManager } from \"./file-manager\";\nimport type { FileOperations } from \"./file-operations\";\nimport { FILE_PROCESSING_BATCH_SIZE } from \"./flags\";\nimport type { ManifestManager } from \"./manifest-manager\";\nimport { clearFileExistsCache } from \"./parse-imports\";\nimport { Path } from \"../utils/normalized-path\";\n\n/**\n * Receives raw watcher events and processes them in a single debounced batch.\n * Order within a batch: adds → changes → deletes, so changes can reference\n * newly-added files and deletes fire last.\n */\nexport class FileEventHandler {\n private pendingChanges = new Set<string>();\n private pendingAdds = new Set<string>();\n private pendingDeletes = new Set<string>();\n\n private readonly processPendingEvents = debounce(() => this.processBatch(), 150);\n\n constructor(\n private readonly fileOperations: FileOperations,\n private readonly manifest: ManifestManager,\n private readonly dependencyGraph: DependencyGraph,\n private readonly files: Map<string, FileManager>,\n ) {}\n\n public handleFileChange(absolutePath: string): void {\n this.pendingChanges.add(Path.toRelative(absolutePath));\n this.processPendingEvents();\n }\n\n public handleFileAdd(absolutePath: string): void {\n this.pendingAdds.add(Path.toRelative(absolutePath));\n this.processPendingEvents();\n }\n\n public handleFileDelete(absolutePath: string): void {\n this.pendingDeletes.add(Path.toRelative(absolutePath));\n this.processPendingEvents();\n }\n\n private async processBatch(): Promise<void> {\n const changes = Array.from(this.pendingChanges);\n const adds = Array.from(this.pendingAdds);\n const deletes = Array.from(this.pendingDeletes);\n\n this.pendingChanges.clear();\n this.pendingAdds.clear();\n this.pendingDeletes.clear();\n\n if (changes.length === 0 && adds.length === 0 && deletes.length === 0) return;\n\n // Both .env files and warlock.config.ts live outside src/ — they should\n // never enter the dep graph, only ride along in the batch event so the\n // dev server can react (config reload / restart warning).\n const externalChanges = changes.filter(isExternalPath);\n const codeChanges = changes.filter((p) => !isExternalPath(p));\n const codeAdds = adds.filter((p) => !isExternalPath(p));\n\n // Multi-file batches can race the filesystem on Windows.\n if (codeAdds.length + codeChanges.length > 1) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n clearFileExistsCache();\n }\n\n await this.processBatchAdds(codeAdds);\n const changedCodePaths = await this.processBatchChanges(codeChanges);\n await this.processBatchDeletes(deletes);\n\n this.fileOperations.updateFileDependents();\n this.fileOperations.syncFilesToManifest();\n await this.manifest.save();\n\n // Emit only the code paths that genuinely changed (hash differs). A no-op\n // save — an editor that fsyncs without writing — reports no change and is\n // dropped here, where we still know the pre-change hash. (Doing this\n // downstream is impossible: the source has already been overwritten, so a\n // content compare always looks unchanged — which is exactly why emptying a\n // file used to silently skip HMR.) External paths (.env / warlock.config.ts)\n // ride along untouched so the dev server can still react to them.\n events.trigger(\"dev-server:batch-complete\", {\n added: adds,\n changed: [...externalChanges, ...changedCodePaths],\n deleted: deletes,\n });\n }\n\n /**\n * Reprocess each changed file and return only the paths that genuinely\n * changed. `updateFile` returns false when the on-disk hash matches the\n * last-processed hash (e.g. an editor that fsyncs on save without writing),\n * so those no-ops are kept out of the reload batch. Emptying a file changes\n * its hash, so it is correctly reported as changed.\n */\n private async processBatchChanges(relativePaths: string[]): Promise<string[]> {\n const changed: string[] = [];\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n if (await this.fileOperations.updateFile(path)) {\n changed.push(path);\n }\n });\n return changed;\n }\n\n private async processBatchAdds(relativePaths: string[]): Promise<void> {\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n try {\n await this.fileOperations.addFile(path);\n devLogSuccess(`Added file: ${path}`);\n } catch (error) {\n console.error(`Failed to add file ${path}:`, error);\n }\n });\n }\n\n private async processBatchDeletes(relativePaths: string[]): Promise<void> {\n for (const relativePath of relativePaths) {\n await this.fileOperations.deleteFile(relativePath);\n devLogSuccess(`Deleted file: ${relativePath}`);\n }\n }\n}\n\nfunction isEnvFile(path: string): boolean {\n const basename = path.split(\"/\").pop() || path;\n return basename === \".env\" || basename.startsWith(\".env.\");\n}\n\n/** Paths watched but never added to the dependency graph. */\nfunction isExternalPath(path: string): boolean {\n return isEnvFile(path) || path === \"warlock.config.ts\";\n}\n\nasync function runInBatches<T>(\n items: T[],\n size: number,\n fn: (item: T) => Promise<unknown>,\n): Promise<void> {\n if (items.length === 0) return;\n for (let i = 0; i < items.length; i += size) {\n await Promise.all(items.slice(i, i + size).map(fn));\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-event-handler.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/file-event-handler.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { debounce } from \"@mongez/reinforcements\";\nimport type { DependencyGraph } from \"./dependency-graph\";\nimport { devLogSuccess } from \"./dev-logger\";\nimport type { FileManager } from \"./file-manager\";\nimport type { FileOperations } from \"./file-operations\";\nimport { FILE_PROCESSING_BATCH_SIZE, isTimingsEnabled } from \"./flags\";\nimport type { ManifestManager } from \"./manifest-manager\";\nimport { clearFileExistsCache } from \"./parse-imports\";\nimport { Path } from \"../utils/normalized-path\";\n\n/**\n * Receives raw watcher events and processes them in a single debounced batch.\n * Order within a batch: adds → changes → deletes, so changes can reference\n * newly-added files and deletes fire last.\n */\nexport class FileEventHandler {\n private pendingChanges = new Set<string>();\n private pendingAdds = new Set<string>();\n private pendingDeletes = new Set<string>();\n\n /** When this batch's first watcher event arrived — the debounce-wait phase start. */\n private batchStartedAt?: number;\n\n /** Slowest watcher-settle duration seen so far this batch, when `devServer.timings` is on. */\n private watcherSettleMs?: number;\n\n private readonly processPendingEvents = debounce(() => this.processBatch(), 50);\n\n constructor(\n private readonly fileOperations: FileOperations,\n private readonly manifest: ManifestManager,\n private readonly dependencyGraph: DependencyGraph,\n private readonly files: Map<string, FileManager>,\n ) {}\n\n public handleFileChange(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingChanges.add(Path.toRelative(absolutePath));\n this.processPendingEvents();\n }\n\n public handleFileAdd(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingAdds.add(Path.toRelative(absolutePath));\n this.processPendingEvents();\n }\n\n public handleFileDelete(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingDeletes.add(Path.toRelative(absolutePath));\n this.processPendingEvents();\n }\n\n /**\n * Mark the debounce-wait phase start on the first event of a batch, and\n * track the slowest watcher-settle duration seen this batch. `settleMs`\n * only arrives when `devServer.timings` is on (see `FilesWatcher`), so\n * this is a no-op past the cheap `performance.now()` call when it's off.\n */\n private recordTimingStart(settleMs?: number): void {\n if (this.batchStartedAt === undefined) {\n this.batchStartedAt = performance.now();\n }\n\n if (settleMs === undefined) return;\n\n this.watcherSettleMs =\n this.watcherSettleMs === undefined ? settleMs : Math.max(this.watcherSettleMs, settleMs);\n }\n\n private async processBatch(): Promise<void> {\n const changes = Array.from(this.pendingChanges);\n const adds = Array.from(this.pendingAdds);\n const deletes = Array.from(this.pendingDeletes);\n\n this.pendingChanges.clear();\n this.pendingAdds.clear();\n this.pendingDeletes.clear();\n\n const debounceWaitMs =\n this.batchStartedAt !== undefined ? performance.now() - this.batchStartedAt : undefined;\n const watcherSettleMs = this.watcherSettleMs;\n this.batchStartedAt = undefined;\n this.watcherSettleMs = undefined;\n\n if (changes.length === 0 && adds.length === 0 && deletes.length === 0) return;\n\n // Both .env files and warlock.config.ts live outside src/ — they should\n // never enter the dep graph, only ride along in the batch event so the\n // dev server can react (config reload / restart warning).\n const externalChanges = changes.filter(isExternalPath);\n const externalAdds = adds.filter(isExternalPath);\n const codeChanges = changes.filter((p) => !isExternalPath(p));\n const codeAdds = adds.filter((p) => !isExternalPath(p));\n\n // Multi-file batches can race the filesystem on Windows.\n if (codeAdds.length + codeChanges.length > 1) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n clearFileExistsCache();\n }\n\n const { added: addedCodePaths, vanished } = await this.processBatchAdds(codeAdds);\n const changedCodePaths = await this.processBatchChanges(codeChanges);\n await this.processBatchDeletes(deletes);\n\n this.fileOperations.updateFileDependents();\n this.fileOperations.syncFilesToManifest();\n await this.manifest.save();\n\n // Emit only the code paths that genuinely changed (hash differs). A no-op\n // save — an editor that fsyncs without writing — reports no change and is\n // dropped here, where we still know the pre-change hash. (Doing this\n // downstream is impossible: the source has already been overwritten, so a\n // content compare always looks unchanged — which is exactly why emptying a\n // file used to silently skip HMR.) External paths (.env / warlock.config.ts)\n // ride along untouched so the dev server can still react to them. Paths\n // that vanished between the add event and their read/stat (a rename or\n // move racing the filesystem) are folded into `deleted` instead of\n // `added` — they were already unwound as deletions in processBatchAdds.\n events.trigger(\"dev-server:batch-complete\", {\n added: [...externalAdds, ...addedCodePaths],\n changed: [...externalChanges, ...changedCodePaths],\n deleted: [...deletes, ...vanished],\n timings: isTimingsEnabled()\n ? { watcherSettleMs: watcherSettleMs ?? 0, debounceWaitMs: debounceWaitMs ?? 0 }\n : undefined,\n });\n }\n\n /**\n * Reprocess each changed file and return only the paths that genuinely\n * changed. `updateFile` returns false when the on-disk hash matches the\n * last-processed hash (e.g. an editor that fsyncs on save without writing),\n * so those no-ops are kept out of the reload batch. Emptying a file changes\n * its hash, so it is correctly reported as changed.\n */\n private async processBatchChanges(relativePaths: string[]): Promise<string[]> {\n const changed: string[] = [];\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n if (await this.fileOperations.updateFile(path)) {\n changed.push(path);\n }\n });\n return changed;\n }\n\n /**\n * Process pending add events. Returns the paths that genuinely became\n * available (`added`) separately from paths that no longer existed by the\n * time they were read/stat'd (`vanished`) — the latter is a rename or move\n * racing the filesystem, not a failure, so it is unwound as a deletion\n * instead of being logged as an error.\n */\n private async processBatchAdds(\n relativePaths: string[],\n ): Promise<{ added: string[]; vanished: string[] }> {\n const added: string[] = [];\n const vanished: string[] = [];\n\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n try {\n const fileManager = await this.fileOperations.addFile(path);\n\n if (fileManager.state === \"deleted\") {\n await this.fileOperations.deleteFile(path);\n vanished.push(path);\n return;\n }\n\n added.push(path);\n devLogSuccess(`Added file: ${path}`);\n } catch (error) {\n console.error(`Failed to add file ${path}:`, error);\n }\n });\n\n return { added, vanished };\n }\n\n private async processBatchDeletes(relativePaths: string[]): Promise<void> {\n for (const relativePath of relativePaths) {\n await this.fileOperations.deleteFile(relativePath);\n devLogSuccess(`Deleted file: ${relativePath}`);\n }\n }\n}\n\nfunction isEnvFile(path: string): boolean {\n const basename = path.split(\"/\").pop() || path;\n return basename === \".env\" || basename.startsWith(\".env.\");\n}\n\n/** Paths watched but never added to the dependency graph. */\nfunction isExternalPath(path: string): boolean {\n return isEnvFile(path) || path === \"warlock.config.ts\";\n}\n\nasync function runInBatches<T>(\n items: T[],\n size: number,\n fn: (item: T) => Promise<unknown>,\n): Promise<void> {\n if (items.length === 0) return;\n for (let i = 0; i < items.length; i += size) {\n await Promise.all(items.slice(i, i + size).map(fn));\n }\n}\n"],"mappings":"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"}
|
|
@@ -48,6 +48,14 @@ declare class FileManager {
|
|
|
48
48
|
* Read source, hash, parse imports, emit ready. The only file-system
|
|
49
49
|
* touch happens here; transpilation is the loader hook's job.
|
|
50
50
|
*
|
|
51
|
+
* Both the read and the stat happen up front, before anything on this
|
|
52
|
+
* instance is mutated. A path that disappears between the watcher event
|
|
53
|
+
* and either of those calls (`ENOENT`) — typically a rename or move
|
|
54
|
+
* racing the filesystem — is treated as a deletion rather than a thrown
|
|
55
|
+
* error, and never leaves `source`/`hash` pointing at content that then
|
|
56
|
+
* vanished. A real failure (`EACCES`, a permissions error, etc.) still
|
|
57
|
+
* throws.
|
|
58
|
+
*
|
|
51
59
|
* @param force - re-parse even when the hash matches.
|
|
52
60
|
* @returns true if the file was (re)parsed.
|
|
53
61
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-manager.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/file-manager.ts"],"mappings":";;;;KASY,eAAA;;AAAZ;;;;AAA2B;
|
|
1
|
+
{"version":3,"file":"file-manager.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/file-manager.ts"],"mappings":";;;;KASY,eAAA;;AAAZ;;;;AAA2B;AAyB3B;;cAAa,WAAA;EAAA,SAgCO,YAAA;EACT,KAAA,EAAO,GAAA,SAAY,WAAA;EACnB,cAAA,EAAgB,cAAA;EAjClB,YAAA;EACA,YAAA;EACA,IAAA;EACA,MAAA;EAiBM;EAdN,YAAA,EAAY,GAAA;EAsBL;;;;EAhBP,oBAAA,EAAoB,GAAA;EAwBkB;EArBtC,SAAA,EAAS,GAAA;EAmCiB;EAhC1B,UAAA,EAAU,GAAA;EAEV,IAAA,EAAM,QAAA;EAwDsD;EArD5D,OAAA,EAAS,eAAA;EAoIuC;EAjIhD,cAAA;EAEA,KAAA,EAAO,SAAA;cAGI,YAAA,UACT,KAAA,EAAO,GAAA,SAAY,WAAA,GACnB,cAAA,EAAgB,cAAA;EAGlB,UAAA,CAAW,OAAA,EAAS,eAAA,GAAkB,eAAA;EAMtC,YAAA;EAXW;;;;EAmBL,IAAA,CAAK,YAAA,GAAe,OAAA,CAAQ,YAAA,IAAgB,OAAA;EAjBhC;;;;;;;;;;;;;;;EA2CZ,OAAA;IAAU;EAAA;IAAmB,KAAA;EAAA,IAAyB,OAAA;EAhD5D;;;;;;EAAA,UA+FS,qBAAA,IAAyB,OAAA;EA1FhB;;;;;EAAA,UA0HT,gBAAA,CAAiB,YAAA,EAAc,OAAA,CAAQ,YAAA,IAAgB,OAAA;EAAA,UA6B7D,cAAA;EAsBH,UAAA,IAAc,YAAA;AAAA"}
|
|
@@ -7,6 +7,15 @@ import crypto from "crypto";
|
|
|
7
7
|
|
|
8
8
|
//#region ../core/src/dev-server/file-manager.ts
|
|
9
9
|
/**
|
|
10
|
+
* Whether `error` is a Node `ENOENT` (path does not exist) error. Used to
|
|
11
|
+
* distinguish a file that has genuinely vanished between the watcher event
|
|
12
|
+
* and its read/stat (a rename or move racing the filesystem) from a real
|
|
13
|
+
* failure such as `EACCES` or a parse error, which must still surface.
|
|
14
|
+
*/
|
|
15
|
+
function isEnoentError(error) {
|
|
16
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
10
19
|
* FileManager — per-file metadata for the dependency graph.
|
|
11
20
|
*
|
|
12
21
|
* The loader hook transpiles and caches on-demand at import time, so this
|
|
@@ -53,6 +62,14 @@ var FileManager = class {
|
|
|
53
62
|
* Read source, hash, parse imports, emit ready. The only file-system
|
|
54
63
|
* touch happens here; transpilation is the loader hook's job.
|
|
55
64
|
*
|
|
65
|
+
* Both the read and the stat happen up front, before anything on this
|
|
66
|
+
* instance is mutated. A path that disappears between the watcher event
|
|
67
|
+
* and either of those calls (`ENOENT`) — typically a rename or move
|
|
68
|
+
* racing the filesystem — is treated as a deletion rather than a thrown
|
|
69
|
+
* error, and never leaves `source`/`hash` pointing at content that then
|
|
70
|
+
* vanished. A real failure (`EACCES`, a permissions error, etc.) still
|
|
71
|
+
* throws.
|
|
72
|
+
*
|
|
56
73
|
* @param force - re-parse even when the hash matches.
|
|
57
74
|
* @returns true if the file was (re)parsed.
|
|
58
75
|
*/
|
|
@@ -61,11 +78,16 @@ var FileManager = class {
|
|
|
61
78
|
if (!this.type) this.detectFileType();
|
|
62
79
|
this.state = "loading";
|
|
63
80
|
let newSource;
|
|
81
|
+
let modifiedTime;
|
|
64
82
|
try {
|
|
65
83
|
newSource = await getFileAsync(this.absolutePath);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
modifiedTime = await lastModifiedAsync(this.absolutePath);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (isEnoentError(error)) {
|
|
87
|
+
this.state = "deleted";
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
69
91
|
}
|
|
70
92
|
const newHash = crypto.createHash("sha256").update(newSource).digest("hex");
|
|
71
93
|
if (!force && newHash === this.hash) {
|
|
@@ -74,7 +96,7 @@ var FileManager = class {
|
|
|
74
96
|
}
|
|
75
97
|
this.source = newSource;
|
|
76
98
|
this.hash = newHash;
|
|
77
|
-
this.lastModified =
|
|
99
|
+
this.lastModified = modifiedTime.getTime();
|
|
78
100
|
await this.rebuildImportMetadata();
|
|
79
101
|
this.isTypeOnlyFile = isTypeOnlyFile(this.source);
|
|
80
102
|
this.state = "ready";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-manager.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/file-manager.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { getFileAsync, lastModifiedAsync } from \"@warlock.js/fs\";\nimport crypto from \"crypto\";\nimport { DEV_SERVER_EVENTS } from \"./events\";\nimport { type FileOperations } from \"./file-operations\";\nimport { isTypeOnlyFile, parseImports } from \"./parse-imports\";\nimport { Path } from \"../utils/normalized-path\";\nimport type { FileManifest, FileState, FileType } from \"./types\";\n\nexport type CleanupFunction = () => void;\n\n/**\n * FileManager — per-file metadata for the dependency graph.\n *\n * The loader hook transpiles and caches on-demand at import time, so this\n * class never touches disk for anything beyond reading source to hash it\n * and parse its imports. Its job is keeping the metadata that the\n * orchestrator needs to invalidate dependents on change.\n */\nexport class FileManager {\n public relativePath = \"\";\n public lastModified = 0;\n public hash = \"\";\n public source = \"\";\n\n /** Files this file imports (relative paths). */\n public dependencies = new Set<string>();\n\n /**\n * Subset of `dependencies` reached only through type-only imports — a single\n * runtime occurrence makes the whole edge runtime.\n */\n public typeOnlyDependencies = new Set<string>();\n\n /** Original specifier → resolved absolute path. */\n public importMap = new Map<string, string>();\n\n /** Files that import this file (relative paths). */\n public dependents = new Set<string>();\n\n public type: FileType | undefined;\n\n /** Cleanup hooks the loaded module registered for itself. */\n public cleanup: CleanupFunction[] = [];\n\n /** Whether the source has zero runtime imports/exports. */\n public isTypeOnlyFile = false;\n\n public state: FileState = \"idle\";\n\n public constructor(\n public readonly absolutePath: string,\n public files: Map<string, FileManager>,\n public fileOperations: FileOperations,\n ) {}\n\n public addCleanup(cleanup: CleanupFunction | CleanupFunction[]) {\n const next = Array.isArray(cleanup) ? cleanup : [cleanup];\n this.cleanup.push(...next);\n this.cleanup = [...new Set(this.cleanup)];\n }\n\n public resetCleanup() {\n this.cleanup = [];\n }\n\n /**\n * Initial setup. Restores from manifest if the hash still matches,\n * otherwise re-processes from disk.\n */\n public async init(fileManifest?: Partial<FileManifest>): Promise<void> {\n this.relativePath = Path.toRelative(this.absolutePath);\n this.detectFileType();\n\n if (fileManifest) {\n await this.initFromManifest(fileManifest);\n } else {\n await this.process();\n }\n }\n\n /**\n * Read source, hash, parse imports, emit ready. The only file-system\n * touch happens here; transpilation is the loader hook's job.\n *\n * @param force - re-parse even when the hash matches.\n * @returns true if the file was (re)parsed.\n */\n public async process({ force = false }: { force?: boolean } = {}): Promise<boolean> {\n if (!this.relativePath) this.relativePath = Path.toRelative(this.absolutePath);\n if (!this.type) this.detectFileType();\n\n this.state = \"loading\";\n\n let newSource: string;\n\n try {\n newSource = await getFileAsync(this.absolutePath);\n } catch {\n this.state = \"deleted\";\n return false;\n }\n\n const newHash = crypto.createHash(\"sha256\").update(newSource).digest(\"hex\");\n\n if (!force && newHash === this.hash) {\n this.state = \"ready\";\n return false;\n }\n\n this.source = newSource;\n this.hash = newHash;\n this.lastModified = (await lastModifiedAsync(this.absolutePath)).getTime();\n\n await this.rebuildImportMetadata();\n this.isTypeOnlyFile = isTypeOnlyFile(this.source);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n\n return true;\n }\n\n /**\n * Parse the current source and refresh `importMap`, `dependencies`, and\n * `typeOnlyDependencies`. A dependency is classified type-only iff every\n * import/export statement that resolves to it is type-only — a single\n * runtime reference makes the whole edge runtime.\n */\n protected async rebuildImportMetadata(): Promise<void> {\n const resolved = await parseImports(this.source, this.absolutePath);\n\n this.importMap = new Map();\n this.dependencies = new Set();\n this.typeOnlyDependencies = new Set();\n\n const runtimePaths = new Set<string>();\n\n for (const [originalPath, { absolutePath, isTypeOnly }] of resolved) {\n this.importMap.set(originalPath, absolutePath);\n\n const relativePath = Path.toRelative(absolutePath);\n this.dependencies.add(relativePath);\n\n if (!isTypeOnly) {\n runtimePaths.add(relativePath);\n }\n }\n\n for (const dependency of this.dependencies) {\n if (!runtimePaths.has(dependency)) {\n this.typeOnlyDependencies.add(dependency);\n }\n }\n }\n\n /**\n * Restore from a cached manifest entry. If the on-disk hash matches the\n * manifest hash, dep-graph metadata is restored from the manifest without\n * re-parsing. Otherwise the file is reprocessed.\n */\n protected async initFromManifest(fileManifest: Partial<FileManifest>): Promise<void> {\n this.type = fileManifest.type;\n\n this.state = \"loading\";\n\n try {\n this.source = await getFileAsync(this.absolutePath);\n } catch {\n this.state = \"deleted\";\n return;\n }\n\n const currentHash = crypto.createHash(\"sha256\").update(this.source).digest(\"hex\");\n\n if (currentHash !== fileManifest.hash) {\n await this.process({ force: true });\n return;\n }\n\n this.hash = fileManifest.hash!;\n this.lastModified = fileManifest.lastModified!;\n this.dependencies = new Set(fileManifest.dependencies || []);\n this.typeOnlyDependencies = new Set(fileManifest.typeOnlyDependencies || []);\n this.dependents = new Set(fileManifest.dependents || []);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n }\n\n protected detectFileType(): void {\n const path = this.relativePath;\n\n if (path.includes(\"main.ts\") || path.includes(\"main.tsx\")) {\n this.type = \"main\";\n } else if (path.startsWith(\"src/config/\")) {\n this.type = \"config\";\n } else if (path.endsWith(\"routes.ts\") || path.endsWith(\"routes.tsx\")) {\n this.type = \"route\";\n } else if (path.includes(\"/events/\")) {\n this.type = \"event\";\n } else if (path.includes(\"controller\")) {\n this.type = \"controller\";\n } else if (path.includes(\"service\")) {\n this.type = \"service\";\n } else if (path.endsWith(\".model.ts\")) {\n this.type = \"model\";\n } else {\n this.type = \"other\";\n }\n }\n\n public toManifest(): FileManifest {\n return {\n absolutePath: this.absolutePath,\n relativePath: this.relativePath,\n lastModified: this.lastModified,\n hash: this.hash,\n dependencies: Array.from(this.dependencies),\n typeOnlyDependencies: Array.from(this.typeOnlyDependencies),\n dependents: Array.from(this.dependents),\n type: this.type!,\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-manager.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/file-manager.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { getFileAsync, lastModifiedAsync } from \"@warlock.js/fs\";\nimport crypto from \"crypto\";\nimport { DEV_SERVER_EVENTS } from \"./events\";\nimport { type FileOperations } from \"./file-operations\";\nimport { isTypeOnlyFile, parseImports } from \"./parse-imports\";\nimport { Path } from \"../utils/normalized-path\";\nimport type { FileManifest, FileState, FileType } from \"./types\";\n\nexport type CleanupFunction = () => void;\n\n/**\n * Whether `error` is a Node `ENOENT` (path does not exist) error. Used to\n * distinguish a file that has genuinely vanished between the watcher event\n * and its read/stat (a rename or move racing the filesystem) from a real\n * failure such as `EACCES` or a parse error, which must still surface.\n */\nfunction isEnoentError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as NodeJS.ErrnoException).code === \"ENOENT\"\n );\n}\n\n/**\n * FileManager — per-file metadata for the dependency graph.\n *\n * The loader hook transpiles and caches on-demand at import time, so this\n * class never touches disk for anything beyond reading source to hash it\n * and parse its imports. Its job is keeping the metadata that the\n * orchestrator needs to invalidate dependents on change.\n */\nexport class FileManager {\n public relativePath = \"\";\n public lastModified = 0;\n public hash = \"\";\n public source = \"\";\n\n /** Files this file imports (relative paths). */\n public dependencies = new Set<string>();\n\n /**\n * Subset of `dependencies` reached only through type-only imports — a single\n * runtime occurrence makes the whole edge runtime.\n */\n public typeOnlyDependencies = new Set<string>();\n\n /** Original specifier → resolved absolute path. */\n public importMap = new Map<string, string>();\n\n /** Files that import this file (relative paths). */\n public dependents = new Set<string>();\n\n public type: FileType | undefined;\n\n /** Cleanup hooks the loaded module registered for itself. */\n public cleanup: CleanupFunction[] = [];\n\n /** Whether the source has zero runtime imports/exports. */\n public isTypeOnlyFile = false;\n\n public state: FileState = \"idle\";\n\n public constructor(\n public readonly absolutePath: string,\n public files: Map<string, FileManager>,\n public fileOperations: FileOperations,\n ) {}\n\n public addCleanup(cleanup: CleanupFunction | CleanupFunction[]) {\n const next = Array.isArray(cleanup) ? cleanup : [cleanup];\n this.cleanup.push(...next);\n this.cleanup = [...new Set(this.cleanup)];\n }\n\n public resetCleanup() {\n this.cleanup = [];\n }\n\n /**\n * Initial setup. Restores from manifest if the hash still matches,\n * otherwise re-processes from disk.\n */\n public async init(fileManifest?: Partial<FileManifest>): Promise<void> {\n this.relativePath = Path.toRelative(this.absolutePath);\n this.detectFileType();\n\n if (fileManifest) {\n await this.initFromManifest(fileManifest);\n } else {\n await this.process();\n }\n }\n\n /**\n * Read source, hash, parse imports, emit ready. The only file-system\n * touch happens here; transpilation is the loader hook's job.\n *\n * Both the read and the stat happen up front, before anything on this\n * instance is mutated. A path that disappears between the watcher event\n * and either of those calls (`ENOENT`) — typically a rename or move\n * racing the filesystem — is treated as a deletion rather than a thrown\n * error, and never leaves `source`/`hash` pointing at content that then\n * vanished. A real failure (`EACCES`, a permissions error, etc.) still\n * throws.\n *\n * @param force - re-parse even when the hash matches.\n * @returns true if the file was (re)parsed.\n */\n public async process({ force = false }: { force?: boolean } = {}): Promise<boolean> {\n if (!this.relativePath) this.relativePath = Path.toRelative(this.absolutePath);\n if (!this.type) this.detectFileType();\n\n this.state = \"loading\";\n\n let newSource: string;\n let modifiedTime: Date;\n\n try {\n newSource = await getFileAsync(this.absolutePath);\n modifiedTime = await lastModifiedAsync(this.absolutePath);\n } catch (error) {\n if (isEnoentError(error)) {\n this.state = \"deleted\";\n return false;\n }\n\n throw error;\n }\n\n const newHash = crypto.createHash(\"sha256\").update(newSource).digest(\"hex\");\n\n if (!force && newHash === this.hash) {\n this.state = \"ready\";\n return false;\n }\n\n this.source = newSource;\n this.hash = newHash;\n this.lastModified = modifiedTime.getTime();\n\n await this.rebuildImportMetadata();\n this.isTypeOnlyFile = isTypeOnlyFile(this.source);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n\n return true;\n }\n\n /**\n * Parse the current source and refresh `importMap`, `dependencies`, and\n * `typeOnlyDependencies`. A dependency is classified type-only iff every\n * import/export statement that resolves to it is type-only — a single\n * runtime reference makes the whole edge runtime.\n */\n protected async rebuildImportMetadata(): Promise<void> {\n const resolved = await parseImports(this.source, this.absolutePath);\n\n this.importMap = new Map();\n this.dependencies = new Set();\n this.typeOnlyDependencies = new Set();\n\n const runtimePaths = new Set<string>();\n\n for (const [originalPath, { absolutePath, isTypeOnly }] of resolved) {\n this.importMap.set(originalPath, absolutePath);\n\n const relativePath = Path.toRelative(absolutePath);\n this.dependencies.add(relativePath);\n\n if (!isTypeOnly) {\n runtimePaths.add(relativePath);\n }\n }\n\n for (const dependency of this.dependencies) {\n if (!runtimePaths.has(dependency)) {\n this.typeOnlyDependencies.add(dependency);\n }\n }\n }\n\n /**\n * Restore from a cached manifest entry. If the on-disk hash matches the\n * manifest hash, dep-graph metadata is restored from the manifest without\n * re-parsing. Otherwise the file is reprocessed.\n */\n protected async initFromManifest(fileManifest: Partial<FileManifest>): Promise<void> {\n this.type = fileManifest.type;\n\n this.state = \"loading\";\n\n try {\n this.source = await getFileAsync(this.absolutePath);\n } catch {\n this.state = \"deleted\";\n return;\n }\n\n const currentHash = crypto.createHash(\"sha256\").update(this.source).digest(\"hex\");\n\n if (currentHash !== fileManifest.hash) {\n await this.process({ force: true });\n return;\n }\n\n this.hash = fileManifest.hash!;\n this.lastModified = fileManifest.lastModified!;\n this.dependencies = new Set(fileManifest.dependencies || []);\n this.typeOnlyDependencies = new Set(fileManifest.typeOnlyDependencies || []);\n this.dependents = new Set(fileManifest.dependents || []);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n }\n\n protected detectFileType(): void {\n const path = this.relativePath;\n\n if (path.includes(\"main.ts\") || path.includes(\"main.tsx\")) {\n this.type = \"main\";\n } else if (path.startsWith(\"src/config/\")) {\n this.type = \"config\";\n } else if (path.endsWith(\"routes.ts\") || path.endsWith(\"routes.tsx\")) {\n this.type = \"route\";\n } else if (path.includes(\"/events/\")) {\n this.type = \"event\";\n } else if (path.includes(\"controller\")) {\n this.type = \"controller\";\n } else if (path.includes(\"service\")) {\n this.type = \"service\";\n } else if (path.endsWith(\".model.ts\")) {\n this.type = \"model\";\n } else {\n this.type = \"other\";\n }\n }\n\n public toManifest(): FileManifest {\n return {\n absolutePath: this.absolutePath,\n relativePath: this.relativePath,\n lastModified: this.lastModified,\n hash: this.hash,\n dependencies: Array.from(this.dependencies),\n typeOnlyDependencies: Array.from(this.typeOnlyDependencies),\n dependents: Array.from(this.dependents),\n type: this.type!,\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -169,9 +169,9 @@ var FilesOrchestrator = class {
|
|
|
169
169
|
}
|
|
170
170
|
async watchFiles() {
|
|
171
171
|
devLogSuccess("watching for file changes (not serving yet)");
|
|
172
|
-
this.filesWatcher.onFileChange((p) => this.eventHandler.handleFileChange(p));
|
|
173
|
-
this.filesWatcher.onFileAdd((p) => this.eventHandler.handleFileAdd(p));
|
|
174
|
-
this.filesWatcher.onFileDelete((p) => this.eventHandler.handleFileDelete(p));
|
|
172
|
+
this.filesWatcher.onFileChange((p, settleMs) => this.eventHandler.handleFileChange(p, settleMs));
|
|
173
|
+
this.filesWatcher.onFileAdd((p, settleMs) => this.eventHandler.handleFileAdd(p, settleMs));
|
|
174
|
+
this.filesWatcher.onFileDelete((p, settleMs) => this.eventHandler.handleFileDelete(p, settleMs));
|
|
175
175
|
const watchConfig = warlockConfigManager.get("devServer")?.watch;
|
|
176
176
|
await this.filesWatcher.watch(watchConfig);
|
|
177
177
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files-orchestrator.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/files-orchestrator.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { init } from \"es-module-lexer\";\r\nimport type { MessagePort } from \"node:worker_threads\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport { DependencyGraph } from \"./dependency-graph\";\r\nimport { devLogDim, devLogSuccess } from \"./dev-logger\";\r\nimport { FileEventHandler } from \"./file-event-handler\";\r\nimport { FileManager } from \"./file-manager\";\r\nimport { FileOperations } from \"./file-operations\";\r\nimport { FilesWatcher } from \"./files-watcher\";\r\nimport { FILE_PROCESSING_BATCH_SIZE } from \"./flags\";\r\nimport { EslintHealthChecker } from \"./health-checker/checkers/eslint-health-checker\";\r\nimport { TypescriptHealthChecker } from \"./health-checker/checkers/typescript-health-checker\";\r\nimport type { FileHealthCheckerContract } from \"./health-checker/file-health-checker.contract\";\r\nimport { FilesHealthcareManager } from \"./health-checker/files-healthcare.manager\";\r\nimport { buildTranspileInit } from \"./loader/build-transpile-init.js\";\r\nimport { registerLoader } from \"./loader/register-loader.js\";\r\nimport { ManifestManager } from \"./manifest-manager\";\r\nimport { ModuleLoader } from \"./module-loader\";\r\nimport { packageJsonManager } from \"./package-json-manager\";\r\nimport { Path } from \"../utils/normalized-path\";\r\nimport { SpecialFilesCollector } from \"./special-files-collector\";\r\nimport { tsconfigManager } from \"./tsconfig-manager\";\r\nimport { ensureWarlockDirectory, getFilesFromDirectory } from \"./utils\";\r\n\r\n/**\r\n * Register a cleanup callback that fires before the current module is\r\n * unloaded by HMR. Stack-inspects the caller so user code doesn't need to\r\n * thread the FileManager through.\r\n */\r\nexport function onCleanup(callback: () => any) {\r\n if (globalThis.__currentModuleFile) {\r\n globalThis.__currentModuleFile.addCleanup(callback);\r\n return;\r\n }\r\n\r\n const stack = new Error().stack;\r\n const callerLine = stack?.split(\"\\n\")[2];\r\n const match = callerLine?.match(/\\((.+?):\\d+:\\d+\\)/) || callerLine?.match(/at (.+?):\\d+:\\d+/);\r\n const callerFile = match?.[1];\r\n\r\n if (!callerFile) return;\r\n const fileManager = filesOrchestrator.files.get(Path.toRelative(callerFile));\r\n fileManager?.addCleanup(callback);\r\n}\r\n\r\n/**\r\n * Top-level coordinator for the dev server's file system.\r\n *\r\n * Owns: file discovery, the dependency graph, the manifest, the special-files\r\n * index, the file watcher, and the lifecycle of the ESM loader hook (which\r\n * provides cache-busting via `?v=N` version tokens).\r\n */\r\nexport class FilesOrchestrator {\r\n public readonly filesWatcher = new FilesWatcher();\r\n public readonly files = new Map<string, FileManager>();\r\n private readonly manifest = new ManifestManager(this.files);\r\n private readonly dependencyGraph = new DependencyGraph();\r\n private readonly healthCheckerManager = new FilesHealthcareManager(this.files);\r\n public readonly specialFilesCollector = new SpecialFilesCollector();\r\n public readonly moduleLoader = new ModuleLoader(this.specialFilesCollector);\r\n public readonly fileOperations: FileOperations;\r\n private readonly eventHandler: FileEventHandler;\r\n\r\n /** Main-thread end of the MessageChannel to the loader hook worker. */\r\n private loaderPort: MessagePort | null = null;\r\n\r\n /** Resolve callbacks for pending `flushVersionBumps()` calls. */\r\n private readonly pendingFlushes: Array<() => void> = [];\r\n\r\n public isInitialized = false;\r\n\r\n public constructor() {\r\n this.fileOperations = new FileOperations(\r\n this.files,\r\n this.dependencyGraph,\r\n this.manifest,\r\n this.specialFilesCollector,\r\n );\r\n\r\n this.eventHandler = new FileEventHandler(\r\n this.fileOperations,\r\n this.manifest,\r\n this.dependencyGraph,\r\n this.files,\r\n );\r\n }\r\n\r\n public async add(relativePath: string): Promise<FileManager> {\r\n return this.fileOperations.addFile(relativePath);\r\n }\r\n\r\n public async load<T>(relativePath: string, type = \"other\") {\r\n // Guarantee the ESM loader hook is registered before we import anything.\r\n // `load()` is the dynamic-import entrypoint, so it is the one place that\r\n // must satisfy `init()`'s documented precondition (\"Must be called before\r\n // any user src/ module is dynamically imported\") on its own — it cannot\r\n // assume a caller ran `init()` first.\r\n //\r\n // The CLI command-resolution path proves why: `cliCommandsLoader.load()`\r\n // imports a project command module during command *lookup* (before the\r\n // command's own `execute()` runs), whereas `loadPreloaders()` only calls\r\n // `init()` from *inside* `execute()`. So a `warlock <command>` whose\r\n // command file imports an `app/*` alias or a `.ts` sibling was imported\r\n // with no hook registered and died on `ERR_MODULE_NOT_FOUND` (f9d8f99f).\r\n //\r\n // `init()` is idempotent (guarded by `isInitialized`), so this is a no-op\r\n // on the dev-server path, which already calls it explicitly at startup.\r\n await this.init();\r\n\r\n const fileManager = await this.add(relativePath);\r\n return this.moduleLoader.loadModule<T>(fileManager, fileManager.type || type);\r\n }\r\n\r\n public getDependencyGraph(): DependencyGraph {\r\n return this.dependencyGraph;\r\n }\r\n\r\n public getInvalidationChain(file: string): string[] {\r\n return this.dependencyGraph.getInvalidationChain(file);\r\n }\r\n\r\n public getFiles(): Map<string, FileManager> {\r\n return this.files;\r\n }\r\n\r\n public getHealthCheckerManager(): FilesHealthcareManager {\r\n return this.healthCheckerManager;\r\n }\r\n\r\n /**\r\n * Initialise managers and register the ESM loader hook. Must be called\r\n * before any user `src/` module is dynamically imported.\r\n */\r\n public async init() {\r\n if (this.isInitialized) return;\r\n this.isInitialized = true;\r\n\r\n await init;\r\n await Promise.all([tsconfigManager.init(), packageJsonManager.init()]);\r\n\r\n // The loader hook writes itself into .warlock/, so ensure the dir exists.\r\n await ensureWarlockDirectory();\r\n\r\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\r\n const transpileInit = buildTranspileInit(\r\n tsconfigManager.tsconfig?.compilerOptions,\r\n devServerConfig?.transpileCacheDebug === true,\r\n );\r\n\r\n this.loaderPort = await registerLoader(transpileInit);\r\n\r\n this.loaderPort.on(\"message\", (msg: { type: string }) => {\r\n if (msg.type === \"sync-ack\") {\r\n this.pendingFlushes.shift()?.();\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Tell the hook worker a file changed. The next `import()` of it will get\r\n * a fresh `?v=N` URL → Node cache miss → fresh content.\r\n */\r\n public bumpVersion(absolutePath: string): void {\r\n this.loaderPort?.postMessage({ type: \"bump\", absolutePath });\r\n }\r\n\r\n /**\r\n * Wait until the hook worker has processed every pending bump.\r\n *\r\n * `postMessage` is async — without this, a follow-up `import()` may\r\n * resolve before the worker increments the counter and Node will return\r\n * the cached old module.\r\n */\r\n public flushVersionBumps(): Promise<void> {\r\n if (!this.loaderPort) return Promise.resolve();\r\n\r\n return new Promise<void>((resolve) => {\r\n this.pendingFlushes.push(resolve);\r\n this.loaderPort!.postMessage({ type: \"sync\" });\r\n });\r\n }\r\n\r\n /**\r\n * Discover files on disk, reconcile against the manifest, build the dep\r\n * graph, and persist the new manifest. Idempotent.\r\n */\r\n public async initializeAll() {\r\n const [filesInFilesystem, manifestExists] = await Promise.all([\r\n this.getAllFilesFromFilesystem(),\r\n this.manifest.init(),\r\n ]);\r\n\r\n if (!manifestExists) {\r\n await this.processFiles(filesInFilesystem);\r\n } else {\r\n await this.reconcileFiles(filesInFilesystem);\r\n }\r\n\r\n this.dependencyGraph.build(this.files);\r\n this.fileOperations.updateFileDependents();\r\n this.fileOperations.syncFilesToManifest();\r\n await this.manifest.save();\r\n }\r\n\r\n public async checkHealth(files: { added: string[]; changed: string[]; deleted: string[] }) {\r\n const filesToCheck = [...files.added, ...files.changed]\r\n .map((file) => this.files.get(Path.toRelative(file)))\r\n .filter((file): file is FileManager => !!file);\r\n\r\n const filesToDelete = files.deleted\r\n .map((file) => this.files.get(Path.toRelative(file)))\r\n .filter((file): file is FileManager => !!file);\r\n\r\n await this.healthCheckerManager.onFileChanges(filesToCheck);\r\n this.healthCheckerManager.removeFiles(filesToDelete);\r\n this.healthCheckerManager.checkFiles(filesToCheck);\r\n }\r\n\r\n public async startCheckingHealth(healthCheckers?: FileHealthCheckerContract[]): Promise<void> {\r\n devLogDim(\"Started File Health Checks in the background.\");\r\n\r\n const checkers = healthCheckers ?? [new TypescriptHealthChecker(), new EslintHealthChecker()];\r\n await this.healthCheckerManager.setHealthCheckers(checkers).initialize();\r\n await this.healthCheckerManager.validateAllFiles();\r\n }\r\n\r\n /**\r\n * Glob the src directory, returning relative paths.\r\n */\r\n public async getAllFilesFromFilesystem(): Promise<string[]> {\r\n const absolutePaths = await getFilesFromDirectory();\r\n return absolutePaths.map((absPath) => Path.toRelative(absPath));\r\n }\r\n\r\n /**\r\n * Process every file from scratch — used when no manifest exists.\r\n */\r\n private async processFiles(filePaths: string[]) {\r\n devLogDim(`processing ${filePaths.length} files...`);\r\n await runInBatches(filePaths, FILE_PROCESSING_BATCH_SIZE, async (relativePath) => {\r\n const fileManager = new FileManager(\r\n Path.toAbsolute(relativePath),\r\n this.files,\r\n this.fileOperations,\r\n );\r\n this.files.set(relativePath, fileManager);\r\n await fileManager.process();\r\n });\r\n devLogSuccess(`processed ${filePaths.length} files`);\r\n }\r\n\r\n private async reconcileFiles(filesInFilesystem: string[]) {\r\n const filesInManifest = new Set(this.manifest.getAllFilePaths());\r\n const filesInFilesystemSet = new Set(filesInFilesystem);\r\n\r\n const newFiles = filesInFilesystem.filter((f) => !filesInManifest.has(f));\r\n const deletedFiles = Array.from(filesInManifest).filter((f) => !filesInFilesystemSet.has(f));\r\n const existingFiles = filesInFilesystem.filter((f) => filesInManifest.has(f));\r\n\r\n if (newFiles.length > 0 || deletedFiles.length > 0) {\r\n devLogDim(\r\n `reconciling: ${colors.green(newFiles.length)} new, ${colors.red(deletedFiles.length)} deleted, ${colors.blue(existingFiles.length)} existing`,\r\n );\r\n }\r\n\r\n await this.processFiles(newFiles);\r\n\r\n for (const relativePath of deletedFiles) {\r\n this.manifest.removeFile(relativePath);\r\n this.files.delete(relativePath);\r\n }\r\n\r\n await runInBatches(existingFiles, FILE_PROCESSING_BATCH_SIZE, async (relativePath) => {\r\n const fileManager = new FileManager(\r\n Path.toAbsolute(relativePath),\r\n this.files,\r\n this.fileOperations,\r\n );\r\n this.files.set(relativePath, fileManager);\r\n await fileManager.init(this.manifest.getFile(relativePath));\r\n });\r\n }\r\n\r\n public async watchFiles() {\r\n devLogSuccess(\"watching for file changes (not serving yet)\");\r\n\r\n this.filesWatcher.onFileChange((p) => this.eventHandler.handleFileChange(p));\r\n this.filesWatcher.onFileAdd((p) => this.eventHandler.handleFileAdd(p));\r\n this.filesWatcher.onFileDelete((p) => this.eventHandler.handleFileDelete(p));\r\n\r\n const watchConfig = warlockConfigManager.get(\"devServer\")?.watch;\r\n await this.filesWatcher.watch(watchConfig);\r\n }\r\n}\r\n\r\nasync function runInBatches<T>(\r\n items: T[],\r\n size: number,\r\n fn: (item: T) => Promise<unknown>,\r\n): Promise<void> {\r\n if (items.length === 0) return;\r\n for (let i = 0; i < items.length; i += size) {\r\n await Promise.all(items.slice(i, i + size).map(fn));\r\n }\r\n}\r\n\r\nexport const filesOrchestrator = new FilesOrchestrator();\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"files-orchestrator.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/files-orchestrator.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { init } from \"es-module-lexer\";\r\nimport type { MessagePort } from \"node:worker_threads\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport { DependencyGraph } from \"./dependency-graph\";\r\nimport { devLogDim, devLogSuccess } from \"./dev-logger\";\r\nimport { FileEventHandler } from \"./file-event-handler\";\r\nimport { FileManager } from \"./file-manager\";\r\nimport { FileOperations } from \"./file-operations\";\r\nimport { FilesWatcher } from \"./files-watcher\";\r\nimport { FILE_PROCESSING_BATCH_SIZE } from \"./flags\";\r\nimport { EslintHealthChecker } from \"./health-checker/checkers/eslint-health-checker\";\r\nimport { TypescriptHealthChecker } from \"./health-checker/checkers/typescript-health-checker\";\r\nimport type { FileHealthCheckerContract } from \"./health-checker/file-health-checker.contract\";\r\nimport { FilesHealthcareManager } from \"./health-checker/files-healthcare.manager\";\r\nimport { buildTranspileInit } from \"./loader/build-transpile-init.js\";\r\nimport { registerLoader } from \"./loader/register-loader.js\";\r\nimport { ManifestManager } from \"./manifest-manager\";\r\nimport { ModuleLoader } from \"./module-loader\";\r\nimport { packageJsonManager } from \"./package-json-manager\";\r\nimport { Path } from \"../utils/normalized-path\";\r\nimport { SpecialFilesCollector } from \"./special-files-collector\";\r\nimport { tsconfigManager } from \"./tsconfig-manager\";\r\nimport { ensureWarlockDirectory, getFilesFromDirectory } from \"./utils\";\r\n\r\n/**\r\n * Register a cleanup callback that fires before the current module is\r\n * unloaded by HMR. Stack-inspects the caller so user code doesn't need to\r\n * thread the FileManager through.\r\n */\r\nexport function onCleanup(callback: () => any) {\r\n if (globalThis.__currentModuleFile) {\r\n globalThis.__currentModuleFile.addCleanup(callback);\r\n return;\r\n }\r\n\r\n const stack = new Error().stack;\r\n const callerLine = stack?.split(\"\\n\")[2];\r\n const match = callerLine?.match(/\\((.+?):\\d+:\\d+\\)/) || callerLine?.match(/at (.+?):\\d+:\\d+/);\r\n const callerFile = match?.[1];\r\n\r\n if (!callerFile) return;\r\n const fileManager = filesOrchestrator.files.get(Path.toRelative(callerFile));\r\n fileManager?.addCleanup(callback);\r\n}\r\n\r\n/**\r\n * Top-level coordinator for the dev server's file system.\r\n *\r\n * Owns: file discovery, the dependency graph, the manifest, the special-files\r\n * index, the file watcher, and the lifecycle of the ESM loader hook (which\r\n * provides cache-busting via `?v=N` version tokens).\r\n */\r\nexport class FilesOrchestrator {\r\n public readonly filesWatcher = new FilesWatcher();\r\n public readonly files = new Map<string, FileManager>();\r\n private readonly manifest = new ManifestManager(this.files);\r\n private readonly dependencyGraph = new DependencyGraph();\r\n private readonly healthCheckerManager = new FilesHealthcareManager(this.files);\r\n public readonly specialFilesCollector = new SpecialFilesCollector();\r\n public readonly moduleLoader = new ModuleLoader(this.specialFilesCollector);\r\n public readonly fileOperations: FileOperations;\r\n private readonly eventHandler: FileEventHandler;\r\n\r\n /** Main-thread end of the MessageChannel to the loader hook worker. */\r\n private loaderPort: MessagePort | null = null;\r\n\r\n /** Resolve callbacks for pending `flushVersionBumps()` calls. */\r\n private readonly pendingFlushes: Array<() => void> = [];\r\n\r\n public isInitialized = false;\r\n\r\n public constructor() {\r\n this.fileOperations = new FileOperations(\r\n this.files,\r\n this.dependencyGraph,\r\n this.manifest,\r\n this.specialFilesCollector,\r\n );\r\n\r\n this.eventHandler = new FileEventHandler(\r\n this.fileOperations,\r\n this.manifest,\r\n this.dependencyGraph,\r\n this.files,\r\n );\r\n }\r\n\r\n public async add(relativePath: string): Promise<FileManager> {\r\n return this.fileOperations.addFile(relativePath);\r\n }\r\n\r\n public async load<T>(relativePath: string, type = \"other\") {\r\n // Guarantee the ESM loader hook is registered before we import anything.\r\n // `load()` is the dynamic-import entrypoint, so it is the one place that\r\n // must satisfy `init()`'s documented precondition (\"Must be called before\r\n // any user src/ module is dynamically imported\") on its own — it cannot\r\n // assume a caller ran `init()` first.\r\n //\r\n // The CLI command-resolution path proves why: `cliCommandsLoader.load()`\r\n // imports a project command module during command *lookup* (before the\r\n // command's own `execute()` runs), whereas `loadPreloaders()` only calls\r\n // `init()` from *inside* `execute()`. So a `warlock <command>` whose\r\n // command file imports an `app/*` alias or a `.ts` sibling was imported\r\n // with no hook registered and died on `ERR_MODULE_NOT_FOUND` (f9d8f99f).\r\n //\r\n // `init()` is idempotent (guarded by `isInitialized`), so this is a no-op\r\n // on the dev-server path, which already calls it explicitly at startup.\r\n await this.init();\r\n\r\n const fileManager = await this.add(relativePath);\r\n return this.moduleLoader.loadModule<T>(fileManager, fileManager.type || type);\r\n }\r\n\r\n public getDependencyGraph(): DependencyGraph {\r\n return this.dependencyGraph;\r\n }\r\n\r\n public getInvalidationChain(file: string): string[] {\r\n return this.dependencyGraph.getInvalidationChain(file);\r\n }\r\n\r\n public getFiles(): Map<string, FileManager> {\r\n return this.files;\r\n }\r\n\r\n public getHealthCheckerManager(): FilesHealthcareManager {\r\n return this.healthCheckerManager;\r\n }\r\n\r\n /**\r\n * Initialise managers and register the ESM loader hook. Must be called\r\n * before any user `src/` module is dynamically imported.\r\n */\r\n public async init() {\r\n if (this.isInitialized) return;\r\n this.isInitialized = true;\r\n\r\n await init;\r\n await Promise.all([tsconfigManager.init(), packageJsonManager.init()]);\r\n\r\n // The loader hook writes itself into .warlock/, so ensure the dir exists.\r\n await ensureWarlockDirectory();\r\n\r\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\r\n const transpileInit = buildTranspileInit(\r\n tsconfigManager.tsconfig?.compilerOptions,\r\n devServerConfig?.transpileCacheDebug === true,\r\n );\r\n\r\n this.loaderPort = await registerLoader(transpileInit);\r\n\r\n this.loaderPort.on(\"message\", (msg: { type: string }) => {\r\n if (msg.type === \"sync-ack\") {\r\n this.pendingFlushes.shift()?.();\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Tell the hook worker a file changed. The next `import()` of it will get\r\n * a fresh `?v=N` URL → Node cache miss → fresh content.\r\n */\r\n public bumpVersion(absolutePath: string): void {\r\n this.loaderPort?.postMessage({ type: \"bump\", absolutePath });\r\n }\r\n\r\n /**\r\n * Wait until the hook worker has processed every pending bump.\r\n *\r\n * `postMessage` is async — without this, a follow-up `import()` may\r\n * resolve before the worker increments the counter and Node will return\r\n * the cached old module.\r\n */\r\n public flushVersionBumps(): Promise<void> {\r\n if (!this.loaderPort) return Promise.resolve();\r\n\r\n return new Promise<void>((resolve) => {\r\n this.pendingFlushes.push(resolve);\r\n this.loaderPort!.postMessage({ type: \"sync\" });\r\n });\r\n }\r\n\r\n /**\r\n * Discover files on disk, reconcile against the manifest, build the dep\r\n * graph, and persist the new manifest. Idempotent.\r\n */\r\n public async initializeAll() {\r\n const [filesInFilesystem, manifestExists] = await Promise.all([\r\n this.getAllFilesFromFilesystem(),\r\n this.manifest.init(),\r\n ]);\r\n\r\n if (!manifestExists) {\r\n await this.processFiles(filesInFilesystem);\r\n } else {\r\n await this.reconcileFiles(filesInFilesystem);\r\n }\r\n\r\n this.dependencyGraph.build(this.files);\r\n this.fileOperations.updateFileDependents();\r\n this.fileOperations.syncFilesToManifest();\r\n await this.manifest.save();\r\n }\r\n\r\n public async checkHealth(files: { added: string[]; changed: string[]; deleted: string[] }) {\r\n const filesToCheck = [...files.added, ...files.changed]\r\n .map((file) => this.files.get(Path.toRelative(file)))\r\n .filter((file): file is FileManager => !!file);\r\n\r\n const filesToDelete = files.deleted\r\n .map((file) => this.files.get(Path.toRelative(file)))\r\n .filter((file): file is FileManager => !!file);\r\n\r\n await this.healthCheckerManager.onFileChanges(filesToCheck);\r\n this.healthCheckerManager.removeFiles(filesToDelete);\r\n this.healthCheckerManager.checkFiles(filesToCheck);\r\n }\r\n\r\n public async startCheckingHealth(healthCheckers?: FileHealthCheckerContract[]): Promise<void> {\r\n devLogDim(\"Started File Health Checks in the background.\");\r\n\r\n const checkers = healthCheckers ?? [new TypescriptHealthChecker(), new EslintHealthChecker()];\r\n await this.healthCheckerManager.setHealthCheckers(checkers).initialize();\r\n await this.healthCheckerManager.validateAllFiles();\r\n }\r\n\r\n /**\r\n * Glob the src directory, returning relative paths.\r\n */\r\n public async getAllFilesFromFilesystem(): Promise<string[]> {\r\n const absolutePaths = await getFilesFromDirectory();\r\n return absolutePaths.map((absPath) => Path.toRelative(absPath));\r\n }\r\n\r\n /**\r\n * Process every file from scratch — used when no manifest exists.\r\n */\r\n private async processFiles(filePaths: string[]) {\r\n devLogDim(`processing ${filePaths.length} files...`);\r\n await runInBatches(filePaths, FILE_PROCESSING_BATCH_SIZE, async (relativePath) => {\r\n const fileManager = new FileManager(\r\n Path.toAbsolute(relativePath),\r\n this.files,\r\n this.fileOperations,\r\n );\r\n this.files.set(relativePath, fileManager);\r\n await fileManager.process();\r\n });\r\n devLogSuccess(`processed ${filePaths.length} files`);\r\n }\r\n\r\n private async reconcileFiles(filesInFilesystem: string[]) {\r\n const filesInManifest = new Set(this.manifest.getAllFilePaths());\r\n const filesInFilesystemSet = new Set(filesInFilesystem);\r\n\r\n const newFiles = filesInFilesystem.filter((f) => !filesInManifest.has(f));\r\n const deletedFiles = Array.from(filesInManifest).filter((f) => !filesInFilesystemSet.has(f));\r\n const existingFiles = filesInFilesystem.filter((f) => filesInManifest.has(f));\r\n\r\n if (newFiles.length > 0 || deletedFiles.length > 0) {\r\n devLogDim(\r\n `reconciling: ${colors.green(newFiles.length)} new, ${colors.red(deletedFiles.length)} deleted, ${colors.blue(existingFiles.length)} existing`,\r\n );\r\n }\r\n\r\n await this.processFiles(newFiles);\r\n\r\n for (const relativePath of deletedFiles) {\r\n this.manifest.removeFile(relativePath);\r\n this.files.delete(relativePath);\r\n }\r\n\r\n await runInBatches(existingFiles, FILE_PROCESSING_BATCH_SIZE, async (relativePath) => {\r\n const fileManager = new FileManager(\r\n Path.toAbsolute(relativePath),\r\n this.files,\r\n this.fileOperations,\r\n );\r\n this.files.set(relativePath, fileManager);\r\n await fileManager.init(this.manifest.getFile(relativePath));\r\n });\r\n }\r\n\r\n public async watchFiles() {\r\n devLogSuccess(\"watching for file changes (not serving yet)\");\r\n\r\n this.filesWatcher.onFileChange((p, settleMs) => this.eventHandler.handleFileChange(p, settleMs));\r\n this.filesWatcher.onFileAdd((p, settleMs) => this.eventHandler.handleFileAdd(p, settleMs));\r\n this.filesWatcher.onFileDelete((p, settleMs) => this.eventHandler.handleFileDelete(p, settleMs));\r\n\r\n const watchConfig = warlockConfigManager.get(\"devServer\")?.watch;\r\n await this.filesWatcher.watch(watchConfig);\r\n }\r\n}\r\n\r\nasync function runInBatches<T>(\r\n items: T[],\r\n size: number,\r\n fn: (item: T) => Promise<unknown>,\r\n): Promise<void> {\r\n if (items.length === 0) return;\r\n for (let i = 0; i < items.length; i += size) {\r\n await Promise.all(items.slice(i, i + size).map(fn));\r\n }\r\n}\r\n\r\nexport const filesOrchestrator = new FilesOrchestrator();\r\n"],"mappings":"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"}
|
|
@@ -32,13 +32,15 @@ const ENV_FILES = [
|
|
|
32
32
|
var FilesWatcher = class {
|
|
33
33
|
constructor() {
|
|
34
34
|
this.id = Random.string();
|
|
35
|
+
this.rawEventTimestamps = /* @__PURE__ */ new Map();
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
38
|
* Watch for files changes
|
|
38
39
|
* @param config Optional watch configuration
|
|
39
40
|
*/
|
|
40
41
|
async watch(config) {
|
|
41
|
-
const
|
|
42
|
+
const devServerConfig = await warlockConfigManager.lazyGet("devServer");
|
|
43
|
+
const userWatchConfig = devServerConfig?.watch;
|
|
42
44
|
const basePaths = [
|
|
43
45
|
...ENV_FILES.map((file) => rootPath(file)),
|
|
44
46
|
rootPath("warlock.config.ts"),
|
|
@@ -51,6 +53,7 @@ var FilesWatcher = class {
|
|
|
51
53
|
...userWatchConfig?.exclude || [],
|
|
52
54
|
...config?.exclude || []
|
|
53
55
|
];
|
|
56
|
+
const timingsEnabled = devServerConfig?.timings === true;
|
|
54
57
|
const watcher = chokidar.watch(paths, {
|
|
55
58
|
ignoreInitial: true,
|
|
56
59
|
ignored,
|
|
@@ -64,6 +67,10 @@ var FilesWatcher = class {
|
|
|
64
67
|
},
|
|
65
68
|
depth: 99
|
|
66
69
|
});
|
|
70
|
+
if (timingsEnabled) watcher.on("raw", (_event, path) => {
|
|
71
|
+
const normalized = Path.normalize(path);
|
|
72
|
+
if (!this.rawEventTimestamps.has(normalized)) this.rawEventTimestamps.set(normalized, performance.now());
|
|
73
|
+
});
|
|
67
74
|
watcher.on("add", (filePath) => this.triggerEvent("add", filePath));
|
|
68
75
|
watcher.on("change", (filePath) => this.triggerEvent("change", filePath));
|
|
69
76
|
watcher.on("unlink", (filePath) => this.triggerEvent("delete", filePath));
|
|
@@ -78,7 +85,14 @@ var FilesWatcher = class {
|
|
|
78
85
|
* Debouncing is handled at the orchestrator level for batch processing
|
|
79
86
|
*/
|
|
80
87
|
triggerEvent(event, filePath, error) {
|
|
81
|
-
|
|
88
|
+
const normalized = Path.normalize(filePath);
|
|
89
|
+
const rawEventAt = this.rawEventTimestamps.get(normalized);
|
|
90
|
+
let settleMs;
|
|
91
|
+
if (rawEventAt !== void 0) {
|
|
92
|
+
settleMs = performance.now() - rawEventAt;
|
|
93
|
+
this.rawEventTimestamps.delete(normalized);
|
|
94
|
+
}
|
|
95
|
+
events.trigger(`file-watcher.${this.id}.${event}`, normalized, error, settleMs);
|
|
82
96
|
}
|
|
83
97
|
/**
|
|
84
98
|
* On file change event
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files-watcher.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/files-watcher.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { Random } from \"@mongez/reinforcements\";\nimport chokidar from \"chokidar\";\nimport { rootPath, srcPath } from \"../utils\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { Path } from \"../utils/normalized-path\";\n\ntype FileWatcherEvent = \"change\" | \"delete\" | \"add\" | \"error\" | \"addDir\" | \"unlinkDir\";\n\ntype FileChangeCallback = (filePath: string) => void;\ntype FileDeleteCallback = (filePath: string) => void;\ntype FileAddCallback = (filePath: string) => void;\ntype FileErrorCallback = (filePath: string, error: Error) => void;\ntype FileAddDirCallback = (filePath: string) => void;\ntype FileUnlinkDirCallback = (filePath: string) => void;\ntype OnFileEventCallback =\n | FileChangeCallback\n | FileDeleteCallback\n | FileAddCallback\n | FileErrorCallback\n | FileAddDirCallback\n | FileUnlinkDirCallback;\n\n/**\n * Watch configuration options\n */\nexport type WatchConfig = {\n /**\n * Glob patterns to include\n */\n include?: string[];\n /**\n * Glob patterns to exclude\n */\n exclude?: string[];\n};\n\n/**\n * Default patterns to exclude from watching\n */\nconst DEFAULT_EXCLUDE = [\"**/node_modules/**\", \"**/dist/**\", \"**/.warlock/**\", \"**/.git/**\"];\n\n/**\n * All .env file variants to watch\n */\nconst ENV_FILES = [\n \".env\",\n \".env.local\",\n \".env.development\",\n \".env.development.local\",\n \".env.test\",\n \".env.test.local\",\n \".env.production\",\n \".env.production.local\",\n];\n\nexport class FilesWatcher {\n /**\n * File watcher id\n */\n private id = Random.string();\n\n /**\n * Watch for files changes\n * @param config Optional watch configuration\n */\n public async watch(config?: WatchConfig) {\n // Get user config from warlock.config.ts\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\n const userWatchConfig = devServerConfig?.watch;\n\n // Build paths to watch:\n // 1. All .env variants that exist\n // 2. warlock.config.ts (project-level settings; restart-required on change)\n // 3. src directory\n // 4. Any additional paths from user config\n const envPaths = ENV_FILES.map((file) => rootPath(file));\n const basePaths = [...envPaths, rootPath(\"warlock.config.ts\"), srcPath()];\n const additionalPaths = userWatchConfig?.include || config?.include || [];\n\n const paths = [...basePaths, ...additionalPaths].map((path) => Path.normalize(path));\n\n // Merge default exclude with config exclude\n const ignored = [\n ...DEFAULT_EXCLUDE,\n ...(userWatchConfig?.exclude || []),\n ...(config?.exclude || []),\n ];\n\n const watcher = chokidar.watch(paths, {\n ignoreInitial: true,\n ignored,\n persistent: true,\n usePolling: false, // Try native first, will fallback if needed\n interval: 100,\n binaryInterval: 300,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n // On Windows, explicitly enable recursive watching\n depth: 99,\n });\n\n watcher.on(\"add\", (filePath) => this.triggerEvent(\"add\", filePath));\n watcher.on(\"change\", (filePath) => this.triggerEvent(\"change\", filePath));\n watcher.on(\"unlink\", (filePath) => this.triggerEvent(\"delete\", filePath));\n watcher.on(\"addDir\", (filePath) => this.triggerEvent(\"addDir\", filePath));\n watcher.on(\"unlinkDir\", (filePath) => this.triggerEvent(\"unlinkDir\", filePath));\n // watcher.on(\"error\", (error: Error, filePath: string) =>\n // this.triggerEvent(\"error\", filePath, error),\n // );\n\n // Cleanup on process exit\n process.on(\"SIGINT\", async () => {\n await watcher.close();\n });\n }\n\n /**\n * Trigger event immediately (no debouncing here)\n * Debouncing is handled at the orchestrator level for batch processing\n */\n private triggerEvent(event: FileWatcherEvent, filePath: string, error?: Error) {\n events.trigger(`file-watcher.${this.id}.${event}`,
|
|
1
|
+
{"version":3,"file":"files-watcher.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/files-watcher.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { Random } from \"@mongez/reinforcements\";\nimport chokidar from \"chokidar\";\nimport { rootPath, srcPath } from \"../utils\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { Path } from \"../utils/normalized-path\";\n\ntype FileWatcherEvent = \"change\" | \"delete\" | \"add\" | \"error\" | \"addDir\" | \"unlinkDir\";\n\ntype FileChangeCallback = (filePath: string, settleMs?: number) => void;\ntype FileDeleteCallback = (filePath: string, settleMs?: number) => void;\ntype FileAddCallback = (filePath: string, settleMs?: number) => void;\ntype FileErrorCallback = (filePath: string, error: Error) => void;\ntype FileAddDirCallback = (filePath: string) => void;\ntype FileUnlinkDirCallback = (filePath: string) => void;\ntype OnFileEventCallback =\n | FileChangeCallback\n | FileDeleteCallback\n | FileAddCallback\n | FileErrorCallback\n | FileAddDirCallback\n | FileUnlinkDirCallback;\n\n/**\n * Watch configuration options\n */\nexport type WatchConfig = {\n /**\n * Glob patterns to include\n */\n include?: string[];\n /**\n * Glob patterns to exclude\n */\n exclude?: string[];\n};\n\n/**\n * Default patterns to exclude from watching\n */\nconst DEFAULT_EXCLUDE = [\"**/node_modules/**\", \"**/dist/**\", \"**/.warlock/**\", \"**/.git/**\"];\n\n/**\n * All .env file variants to watch\n */\nconst ENV_FILES = [\n \".env\",\n \".env.local\",\n \".env.development\",\n \".env.development.local\",\n \".env.test\",\n \".env.test.local\",\n \".env.production\",\n \".env.production.local\",\n];\n\nexport class FilesWatcher {\n /**\n * File watcher id\n */\n private id = Random.string();\n\n /**\n * First-seen timestamp per path from chokidar's `raw` event, which fires\n * BEFORE `awaitWriteFinish` stabilises the change. Diffing it against the\n * stabilised `add`/`change` timestamp gives the actual watcher-settle\n * duration for the phase-timing line. Only populated when\n * `devServer.timings` is on — see `watch()` — so a disabled flag costs\n * nothing here beyond the `undefined` checks below.\n */\n private readonly rawEventTimestamps = new Map<string, number>();\n\n /**\n * Watch for files changes\n * @param config Optional watch configuration\n */\n public async watch(config?: WatchConfig) {\n // Get user config from warlock.config.ts\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\n const userWatchConfig = devServerConfig?.watch;\n\n // Build paths to watch:\n // 1. All .env variants that exist\n // 2. warlock.config.ts (project-level settings; restart-required on change)\n // 3. src directory\n // 4. Any additional paths from user config\n const envPaths = ENV_FILES.map((file) => rootPath(file));\n const basePaths = [...envPaths, rootPath(\"warlock.config.ts\"), srcPath()];\n const additionalPaths = userWatchConfig?.include || config?.include || [];\n\n const paths = [...basePaths, ...additionalPaths].map((path) => Path.normalize(path));\n\n // Merge default exclude with config exclude\n const ignored = [\n ...DEFAULT_EXCLUDE,\n ...(userWatchConfig?.exclude || []),\n ...(config?.exclude || []),\n ];\n\n const timingsEnabled = devServerConfig?.timings === true;\n\n const watcher = chokidar.watch(paths, {\n ignoreInitial: true,\n ignored,\n persistent: true,\n usePolling: false, // Try native first, will fallback if needed\n interval: 100,\n binaryInterval: 300,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n // On Windows, explicitly enable recursive watching\n depth: 99,\n });\n\n if (timingsEnabled) {\n watcher.on(\"raw\", (_event, path) => {\n const normalized = Path.normalize(path);\n if (!this.rawEventTimestamps.has(normalized)) {\n this.rawEventTimestamps.set(normalized, performance.now());\n }\n });\n }\n\n watcher.on(\"add\", (filePath) => this.triggerEvent(\"add\", filePath));\n watcher.on(\"change\", (filePath) => this.triggerEvent(\"change\", filePath));\n watcher.on(\"unlink\", (filePath) => this.triggerEvent(\"delete\", filePath));\n watcher.on(\"addDir\", (filePath) => this.triggerEvent(\"addDir\", filePath));\n watcher.on(\"unlinkDir\", (filePath) => this.triggerEvent(\"unlinkDir\", filePath));\n // watcher.on(\"error\", (error: Error, filePath: string) =>\n // this.triggerEvent(\"error\", filePath, error),\n // );\n\n // Cleanup on process exit\n process.on(\"SIGINT\", async () => {\n await watcher.close();\n });\n }\n\n /**\n * Trigger event immediately (no debouncing here)\n * Debouncing is handled at the orchestrator level for batch processing\n */\n private triggerEvent(event: FileWatcherEvent, filePath: string, error?: Error) {\n const normalized = Path.normalize(filePath);\n const rawEventAt = this.rawEventTimestamps.get(normalized);\n let settleMs: number | undefined;\n\n if (rawEventAt !== undefined) {\n settleMs = performance.now() - rawEventAt;\n this.rawEventTimestamps.delete(normalized);\n }\n\n events.trigger(`file-watcher.${this.id}.${event}`, normalized, error, settleMs);\n }\n\n /**\n * On file change event\n */\n public onFileChange(callback: FileChangeCallback) {\n return this.on(\"change\", callback);\n }\n\n /**\n * On file delete event\n */\n public onFileDelete(callback: FileDeleteCallback) {\n return this.on(\"delete\", callback);\n }\n\n /**\n * On file add event\n */\n public onFileAdd(callback: FileAddCallback) {\n return this.on(\"add\", callback);\n }\n\n /**\n * On file error event\n */\n public onFileError(callback: FileErrorCallback) {\n return this.on(\"error\", callback);\n }\n\n /**\n * On file add dir event\n */\n public onDirectoryAdd(callback: FileAddDirCallback) {\n return this.on(\"addDir\", callback);\n }\n\n /**\n * On file unlink dir event\n */\n public onDirectoryRemove(callback: FileUnlinkDirCallback) {\n return this.on(\"unlinkDir\", callback);\n }\n\n /**\n * On file event\n */\n public on(event: FileWatcherEvent, callback: OnFileEventCallback) {\n return events.subscribe(`file-watcher.${this.id}.${event}`, callback);\n }\n}\n"],"mappings":";;;;;;;;;;;;AAwCA,MAAM,kBAAkB;CAAC;CAAsB;CAAc;CAAkB;AAAY;;;;AAK3F,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,IAAa,eAAb,MAA0B;;YAIX,OAAO,OAAO;4CAUW,IAAI,IAAoB;;;;;;CAM9D,MAAa,MAAM,QAAsB;EAEvC,MAAM,kBAAkB,MAAM,qBAAqB,QAAQ,WAAW;EACtE,MAAM,kBAAkB,iBAAiB;EAQzC,MAAM,YAAY;GAAC,GADF,UAAU,KAAK,SAAS,SAAS,IAAI,CACzB;GAAG,SAAS,mBAAmB;GAAG,QAAQ;EAAC;EACxE,MAAM,kBAAkB,iBAAiB,WAAW,QAAQ,WAAW,CAAC;EAExE,MAAM,QAAQ,CAAC,GAAG,WAAW,GAAG,eAAe,CAAC,CAAC,KAAK,SAAS,KAAK,UAAU,IAAI,CAAC;EAGnF,MAAM,UAAU;GACd,GAAG;GACH,GAAI,iBAAiB,WAAW,CAAC;GACjC,GAAI,QAAQ,WAAW,CAAC;EAC1B;EAEA,MAAM,iBAAiB,iBAAiB,YAAY;EAEpD,MAAM,UAAU,SAAS,MAAM,OAAO;GACpC,eAAe;GACf;GACA,YAAY;GACZ,YAAY;GACZ,UAAU;GACV,gBAAgB;GAChB,kBAAkB;IAChB,oBAAoB;IACpB,cAAc;GAChB;GAEA,OAAO;EACT,CAAC;EAED,IAAI,gBACF,QAAQ,GAAG,QAAQ,QAAQ,SAAS;GAClC,MAAM,aAAa,KAAK,UAAU,IAAI;GACtC,IAAI,CAAC,KAAK,mBAAmB,IAAI,UAAU,GACzC,KAAK,mBAAmB,IAAI,YAAY,YAAY,IAAI,CAAC;EAE7D,CAAC;EAGH,QAAQ,GAAG,QAAQ,aAAa,KAAK,aAAa,OAAO,QAAQ,CAAC;EAClE,QAAQ,GAAG,WAAW,aAAa,KAAK,aAAa,UAAU,QAAQ,CAAC;EACxE,QAAQ,GAAG,WAAW,aAAa,KAAK,aAAa,UAAU,QAAQ,CAAC;EACxE,QAAQ,GAAG,WAAW,aAAa,KAAK,aAAa,UAAU,QAAQ,CAAC;EACxE,QAAQ,GAAG,cAAc,aAAa,KAAK,aAAa,aAAa,QAAQ,CAAC;EAM9E,QAAQ,GAAG,UAAU,YAAY;GAC/B,MAAM,QAAQ,MAAM;EACtB,CAAC;CACH;;;;;CAMA,AAAQ,aAAa,OAAyB,UAAkB,OAAe;EAC7E,MAAM,aAAa,KAAK,UAAU,QAAQ;EAC1C,MAAM,aAAa,KAAK,mBAAmB,IAAI,UAAU;EACzD,IAAI;EAEJ,IAAI,eAAe,QAAW;GAC5B,WAAW,YAAY,IAAI,IAAI;GAC/B,KAAK,mBAAmB,OAAO,UAAU;EAC3C;EAEA,OAAO,QAAQ,gBAAgB,KAAK,GAAG,GAAG,SAAS,YAAY,OAAO,QAAQ;CAChF;;;;CAKA,AAAO,aAAa,UAA8B;EAChD,OAAO,KAAK,GAAG,UAAU,QAAQ;CACnC;;;;CAKA,AAAO,aAAa,UAA8B;EAChD,OAAO,KAAK,GAAG,UAAU,QAAQ;CACnC;;;;CAKA,AAAO,UAAU,UAA2B;EAC1C,OAAO,KAAK,GAAG,OAAO,QAAQ;CAChC;;;;CAKA,AAAO,YAAY,UAA6B;EAC9C,OAAO,KAAK,GAAG,SAAS,QAAQ;CAClC;;;;CAKA,AAAO,eAAe,UAA8B;EAClD,OAAO,KAAK,GAAG,UAAU,QAAQ;CACnC;;;;CAKA,AAAO,kBAAkB,UAAiC;EACxD,OAAO,KAAK,GAAG,aAAa,QAAQ;CACtC;;;;CAKA,AAAO,GAAG,OAAyB,UAA+B;EAChE,OAAO,OAAO,UAAU,gBAAgB,KAAK,GAAG,GAAG,SAAS,QAAQ;CACtE;AACF"}
|
package/esm/dev-server/flags.mjs
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { warlockPath } from "../utils/paths.mjs";
|
|
2
2
|
import "../utils/index.mjs";
|
|
3
|
+
import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
|
|
3
4
|
|
|
4
5
|
//#region ../core/src/dev-server/flags.ts
|
|
5
6
|
const MANIFEST_PATH = warlockPath("manifest.json");
|
|
7
|
+
/**
|
|
8
|
+
* Whether the opt-in per-phase reload timing line (`devServer.timings`) is
|
|
9
|
+
* on. Read through the same `warlockConfigManager` every other dev-server
|
|
10
|
+
* debug knob already goes through (e.g. `transpileCacheDebug`) rather than
|
|
11
|
+
* an env var, so a project author looks in one place for every dev-server
|
|
12
|
+
* debug switch. `isLoaded` guards the synchronous `get()` call — which
|
|
13
|
+
* throws before config has loaded — so this stays safe to call from
|
|
14
|
+
* anywhere in the reload pipeline, including paths that could in principle
|
|
15
|
+
* run before boot finishes.
|
|
16
|
+
*/
|
|
17
|
+
function isTimingsEnabled() {
|
|
18
|
+
if (!warlockConfigManager.isLoaded) return false;
|
|
19
|
+
return warlockConfigManager.get("devServer")?.timings === true;
|
|
20
|
+
}
|
|
6
21
|
|
|
7
22
|
//#endregion
|
|
8
|
-
export { MANIFEST_PATH };
|
|
23
|
+
export { MANIFEST_PATH, isTimingsEnabled };
|
|
9
24
|
//# sourceMappingURL=flags.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flags.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/flags.ts"],"sourcesContent":["import { warlockPath } from \"../utils\";\n\nexport const MANIFEST_PATH = warlockPath(\"manifest.json\");\n\n/**\n * Number of files to process in parallel per batch\n * Adjust this value to optimize performance vs memory usage\n * - Lower values (10-20): More stable, less memory, slower\n * - Higher values (100-200): Faster, more memory, potential instability\n * - Recommended: 50 for most projects\n */\nexport const FILE_PROCESSING_BATCH_SIZE = 500;\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"flags.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/flags.ts"],"sourcesContent":["import { warlockPath } from \"../utils\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\n\nexport const MANIFEST_PATH = warlockPath(\"manifest.json\");\n\n/**\n * Number of files to process in parallel per batch\n * Adjust this value to optimize performance vs memory usage\n * - Lower values (10-20): More stable, less memory, slower\n * - Higher values (100-200): Faster, more memory, potential instability\n * - Recommended: 50 for most projects\n */\nexport const FILE_PROCESSING_BATCH_SIZE = 500;\n\n/**\n * Whether the opt-in per-phase reload timing line (`devServer.timings`) is\n * on. Read through the same `warlockConfigManager` every other dev-server\n * debug knob already goes through (e.g. `transpileCacheDebug`) rather than\n * an env var, so a project author looks in one place for every dev-server\n * debug switch. `isLoaded` guards the synchronous `get()` call — which\n * throws before config has loaded — so this stays safe to call from\n * anywhere in the reload pipeline, including paths that could in principle\n * run before boot finishes.\n */\nexport function isTimingsEnabled(): boolean {\n if (!warlockConfigManager.isLoaded) return false;\n return warlockConfigManager.get(\"devServer\")?.timings === true;\n}\n"],"mappings":";;;;;AAGA,MAAa,gBAAgB,YAAY,eAAe;;;;;;;;;;;AAqBxD,SAAgB,mBAA4B;CAC1C,IAAI,CAAC,qBAAqB,UAAU,OAAO;CAC3C,OAAO,qBAAqB,IAAI,WAAW,CAAC,EAAE,YAAY;AAC5D"}
|