@warlock.js/core 5.24.0 → 5.25.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 +30 -1
- package/bin/warlock.js +0 -0
- package/esm/application/application.d.mts +21 -0
- package/esm/application/application.d.mts.map +1 -1
- package/esm/application/application.mjs +29 -1
- package/esm/application/application.mjs.map +1 -1
- package/esm/application/boot-signal.d.mts +2 -1
- package/esm/application/boot-signal.d.mts.map +1 -1
- package/esm/application/boot-signal.mjs.map +1 -1
- package/esm/application/index.d.mts +1 -0
- package/esm/application/index.mjs +1 -0
- package/esm/application/roles.d.mts +72 -0
- package/esm/application/roles.d.mts.map +1 -0
- package/esm/application/roles.mjs +124 -0
- package/esm/application/roles.mjs.map +1 -0
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/create-database.command.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -1
- package/esm/cli/commands/drop-tables.command.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/path-resolver.mjs.map +1 -1
- package/esm/cli/commands/migrate.command.mjs.map +1 -1
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/commands/seed.command.mjs.map +1 -1
- package/esm/cli/commands/start-production.command.mjs +54 -5
- package/esm/cli/commands/start-production.command.mjs.map +1 -1
- package/esm/cli/commands/storage-put.action.mjs.map +1 -1
- package/esm/cli/commands/storage-put.command.mjs.map +1 -1
- package/esm/cli/commands/typings-generator.command.mjs +17 -1
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/cli/commands/update.command.mjs.map +1 -1
- package/esm/cli/commands-loader.mjs.map +1 -1
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +3 -1
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/database-connector.mjs.map +1 -1
- package/esm/connectors/herald-connector.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +7 -0
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/logger-connector.mjs.map +1 -1
- package/esm/connectors/mail-connector.mjs.map +1 -1
- package/esm/connectors/types.d.mts +8 -0
- package/esm/connectors/types.d.mts.map +1 -1
- package/esm/connectors/types.mjs.map +1 -1
- package/esm/database/create-database-action.mjs.map +1 -1
- package/esm/database/drop-tables-action.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +2 -1
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs +2 -1
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/dependency-graph.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +2 -0
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/dev-server/manifest-manager.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +10 -5
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/register-installed-devtools.mjs +47 -0
- package/esm/dev-server/register-installed-devtools.mjs.map +1 -0
- package/esm/dev-server/run-connector-typings.mjs +28 -0
- package/esm/dev-server/run-connector-typings.mjs.map +1 -0
- package/esm/dev-server/special-file-patterns.mjs +3 -1
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/dev-server/special-files-collector.d.mts +2 -2
- package/esm/dev-server/special-files-collector.d.mts.map +1 -1
- package/esm/dev-server/special-files-collector.mjs +7 -4
- package/esm/dev-server/special-files-collector.mjs.map +1 -1
- package/esm/encryption/password.mjs.map +1 -1
- package/esm/generations/add-command.action.mjs +3 -1
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs.map +1 -1
- package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
- package/esm/generations/features/ai-google.feature.mjs.map +1 -1
- package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
- package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
- package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
- package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
- package/esm/generations/features/ai.feature.mjs.map +1 -1
- package/esm/generations/features/devtools.feature.mjs +23 -0
- package/esm/generations/features/devtools.feature.mjs.map +1 -0
- package/esm/generations/features/herald.feature.mjs.map +1 -1
- package/esm/generations/features/image.feature.mjs.map +1 -1
- package/esm/generations/features/index.mjs +2 -0
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/mail.feature.mjs.map +1 -1
- package/esm/generations/features/mongodb.feature.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/postgres.feature.mjs.map +1 -1
- package/esm/generations/features/react-email.feature.mjs.map +1 -1
- package/esm/generations/features/react.feature.mjs.map +1 -1
- package/esm/generations/features/redis.feature.mjs.map +1 -1
- package/esm/generations/features/s3.feature.mjs.map +1 -1
- package/esm/generations/features/scheduler.feature.mjs.map +1 -1
- package/esm/generations/features/ses.feature.mjs.map +1 -1
- package/esm/generations/features/socket.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +6 -2
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/types.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/http/database/RequestLog.mjs.map +1 -1
- package/esm/http/index.d.mts +2 -1
- package/esm/http/index.mjs +2 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +9 -1
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/response.d.mts +5 -5
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server-rebuilt.d.mts +20 -0
- package/esm/http/server-rebuilt.d.mts.map +1 -0
- package/esm/http/server-rebuilt.mjs +21 -0
- package/esm/http/server-rebuilt.mjs.map +1 -0
- package/esm/http/tracing/index.mjs +1 -1
- package/esm/http/tracing/tracing-dispatcher.d.mts +13 -6
- package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -1
- package/esm/http/tracing/tracing-dispatcher.mjs +46 -14
- package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -1
- package/esm/http/tracing/tracing.type.d.mts +2 -1
- package/esm/http/tracing/tracing.type.d.mts.map +1 -1
- package/esm/http/types.d.mts +9 -0
- package/esm/http/types.d.mts.map +1 -1
- package/esm/image/image.mjs.map +1 -1
- package/esm/index.d.mts +4 -2
- package/esm/index.mjs +4 -2
- package/esm/loader/app-module-importer.d.mts +17 -0
- package/esm/loader/app-module-importer.d.mts.map +1 -0
- package/esm/loader/app-module-importer.mjs +20 -0
- package/esm/loader/app-module-importer.mjs.map +1 -0
- package/esm/logger/logger.mjs.map +1 -1
- package/esm/mail/events.d.mts +7 -0
- package/esm/mail/events.d.mts.map +1 -1
- package/esm/mail/events.mjs +7 -0
- package/esm/mail/events.mjs.map +1 -1
- package/esm/mail/send-mail.d.mts.map +1 -1
- package/esm/mail/send-mail.mjs +23 -2
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/mail/types.d.mts +5 -1
- package/esm/mail/types.d.mts.map +1 -1
- package/esm/mail/types.mjs.map +1 -1
- package/esm/manifest/manifest-manager.mjs.map +1 -1
- package/esm/production/build-contributions.mjs +19 -3
- package/esm/production/build-contributions.mjs.map +1 -1
- package/esm/production/production-builder.mjs +28 -8
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs +6 -0
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +1 -1
- package/esm/production/resolve-build-config.mjs +15 -0
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/production/route-registration-child.mjs +14 -2
- package/esm/production/route-registration-child.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs +9 -2
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/resource/resource-field-builder.mjs +1 -1
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/resource.d.mts +5 -0
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs +17 -6
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +1 -0
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +2 -1
- package/esm/router/types.d.mts.map +1 -1
- package/esm/tests/index.d.mts +3 -2
- package/esm/tests/index.mjs +3 -2
- package/esm/tests/vitest-setup.d.mts +16 -1
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs +20 -2
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/use-cases/use-case-events.mjs.map +1 -1
- package/esm/utils/is-test-file.mjs +15 -0
- package/esm/utils/is-test-file.mjs.map +1 -0
- package/esm/utils/normalized-path.d.mts +3 -1
- package/esm/utils/normalized-path.d.mts.map +1 -1
- package/esm/utils/normalized-path.mjs +4 -2
- package/esm/utils/normalized-path.mjs.map +1 -1
- package/esm/utils/paths.mjs.map +1 -1
- package/esm/validation/plugins/database-plugin.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +9 -3
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/warlock-config/default-configurations.mjs +1 -0
- package/esm/warlock-config/default-configurations.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +7 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
- package/llms-full.txt +264 -139
- package/package.json +15 -11
- package/skills/configure-app/SKILL.md +2 -0
- package/skills/define-resource/SKILL.md +4 -0
- package/skills/request-tracing/SKILL.md +38 -15
- package/skills/run-app/SKILL.md +71 -34
- package/skills/send-mail/SKILL.md +55 -25
- package/skills/test-http/SKILL.md +19 -8
- package/skills/test-service/SKILL.md +61 -40
- package/skills/warlock-conventions/SKILL.md +26 -18
- package/skills/write-cli-command/SKILL.md +50 -45
|
@@ -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 { router } from \"../router/router\";\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 { publishCurrentRouteTypes } from \"./route-types-publisher\";\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 /** Non-code `watch.include` files that changed; only a restart applies them. */\r\n restartTriggers?: 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\r\n /** Set once boot begins, so a failed boot still closes connectors that already started. */\r\n private connectorsStarted = 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 this.connectorsStarted = true;\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 // Web installs its page table from its late connector. Generate only\r\n // after that installation so `href()` and the declaration table agree.\r\n await publishCurrentRouteTypes(process.cwd(), router.getNamedApiRoutes());\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 // Tell the supervisor (when there is one) that boot completed, so its\r\n // healthy-uptime window counts from here rather than from spawn.\r\n process.send?.(\"warlock:booted\");\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) await 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 /** Tail of the batch-reload chain (see `setupEventListeners`). */\r\n private reloadQueue: Promise<void> = Promise.resolve();\r\n\r\n private setupEventListeners(): void {\r\n // Reloads run one at a time: a second batch waits for the first to finish\r\n // so route re-registration and connector restarts never interleave.\r\n events.on(\"dev-server:batch-complete\", (batch: Batch) => {\r\n this.reloadQueue = this.reloadQueue\r\n .then(() => this.handleBatchComplete(batch))\r\n .catch((error) => devServeLog(colors.redBright(`Batch reload failed: ${error}`)));\r\n });\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\" && !batch.restartTriggers?.includes(p),\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 // `executeBatchReload` includes route re-imports and affected connector\r\n // restarts. Publishing earlier could freeze a partial page/API view.\r\n // This also refreshes deletion-only batches after their cleanup/restart.\r\n await publishCurrentRouteTypes(process.cwd(), router.getNamedApiRoutes());\r\n\r\n typeGenerator.executeTypingsGenerator([...batch.added, ...batch.changed, ...batch.deleted]);\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) ?? batch.restartTriggers?.[0];\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 && !this.connectorsStarted) return;\r\n devServeLog(colors.redBright(\"Shutting down Development Server...\"));\r\n this.running = false;\r\n this.connectorsStarted = 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\";\nimport events from \"@mongez/events\";\nimport { fileExistsAsync, unlinkAsync } from \"@warlock.js/fs\";\nimport { Application } from \"../application\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\nimport { router } from \"../router/router\";\nimport { warlockConfigManager } from \"../warlock-config\";\nimport { BootPreconditionError } from \"./boot-precondition-error\";\nimport { devLogInfo, devLogSection, devLogWarn, devServeLog } from \"./dev-logger\";\nimport { filesOrchestrator } from \"./files-orchestrator\";\nimport { MANIFEST_PATH } from \"./flags\";\nimport type { IncomingReloadTimings } from \"./layer-executor\";\nimport { LayerExecutor } from \"./layer-executor\";\nimport { printReadyBlock } from \"./ready-block\";\nimport { registerInstalledDevtools } from \"./register-installed-devtools\";\nimport { restartDevServer } from \"./restart-dev-server\";\nimport { publishCurrentRouteTypes } from \"./route-types-publisher\";\nimport { devServerShortcuts } from \"./shortcuts\";\nimport type { StartDevServerOptions } from \"./start-development-server\";\nimport { typeGenerator } from \"./type-generator\";\n\ntype Batch = {\n added: string[];\n changed: string[];\n deleted: string[];\n /** Non-code `watch.include` files that changed; only a restart applies them. */\n restartTriggers?: string[];\n timings?: IncomingReloadTimings;\n};\n\n/**\n * Top-level coordinator for `warlock dev`. Wires the file orchestrator, the\n * connectors, and the layer executor together, and listens for the watcher's\n * batched events to drive HMR.\n */\nexport class DevelopmentServer {\n private layerExecutor?: LayerExecutor;\n private running = false;\n\n /** Set once boot begins, so a failed boot still closes connectors that already started. */\n private connectorsStarted = false;\n private readonly options: StartDevServerOptions;\n\n public constructor(options: StartDevServerOptions = {}) {\n this.options = options;\n devLogSection(\"Starting Development Server...\");\n }\n\n public async start(): Promise<void> {\n try {\n this.connectorsStarted = true;\n const startedAt = performance.now();\n\n // --fresh deletes the manifest so reconciliation re-parses every file\n // from disk. Transpile caching is owned by the loader hook (in-memory).\n if (this.options.fresh && (await fileExistsAsync(MANIFEST_PATH))) {\n await unlinkAsync(MANIFEST_PATH);\n devServeLog(colors.cyanBright(\"Cleared manifest (--fresh)\"));\n }\n\n await filesOrchestrator.init();\n await filesOrchestrator.initializeAll();\n await filesOrchestrator.watchFiles();\n\n filesOrchestrator.specialFilesCollector.collect(filesOrchestrator.getFiles());\n\n this.setupEventListeners();\n\n // Decorator-driven registries (models, etc.) must be populated before\n // routes/services can resolve symbols by name.\n await this.autoDiscoverFiles();\n\n await filesOrchestrator.moduleLoader.loadAll();\n\n // A rejecting validator (Application.onValidateBoot) must abort boot\n // before late-phase connectors bind a port — this is what makes the\n // hook actually prevent serving instead of merely being defined.\n //\n // Wrapped and re-thrown as a BootPreconditionError: a rejected\n // validator is, by definition, a precondition whose cause cannot\n // change because the worker tried again — the supervisor must not\n // restart into the same rejection. See boot-precondition-error.ts.\n try {\n await Application.runStartupValidators();\n } catch (error) {\n throw new BootPreconditionError((error as Error).message, { cause: error });\n }\n\n // `@warlock.js/devtools` is a dev dependency the config never names;\n // when installed it joins the late phase here, and only here.\n await registerInstalledDevtools();\n\n // Late-phase connectors (http, socket) bind after app code has\n // registered routes/listeners.\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\n\n // Web installs its page table from its late connector. Generate only\n // after that installation so `href()` and the declaration table agree.\n await publishCurrentRouteTypes(process.cwd(), router.getNamedApiRoutes());\n\n this.layerExecutor = new LayerExecutor(\n filesOrchestrator.getDependencyGraph(),\n filesOrchestrator.specialFilesCollector,\n filesOrchestrator.moduleLoader,\n (absolutePath) => filesOrchestrator.bumpVersion(absolutePath),\n () => filesOrchestrator.flushVersionBumps(),\n );\n\n this.running = true;\n\n const duration = performance.now() - startedAt;\n\n // App modules are loaded and both connector phases are active — signal a\n // complete boot so `Application.onceBooted(...)` listeners fire.\n Application.markBooted({\n environment: Application.environment,\n runtimeStrategy: Application.runtimeStrategy,\n bootDurationMs: duration,\n });\n\n // ONE block, AFTER the boot it summarises, replacing the scattered\n // \"N route(s) registered\" / \"Server ready at …\" / \"ready in …\" lines that\n // used to arrive in completion order. Warnings logged during boot sit\n // above it and stay there — see `ready-block.ts`.\n printReadyBlock(duration);\n\n // Tell the supervisor (when there is one) that boot completed, so its\n // healthy-uptime window counts from here rather than from spawn.\n process.send?.(\"warlock:booted\");\n\n // Precedence: explicit CLI option > devServer.* config > default.\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\n const generateTypings =\n this.options.generateTypings ?? devServerConfig?.generateTypings ?? true;\n const healthCheckers = this.options.healthCheckers ?? devServerConfig?.healthCheckers ?? true;\n\n if (generateTypings) await typeGenerator.executeGenerateAllCommand();\n\n if (healthCheckers) {\n filesOrchestrator.startCheckingHealth(healthCheckers === true ? undefined : healthCheckers);\n }\n } catch (error) {\n devServeLog(colors.redBright(`Failed to start Development Server: ${error}`));\n await this.shutdown();\n throw error;\n }\n }\n\n /**\n * Eagerly import files whose decorators populate global registries so any\n * symbol-by-name resolution later in boot finds them.\n */\n private async autoDiscoverFiles(): Promise<void> {\n const discoveryTypes = [\"model\"] as const;\n for (const file of filesOrchestrator.files.values()) {\n if (file.type && (discoveryTypes as readonly string[]).includes(file.type)) {\n await filesOrchestrator.moduleLoader.loadModule(file, file.type);\n }\n }\n }\n\n /** Tail of the batch-reload chain (see `setupEventListeners`). */\n private reloadQueue: Promise<void> = Promise.resolve();\n\n private setupEventListeners(): void {\n // Reloads run one at a time: a second batch waits for the first to finish\n // so route re-registration and connector restarts never interleave.\n events.on(\"dev-server:batch-complete\", (batch: Batch) => {\n this.reloadQueue = this.reloadQueue\n .then(() => this.handleBatchComplete(batch))\n .catch((error) => devServeLog(colors.redBright(`Batch reload failed: ${error}`)));\n });\n }\n\n private async handleBatchComplete(batch: Batch): Promise<void> {\n if (!this.running || !this.layerExecutor) return;\n\n // warlock.config.ts holds settings read at boot (CLI commands, build\n // options, watch patterns, scheduled jobs) and .env feeds every config\n // that reads it. Neither can be hot-reloaded without leaving running\n // services configured with stale values, so the only honest response is\n // a full restart.\n const restartTrigger = this.findRestartTrigger(batch);\n\n if (restartTrigger) {\n // On success this never comes back — the process exits and the\n // supervisor replaces it. Reaching the next line means the restart was\n // declined by config or wasn't possible, so we fall through and still\n // hot-reload whatever ordinary code shared the batch.\n await this.restartForConfigChange(restartTrigger);\n }\n\n // No-op saves (an editor that fsyncs without a content write) are already\n // filtered upstream by the file hash in FileEventHandler, so batch.changed\n // only contains genuinely-changed paths by the time it reaches here.\n\n const total = batch.added.length + batch.changed.length + batch.deleted.length;\n if (total === 0) return;\n\n // Boot-time files still ride along in `changed` when a restart was\n // declined (`restartOnConfigChange: false`) or wasn't possible — they are\n // never hot-reloaded, so keep them out of the reload set either way.\n const codeFiles = [...batch.added, ...batch.changed].filter(\n (p) => !isEnvPath(p) && p !== \"warlock.config.ts\" && !batch.restartTriggers?.includes(p),\n );\n\n try {\n await this.layerExecutor.executeBatchReload(\n codeFiles,\n filesOrchestrator.getFiles(),\n batch.deleted,\n batch.changed,\n batch.timings,\n );\n\n // `executeBatchReload` includes route re-imports and affected connector\n // restarts. Publishing earlier could freeze a partial page/API view.\n // This also refreshes deletion-only batches after their cleanup/restart.\n await publishCurrentRouteTypes(process.cwd(), router.getNamedApiRoutes());\n\n typeGenerator.executeTypingsGenerator([...batch.added, ...batch.changed, ...batch.deleted]);\n\n filesOrchestrator.checkHealth(batch);\n } catch (error) {\n devServeLog(colors.redBright(`Failed to execute batch reload: ${error}`));\n }\n }\n\n /**\n * The boot-time file in this batch that can only be applied by restarting,\n * or `undefined` when the batch is ordinary application code.\n */\n private findRestartTrigger(batch: Batch): string | undefined {\n if (batch.changed.includes(\"warlock.config.ts\")) {\n return \"warlock.config.ts\";\n }\n\n return [...batch.added, ...batch.changed].find(isEnvPath) ?? batch.restartTriggers?.[0];\n }\n\n /**\n * Restart so a changed boot-time file actually takes effect.\n *\n * Opt out with `devServer.restartOnConfigChange: false` and the old\n * behaviour returns: a warning telling you to restart yourself. If the\n * restart can't be performed at all (no supervisor, a shutdown that threw),\n * `restartDevServer` reports it and we fall back to the same warning rather\n * than leaving the server running against config it isn't using.\n */\n private async restartForConfigChange(file: string): Promise<void> {\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\n\n if (devServerConfig?.restartOnConfigChange === false) {\n devLogWarn(`${file} changed — restart the dev server to apply.`);\n return;\n }\n\n devLogInfo(`${file} changed — restarting to apply.`);\n\n const restarted = await restartDevServer(this);\n\n if (!restarted) {\n devLogWarn(`${file} changed — restart the dev server to apply.`);\n }\n }\n\n public async shutdown(): Promise<void> {\n // Always hand the terminal back, even on a repeat/no-op shutdown — a\n // process that exits while stdin is still in raw mode leaves the user's\n // shell without line editing or Ctrl+C.\n devServerShortcuts.release();\n\n if (!this.running && !this.connectorsStarted) return;\n devServeLog(colors.redBright(\"Shutting down Development Server...\"));\n this.running = false;\n this.connectorsStarted = false;\n await connectorsManager.shutdown();\n devServeLog(colors.greenBright(\"Development Server stopped\"));\n }\n\n public isRunning(): boolean {\n return this.running;\n }\n}\n\nfunction isEnvPath(path: string): boolean {\n const basename = path.split(\"/\").pop() ?? path;\n return basename === \".env\" || basename.startsWith(\".env.\");\n}\n"],"mappings":"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"}
|
|
@@ -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 { fileURLToPath } from \"node:url\";\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, createIgnoredMatcher } 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 rawCaller = match?.[1];\r\n\r\n // Loaded modules report `file:///…?v=N`; turn that back into a plain path.\r\n const callerFile = rawCaller?.startsWith(\"file://\")\r\n ? fileURLToPath(rawCaller.replace(/\\?.*$/, \"\"))\r\n : rawCaller;\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 if (watchConfig?.include?.length) {\n this.eventHandler.setIncludeMatcher(createIgnoredMatcher(watchConfig.include));\n }\n\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\";\nimport { init } from \"es-module-lexer\";\nimport { fileURLToPath } from \"node:url\";\nimport type { MessagePort } from \"node:worker_threads\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { DependencyGraph } from \"./dependency-graph\";\nimport { devLogDim, devLogSuccess } from \"./dev-logger\";\nimport { FileEventHandler } from \"./file-event-handler\";\nimport { FileManager } from \"./file-manager\";\nimport { FileOperations } from \"./file-operations\";\nimport { FilesWatcher, createIgnoredMatcher } from \"./files-watcher\";\nimport { FILE_PROCESSING_BATCH_SIZE } from \"./flags\";\nimport { EslintHealthChecker } from \"./health-checker/checkers/eslint-health-checker\";\nimport { TypescriptHealthChecker } from \"./health-checker/checkers/typescript-health-checker\";\nimport type { FileHealthCheckerContract } from \"./health-checker/file-health-checker.contract\";\nimport { FilesHealthcareManager } from \"./health-checker/files-healthcare.manager\";\nimport { buildTranspileInit } from \"./loader/build-transpile-init.js\";\nimport { registerLoader } from \"./loader/register-loader.js\";\nimport { ManifestManager } from \"./manifest-manager\";\nimport { ModuleLoader } from \"./module-loader\";\nimport { packageJsonManager } from \"./package-json-manager\";\nimport { Path } from \"../utils/normalized-path\";\nimport { SpecialFilesCollector } from \"./special-files-collector\";\nimport { tsconfigManager } from \"./tsconfig-manager\";\nimport { ensureWarlockDirectory, getFilesFromDirectory } from \"./utils\";\n\n/**\n * Register a cleanup callback that fires before the current module is\n * unloaded by HMR. Stack-inspects the caller so user code doesn't need to\n * thread the FileManager through.\n */\nexport function onCleanup(callback: () => any) {\n if (globalThis.__currentModuleFile) {\n globalThis.__currentModuleFile.addCleanup(callback);\n return;\n }\n\n const stack = new Error().stack;\n const callerLine = stack?.split(\"\\n\")[2];\n const match = callerLine?.match(/\\((.+?):\\d+:\\d+\\)/) || callerLine?.match(/at (.+?):\\d+:\\d+/);\n const rawCaller = match?.[1];\n\n // Loaded modules report `file:///…?v=N`; turn that back into a plain path.\n const callerFile = rawCaller?.startsWith(\"file://\")\n ? fileURLToPath(rawCaller.replace(/\\?.*$/, \"\"))\n : rawCaller;\n\n if (!callerFile) return;\n const fileManager = filesOrchestrator.files.get(Path.toRelative(callerFile));\n fileManager?.addCleanup(callback);\n}\n\n/**\n * Top-level coordinator for the dev server's file system.\n *\n * Owns: file discovery, the dependency graph, the manifest, the special-files\n * index, the file watcher, and the lifecycle of the ESM loader hook (which\n * provides cache-busting via `?v=N` version tokens).\n */\nexport class FilesOrchestrator {\n public readonly filesWatcher = new FilesWatcher();\n public readonly files = new Map<string, FileManager>();\n private readonly manifest = new ManifestManager(this.files);\n private readonly dependencyGraph = new DependencyGraph();\n private readonly healthCheckerManager = new FilesHealthcareManager(this.files);\n public readonly specialFilesCollector = new SpecialFilesCollector();\n public readonly moduleLoader = new ModuleLoader(this.specialFilesCollector);\n public readonly fileOperations: FileOperations;\n private readonly eventHandler: FileEventHandler;\n\n /** Main-thread end of the MessageChannel to the loader hook worker. */\n private loaderPort: MessagePort | null = null;\n\n /** Resolve callbacks for pending `flushVersionBumps()` calls. */\n private readonly pendingFlushes: Array<() => void> = [];\n\n public isInitialized = false;\n\n public constructor() {\n this.fileOperations = new FileOperations(\n this.files,\n this.dependencyGraph,\n this.manifest,\n this.specialFilesCollector,\n );\n\n this.eventHandler = new FileEventHandler(\n this.fileOperations,\n this.manifest,\n this.dependencyGraph,\n this.files,\n );\n }\n\n public async add(relativePath: string): Promise<FileManager> {\n return this.fileOperations.addFile(relativePath);\n }\n\n public async load<T>(relativePath: string, type = \"other\") {\n // Guarantee the ESM loader hook is registered before we import anything.\n // `load()` is the dynamic-import entrypoint, so it is the one place that\n // must satisfy `init()`'s documented precondition (\"Must be called before\n // any user src/ module is dynamically imported\") on its own — it cannot\n // assume a caller ran `init()` first.\n //\n // The CLI command-resolution path proves why: `cliCommandsLoader.load()`\n // imports a project command module during command *lookup* (before the\n // command's own `execute()` runs), whereas `loadPreloaders()` only calls\n // `init()` from *inside* `execute()`. So a `warlock <command>` whose\n // command file imports an `app/*` alias or a `.ts` sibling was imported\n // with no hook registered and died on `ERR_MODULE_NOT_FOUND` (f9d8f99f).\n //\n // `init()` is idempotent (guarded by `isInitialized`), so this is a no-op\n // on the dev-server path, which already calls it explicitly at startup.\n await this.init();\n\n const fileManager = await this.add(relativePath);\n return this.moduleLoader.loadModule<T>(fileManager, fileManager.type || type);\n }\n\n public getDependencyGraph(): DependencyGraph {\n return this.dependencyGraph;\n }\n\n public getInvalidationChain(file: string): string[] {\n return this.dependencyGraph.getInvalidationChain(file);\n }\n\n public getFiles(): Map<string, FileManager> {\n return this.files;\n }\n\n public getHealthCheckerManager(): FilesHealthcareManager {\n return this.healthCheckerManager;\n }\n\n /**\n * Initialise managers and register the ESM loader hook. Must be called\n * before any user `src/` module is dynamically imported.\n */\n public async init() {\n if (this.isInitialized) return;\n this.isInitialized = true;\n\n await init;\n await Promise.all([tsconfigManager.init(), packageJsonManager.init()]);\n\n // The loader hook writes itself into .warlock/, so ensure the dir exists.\n await ensureWarlockDirectory();\n\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\n const transpileInit = buildTranspileInit(\n tsconfigManager.tsconfig?.compilerOptions,\n devServerConfig?.transpileCacheDebug === true,\n );\n\n this.loaderPort = await registerLoader(transpileInit);\n\n this.loaderPort.on(\"message\", (msg: { type: string }) => {\n if (msg.type === \"sync-ack\") {\n this.pendingFlushes.shift()?.();\n }\n });\n }\n\n /**\n * Tell the hook worker a file changed. The next `import()` of it will get\n * a fresh `?v=N` URL → Node cache miss → fresh content.\n */\n public bumpVersion(absolutePath: string): void {\n this.loaderPort?.postMessage({ type: \"bump\", absolutePath });\n }\n\n /**\n * Wait until the hook worker has processed every pending bump.\n *\n * `postMessage` is async — without this, a follow-up `import()` may\n * resolve before the worker increments the counter and Node will return\n * the cached old module.\n */\n public flushVersionBumps(): Promise<void> {\n if (!this.loaderPort) return Promise.resolve();\n\n return new Promise<void>((resolve) => {\n this.pendingFlushes.push(resolve);\n this.loaderPort!.postMessage({ type: \"sync\" });\n });\n }\n\n /**\n * Discover files on disk, reconcile against the manifest, build the dep\n * graph, and persist the new manifest. Idempotent.\n */\n public async initializeAll() {\n const [filesInFilesystem, manifestExists] = await Promise.all([\n this.getAllFilesFromFilesystem(),\n this.manifest.init(),\n ]);\n\n if (!manifestExists) {\n await this.processFiles(filesInFilesystem);\n } else {\n await this.reconcileFiles(filesInFilesystem);\n }\n\n this.dependencyGraph.build(this.files);\n this.fileOperations.updateFileDependents();\n this.fileOperations.syncFilesToManifest();\n await this.manifest.save();\n }\n\n public async checkHealth(files: { added: string[]; changed: string[]; deleted: string[] }) {\n const filesToCheck = [...files.added, ...files.changed]\n .map((file) => this.files.get(Path.toRelative(file)))\n .filter((file): file is FileManager => !!file);\n\n const filesToDelete = files.deleted\n .map((file) => this.files.get(Path.toRelative(file)))\n .filter((file): file is FileManager => !!file);\n\n await this.healthCheckerManager.onFileChanges(filesToCheck);\n this.healthCheckerManager.removeFiles(filesToDelete);\n this.healthCheckerManager.checkFiles(filesToCheck);\n }\n\n public async startCheckingHealth(healthCheckers?: FileHealthCheckerContract[]): Promise<void> {\n devLogDim(\"Started File Health Checks in the background.\");\n\n const checkers = healthCheckers ?? [new TypescriptHealthChecker(), new EslintHealthChecker()];\n await this.healthCheckerManager.setHealthCheckers(checkers).initialize();\n await this.healthCheckerManager.validateAllFiles();\n }\n\n /**\n * Glob the src directory, returning relative paths.\n */\n public async getAllFilesFromFilesystem(): Promise<string[]> {\n const absolutePaths = await getFilesFromDirectory();\n return absolutePaths.map((absPath) => Path.toRelative(absPath));\n }\n\n /**\n * Process every file from scratch — used when no manifest exists.\n */\n private async processFiles(filePaths: string[]) {\n devLogDim(`processing ${filePaths.length} files...`);\n await runInBatches(filePaths, FILE_PROCESSING_BATCH_SIZE, async (relativePath) => {\n const fileManager = new FileManager(\n Path.toAbsolute(relativePath),\n this.files,\n this.fileOperations,\n );\n this.files.set(relativePath, fileManager);\n await fileManager.process();\n });\n devLogSuccess(`processed ${filePaths.length} files`);\n }\n\n private async reconcileFiles(filesInFilesystem: string[]) {\n const filesInManifest = new Set(this.manifest.getAllFilePaths());\n const filesInFilesystemSet = new Set(filesInFilesystem);\n\n const newFiles = filesInFilesystem.filter((f) => !filesInManifest.has(f));\n const deletedFiles = Array.from(filesInManifest).filter((f) => !filesInFilesystemSet.has(f));\n const existingFiles = filesInFilesystem.filter((f) => filesInManifest.has(f));\n\n if (newFiles.length > 0 || deletedFiles.length > 0) {\n devLogDim(\n `reconciling: ${colors.green(newFiles.length)} new, ${colors.red(deletedFiles.length)} deleted, ${colors.blue(existingFiles.length)} existing`,\n );\n }\n\n await this.processFiles(newFiles);\n\n for (const relativePath of deletedFiles) {\n this.manifest.removeFile(relativePath);\n this.files.delete(relativePath);\n }\n\n await runInBatches(existingFiles, FILE_PROCESSING_BATCH_SIZE, async (relativePath) => {\n const fileManager = new FileManager(\n Path.toAbsolute(relativePath),\n this.files,\n this.fileOperations,\n );\n this.files.set(relativePath, fileManager);\n await fileManager.init(this.manifest.getFile(relativePath));\n });\n }\n\n public async watchFiles() {\n devLogSuccess(\"watching for file changes (not serving yet)\");\n\n this.filesWatcher.onFileChange((p, settleMs) => this.eventHandler.handleFileChange(p, settleMs));\n this.filesWatcher.onFileAdd((p, settleMs) => this.eventHandler.handleFileAdd(p, settleMs));\n this.filesWatcher.onFileDelete((p, settleMs) => this.eventHandler.handleFileDelete(p, settleMs));\n\n const watchConfig = warlockConfigManager.get(\"devServer\")?.watch;\n if (watchConfig?.include?.length) {\n this.eventHandler.setIncludeMatcher(createIgnoredMatcher(watchConfig.include));\n }\n\n await this.filesWatcher.watch(watchConfig);\n }\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\nexport const filesOrchestrator = new FilesOrchestrator();\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layer-executor.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/layer-executor.ts"],"sourcesContent":["import { loadEnv } from \"@mongez/dotenv\";\r\nimport { configManager } from \"../config/config-manager\";\r\nimport { connectorsManager } from \"../connectors/connectors-manager\";\r\nimport type { DependencyGraph } from \"./dependency-graph\";\r\nimport { devLogHMR, devLogTimings } from \"./dev-logger\";\r\nimport { FileManager } from \"./file-manager\";\r\nimport { isTimingsEnabled } from \"./flags\";\r\nimport type { ModuleLoader } from \"./module-loader\";\r\nimport type { SpecialFilesCollector } from \"./special-files-collector\";\r\nimport { environmentLoaderOptions } from \"../utils/load-environment\";\r\n\r\n/** Watcher-observed timings for the batch that triggered this reload — see `FileEventHandler`. */\r\nexport type IncomingReloadTimings = {\r\n watcherSettleMs: number;\r\n debounceWaitMs: number;\r\n};\r\n\r\n/**\r\n * Decides what to reload when a batch of files changes.\r\n *\r\n * Strategy: bump the hook's version counter for every file in the\r\n * invalidation chain, wait for the hook worker to flush, then re-import\r\n * any special files (config / main / routes / events / locales) the chain\r\n * touched and restart any connector whose watched-files overlap the change.\r\n */\r\nexport class LayerExecutor {\r\n public constructor(\r\n private readonly dependencyGraph: DependencyGraph,\r\n private readonly specialFilesCollector: SpecialFilesCollector,\r\n private readonly moduleLoader: ModuleLoader,\r\n private readonly bumpVersion: (absolutePath: string) => void,\r\n private readonly flushVersionBumps: () => Promise<void>,\r\n ) {}\r\n\r\n /**\r\n * Entry point for the file watcher batch.\r\n *\r\n * @param changedPaths - code files added or changed in this batch\r\n * @param filesMap - all tracked files (relativePath → FileManager)\r\n * @param deletedFiles - paths that were removed from disk\r\n * @param allChangedPaths - includes .env so we can detect config reloads\r\n * @param incomingTimings - watcher-settle/debounce-wait timings observed\r\n * upstream in `FileEventHandler`, folded into the printed phase line\r\n */\r\n public async executeBatchReload(\r\n changedPaths: string[],\r\n filesMap: Map<string, FileManager>,\r\n deletedFiles: string[],\r\n allChangedPaths?: string[],\r\n incomingTimings?: IncomingReloadTimings,\r\n ): Promise<void> {\r\n const envFilesChanged = (allChangedPaths ?? []).some(isEnvPath);\r\n\r\n if (changedPaths.length === 0 && deletedFiles.length === 0 && !envFilesChanged) {\r\n return;\r\n }\r\n\r\n // Deletes: clean up routes/cleanup hooks for files that no longer exist.\r\n for (const path of deletedFiles) {\r\n const file = filesMap.get(path);\r\n if (file) this.moduleLoader.cleanupDeletedModule(file);\r\n }\r\n\r\n // Env-only change: reload all configs, restart connectors that watch them.\r\n if (changedPaths.length === 0 && envFilesChanged) {\r\n const configPaths = await this.reloadAffectedModules([\".env\"], filesMap);\r\n await this.restartAffectedConnectors([...deletedFiles, ...configPaths]);\r\n return;\r\n }\r\n\r\n if (changedPaths.length === 0) {\r\n await this.restartAffectedConnectors(deletedFiles);\r\n return;\r\n }\r\n\r\n const invalidationChain = new Set<string>();\r\n const pendingHmrLogs: { path: string; dependents: number }[] = [];\r\n for (const path of changedPaths) {\r\n for (const file of this.dependencyGraph.getInvalidationChain(path)) {\r\n invalidationChain.add(file);\r\n }\r\n pendingHmrLogs.push({ path, dependents: invalidationChain.size - 1 });\r\n }\r\n\r\n const chain = Array.from(invalidationChain);\r\n const reloadStartedAt = Date.now();\r\n const timingsEnabled = isTimingsEnabled();\r\n\r\n // Step 1: bump version counters so the next import() is fresh.\r\n const moduleGraphStartedAt = timingsEnabled ? performance.now() : 0;\r\n for (const relativePath of chain) {\r\n const file = filesMap.get(relativePath);\r\n if (!file) continue;\r\n this.moduleLoader.runCleanup(file);\r\n this.bumpVersion(file.absolutePath);\r\n await file.process({ force: true });\r\n }\r\n const moduleGraphInvalidationMs = timingsEnabled ? performance.now() - moduleGraphStartedAt : 0;\r\n\r\n // Step 2: wait for the hook worker to ack every bump.\r\n // Without this, resolve() may still return the old ?v=N URL.\r\n // Step 3: re-import affected special files.\r\n const reimportStartedAt = timingsEnabled ? performance.now() : 0;\r\n await this.flushVersionBumps();\r\n const affectedConfigPaths = await this.reloadAffectedModules(chain, filesMap);\r\n const reimportMs = timingsEnabled ? performance.now() - reimportStartedAt : 0;\r\n\r\n // Step 4: restart any connector whose watched-files overlap the chain.\r\n const connectorRestartStartedAt = timingsEnabled ? performance.now() : 0;\r\n await this.restartAffectedConnectors([\r\n ...changedPaths,\r\n ...deletedFiles,\r\n ...affectedConfigPaths,\r\n ]);\r\n const connectorRestartMs = timingsEnabled ? performance.now() - connectorRestartStartedAt : 0;\r\n\r\n // The log only fires here, once re-import and connector restarts have\r\n // actually completed — not before the work starts. Printed earlier, the\r\n // line claims the change is live while a request could still hit the old\r\n // code (and, thrown from any step above, this line is never reached at\r\n // all — the caller's error path is the only report a failed reload gets).\r\n const elapsedMs = Date.now() - reloadStartedAt;\r\n for (const { path, dependents } of pendingHmrLogs) {\r\n devLogHMR(path, dependents, elapsedMs);\r\n }\r\n\r\n if (timingsEnabled) {\r\n devLogTimings({\r\n watcherSettleMs: incomingTimings?.watcherSettleMs ?? 0,\r\n debounceWaitMs: incomingTimings?.debounceWaitMs ?? 0,\r\n moduleGraphInvalidationMs,\r\n reimportMs,\r\n connectorRestartMs,\r\n });\r\n }\r\n }\r\n\r\n private async restartAffectedConnectors(affectedFiles: string[]): Promise<void> {\r\n const toRestart = connectorsManager\r\n .list()\r\n .filter((connector) => connector.shouldRestart(affectedFiles));\r\n\r\n for (const connector of toRestart) {\r\n await connector.restart();\r\n }\r\n }\r\n\r\n /**\r\n * Re-import every special file whose path or dependency-set intersects the\r\n * invalidation chain. Returns the relative paths of any config files that\r\n * reloaded so the caller can pass them to the connector-restart pass.\r\n */\r\n private async reloadAffectedModules(\r\n chain: string[],\r\n filesMap: Map<string, FileManager>,\r\n ): Promise<string[]> {\r\n const isEnvAffected = chain.some(isEnvPath);\r\n // Same precedence policy as the boot-time load: an exported variable is not\r\n // demoted to the file's value just because the file was touched.\r\n if (isEnvAffected) await loadEnv(undefined, environmentLoaderOptions);\r\n\r\n const isAffected = (file: FileManager) => isFileAffected(file, chain);\r\n\r\n // Models self-register via the @RegisterModel decorator and rely on the\r\n // module-loader's registerCleanup() to attach Model.$cleanup (which\r\n // unregisters them on the next reload). That only happens inside\r\n // loadModule(), which models hit exactly once — at boot, via\r\n // autoDiscoverFiles. During HMR they're otherwise re-imported\r\n // *transitively* through routes, which never re-runs registerCleanup, so\r\n // after the first reload the cleanup list is empty and the registration\r\n // leaks (\"Model X is already registered\" on every subsequent edit).\r\n //\r\n // Re-importing changed model files through loadModule here re-attaches\r\n // $cleanup every cycle. It runs before the route pass so the decorator\r\n // registers once; the transitive route import then hits the cached ?v=N\r\n // and does not double-register.\r\n const affectedModels = chain\r\n .map((path) => filesMap.get(path))\r\n .filter((file): file is FileManager => !!file && file.type === \"model\");\r\n\r\n for (const file of affectedModels) {\r\n await this.moduleLoader.loadModule(file, \"model\");\r\n }\r\n\r\n const collector = this.specialFilesCollector;\r\n const affectedConfigs = collector\r\n .getFilesByType(\"config\")\r\n .filter((file) => (isEnvAffected ? true : isAffected(file)));\r\n const affectedMains = collector.getFilesByType(\"main\").filter(isAffected);\r\n const affectedRoutes = collector.getFilesByType(\"route\").filter(isAffected);\r\n const affectedEvents = collector.getFilesByType(\"event\").filter(isAffected);\r\n const affectedLocales = collector.getFilesByType(\"locale\").filter(isAffected);\r\n\r\n const hasSpecialFiles =\r\n affectedConfigs.length > 0 ||\r\n affectedMains.length > 0 ||\r\n affectedRoutes.length > 0 ||\r\n affectedEvents.length > 0 ||\r\n affectedLocales.length > 0;\r\n\r\n // No entry points touched: reloading internal files alone is wasted work\r\n // because the hook will re-import them on next access anyway. But the\r\n // dep chain's last hop is usually the user-facing edge — give it a kick.\r\n if (!hasSpecialFiles) {\r\n const tail = filesMap.get(chain[chain.length - 1]);\r\n if (tail) await this.moduleLoader.reloadModule(tail);\r\n return [];\r\n }\r\n\r\n const configPaths: string[] = [];\r\n for (const file of affectedConfigs) {\r\n await configManager.reload(file);\r\n configPaths.push(file.relativePath);\r\n }\r\n\r\n // Order matters: locales first (translations used by main), main before\r\n // routes (registers state routes consume), events between (listeners).\r\n for (const file of affectedLocales) await this.moduleLoader.reloadModule(file);\r\n for (const file of affectedMains) await this.moduleLoader.reloadModule(file);\r\n for (const file of affectedEvents) await this.moduleLoader.reloadModule(file);\r\n for (const file of affectedRoutes) await this.moduleLoader.reloadModule(file);\r\n\r\n return configPaths;\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\r\n/**\r\n * A file is \"affected\" if it itself is in the chain or imports something in it.\r\n */\r\nfunction isFileAffected(file: FileManager, chain: string[]): boolean {\r\n if (chain.includes(file.relativePath)) return true;\r\n for (const dep of file.dependencies) {\r\n if (chain.includes(dep)) return true;\r\n }\r\n return false;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,IAAa,gBAAb,MAA2B;CACzB,AAAO,YACL,AAAiB,iBACjB,AAAiB,uBACjB,AAAiB,cACjB,AAAiB,aACjB,AAAiB,mBACjB;EALiB;EACA;EACA;EACA;EACA;CAChB;;;;;;;;;;;CAYH,MAAa,mBACX,cACA,UACA,cACA,iBACA,iBACe;EACf,MAAM,mBAAmB,mBAAmB,CAAC,EAAC,CAAE,KAAK,SAAS;EAE9D,IAAI,aAAa,WAAW,KAAK,aAAa,WAAW,KAAK,CAAC,iBAC7D;EAIF,KAAK,MAAM,QAAQ,cAAc;GAC/B,MAAM,OAAO,SAAS,IAAI,IAAI;GAC9B,IAAI,MAAM,KAAK,aAAa,qBAAqB,IAAI;EACvD;EAGA,IAAI,aAAa,WAAW,KAAK,iBAAiB;GAChD,MAAM,cAAc,MAAM,KAAK,sBAAsB,CAAC,MAAM,GAAG,QAAQ;GACvE,MAAM,KAAK,0BAA0B,CAAC,GAAG,cAAc,GAAG,WAAW,CAAC;GACtE;EACF;EAEA,IAAI,aAAa,WAAW,GAAG;GAC7B,MAAM,KAAK,0BAA0B,YAAY;GACjD;EACF;EAEA,MAAM,oCAAoB,IAAI,IAAY;EAC1C,MAAM,iBAAyD,CAAC;EAChE,KAAK,MAAM,QAAQ,cAAc;GAC/B,KAAK,MAAM,QAAQ,KAAK,gBAAgB,qBAAqB,IAAI,GAC/D,kBAAkB,IAAI,IAAI;GAE5B,eAAe,KAAK;IAAE;IAAM,YAAY,kBAAkB,OAAO;GAAE,CAAC;EACtE;EAEA,MAAM,QAAQ,MAAM,KAAK,iBAAiB;EAC1C,MAAM,kBAAkB,KAAK,IAAI;EACjC,MAAM,iBAAiB,iBAAiB;EAGxC,MAAM,uBAAuB,iBAAiB,YAAY,IAAI,IAAI;EAClE,KAAK,MAAM,gBAAgB,OAAO;GAChC,MAAM,OAAO,SAAS,IAAI,YAAY;GACtC,IAAI,CAAC,MAAM;GACX,KAAK,aAAa,WAAW,IAAI;GACjC,KAAK,YAAY,KAAK,YAAY;GAClC,MAAM,KAAK,QAAQ,EAAE,OAAO,KAAK,CAAC;EACpC;EACA,MAAM,4BAA4B,iBAAiB,YAAY,IAAI,IAAI,uBAAuB;EAK9F,MAAM,oBAAoB,iBAAiB,YAAY,IAAI,IAAI;EAC/D,MAAM,KAAK,kBAAkB;EAC7B,MAAM,sBAAsB,MAAM,KAAK,sBAAsB,OAAO,QAAQ;EAC5E,MAAM,aAAa,iBAAiB,YAAY,IAAI,IAAI,oBAAoB;EAG5E,MAAM,4BAA4B,iBAAiB,YAAY,IAAI,IAAI;EACvE,MAAM,KAAK,0BAA0B;GACnC,GAAG;GACH,GAAG;GACH,GAAG;EACL,CAAC;EACD,MAAM,qBAAqB,iBAAiB,YAAY,IAAI,IAAI,4BAA4B;EAO5F,MAAM,YAAY,KAAK,IAAI,IAAI;EAC/B,KAAK,MAAM,EAAE,MAAM,gBAAgB,gBACjC,UAAU,MAAM,YAAY,SAAS;EAGvC,IAAI,gBACF,cAAc;GACZ,iBAAiB,iBAAiB,mBAAmB;GACrD,gBAAgB,iBAAiB,kBAAkB;GACnD;GACA;GACA;EACF,CAAC;CAEL;CAEA,MAAc,0BAA0B,eAAwC;EAC9E,MAAM,YAAY,kBACf,KAAK,CAAC,CACN,QAAQ,cAAc,UAAU,cAAc,aAAa,CAAC;EAE/D,KAAK,MAAM,aAAa,WACtB,MAAM,UAAU,QAAQ;CAE5B;;;;;;CAOA,MAAc,sBACZ,OACA,UACmB;EACnB,MAAM,gBAAgB,MAAM,KAAK,SAAS;EAG1C,IAAI,eAAe,MAAM,QAAQ,QAAW,wBAAwB;EAEpE,MAAM,cAAc,SAAsB,eAAe,MAAM,KAAK;EAepE,MAAM,iBAAiB,MACpB,KAAK,SAAS,SAAS,IAAI,IAAI,CAAC,CAAC,CACjC,QAAQ,SAA8B,CAAC,CAAC,QAAQ,KAAK,SAAS,OAAO;EAExE,KAAK,MAAM,QAAQ,gBACjB,MAAM,KAAK,aAAa,WAAW,MAAM,OAAO;EAGlD,MAAM,YAAY,KAAK;EACvB,MAAM,kBAAkB,UACrB,eAAe,QAAQ,CAAC,CACxB,QAAQ,SAAU,gBAAgB,OAAO,WAAW,IAAI,CAAE;EAC7D,MAAM,gBAAgB,UAAU,eAAe,MAAM,CAAC,CAAC,OAAO,UAAU;EACxE,MAAM,iBAAiB,UAAU,eAAe,OAAO,CAAC,CAAC,OAAO,UAAU;EAC1E,MAAM,iBAAiB,UAAU,eAAe,OAAO,CAAC,CAAC,OAAO,UAAU;EAC1E,MAAM,kBAAkB,UAAU,eAAe,QAAQ,CAAC,CAAC,OAAO,UAAU;EAY5E,IAAI,EATF,gBAAgB,SAAS,KACzB,cAAc,SAAS,KACvB,eAAe,SAAS,KACxB,eAAe,SAAS,KACxB,gBAAgB,SAAS,IAKL;GACpB,MAAM,OAAO,SAAS,IAAI,MAAM,MAAM,SAAS,EAAE;GACjD,IAAI,MAAM,MAAM,KAAK,aAAa,aAAa,IAAI;GACnD,OAAO,CAAC;EACV;EAEA,MAAM,cAAwB,CAAC;EAC/B,KAAK,MAAM,QAAQ,iBAAiB;GAClC,MAAM,cAAc,OAAO,IAAI;GAC/B,YAAY,KAAK,KAAK,YAAY;EACpC;EAIA,KAAK,MAAM,QAAQ,iBAAiB,MAAM,KAAK,aAAa,aAAa,IAAI;EAC7E,KAAK,MAAM,QAAQ,eAAe,MAAM,KAAK,aAAa,aAAa,IAAI;EAC3E,KAAK,MAAM,QAAQ,gBAAgB,MAAM,KAAK,aAAa,aAAa,IAAI;EAC5E,KAAK,MAAM,QAAQ,gBAAgB,MAAM,KAAK,aAAa,aAAa,IAAI;EAE5E,OAAO;CACT;AACF;AAEA,SAAS,UAAU,MAAuB;CACxC,MAAM,WAAW,KAAK,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;CAC1C,OAAO,aAAa,UAAU,SAAS,WAAW,OAAO;AAC3D;;;;AAKA,SAAS,eAAe,MAAmB,OAA0B;CACnE,IAAI,MAAM,SAAS,KAAK,YAAY,GAAG,OAAO;CAC9C,KAAK,MAAM,OAAO,KAAK,cACrB,IAAI,MAAM,SAAS,GAAG,GAAG,OAAO;CAElC,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"layer-executor.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/layer-executor.ts"],"sourcesContent":["import { loadEnv } from \"@mongez/dotenv\";\nimport { configManager } from \"../config/config-manager\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport type { DependencyGraph } from \"./dependency-graph\";\nimport { devLogHMR, devLogTimings } from \"./dev-logger\";\nimport { FileManager } from \"./file-manager\";\nimport { isTimingsEnabled } from \"./flags\";\nimport type { ModuleLoader } from \"./module-loader\";\nimport type { SpecialFilesCollector } from \"./special-files-collector\";\nimport { environmentLoaderOptions } from \"../utils/load-environment\";\n\n/** Watcher-observed timings for the batch that triggered this reload — see `FileEventHandler`. */\nexport type IncomingReloadTimings = {\n watcherSettleMs: number;\n debounceWaitMs: number;\n};\n\n/**\n * Decides what to reload when a batch of files changes.\n *\n * Strategy: bump the hook's version counter for every file in the\n * invalidation chain, wait for the hook worker to flush, then re-import\n * any special files (config / main / routes / events / locales) the chain\n * touched and restart any connector whose watched-files overlap the change.\n */\nexport class LayerExecutor {\n public constructor(\n private readonly dependencyGraph: DependencyGraph,\n private readonly specialFilesCollector: SpecialFilesCollector,\n private readonly moduleLoader: ModuleLoader,\n private readonly bumpVersion: (absolutePath: string) => void,\n private readonly flushVersionBumps: () => Promise<void>,\n ) {}\n\n /**\n * Entry point for the file watcher batch.\n *\n * @param changedPaths - code files added or changed in this batch\n * @param filesMap - all tracked files (relativePath → FileManager)\n * @param deletedFiles - paths that were removed from disk\n * @param allChangedPaths - includes .env so we can detect config reloads\n * @param incomingTimings - watcher-settle/debounce-wait timings observed\n * upstream in `FileEventHandler`, folded into the printed phase line\n */\n public async executeBatchReload(\n changedPaths: string[],\n filesMap: Map<string, FileManager>,\n deletedFiles: string[],\n allChangedPaths?: string[],\n incomingTimings?: IncomingReloadTimings,\n ): Promise<void> {\n const envFilesChanged = (allChangedPaths ?? []).some(isEnvPath);\n\n if (changedPaths.length === 0 && deletedFiles.length === 0 && !envFilesChanged) {\n return;\n }\n\n // Deletes: clean up routes/cleanup hooks for files that no longer exist.\n for (const path of deletedFiles) {\n const file = filesMap.get(path);\n if (file) this.moduleLoader.cleanupDeletedModule(file);\n }\n\n // Env-only change: reload all configs, restart connectors that watch them.\n if (changedPaths.length === 0 && envFilesChanged) {\n const configPaths = await this.reloadAffectedModules([\".env\"], filesMap);\n await this.restartAffectedConnectors([...deletedFiles, ...configPaths]);\n return;\n }\n\n if (changedPaths.length === 0) {\n await this.restartAffectedConnectors(deletedFiles);\n return;\n }\n\n const invalidationChain = new Set<string>();\n const pendingHmrLogs: { path: string; dependents: number }[] = [];\n for (const path of changedPaths) {\n for (const file of this.dependencyGraph.getInvalidationChain(path)) {\n invalidationChain.add(file);\n }\n pendingHmrLogs.push({ path, dependents: invalidationChain.size - 1 });\n }\n\n const chain = Array.from(invalidationChain);\n const reloadStartedAt = Date.now();\n const timingsEnabled = isTimingsEnabled();\n\n // Step 1: bump version counters so the next import() is fresh.\n const moduleGraphStartedAt = timingsEnabled ? performance.now() : 0;\n for (const relativePath of chain) {\n const file = filesMap.get(relativePath);\n if (!file) continue;\n this.moduleLoader.runCleanup(file);\n this.bumpVersion(file.absolutePath);\n await file.process({ force: true });\n }\n const moduleGraphInvalidationMs = timingsEnabled ? performance.now() - moduleGraphStartedAt : 0;\n\n // Step 2: wait for the hook worker to ack every bump.\n // Without this, resolve() may still return the old ?v=N URL.\n // Step 3: re-import affected special files.\n const reimportStartedAt = timingsEnabled ? performance.now() : 0;\n await this.flushVersionBumps();\n const affectedConfigPaths = await this.reloadAffectedModules(chain, filesMap);\n const reimportMs = timingsEnabled ? performance.now() - reimportStartedAt : 0;\n\n // Step 4: restart any connector whose watched-files overlap the chain.\n const connectorRestartStartedAt = timingsEnabled ? performance.now() : 0;\n await this.restartAffectedConnectors([\n ...changedPaths,\n ...deletedFiles,\n ...affectedConfigPaths,\n ]);\n const connectorRestartMs = timingsEnabled ? performance.now() - connectorRestartStartedAt : 0;\n\n // The log only fires here, once re-import and connector restarts have\n // actually completed — not before the work starts. Printed earlier, the\n // line claims the change is live while a request could still hit the old\n // code (and, thrown from any step above, this line is never reached at\n // all — the caller's error path is the only report a failed reload gets).\n const elapsedMs = Date.now() - reloadStartedAt;\n for (const { path, dependents } of pendingHmrLogs) {\n devLogHMR(path, dependents, elapsedMs);\n }\n\n if (timingsEnabled) {\n devLogTimings({\n watcherSettleMs: incomingTimings?.watcherSettleMs ?? 0,\n debounceWaitMs: incomingTimings?.debounceWaitMs ?? 0,\n moduleGraphInvalidationMs,\n reimportMs,\n connectorRestartMs,\n });\n }\n }\n\n private async restartAffectedConnectors(affectedFiles: string[]): Promise<void> {\n const toRestart = connectorsManager\n .list()\n .filter((connector) => connector.shouldRestart(affectedFiles));\n\n for (const connector of toRestart) {\n await connector.restart();\n }\n }\n\n /**\n * Re-import every special file whose path or dependency-set intersects the\n * invalidation chain. Returns the relative paths of any config files that\n * reloaded so the caller can pass them to the connector-restart pass.\n */\n private async reloadAffectedModules(\n chain: string[],\n filesMap: Map<string, FileManager>,\n ): Promise<string[]> {\n const isEnvAffected = chain.some(isEnvPath);\n // Same precedence policy as the boot-time load: an exported variable is not\n // demoted to the file's value just because the file was touched.\n if (isEnvAffected) await loadEnv(undefined, environmentLoaderOptions);\n\n const isAffected = (file: FileManager) => isFileAffected(file, chain);\n\n // Models self-register via the @RegisterModel decorator and rely on the\n // module-loader's registerCleanup() to attach Model.$cleanup (which\n // unregisters them on the next reload). That only happens inside\n // loadModule(), which models hit exactly once — at boot, via\n // autoDiscoverFiles. During HMR they're otherwise re-imported\n // *transitively* through routes, which never re-runs registerCleanup, so\n // after the first reload the cleanup list is empty and the registration\n // leaks (\"Model X is already registered\" on every subsequent edit).\n //\n // Re-importing changed model files through loadModule here re-attaches\n // $cleanup every cycle. It runs before the route pass so the decorator\n // registers once; the transitive route import then hits the cached ?v=N\n // and does not double-register.\n const affectedModels = chain\n .map((path) => filesMap.get(path))\n .filter((file): file is FileManager => !!file && file.type === \"model\");\n\n for (const file of affectedModels) {\n await this.moduleLoader.loadModule(file, \"model\");\n }\n\n const collector = this.specialFilesCollector;\n const affectedConfigs = collector\n .getFilesByType(\"config\")\n .filter((file) => (isEnvAffected ? true : isAffected(file)));\n const affectedMains = collector.getFilesByType(\"main\").filter(isAffected);\n const affectedRoutes = collector.getFilesByType(\"route\").filter(isAffected);\n const affectedEvents = collector.getFilesByType(\"event\").filter(isAffected);\n const affectedLocales = collector.getFilesByType(\"locale\").filter(isAffected);\n\n const hasSpecialFiles =\n affectedConfigs.length > 0 ||\n affectedMains.length > 0 ||\n affectedRoutes.length > 0 ||\n affectedEvents.length > 0 ||\n affectedLocales.length > 0;\n\n // No entry points touched: reloading internal files alone is wasted work\n // because the hook will re-import them on next access anyway. But the\n // dep chain's last hop is usually the user-facing edge — give it a kick.\n if (!hasSpecialFiles) {\n const tail = filesMap.get(chain[chain.length - 1]);\n if (tail) await this.moduleLoader.reloadModule(tail);\n return [];\n }\n\n const configPaths: string[] = [];\n for (const file of affectedConfigs) {\n await configManager.reload(file);\n configPaths.push(file.relativePath);\n }\n\n // Order matters: locales first (translations used by main), main before\n // routes (registers state routes consume), events between (listeners).\n for (const file of affectedLocales) await this.moduleLoader.reloadModule(file);\n for (const file of affectedMains) await this.moduleLoader.reloadModule(file);\n for (const file of affectedEvents) await this.moduleLoader.reloadModule(file);\n for (const file of affectedRoutes) await this.moduleLoader.reloadModule(file);\n\n return configPaths;\n }\n}\n\nfunction isEnvPath(path: string): boolean {\n const basename = path.split(\"/\").pop() ?? path;\n return basename === \".env\" || basename.startsWith(\".env.\");\n}\n\n/**\n * A file is \"affected\" if it itself is in the chain or imports something in it.\n */\nfunction isFileAffected(file: FileManager, chain: string[]): boolean {\n if (chain.includes(file.relativePath)) return true;\n for (const dep of file.dependencies) {\n if (chain.includes(dep)) return true;\n }\n return false;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-manager.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/manifest-manager.ts"],"sourcesContent":["import { atomicWriteAsync, fileExistsAsync, getJsonFileAsync } from \"@warlock.js/fs\";\
|
|
1
|
+
{"version":3,"file":"manifest-manager.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/manifest-manager.ts"],"sourcesContent":["import { atomicWriteAsync, fileExistsAsync, getJsonFileAsync } from \"@warlock.js/fs\";\nimport { FileManager } from \"./file-manager\";\nimport type { FileManifest } from \"./types\";\nimport { MANIFEST_PATH } from \"./flags\";\n\nconst MANIFEST_VERSION = \"1.0.0\";\n\n/**\n * Manifest structure with metadata and files\n */\nexport type Manifest = {\n version: string;\n lastBuildTime: number;\n projectHash?: string;\n stats: {\n totalFiles: number;\n totalDependencies: number;\n };\n files: Record<string, FileManifest>;\n};\n\nexport class ManifestManager {\n /**\n * Manifest data with metadata\n */\n private manifest: Manifest = {\n version: MANIFEST_VERSION,\n lastBuildTime: Date.now(),\n stats: {\n totalFiles: 0,\n totalDependencies: 0,\n },\n files: {},\n };\n\n /**\n * Constructor\n */\n public constructor(private readonly files: Map<string, FileManager>) {}\n\n /**\n * Initialize manifest manager\n * @returns true if manifest exists, false otherwise\n */\n public async init(): Promise<boolean> {\n if (await fileExistsAsync(MANIFEST_PATH)) {\n try {\n const loaded = await getJsonFileAsync<Manifest>(MANIFEST_PATH);\n\n // A torn write or a format change across upgrades is treated as absent.\n if (loaded?.version !== MANIFEST_VERSION || typeof loaded.files !== \"object\") {\n return false;\n }\n\n this.manifest = loaded;\n return true;\n } catch {\n return false;\n }\n }\n\n return false;\n }\n\n /**\n * Save manifest to disk\n */\n public async save(): Promise<void> {\n // Update stats before saving\n this.manifest.stats.totalFiles = Object.keys(this.manifest.files).length;\n this.manifest.stats.totalDependencies = this.calculateTotalDependencies();\n this.manifest.lastBuildTime = Date.now();\n\n await atomicWriteAsync(MANIFEST_PATH, JSON.stringify(this.manifest, null, 2));\n }\n\n /**\n * Get file manifest data\n */\n public getFile(filePath: string): FileManifest | undefined {\n return this.manifest.files[filePath];\n }\n\n /**\n * Check if file exists in manifest\n */\n public hasFile(filePath: string): boolean {\n return filePath in this.manifest.files;\n }\n\n /**\n * Set file manifest data\n */\n public setFile(filePath: string, fileManifest: FileManifest): void {\n this.manifest.files[filePath] = fileManifest;\n }\n\n /**\n * Remove file from manifest\n */\n public removeFile(filePath: string): void {\n delete this.manifest.files[filePath];\n }\n\n /**\n * Get all file paths in manifest\n */\n public getAllFilePaths(): string[] {\n return Object.keys(this.manifest.files);\n }\n\n /**\n * Get all file manifests\n */\n public getAllFiles(): Record<string, FileManifest> {\n return this.manifest.files;\n }\n\n /**\n * Get manifest metadata\n */\n public getMetadata() {\n return {\n version: this.manifest.version,\n lastBuildTime: this.manifest.lastBuildTime,\n projectHash: this.manifest.projectHash,\n stats: this.manifest.stats,\n };\n }\n\n /**\n * Calculate total dependencies across all files\n */\n private calculateTotalDependencies(): number {\n return Object.values(this.manifest.files).reduce(\n (total, file) => total + (file.dependencies?.length || 0),\n 0,\n );\n }\n\n /**\n * Clear all files from manifest\n */\n public clear(): void {\n this.manifest.files = {};\n this.manifest.stats = {\n totalFiles: 0,\n totalDependencies: 0,\n };\n }\n}\n"],"mappings":";;;;AAKA,MAAM,mBAAmB;AAgBzB,IAAa,kBAAb,MAA6B;;;;CAiB3B,AAAO,YAAY,AAAiB,OAAiC;EAAjC;kBAbP;GAC3B,SAAS;GACT,eAAe,KAAK,IAAI;GACxB,OAAO;IACL,YAAY;IACZ,mBAAmB;GACrB;GACA,OAAO,CAAC;EACV;CAKsE;;;;;CAMtE,MAAa,OAAyB;EACpC,IAAI,MAAM,gBAAgB,aAAa,GACrC,IAAI;GACF,MAAM,SAAS,MAAM,iBAA2B,aAAa;GAG7D,IAAI,QAAQ,YAAY,oBAAoB,OAAO,OAAO,UAAU,UAClE,OAAO;GAGT,KAAK,WAAW;GAChB,OAAO;EACT,QAAQ;GACN,OAAO;EACT;EAGF,OAAO;CACT;;;;CAKA,MAAa,OAAsB;EAEjC,KAAK,SAAS,MAAM,aAAa,OAAO,KAAK,KAAK,SAAS,KAAK,CAAC,CAAC;EAClE,KAAK,SAAS,MAAM,oBAAoB,KAAK,2BAA2B;EACxE,KAAK,SAAS,gBAAgB,KAAK,IAAI;EAEvC,MAAM,iBAAiB,eAAe,KAAK,UAAU,KAAK,UAAU,MAAM,CAAC,CAAC;CAC9E;;;;CAKA,AAAO,QAAQ,UAA4C;EACzD,OAAO,KAAK,SAAS,MAAM;CAC7B;;;;CAKA,AAAO,QAAQ,UAA2B;EACxC,OAAO,YAAY,KAAK,SAAS;CACnC;;;;CAKA,AAAO,QAAQ,UAAkB,cAAkC;EACjE,KAAK,SAAS,MAAM,YAAY;CAClC;;;;CAKA,AAAO,WAAW,UAAwB;EACxC,OAAO,KAAK,SAAS,MAAM;CAC7B;;;;CAKA,AAAO,kBAA4B;EACjC,OAAO,OAAO,KAAK,KAAK,SAAS,KAAK;CACxC;;;;CAKA,AAAO,cAA4C;EACjD,OAAO,KAAK,SAAS;CACvB;;;;CAKA,AAAO,cAAc;EACnB,OAAO;GACL,SAAS,KAAK,SAAS;GACvB,eAAe,KAAK,SAAS;GAC7B,aAAa,KAAK,SAAS;GAC3B,OAAO,KAAK,SAAS;EACvB;CACF;;;;CAKA,AAAQ,6BAAqC;EAC3C,OAAO,OAAO,OAAO,KAAK,SAAS,KAAK,CAAC,CAAC,QACvC,OAAO,SAAS,SAAS,KAAK,cAAc,UAAU,IACvD,CACF;CACF;;;;CAKA,AAAO,QAAc;EACnB,KAAK,SAAS,QAAQ,CAAC;EACvB,KAAK,SAAS,QAAQ;GACpB,YAAY;GACZ,mBAAmB;EACrB;CACF;AACF"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { router } from "../router/router.mjs";
|
|
2
2
|
import { devLogError, formatModuleNotFoundError } from "./dev-logger.mjs";
|
|
3
|
+
import { importAppModule } from "../loader/app-module-importer.mjs";
|
|
3
4
|
import { getDevelopmentModelModuleRegistry } from "./model-module-registry.mjs";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
6
|
|
|
@@ -20,15 +21,18 @@ var ModuleLoadError = class extends Error {
|
|
|
20
21
|
}
|
|
21
22
|
};
|
|
22
23
|
/**
|
|
23
|
-
* The
|
|
24
|
+
* The "special" file kinds that the dev server eagerly imports at boot.
|
|
24
25
|
* Order matters: locales first (used by everything), events before main so
|
|
25
|
-
* listeners are registered when main runs, routes
|
|
26
|
+
* listeners are registered when main runs, routes after main so handlers are
|
|
27
|
+
* bound, worker last so scheduled jobs and queue definitions see routes
|
|
28
|
+
* already registered. Dev always loads worker files since dev runs every role.
|
|
26
29
|
*/
|
|
27
30
|
const SPECIAL_TYPES = [
|
|
28
31
|
"locale",
|
|
29
32
|
"event",
|
|
30
33
|
"main",
|
|
31
|
-
"route"
|
|
34
|
+
"route",
|
|
35
|
+
"worker"
|
|
32
36
|
];
|
|
33
37
|
/**
|
|
34
38
|
* Loads application modules through the ESM loader hook. The hook stamps
|
|
@@ -52,9 +56,10 @@ var ModuleLoader = class {
|
|
|
52
56
|
* try/catch that logs and aborts the boot — so a broken route module aborts
|
|
53
57
|
* boot loudly rather than booting into a surface that 404s.
|
|
54
58
|
*/
|
|
55
|
-
async loadAll() {
|
|
59
|
+
async loadAll(options = {}) {
|
|
56
60
|
const failures = [];
|
|
57
61
|
for (const type of SPECIAL_TYPES) for (const file of this.specialFilesCollector.getFilesByType(type)) try {
|
|
62
|
+
await options.onBeforeLoad?.(file);
|
|
58
63
|
await this.loadModule(file, type);
|
|
59
64
|
} catch (error) {
|
|
60
65
|
failures.push(error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error));
|
|
@@ -71,7 +76,7 @@ var ModuleLoader = class {
|
|
|
71
76
|
try {
|
|
72
77
|
const fileUrl = pathToFileURL(file.absolutePath).href;
|
|
73
78
|
const load = async () => {
|
|
74
|
-
const module = await
|
|
79
|
+
const module = await importAppModule(fileUrl);
|
|
75
80
|
this.loadedModules.set(file.absolutePath, module);
|
|
76
81
|
this.registerCleanup(file, module);
|
|
77
82
|
if (type === "model") getDevelopmentModelModuleRegistry().publish(file.absolutePath, fileUrl, module);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-loader.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/module-loader.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { router } from \"../router/router\";\nimport { devLogError, formatModuleNotFoundError } from \"./dev-logger\";\nimport type { CleanupFunction, FileManager } from \"./file-manager\";\nimport { getDevelopmentModelModuleRegistry } from \"./model-module-registry\";\nimport type { SpecialFileType, SpecialFilesCollector } from \"./special-files-collector\";\n\ndeclare global {\n var __currentModuleFile: FileManager | undefined;\n}\n\n/**\n * Thrown when a special file (route/event/main/locale) fails to import or\n * register. Carries the file that failed and the original cause so a broken\n * module surfaces with its origin instead of vanishing into a 404.\n */\nexport class ModuleLoadError extends Error {\n public constructor(\n public readonly type: string,\n public readonly file: FileManager,\n public readonly cause: unknown,\n ) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n super(`Failed to load ${type}: ${file.relativePath} - ${reason}`);\n this.name = \"ModuleLoadError\";\n }\n}\n\n/**\n * The four \"special\" file kinds that the dev server eagerly imports at boot.\n * Order matters: locales first (used by everything), events before main so\n * listeners are registered when main runs, routes last so handlers are bound.\n */\nconst SPECIAL_TYPES: readonly SpecialFileType[] = [\"locale\", \"event\", \"main\", \"route\"] as const;\n\n/**\n * Loads application modules through the ESM loader hook. The hook stamps\n * `?v=N` on every import, so each reload is a fresh module without any\n * userland cache-busting plumbing.\n */\nexport class ModuleLoader {\n private readonly loadedModules = new Map<string, unknown>();\n\n public constructor(private readonly specialFilesCollector: SpecialFilesCollector) {\n // Web creates its Vite bridge after Core's development orchestration has\n // constructed ModuleLoader. Create the carrier now so a zero-model app can\n // subscribe before a model is added later through HMR.\n getDevelopmentModelModuleRegistry();\n }\n\n /**\n * Eagerly load every special file at boot, in the canonical order so\n * later phases (e.g. routes) see state earlier phases registered.\n *\n * A failing module no longer vanishes silently: each failure is collected\n * (so one broken file doesn't hide the others) and, once every file has been\n * attempted, an aggregate error is thrown. The boot callers\n * (`DevelopmentServer.start`, the HTTP test bootstrap) already wrap this in a\n * try/catch that logs and aborts the boot — so a broken route module aborts\n * boot loudly rather than booting into a surface that 404s.\n */\n public async loadAll(): Promise<void> {\n const failures: ModuleLoadError[] = [];\n\n for (const type of SPECIAL_TYPES) {\n for (const file of this.specialFilesCollector.getFilesByType(type)) {\n try {\n await this.loadModule(file, type);\n } catch (error) {\n failures.push(\n error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error),\n );\n }\n }\n }\n\n if (failures.length > 0) {\n throw new AggregateError(\n failures,\n `${failures.length} module(s) failed to load during boot:\\n` +\n failures.map((failure) => ` - ${failure.message}`).join(\"\\n\"),\n );\n }\n }\n\n /**\n * Import a file through the loader hook. For routes, scopes the import in\n * `router.withSourceFile()` so the added routes carry their origin.\n */\n public async loadModule<T = unknown>(file: FileManager, type: string): Promise<T | undefined> {\n if (file.relativePath.endsWith(\".env\")) return undefined;\n\n globalThis.__currentModuleFile = file;\n\n try {\n const fileUrl = pathToFileURL(file.absolutePath).href;\n\n const load = async () => {\n const module = (await import(fileUrl)) as Record<string, unknown>;\n this.loadedModules.set(file.absolutePath, module);\n this.registerCleanup(file, module);\n if (type === \"model\") {\n getDevelopmentModelModuleRegistry().publish(file.absolutePath, fileUrl, module);\n }\n return module as T;\n };\n\n return type === \"route\" ? await router.withSourceFile(file.relativePath, load) : await load();\n } catch (error: any) {\n if (error.code === \"ERR_MODULE_NOT_FOUND\") {\n devLogError(formatModuleNotFoundError(error));\n } else {\n // Pass the error through so devLogError prints the stack. With dev\n // source maps enabled, frames remap to the original `.ts` line/col\n // (e.g. the actual call site of an undefined symbol) instead of\n // vanishing — a one-line \"x is not defined\" is undebuggable.\n devLogError(\n `Failed to load ${type}: ${file.relativePath} - ${error?.message || error}`,\n error,\n );\n }\n\n // RETHROW after logging. Previously this returned undefined, so a route\n // file that threw on import or registration disappeared with no boot\n // error — the whole surface 404'd silently. Boot callers abort on the\n // throw; the HMR batch-reload handler catches it, keeps the server\n // alive, and shows it loudly.\n throw error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error);\n } finally {\n globalThis.__currentModuleFile = undefined;\n }\n }\n\n /**\n * Reload a previously-loaded special file. The version-bump that makes the\n * import fresh is done by the caller (layer-executor) before invoking this.\n * Routes are removed from the registry first so re-registration is clean.\n */\n public async reloadModule(file: FileManager): Promise<void> {\n const moduleType = this.specialFilesCollector.getFileType(file.relativePath);\n if (!moduleType) return;\n\n this.runCleanup(file);\n\n if (moduleType === \"route\") {\n router.removeRoutesBySourceFile(file.relativePath);\n }\n\n this.loadedModules.delete(file.absolutePath);\n await this.loadModule(file, moduleType);\n }\n\n /**\n * Run every cleanup hook the file registered (or `$cleanup` on its exports)\n * and reset the list. Called once per HMR reload.\n */\n public runCleanup(file: FileManager): void {\n for (const hook of file.cleanup) {\n try {\n if (typeof hook === \"function\") {\n hook();\n } else if (\n hook &&\n typeof (hook as { unsubscribe?: () => void }).unsubscribe === \"function\"\n ) {\n (hook as { unsubscribe: () => void }).unsubscribe();\n }\n } catch {\n // Cleanup errors shouldn't break HMR.\n }\n }\n file.resetCleanup();\n }\n\n /**\n * Clean up after a file whose source was deleted from disk.\n */\n public cleanupDeletedModule(file: FileManager): void {\n this.loadedModules.delete(file.absolutePath);\n\n if (file.type === \"model\") {\n getDevelopmentModelModuleRegistry().remove(file.absolutePath);\n }\n\n if (file.type === \"route\") {\n router.removeRoutesBySourceFile(file.relativePath);\n }\n\n this.runCleanup(file);\n }\n\n /**\n * Scan a freshly-loaded module for cleanup handlers and register them on\n * the FileManager so they run before the next reload. Priority: explicit\n * `export function cleanup()` → `.$cleanup` on any exported value.\n */\n private registerCleanup(file: FileManager, module: Record<string, unknown>): void {\n if (typeof module.cleanup === \"function\") {\n file.addCleanup(module.cleanup as CleanupFunction);\n return;\n }\n\n const cleanups: CleanupFunction[] = [];\n\n for (const exportedValue of Object.values(module)) {\n const cleanup = (exportedValue as { $cleanup?: unknown })?.$cleanup;\n if (typeof cleanup === \"function\") {\n cleanups.push(cleanup.bind(exportedValue) as CleanupFunction);\n }\n }\n\n if (cleanups.length > 0) {\n file.addCleanup(cleanups);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"module-loader.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/module-loader.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { importAppModule } from \"../loader/app-module-importer\";\nimport { router } from \"../router/router\";\nimport { devLogError, formatModuleNotFoundError } from \"./dev-logger\";\nimport type { CleanupFunction, FileManager } from \"./file-manager\";\nimport { getDevelopmentModelModuleRegistry } from \"./model-module-registry\";\nimport type { SpecialFileType, SpecialFilesCollector } from \"./special-files-collector\";\n\ndeclare global {\n var __currentModuleFile: FileManager | undefined;\n}\n\n/**\n * Thrown when a special file (route/event/main/locale) fails to import or\n * register. Carries the file that failed and the original cause so a broken\n * module surfaces with its origin instead of vanishing into a 404.\n */\nexport class ModuleLoadError extends Error {\n public constructor(\n public readonly type: string,\n public readonly file: FileManager,\n public readonly cause: unknown,\n ) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n super(`Failed to load ${type}: ${file.relativePath} - ${reason}`);\n this.name = \"ModuleLoadError\";\n }\n}\n\n/**\n * The \"special\" file kinds that the dev server eagerly imports at boot.\n * Order matters: locales first (used by everything), events before main so\n * listeners are registered when main runs, routes after main so handlers are\n * bound, worker last so scheduled jobs and queue definitions see routes\n * already registered. Dev always loads worker files since dev runs every role.\n */\nconst SPECIAL_TYPES: readonly SpecialFileType[] = [\n \"locale\",\n \"event\",\n \"main\",\n \"route\",\n \"worker\",\n] as const;\n\n/**\n * Loads application modules through the ESM loader hook. The hook stamps\n * `?v=N` on every import, so each reload is a fresh module without any\n * userland cache-busting plumbing.\n */\nexport class ModuleLoader {\n private readonly loadedModules = new Map<string, unknown>();\n\n public constructor(private readonly specialFilesCollector: SpecialFilesCollector) {\n // Web creates its Vite bridge after Core's development orchestration has\n // constructed ModuleLoader. Create the carrier now so a zero-model app can\n // subscribe before a model is added later through HMR.\n getDevelopmentModelModuleRegistry();\n }\n\n /**\n * Eagerly load every special file at boot, in the canonical order so\n * later phases (e.g. routes) see state earlier phases registered.\n *\n * A failing module no longer vanishes silently: each failure is collected\n * (so one broken file doesn't hide the others) and, once every file has been\n * attempted, an aggregate error is thrown. The boot callers\n * (`DevelopmentServer.start`, the HTTP test bootstrap) already wrap this in a\n * try/catch that logs and aborts the boot — so a broken route module aborts\n * boot loudly rather than booting into a surface that 404s.\n */\n public async loadAll(\n options: { onBeforeLoad?: (file: FileManager) => void | Promise<void> } = {},\n ): Promise<void> {\n const failures: ModuleLoadError[] = [];\n\n for (const type of SPECIAL_TYPES) {\n for (const file of this.specialFilesCollector.getFilesByType(type)) {\n try {\n await options.onBeforeLoad?.(file);\n await this.loadModule(file, type);\n } catch (error) {\n failures.push(\n error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error),\n );\n }\n }\n }\n\n if (failures.length > 0) {\n throw new AggregateError(\n failures,\n `${failures.length} module(s) failed to load during boot:\\n` +\n failures.map((failure) => ` - ${failure.message}`).join(\"\\n\"),\n );\n }\n }\n\n /**\n * Import a file through the loader hook. For routes, scopes the import in\n * `router.withSourceFile()` so the added routes carry their origin.\n */\n public async loadModule<T = unknown>(file: FileManager, type: string): Promise<T | undefined> {\n if (file.relativePath.endsWith(\".env\")) return undefined;\n\n globalThis.__currentModuleFile = file;\n\n try {\n const fileUrl = pathToFileURL(file.absolutePath).href;\n\n const load = async () => {\n const module = (await importAppModule(fileUrl)) as Record<string, unknown>;\n this.loadedModules.set(file.absolutePath, module);\n this.registerCleanup(file, module);\n if (type === \"model\") {\n getDevelopmentModelModuleRegistry().publish(file.absolutePath, fileUrl, module);\n }\n return module as T;\n };\n\n return type === \"route\" ? await router.withSourceFile(file.relativePath, load) : await load();\n } catch (error: any) {\n if (error.code === \"ERR_MODULE_NOT_FOUND\") {\n devLogError(formatModuleNotFoundError(error));\n } else {\n // Pass the error through so devLogError prints the stack. With dev\n // source maps enabled, frames remap to the original `.ts` line/col\n // (e.g. the actual call site of an undefined symbol) instead of\n // vanishing — a one-line \"x is not defined\" is undebuggable.\n devLogError(\n `Failed to load ${type}: ${file.relativePath} - ${error?.message || error}`,\n error,\n );\n }\n\n // RETHROW after logging. Previously this returned undefined, so a route\n // file that threw on import or registration disappeared with no boot\n // error — the whole surface 404'd silently. Boot callers abort on the\n // throw; the HMR batch-reload handler catches it, keeps the server\n // alive, and shows it loudly.\n throw error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error);\n } finally {\n globalThis.__currentModuleFile = undefined;\n }\n }\n\n /**\n * Reload a previously-loaded special file. The version-bump that makes the\n * import fresh is done by the caller (layer-executor) before invoking this.\n * Routes are removed from the registry first so re-registration is clean.\n */\n public async reloadModule(file: FileManager): Promise<void> {\n const moduleType = this.specialFilesCollector.getFileType(file.relativePath);\n if (!moduleType) return;\n\n this.runCleanup(file);\n\n if (moduleType === \"route\") {\n router.removeRoutesBySourceFile(file.relativePath);\n }\n\n this.loadedModules.delete(file.absolutePath);\n await this.loadModule(file, moduleType);\n }\n\n /**\n * Run every cleanup hook the file registered (or `$cleanup` on its exports)\n * and reset the list. Called once per HMR reload.\n */\n public runCleanup(file: FileManager): void {\n for (const hook of file.cleanup) {\n try {\n if (typeof hook === \"function\") {\n hook();\n } else if (\n hook &&\n typeof (hook as { unsubscribe?: () => void }).unsubscribe === \"function\"\n ) {\n (hook as { unsubscribe: () => void }).unsubscribe();\n }\n } catch {\n // Cleanup errors shouldn't break HMR.\n }\n }\n file.resetCleanup();\n }\n\n /**\n * Clean up after a file whose source was deleted from disk.\n */\n public cleanupDeletedModule(file: FileManager): void {\n this.loadedModules.delete(file.absolutePath);\n\n if (file.type === \"model\") {\n getDevelopmentModelModuleRegistry().remove(file.absolutePath);\n }\n\n if (file.type === \"route\") {\n router.removeRoutesBySourceFile(file.relativePath);\n }\n\n this.runCleanup(file);\n }\n\n /**\n * Scan a freshly-loaded module for cleanup handlers and register them on\n * the FileManager so they run before the next reload. Priority: explicit\n * `export function cleanup()` → `.$cleanup` on any exported value.\n */\n private registerCleanup(file: FileManager, module: Record<string, unknown>): void {\n if (typeof module.cleanup === \"function\") {\n file.addCleanup(module.cleanup as CleanupFunction);\n return;\n }\n\n const cleanups: CleanupFunction[] = [];\n\n for (const exportedValue of Object.values(module)) {\n const cleanup = (exportedValue as { $cleanup?: unknown })?.$cleanup;\n if (typeof cleanup === \"function\") {\n cleanups.push(cleanup.bind(exportedValue) as CleanupFunction);\n }\n }\n\n if (cleanups.length > 0) {\n file.addCleanup(cleanups);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { connectorsManager } from "../connectors/connectors-manager.mjs";
|
|
2
|
+
import { devLogWarn } from "./dev-logger.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../core/src/dev-server/register-installed-devtools.ts
|
|
5
|
+
const DEVTOOLS_PACKAGE = "@warlock.js/devtools";
|
|
6
|
+
/**
|
|
7
|
+
* Register `@warlock.js/devtools` when the app has it installed.
|
|
8
|
+
*
|
|
9
|
+
* Called by `warlock dev` only, before the late connector phase, so the app
|
|
10
|
+
* never names devtools in `warlock.config.ts`. That matters because devtools
|
|
11
|
+
* is a DEV dependency: a production install omits it, and a config import of
|
|
12
|
+
* it would fail to resolve there. The production entry never calls this.
|
|
13
|
+
*
|
|
14
|
+
* Skipped when a connector named `devtools` is already registered (an app
|
|
15
|
+
* that wires it by hand keeps control). A missing package is the normal case
|
|
16
|
+
* and stays silent; any other load failure is reported and ignored, because
|
|
17
|
+
* a dev tool must never stop the app from booting.
|
|
18
|
+
*/
|
|
19
|
+
async function registerInstalledDevtools(load = () => import(DEVTOOLS_PACKAGE)) {
|
|
20
|
+
if (connectorsManager.has("devtools")) return false;
|
|
21
|
+
let devtools;
|
|
22
|
+
try {
|
|
23
|
+
devtools = await load();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (!isMissingPackage(error)) devLogWarn(`${DEVTOOLS_PACKAGE} is installed but failed to load: ${describe(error)}`);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (typeof devtools.devtoolsConnector !== "function") {
|
|
29
|
+
devLogWarn(`${DEVTOOLS_PACKAGE} does not export devtoolsConnector(); it was not started`);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
connectorsManager.register(devtools.devtoolsConnector());
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/** True when the error says the devtools package itself is not installed. */
|
|
36
|
+
function isMissingPackage(error) {
|
|
37
|
+
const code = error?.code;
|
|
38
|
+
if (code !== "ERR_MODULE_NOT_FOUND" && code !== "MODULE_NOT_FOUND") return false;
|
|
39
|
+
return describe(error).includes(`'${DEVTOOLS_PACKAGE}'`) || describe(error).includes(`"${DEVTOOLS_PACKAGE}"`);
|
|
40
|
+
}
|
|
41
|
+
function describe(error) {
|
|
42
|
+
return error instanceof Error ? error.message : String(error);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
export { registerInstalledDevtools };
|
|
47
|
+
//# sourceMappingURL=register-installed-devtools.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-installed-devtools.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/register-installed-devtools.ts"],"sourcesContent":["import { connectorsManager } from \"../connectors/connectors-manager\";\nimport type { Connector } from \"../connectors/types\";\nimport { devLogWarn } from \"./dev-logger\";\n\nconst DEVTOOLS_PACKAGE = \"@warlock.js/devtools\";\n\ntype DevtoolsModule = {\n devtoolsConnector?: () => Connector;\n};\n\n/**\n * Register `@warlock.js/devtools` when the app has it installed.\n *\n * Called by `warlock dev` only, before the late connector phase, so the app\n * never names devtools in `warlock.config.ts`. That matters because devtools\n * is a DEV dependency: a production install omits it, and a config import of\n * it would fail to resolve there. The production entry never calls this.\n *\n * Skipped when a connector named `devtools` is already registered (an app\n * that wires it by hand keeps control). A missing package is the normal case\n * and stays silent; any other load failure is reported and ignored, because\n * a dev tool must never stop the app from booting.\n */\nexport async function registerInstalledDevtools(\n load: () => Promise<DevtoolsModule> = () => import(DEVTOOLS_PACKAGE),\n): Promise<boolean> {\n if (connectorsManager.has(\"devtools\")) {\n return false;\n }\n\n let devtools: DevtoolsModule;\n\n try {\n devtools = await load();\n } catch (error) {\n if (!isMissingPackage(error)) {\n devLogWarn(`${DEVTOOLS_PACKAGE} is installed but failed to load: ${describe(error)}`);\n }\n\n return false;\n }\n\n if (typeof devtools.devtoolsConnector !== \"function\") {\n devLogWarn(`${DEVTOOLS_PACKAGE} does not export devtoolsConnector(); it was not started`);\n\n return false;\n }\n\n connectorsManager.register(devtools.devtoolsConnector());\n\n return true;\n}\n\n/** True when the error says the devtools package itself is not installed. */\nfunction isMissingPackage(error: unknown): boolean {\n const code = (error as { code?: unknown } | undefined)?.code;\n\n if (code !== \"ERR_MODULE_NOT_FOUND\" && code !== \"MODULE_NOT_FOUND\") {\n return false;\n }\n\n // A missing module INSIDE an installed devtools is a broken install, not an\n // absent package — only the package's own name counts as \"not installed\".\n return (\n describe(error).includes(`'${DEVTOOLS_PACKAGE}'`) ||\n describe(error).includes(`\"${DEVTOOLS_PACKAGE}\"`)\n );\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":";;;;AAIA,MAAM,mBAAmB;;;;;;;;;;;;;;AAmBzB,eAAsB,0BACpB,aAA4C,OAAO,mBACjC;CAClB,IAAI,kBAAkB,IAAI,UAAU,GAClC,OAAO;CAGT,IAAI;CAEJ,IAAI;EACF,WAAW,MAAM,KAAK;CACxB,SAAS,OAAO;EACd,IAAI,CAAC,iBAAiB,KAAK,GACzB,WAAW,GAAG,iBAAiB,oCAAoC,SAAS,KAAK,GAAG;EAGtF,OAAO;CACT;CAEA,IAAI,OAAO,SAAS,sBAAsB,YAAY;EACpD,WAAW,GAAG,iBAAiB,yDAAyD;EAExF,OAAO;CACT;CAEA,kBAAkB,SAAS,SAAS,kBAAkB,CAAC;CAEvD,OAAO;AACT;;AAGA,SAAS,iBAAiB,OAAyB;CACjD,MAAM,OAAQ,OAA0C;CAExD,IAAI,SAAS,0BAA0B,SAAS,oBAC9C,OAAO;CAKT,OACE,SAAS,KAAK,CAAC,CAAC,SAAS,IAAI,iBAAiB,EAAE,KAChD,SAAS,KAAK,CAAC,CAAC,SAAS,IAAI,iBAAiB,EAAE;AAEpD;AAEA,SAAS,SAAS,OAAwB;CACxC,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { warlockPath } from "../utils/paths.mjs";
|
|
2
|
+
import "../utils/index.mjs";
|
|
3
|
+
import { runTypingsContributions } from "../production/build-contributions.mjs";
|
|
4
|
+
|
|
5
|
+
//#region ../core/src/dev-server/run-connector-typings.ts
|
|
6
|
+
/**
|
|
7
|
+
* Let connectors write their own typings during `warlock generate.typings`.
|
|
8
|
+
*
|
|
9
|
+
* Core cannot import web, so this drains the `typings` hook of each connector's
|
|
10
|
+
* `build` contribution — the seam `warlock build` reads. The named API snapshot
|
|
11
|
+
* (a child process) is collected only when a hook exists AND the web connector
|
|
12
|
+
* is configured, exactly as the builder does; an API-only app pays nothing and
|
|
13
|
+
* cannot fail here.
|
|
14
|
+
*/
|
|
15
|
+
async function runConnectorTypings({ connectors, appRoot, options, collectRoutes }) {
|
|
16
|
+
if (!connectors.some((connector) => Boolean(connector.build?.typings))) return;
|
|
17
|
+
const namedApiRoutes = connectors.some((connector) => connector.name === "web") ? await collectRoutes() : void 0;
|
|
18
|
+
await runTypingsContributions(connectors, {
|
|
19
|
+
productionDir: warlockPath("production"),
|
|
20
|
+
appRoot,
|
|
21
|
+
options: options(),
|
|
22
|
+
namedApiRoutes
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { runConnectorTypings };
|
|
28
|
+
//# sourceMappingURL=run-connector-typings.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-connector-typings.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/run-connector-typings.ts"],"sourcesContent":["import type { Connector } from \"../connectors/types\";\nimport { runTypingsContributions } from \"../production/build-contributions\";\nimport type { NamedApiRoute } from \"../router/types\";\nimport { warlockPath } from \"../utils\";\nimport type { ResolvedBuildConfig } from \"../production/resolve-build-config\";\n\nexport type ConnectorTypingsInput = {\n connectors: readonly Connector[];\n appRoot: string;\n /** Resolved lazily: an app with no typings hook never needs a loaded build config. */\n options(): ResolvedBuildConfig;\n /** Fresh registration-only named API route snapshot (a child process in production). */\n collectRoutes(): Promise<readonly NamedApiRoute[]>;\n};\n\n/**\n * Let connectors write their own typings during `warlock generate.typings`.\n *\n * Core cannot import web, so this drains the `typings` hook of each connector's\n * `build` contribution — the seam `warlock build` reads. The named API snapshot\n * (a child process) is collected only when a hook exists AND the web connector\n * is configured, exactly as the builder does; an API-only app pays nothing and\n * cannot fail here.\n */\nexport async function runConnectorTypings({\n connectors,\n appRoot,\n options,\n collectRoutes,\n}: ConnectorTypingsInput): Promise<void> {\n if (!connectors.some((connector) => Boolean(connector.build?.typings))) return;\n\n const namedApiRoutes = connectors.some((connector) => connector.name === \"web\")\n ? await collectRoutes()\n : undefined;\n\n await runTypingsContributions(connectors, {\n productionDir: warlockPath(\"production\"),\n appRoot,\n options: options(),\n namedApiRoutes,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,eAAsB,oBAAoB,EACxC,YACA,SACA,SACA,iBACuC;CACvC,IAAI,CAAC,WAAW,MAAM,cAAc,QAAQ,UAAU,OAAO,OAAO,CAAC,GAAG;CAExE,MAAM,iBAAiB,WAAW,MAAM,cAAc,UAAU,SAAS,KAAK,IAC1E,MAAM,cAAc,IACpB;CAEJ,MAAM,wBAAwB,YAAY;EACxC,eAAe,YAAY,YAAY;EACvC;EACA,SAAS,QAAQ;EACjB;CACF,CAAC;AACH"}
|
|
@@ -6,6 +6,8 @@ const isConfigFile = (path) => /^src\/config\/.*\.(ts|tsx)$/.test(normalize(path
|
|
|
6
6
|
const isMainFile = (path) => /^src\/app\/([^/]+\/)?main\.(ts|tsx)$/.test(normalize(path));
|
|
7
7
|
/** `src/app/<module>/routes.ts` — one level deep */
|
|
8
8
|
const isRouteFile = (path) => /^src\/app\/[^/]+\/routes\.(ts|tsx)$/.test(normalize(path));
|
|
9
|
+
/** `src/app/worker.ts` or `src/app/<module>/worker.ts` — mirrors `isMainFile`'s optional module dir */
|
|
10
|
+
const isWorkerFile = (path) => /^src\/app\/([^/]+\/)?worker\.(ts|tsx)$/.test(normalize(path));
|
|
9
11
|
/** `src/app/<module>/events/<file>.ts` */
|
|
10
12
|
const isEventFile = (path) => /^src\/app\/[^/]+\/events\/[^/]+\.(ts|tsx)$/.test(normalize(path));
|
|
11
13
|
/** `src/app/<module>/utils/locales.ts` */
|
|
@@ -14,5 +16,5 @@ const isLocaleFile = (path) => /^src\/app\/[^/]+\/utils\/locales\.(ts|tsx)$/.tes
|
|
|
14
16
|
const isCodeFile = (path) => /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/.test(normalize(path));
|
|
15
17
|
|
|
16
18
|
//#endregion
|
|
17
|
-
export { isCodeFile, isConfigFile, isEventFile, isLocaleFile, isMainFile, isRouteFile };
|
|
19
|
+
export { isCodeFile, isConfigFile, isEventFile, isLocaleFile, isMainFile, isRouteFile, isWorkerFile };
|
|
18
20
|
//# sourceMappingURL=special-file-patterns.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"special-file-patterns.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/special-file-patterns.ts"],"sourcesContent":["const normalize = (path: string) => path.replace(/\\\\/g, \"/\");\n\n/** `src/config/**` — any depth, the config key is the raw path */\nexport const isConfigFile = (path: string) => /^src\\/config\\/.*\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/main.ts` or `src/app/<module>/main.ts` */\nexport const isMainFile = (path: string) =>\n /^src\\/app\\/([^/]+\\/)?main\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/routes.ts` — one level deep */\nexport const isRouteFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/routes\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/events/<file>.ts` */\nexport const isEventFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/events\\/[^/]+\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/utils/locales.ts` */\nexport const isLocaleFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/utils\\/locales\\.(ts|tsx)$/.test(normalize(path));\n\n/** Source files the dev server can hot-reload; anything else is data (`.sql`, `.json`, …) */\nexport const isCodeFile = (path: string) =>\n /\\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/.test(normalize(path));\n"],"mappings":";AAAA,MAAM,aAAa,SAAiB,KAAK,QAAQ,OAAO,GAAG;;AAG3D,MAAa,gBAAgB,SAAiB,8BAA8B,KAAK,UAAU,IAAI,CAAC;;AAGhG,MAAa,cAAc,SACzB,uCAAuC,KAAK,UAAU,IAAI,CAAC;;AAG7D,MAAa,eAAe,SAC1B,sCAAsC,KAAK,UAAU,IAAI,CAAC;;AAG5D,MAAa,eAAe,SAC1B,6CAA6C,KAAK,UAAU,IAAI,CAAC;;AAGnE,MAAa,gBAAgB,SAC3B,8CAA8C,KAAK,UAAU,IAAI,CAAC;;AAGpE,MAAa,cAAc,SACzB,qCAAqC,KAAK,UAAU,IAAI,CAAC"}
|
|
1
|
+
{"version":3,"file":"special-file-patterns.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/special-file-patterns.ts"],"sourcesContent":["const normalize = (path: string) => path.replace(/\\\\/g, \"/\");\n\n/** `src/config/**` — any depth, the config key is the raw path */\nexport const isConfigFile = (path: string) => /^src\\/config\\/.*\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/main.ts` or `src/app/<module>/main.ts` */\nexport const isMainFile = (path: string) =>\n /^src\\/app\\/([^/]+\\/)?main\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/routes.ts` — one level deep */\nexport const isRouteFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/routes\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/worker.ts` or `src/app/<module>/worker.ts` — mirrors `isMainFile`'s optional module dir */\nexport const isWorkerFile = (path: string) =>\n /^src\\/app\\/([^/]+\\/)?worker\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/events/<file>.ts` */\nexport const isEventFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/events\\/[^/]+\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/utils/locales.ts` */\nexport const isLocaleFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/utils\\/locales\\.(ts|tsx)$/.test(normalize(path));\n\n/** Source files the dev server can hot-reload; anything else is data (`.sql`, `.json`, …) */\nexport const isCodeFile = (path: string) =>\n /\\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/.test(normalize(path));\n"],"mappings":";AAAA,MAAM,aAAa,SAAiB,KAAK,QAAQ,OAAO,GAAG;;AAG3D,MAAa,gBAAgB,SAAiB,8BAA8B,KAAK,UAAU,IAAI,CAAC;;AAGhG,MAAa,cAAc,SACzB,uCAAuC,KAAK,UAAU,IAAI,CAAC;;AAG7D,MAAa,eAAe,SAC1B,sCAAsC,KAAK,UAAU,IAAI,CAAC;;AAG5D,MAAa,gBAAgB,SAC3B,yCAAyC,KAAK,UAAU,IAAI,CAAC;;AAG/D,MAAa,eAAe,SAC1B,6CAA6C,KAAK,UAAU,IAAI,CAAC;;AAGnE,MAAa,gBAAgB,SAC3B,8CAA8C,KAAK,UAAU,IAAI,CAAC;;AAGpE,MAAa,cAAc,SACzB,qCAAqC,KAAK,UAAU,IAAI,CAAC"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { FileManager } from "./file-manager.mjs";
|
|
2
2
|
|
|
3
3
|
//#region ../core/src/dev-server/special-files-collector.d.ts
|
|
4
|
-
type SpecialFileType = "config" | "main" | "route" | "event" | "locale";
|
|
4
|
+
type SpecialFileType = "config" | "main" | "route" | "event" | "locale" | "worker";
|
|
5
5
|
/**
|
|
6
6
|
* Categorises files into the kinds the dev server treats specially
|
|
7
|
-
* (config / main / route / event / locale) and exposes typed accessors.
|
|
7
|
+
* (config / main / route / event / locale / worker) and exposes typed accessors.
|
|
8
8
|
*/
|
|
9
9
|
declare class SpecialFilesCollector {
|
|
10
10
|
private readonly buckets;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"special-files-collector.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/special-files-collector.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"special-files-collector.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/special-files-collector.ts"],"mappings":";;;KAUY,eAAA;;AAAZ;;;cAMa,qBAAA;EAAA,iBACM,OAAA;EASV,OAAA,CAAQ,KAAA,EAAO,GAAA,SAAY,WAAA;EAO3B,OAAA,CAAQ,WAAA,EAAa,WAAA;EAIrB,UAAA,CAAW,YAAA;EAIX,UAAA,CAAW,WAAA,EAAa,WAAA;EAKxB,WAAA,CAAY,YAAA,WAAuB,eAAA;EAUnC,cAAA,CAAe,IAAA,EAAM,eAAA,GAAkB,WAAA;EAUvC,QAAA,IAAY,MAAA,CAAO,eAAA;EAWnB,KAAA;EAAA,QAIC,UAAA;AAAA"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { isConfigFile, isEventFile, isLocaleFile, isMainFile, isRouteFile } from "./special-file-patterns.mjs";
|
|
1
|
+
import { isConfigFile, isEventFile, isLocaleFile, isMainFile, isRouteFile, isWorkerFile } from "./special-file-patterns.mjs";
|
|
2
2
|
|
|
3
3
|
//#region ../core/src/dev-server/special-files-collector.ts
|
|
4
4
|
/**
|
|
5
5
|
* Categorises files into the kinds the dev server treats specially
|
|
6
|
-
* (config / main / route / event / locale) and exposes typed accessors.
|
|
6
|
+
* (config / main / route / event / locale / worker) and exposes typed accessors.
|
|
7
7
|
*/
|
|
8
8
|
var SpecialFilesCollector = class {
|
|
9
9
|
constructor() {
|
|
@@ -12,7 +12,8 @@ var SpecialFilesCollector = class {
|
|
|
12
12
|
main: /* @__PURE__ */ new Map(),
|
|
13
13
|
route: /* @__PURE__ */ new Map(),
|
|
14
14
|
event: /* @__PURE__ */ new Map(),
|
|
15
|
-
locale: /* @__PURE__ */ new Map()
|
|
15
|
+
locale: /* @__PURE__ */ new Map(),
|
|
16
|
+
worker: /* @__PURE__ */ new Map()
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
collect(files) {
|
|
@@ -44,7 +45,8 @@ var SpecialFilesCollector = class {
|
|
|
44
45
|
main: this.buckets.main.size,
|
|
45
46
|
route: this.buckets.route.size,
|
|
46
47
|
event: this.buckets.event.size,
|
|
47
|
-
locale: this.buckets.locale.size
|
|
48
|
+
locale: this.buckets.locale.size,
|
|
49
|
+
worker: this.buckets.worker.size
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
clear() {
|
|
@@ -56,6 +58,7 @@ var SpecialFilesCollector = class {
|
|
|
56
58
|
else if (isRouteFile(relativePath)) this.buckets.route.set(relativePath, fileManager);
|
|
57
59
|
else if (isEventFile(relativePath)) this.buckets.event.set(relativePath, fileManager);
|
|
58
60
|
else if (isLocaleFile(relativePath)) this.buckets.locale.set(relativePath, fileManager);
|
|
61
|
+
else if (isWorkerFile(relativePath)) this.buckets.worker.set(relativePath, fileManager);
|
|
59
62
|
}
|
|
60
63
|
};
|
|
61
64
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"special-files-collector.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/special-files-collector.ts"],"sourcesContent":["import { FileManager } from \"./file-manager\";\
|
|
1
|
+
{"version":3,"file":"special-files-collector.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/special-files-collector.ts"],"sourcesContent":["import { FileManager } from \"./file-manager\";\nimport {\n isConfigFile,\n isEventFile,\n isLocaleFile,\n isMainFile,\n isRouteFile,\n isWorkerFile,\n} from \"./special-file-patterns\";\n\nexport type SpecialFileType = \"config\" | \"main\" | \"route\" | \"event\" | \"locale\" | \"worker\";\n\n/**\n * Categorises files into the kinds the dev server treats specially\n * (config / main / route / event / locale / worker) and exposes typed accessors.\n */\nexport class SpecialFilesCollector {\n private readonly buckets: Record<SpecialFileType, Map<string, FileManager>> = {\n config: new Map(),\n main: new Map(),\n route: new Map(),\n event: new Map(),\n locale: new Map(),\n worker: new Map(),\n };\n\n public collect(files: Map<string, FileManager>): void {\n for (const bucket of Object.values(this.buckets)) bucket.clear();\n for (const [relativePath, fileManager] of files) {\n this.categorise(relativePath, fileManager);\n }\n }\n\n public addFile(fileManager: FileManager): void {\n this.categorise(fileManager.relativePath, fileManager);\n }\n\n public removeFile(relativePath: string): void {\n for (const bucket of Object.values(this.buckets)) bucket.delete(relativePath);\n }\n\n public updateFile(fileManager: FileManager): void {\n this.removeFile(fileManager.relativePath);\n this.categorise(fileManager.relativePath, fileManager);\n }\n\n public getFileType(relativePath: string): SpecialFileType | null {\n for (const [type, bucket] of Object.entries(this.buckets) as [\n SpecialFileType,\n Map<string, FileManager>,\n ][]) {\n if (bucket.has(relativePath)) return type;\n }\n return null;\n }\n\n public getFilesByType(type: SpecialFileType): FileManager[] {\n const files = Array.from(this.buckets[type].values());\n // Routes are loaded sequentially in alphabetical order so registration\n // is deterministic across runs.\n if (type === \"route\") {\n files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));\n }\n return files;\n }\n\n public getStats(): Record<SpecialFileType, number> {\n return {\n config: this.buckets.config.size,\n main: this.buckets.main.size,\n route: this.buckets.route.size,\n event: this.buckets.event.size,\n locale: this.buckets.locale.size,\n worker: this.buckets.worker.size,\n };\n }\n\n public clear(): void {\n for (const bucket of Object.values(this.buckets)) bucket.clear();\n }\n\n private categorise(relativePath: string, fileManager: FileManager): void {\n if (isConfigFile(relativePath)) {\n this.buckets.config.set(relativePath, fileManager);\n } else if (isMainFile(relativePath)) {\n this.buckets.main.set(relativePath, fileManager);\n } else if (isRouteFile(relativePath)) {\n this.buckets.route.set(relativePath, fileManager);\n } else if (isEventFile(relativePath)) {\n this.buckets.event.set(relativePath, fileManager);\n } else if (isLocaleFile(relativePath)) {\n this.buckets.locale.set(relativePath, fileManager);\n } else if (isWorkerFile(relativePath)) {\n this.buckets.worker.set(relativePath, fileManager);\n }\n }\n}\n"],"mappings":";;;;;;;AAgBA,IAAa,wBAAb,MAAmC;;iBAC6C;GAC5E,wBAAQ,IAAI,IAAI;GAChB,sBAAM,IAAI,IAAI;GACd,uBAAO,IAAI,IAAI;GACf,uBAAO,IAAI,IAAI;GACf,wBAAQ,IAAI,IAAI;GAChB,wBAAQ,IAAI,IAAI;EAClB;;CAEA,AAAO,QAAQ,OAAuC;EACpD,KAAK,MAAM,UAAU,OAAO,OAAO,KAAK,OAAO,GAAG,OAAO,MAAM;EAC/D,KAAK,MAAM,CAAC,cAAc,gBAAgB,OACxC,KAAK,WAAW,cAAc,WAAW;CAE7C;CAEA,AAAO,QAAQ,aAAgC;EAC7C,KAAK,WAAW,YAAY,cAAc,WAAW;CACvD;CAEA,AAAO,WAAW,cAA4B;EAC5C,KAAK,MAAM,UAAU,OAAO,OAAO,KAAK,OAAO,GAAG,OAAO,OAAO,YAAY;CAC9E;CAEA,AAAO,WAAW,aAAgC;EAChD,KAAK,WAAW,YAAY,YAAY;EACxC,KAAK,WAAW,YAAY,cAAc,WAAW;CACvD;CAEA,AAAO,YAAY,cAA8C;EAC/D,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,KAAK,OAAO,GAItD,IAAI,OAAO,IAAI,YAAY,GAAG,OAAO;EAEvC,OAAO;CACT;CAEA,AAAO,eAAe,MAAsC;EAC1D,MAAM,QAAQ,MAAM,KAAK,KAAK,QAAQ,KAAK,CAAC,OAAO,CAAC;EAGpD,IAAI,SAAS,SACX,MAAM,MAAM,GAAG,MAAM,EAAE,aAAa,cAAc,EAAE,YAAY,CAAC;EAEnE,OAAO;CACT;CAEA,AAAO,WAA4C;EACjD,OAAO;GACL,QAAQ,KAAK,QAAQ,OAAO;GAC5B,MAAM,KAAK,QAAQ,KAAK;GACxB,OAAO,KAAK,QAAQ,MAAM;GAC1B,OAAO,KAAK,QAAQ,MAAM;GAC1B,QAAQ,KAAK,QAAQ,OAAO;GAC5B,QAAQ,KAAK,QAAQ,OAAO;EAC9B;CACF;CAEA,AAAO,QAAc;EACnB,KAAK,MAAM,UAAU,OAAO,OAAO,KAAK,OAAO,GAAG,OAAO,MAAM;CACjE;CAEA,AAAQ,WAAW,cAAsB,aAAgC;EACvE,IAAI,aAAa,YAAY,GAC3B,KAAK,QAAQ,OAAO,IAAI,cAAc,WAAW;OAC5C,IAAI,WAAW,YAAY,GAChC,KAAK,QAAQ,KAAK,IAAI,cAAc,WAAW;OAC1C,IAAI,YAAY,YAAY,GACjC,KAAK,QAAQ,MAAM,IAAI,cAAc,WAAW;OAC3C,IAAI,YAAY,YAAY,GACjC,KAAK,QAAQ,MAAM,IAAI,cAAc,WAAW;OAC3C,IAAI,aAAa,YAAY,GAClC,KAAK,QAAQ,OAAO,IAAI,cAAc,WAAW;OAC5C,IAAI,aAAa,YAAY,GAClC,KAAK,QAAQ,OAAO,IAAI,cAAc,WAAW;CAErD;AACF"}
|