@warlock.js/core 5.34.3 → 5.35.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 +44 -0
- package/esm/cli/cli-commands.manager.mjs +1 -1
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -0
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/build.command.mjs +30 -3
- package/esm/cli/commands/build.command.mjs.map +1 -1
- package/esm/cli/commands/check/check-command.action.mjs +1 -0
- package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
- package/esm/cli/commands/check.command.mjs +5 -0
- package/esm/cli/commands/check.command.mjs.map +1 -1
- package/esm/cli/commands/explain/explain-command.action.mjs +21 -0
- package/esm/cli/commands/explain/explain-command.action.mjs.map +1 -1
- package/esm/cli/commands/explain.command.mjs +14 -4
- package/esm/cli/commands/explain.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +19 -1
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/web.generator.mjs +169 -0
- package/esm/cli/commands/generate/generators/web.generator.mjs.map +1 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs +231 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs.map +1 -0
- package/esm/cli/commands/inspect/inspect-command.action.mjs +4 -3
- package/esm/cli/commands/inspect/inspect-command.action.mjs.map +1 -1
- package/esm/cli/commands/inspect.command.mjs +13 -8
- package/esm/cli/commands/inspect.command.mjs.map +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs.map +1 -1
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +2 -0
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/load-config-files.mjs +4 -0
- package/esm/config/load-config-files.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +11 -1
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts +1 -1
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +16 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +13 -4
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/loader/loader-port-acks.mjs +21 -9
- package/esm/dev-server/loader/loader-port-acks.mjs.map +1 -1
- package/esm/dev-server/loader/register-loader.mjs +31 -12
- package/esm/dev-server/loader/register-loader.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +14 -0
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +4 -1
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/e2e/index.d.mts +2 -2
- package/esm/e2e/page.d.mts +6 -1
- package/esm/e2e/page.d.mts.map +1 -1
- package/esm/e2e/page.mjs +36 -1
- package/esm/e2e/page.mjs.map +1 -1
- package/esm/e2e/types.d.mts +31 -0
- package/esm/e2e/types.d.mts.map +1 -1
- package/esm/e2e/types.mjs.map +1 -1
- package/esm/errors/error-codes.d.mts +48 -0
- package/esm/errors/error-codes.d.mts.map +1 -1
- package/esm/errors/error-codes.mjs +48 -0
- package/esm/errors/error-codes.mjs.map +1 -1
- package/esm/errors/index.d.mts +2 -2
- package/esm/errors/index.mjs +2 -2
- package/esm/generations/add-command.action.mjs +2 -0
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/design-system-payload.mjs +53 -0
- package/esm/generations/design-system-payload.mjs.map +1 -0
- package/esm/generations/features/design-system.feature.mjs +38 -0
- package/esm/generations/features/design-system.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +3 -1
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs +13 -2
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +48 -13
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +9 -5
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/error-mapper.d.mts +1 -1
- package/esm/http/error-mapper.mjs +3 -3
- package/esm/http/error-mapper.mjs.map +1 -1
- package/esm/http/error-response.d.mts +3 -0
- package/esm/http/error-response.d.mts.map +1 -1
- package/esm/http/error-response.mjs +8 -2
- package/esm/http/error-response.mjs.map +1 -1
- package/esm/http/errors/errors.d.mts +8 -0
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +13 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +3 -2
- package/esm/http/index.mjs +3 -2
- package/esm/http/list-endpoint.d.mts +35 -0
- package/esm/http/list-endpoint.d.mts.map +1 -0
- package/esm/http/list-endpoint.mjs +28 -0
- package/esm/http/list-endpoint.mjs.map +1 -0
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +12 -2
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/server.mjs +1 -1
- package/esm/index.d.mts +8 -6
- package/esm/index.mjs +3 -2
- package/esm/openapi/security.mjs +12 -4
- package/esm/openapi/security.mjs.map +1 -1
- package/esm/openapi/types.d.mts +2 -0
- package/esm/openapi/types.d.mts.map +1 -1
- package/esm/production/build-budgets.d.mts +46 -0
- package/esm/production/build-budgets.d.mts.map +1 -0
- package/esm/production/build-budgets.mjs +109 -0
- package/esm/production/build-budgets.mjs.map +1 -0
- package/esm/production/production-builder.mjs +13 -1
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +1 -1
- package/esm/production/resolve-build-config.d.mts.map +1 -1
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/restful/restful.d.mts +1 -1
- package/esm/tests/fakes/storage-fake.d.mts +3 -1
- package/esm/tests/fakes/storage-fake.d.mts.map +1 -1
- package/esm/tests/fakes/storage-fake.mjs +3 -3
- package/esm/tests/fakes/storage-fake.mjs.map +1 -1
- package/esm/tests/fakes/test-fakes.d.mts +3 -1
- package/esm/tests/fakes/test-fakes.d.mts.map +1 -1
- package/esm/tests/fakes/test-fakes.mjs +3 -2
- package/esm/tests/fakes/test-fakes.mjs.map +1 -1
- package/esm/tests/index.d.mts +4 -2
- package/esm/tests/index.mjs +3 -1
- package/esm/tests/load-unit-config.mjs +24 -0
- package/esm/tests/load-unit-config.mjs.map +1 -0
- package/esm/tests/response-contract.d.mts +32 -0
- package/esm/tests/response-contract.d.mts.map +1 -0
- package/esm/tests/response-contract.mjs +116 -0
- package/esm/tests/response-contract.mjs.map +1 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +10 -2
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-lifecycle-state.mjs.map +1 -1
- package/esm/tests/test-locales.mjs +33 -0
- package/esm/tests/test-locales.mjs.map +1 -0
- package/esm/tests/test-runtime-scope.d.mts +15 -0
- package/esm/tests/test-runtime-scope.d.mts.map +1 -0
- package/esm/tests/test-runtime-scope.mjs +80 -0
- package/esm/tests/test-runtime-scope.mjs.map +1 -0
- package/esm/tests/vitest-setup.d.mts +13 -3
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs +146 -18
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/tooling/check/affected-files.d.mts +5 -0
- package/esm/tooling/check/affected-files.d.mts.map +1 -0
- package/esm/tooling/check/affected-files.mjs +56 -0
- package/esm/tooling/check/affected-files.mjs.map +1 -0
- package/esm/tooling/check/changed-files.d.mts +2 -0
- package/esm/tooling/check/changed-files.d.mts.map +1 -1
- package/esm/tooling/check/changed-files.mjs +2 -2
- package/esm/tooling/check/changed-files.mjs.map +1 -1
- package/esm/tooling/check/conventions-step.d.mts +2 -1
- package/esm/tooling/check/conventions-step.d.mts.map +1 -1
- package/esm/tooling/check/conventions-step.mjs +48 -6
- package/esm/tooling/check/conventions-step.mjs.map +1 -1
- package/esm/tooling/check/fixes.d.mts +8 -0
- package/esm/tooling/check/fixes.d.mts.map +1 -0
- package/esm/tooling/check/fixes.mjs +31 -0
- package/esm/tooling/check/fixes.mjs.map +1 -0
- package/esm/tooling/check/index.d.mts +12 -0
- package/esm/tooling/check/index.mjs +3 -2
- package/esm/tooling/check/run-check.d.mts +2 -0
- package/esm/tooling/check/run-check.d.mts.map +1 -1
- package/esm/tooling/check/run-check.mjs +20 -4
- package/esm/tooling/check/run-check.mjs.map +1 -1
- package/esm/tooling/check/run-tests.mjs +1 -1
- package/esm/tooling/check/source-ast.d.mts +1 -0
- package/esm/tooling/check/web-conventions.d.mts +10 -0
- package/esm/tooling/check/web-conventions.d.mts.map +1 -0
- package/esm/tooling/check/web-conventions.mjs +126 -0
- package/esm/tooling/check/web-conventions.mjs.map +1 -0
- package/esm/tooling/index.d.mts +7 -2
- package/esm/tooling/index.mjs +4 -2
- package/esm/tooling/inspect/collect-files.mjs +172 -0
- package/esm/tooling/inspect/collect-files.mjs.map +1 -0
- package/esm/tooling/inspect/collect-pages.mjs +3 -2
- package/esm/tooling/inspect/collect-pages.mjs.map +1 -1
- package/esm/tooling/inspect/collect-routes.mjs +1 -1
- package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
- package/esm/tooling/inspect/explain-file.d.mts +12 -0
- package/esm/tooling/inspect/explain-file.d.mts.map +1 -0
- package/esm/tooling/inspect/explain-file.mjs +45 -0
- package/esm/tooling/inspect/explain-file.mjs.map +1 -0
- package/esm/tooling/inspect/index.d.mts +3 -2
- package/esm/tooling/inspect/index.mjs +2 -1
- package/esm/tooling/inspect/inspect-app.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-app.mjs +9 -3
- package/esm/tooling/inspect/inspect-app.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs +34 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts +17 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.mjs +2 -1
- package/esm/tooling/inspect/inspect-report.types.mjs.map +1 -1
- package/esm/tooling/mcp/mcp-tools.d.mts.map +1 -1
- package/esm/tooling/mcp/mcp-tools.mjs +8 -2
- package/esm/tooling/mcp/mcp-tools.mjs.map +1 -1
- package/esm/tooling/problem.d.mts +4 -0
- package/esm/tooling/problem.d.mts.map +1 -1
- package/esm/tooling/problem.mjs +1 -0
- package/esm/tooling/problem.mjs.map +1 -1
- package/esm/tooling/syntax/ast.d.mts +2 -0
- package/esm/tooling/typecheck/run-typecheck.mjs +5 -1
- package/esm/tooling/typecheck/run-typecheck.mjs.map +1 -1
- package/esm/vite/index.d.mts +3 -1
- package/esm/vite/index.mjs +3 -1
- package/esm/vite/test-setup-reporter.d.mts +16 -0
- package/esm/vite/test-setup-reporter.d.mts.map +1 -0
- package/esm/vite/test-setup-reporter.mjs +55 -0
- package/esm/vite/test-setup-reporter.mjs.map +1 -0
- package/esm/vite/test-workers.d.mts +5 -0
- package/esm/vite/test-workers.d.mts.map +1 -0
- package/esm/vite/test-workers.mjs +38 -0
- package/esm/vite/test-workers.mjs.map +1 -0
- package/esm/warlock-config/index.d.mts +2 -1
- package/esm/warlock-config/types.d.mts +3 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +763 -9
- package/llms.txt +2 -0
- package/package.json +15 -14
- package/schemas/inspect-report.schema.json +47 -1
- package/skills/build-budgets/SKILL.md +51 -0
- package/skills/build-restful/SKILL.md +56 -0
- package/skills/configure-app/SKILL.md +7 -0
- package/skills/create-module/SKILL.md +9 -0
- package/skills/generate-web-ui/SKILL.md +71 -0
- package/skills/index.md +2 -2
- package/skills/run-app/SKILL.md +1 -1
- package/skills/send-mail/SKILL.md +25 -0
- package/skills/send-response/SKILL.md +50 -0
- package/skills/test-browser/SKILL.md +53 -0
- package/skills/test-http/SKILL.md +91 -0
- package/skills/test-service/SKILL.md +161 -3
- package/skills/upgrade-from-5-29/SKILL.md +18 -0
- package/skills/use-localization/SKILL.md +21 -0
- package/skills/warlock-check/SKILL.md +72 -1
- package/skills/warlock-inspect/SKILL.md +45 -2
- package/skills/warlock-mcp/SKILL.md +4 -0
- package/skills/write-cli-command/SKILL.md +18 -0
- package/esm/production/build-app-production.mjs +0 -14
- package/esm/production/build-app-production.mjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FileManifest, FileState, FileType } from "./types.mjs";
|
|
2
2
|
import { FileOperations } from "./file-operations.mjs";
|
|
3
3
|
//#region ../../packages/core/src/dev-server/file-manager.d.ts
|
|
4
|
-
export type CleanupFunction = () => void
|
|
4
|
+
export type CleanupFunction = () => void | Promise<void>;
|
|
5
5
|
/**
|
|
6
6
|
* FileManager — per-file metadata for the dependency graph.
|
|
7
7
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-manager.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/dev-server/file-manager.ts"],"mappings":";;;YASY;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"file-manager.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/dev-server/file-manager.ts"],"mappings":";;;YASY,+BAA+B;;;;;;;;;qBAyB9B;WAgCO;EACT,OAAO,YAAY;EACnB,gBAAgB;EAjClB;EACA;EACA;EACA;;EAGA,cAAY;;;;;EAMZ,sBAAoB;;EAGpB,WAAS;;EAGT,YAAU;EAEV,MAAM;;EAGN,SAAS;;EAGT;EAEA,OAAO;EAEd,YACkB,sBACT,OAAO,YAAY,cACnB,gBAAgB;EAGlB,WAAW,SAAS,kBAAkB;EAMtC;;;;;EAQM,KAAK,eAAe,QAAQ,gBAAgB;;;;;;;;;;;;;;;;EA0B5C,UAAU;IAAmB;MAAyB;;;;;;;YA+CnD,yBAAyB;;;;;;YAgCzB,iBAAiB,cAAc,QAAQ,gBAAgB;YA6B7D;EAuBH,cAAc"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-manager.mjs","names":[],"sources":["../../../../../../../../packages/core/src/dev-server/file-manager.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { getFileAsync, lastModifiedAsync } from \"@warlock.js/fs\";\nimport crypto from \"crypto\";\nimport { DEV_SERVER_EVENTS } from \"./events\";\nimport { type FileOperations } from \"./file-operations\";\nimport { isTypeOnlyFile, parseImports } from \"./parse-imports\";\nimport { Path } from \"../utils/normalized-path\";\nimport type { FileManifest, FileState, FileType } from \"./types\";\n\nexport type CleanupFunction = () => void;\n\n/**\n * Whether `error` is a Node `ENOENT` (path does not exist) error. Used to\n * distinguish a file that has genuinely vanished between the watcher event\n * and its read/stat (a rename or move racing the filesystem) from a real\n * failure such as `EACCES` or a parse error, which must still surface.\n */\nfunction isEnoentError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as NodeJS.ErrnoException).code === \"ENOENT\"\n );\n}\n\n/**\n * FileManager — per-file metadata for the dependency graph.\n *\n * The loader hook transpiles and caches on-demand at import time, so this\n * class never touches disk for anything beyond reading source to hash it\n * and parse its imports. Its job is keeping the metadata that the\n * orchestrator needs to invalidate dependents on change.\n */\nexport class FileManager {\n public relativePath = \"\";\n public lastModified = 0;\n public hash = \"\";\n public source = \"\";\n\n /** Files this file imports (relative paths). */\n public dependencies = new Set<string>();\n\n /**\n * Subset of `dependencies` reached only through type-only imports — a single\n * runtime occurrence makes the whole edge runtime.\n */\n public typeOnlyDependencies = new Set<string>();\n\n /** Original specifier → resolved absolute path. */\n public importMap = new Map<string, string>();\n\n /** Files that import this file (relative paths). */\n public dependents = new Set<string>();\n\n public type: FileType | undefined;\n\n /** Cleanup hooks the loaded module registered for itself. */\n public cleanup: CleanupFunction[] = [];\n\n /** Whether the source has zero runtime imports/exports. */\n public isTypeOnlyFile = false;\n\n public state: FileState = \"idle\";\n\n public constructor(\n public readonly absolutePath: string,\n public files: Map<string, FileManager>,\n public fileOperations: FileOperations,\n ) {}\n\n public addCleanup(cleanup: CleanupFunction | CleanupFunction[]) {\n const next = Array.isArray(cleanup) ? cleanup : [cleanup];\n this.cleanup.push(...next);\n this.cleanup = [...new Set(this.cleanup)];\n }\n\n public resetCleanup() {\n this.cleanup = [];\n }\n\n /**\n * Initial setup. Restores from manifest if the hash still matches,\n * otherwise re-processes from disk.\n */\n public async init(fileManifest?: Partial<FileManifest>): Promise<void> {\n this.relativePath = Path.toRelative(this.absolutePath);\n this.detectFileType();\n\n if (fileManifest) {\n await this.initFromManifest(fileManifest);\n } else {\n await this.process();\n }\n }\n\n /**\n * Read source, hash, parse imports, emit ready. The only file-system\n * touch happens here; transpilation is the loader hook's job.\n *\n * Both the read and the stat happen up front, before anything on this\n * instance is mutated. A path that disappears between the watcher event\n * and either of those calls (`ENOENT`) — typically a rename or move\n * racing the filesystem — is treated as a deletion rather than a thrown\n * error, and never leaves `source`/`hash` pointing at content that then\n * vanished. A real failure (`EACCES`, a permissions error, etc.) still\n * throws.\n *\n * @param force - re-parse even when the hash matches.\n * @returns true if the file was (re)parsed.\n */\n public async process({ force = false }: { force?: boolean } = {}): Promise<boolean> {\n if (!this.relativePath) this.relativePath = Path.toRelative(this.absolutePath);\n if (!this.type) this.detectFileType();\n\n this.state = \"loading\";\n\n let newSource: string;\n let modifiedTime: Date;\n\n try {\n newSource = await getFileAsync(this.absolutePath);\n modifiedTime = await lastModifiedAsync(this.absolutePath);\n } catch (error) {\n if (isEnoentError(error)) {\n this.state = \"deleted\";\n return false;\n }\n\n throw error;\n }\n\n const newHash = crypto.createHash(\"sha256\").update(newSource).digest(\"hex\");\n\n if (!force && newHash === this.hash) {\n this.state = \"ready\";\n return false;\n }\n\n this.source = newSource;\n this.hash = newHash;\n this.lastModified = modifiedTime.getTime();\n\n await this.rebuildImportMetadata();\n this.isTypeOnlyFile = isTypeOnlyFile(this.source);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n\n return true;\n }\n\n /**\n * Parse the current source and refresh `importMap`, `dependencies`, and\n * `typeOnlyDependencies`. A dependency is classified type-only iff every\n * import/export statement that resolves to it is type-only — a single\n * runtime reference makes the whole edge runtime.\n */\n protected async rebuildImportMetadata(): Promise<void> {\n const resolved = await parseImports(this.source, this.absolutePath);\n\n this.importMap = new Map();\n this.dependencies = new Set();\n this.typeOnlyDependencies = new Set();\n\n const runtimePaths = new Set<string>();\n\n for (const [originalPath, { absolutePath, isTypeOnly }] of resolved) {\n this.importMap.set(originalPath, absolutePath);\n\n const relativePath = Path.toRelative(absolutePath);\n this.dependencies.add(relativePath);\n\n if (!isTypeOnly) {\n runtimePaths.add(relativePath);\n }\n }\n\n for (const dependency of this.dependencies) {\n if (!runtimePaths.has(dependency)) {\n this.typeOnlyDependencies.add(dependency);\n }\n }\n }\n\n /**\n * Restore from a cached manifest entry. If the on-disk hash matches the\n * manifest hash, dep-graph metadata is restored from the manifest without\n * re-parsing. Otherwise the file is reprocessed.\n */\n protected async initFromManifest(fileManifest: Partial<FileManifest>): Promise<void> {\n this.type = fileManifest.type;\n\n this.state = \"loading\";\n\n try {\n this.source = await getFileAsync(this.absolutePath);\n } catch {\n this.state = \"deleted\";\n return;\n }\n\n const currentHash = crypto.createHash(\"sha256\").update(this.source).digest(\"hex\");\n\n if (currentHash !== fileManifest.hash) {\n await this.process({ force: true });\n return;\n }\n\n this.hash = fileManifest.hash!;\n this.lastModified = fileManifest.lastModified!;\n this.dependencies = new Set(fileManifest.dependencies || []);\n this.typeOnlyDependencies = new Set(fileManifest.typeOnlyDependencies || []);\n this.dependents = new Set(fileManifest.dependents || []);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n }\n\n protected detectFileType(): void {\n const path = this.relativePath;\n\n if (/(^|\\/)main\\.tsx?$/.test(path)) {\n this.type = \"main\";\n } else if (/\\.model\\.tsx?$/.test(path)) {\n // Before the folder-name checks below: `events/models/x.model.ts` is a model.\n this.type = \"model\";\n } else if (path.startsWith(\"src/config/\")) {\n this.type = \"config\";\n } else if (path.endsWith(\"routes.ts\") || path.endsWith(\"routes.tsx\")) {\n this.type = \"route\";\n } else if (path.includes(\"/events/\")) {\n this.type = \"event\";\n } else if (path.includes(\"controller\")) {\n this.type = \"controller\";\n } else if (path.includes(\"service\")) {\n this.type = \"service\";\n } else {\n this.type = \"other\";\n }\n }\n\n public toManifest(): FileManifest {\n return {\n absolutePath: this.absolutePath,\n relativePath: this.relativePath,\n lastModified: this.lastModified,\n hash: this.hash,\n dependencies: Array.from(this.dependencies),\n typeOnlyDependencies: Array.from(this.typeOnlyDependencies),\n dependents: Array.from(this.dependents),\n type: this.type!,\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-manager.mjs","names":[],"sources":["../../../../../../../../packages/core/src/dev-server/file-manager.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { getFileAsync, lastModifiedAsync } from \"@warlock.js/fs\";\nimport crypto from \"crypto\";\nimport { DEV_SERVER_EVENTS } from \"./events\";\nimport { type FileOperations } from \"./file-operations\";\nimport { isTypeOnlyFile, parseImports } from \"./parse-imports\";\nimport { Path } from \"../utils/normalized-path\";\nimport type { FileManifest, FileState, FileType } from \"./types\";\n\nexport type CleanupFunction = () => void | Promise<void>;\n\n/**\n * Whether `error` is a Node `ENOENT` (path does not exist) error. Used to\n * distinguish a file that has genuinely vanished between the watcher event\n * and its read/stat (a rename or move racing the filesystem) from a real\n * failure such as `EACCES` or a parse error, which must still surface.\n */\nfunction isEnoentError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as NodeJS.ErrnoException).code === \"ENOENT\"\n );\n}\n\n/**\n * FileManager — per-file metadata for the dependency graph.\n *\n * The loader hook transpiles and caches on-demand at import time, so this\n * class never touches disk for anything beyond reading source to hash it\n * and parse its imports. Its job is keeping the metadata that the\n * orchestrator needs to invalidate dependents on change.\n */\nexport class FileManager {\n public relativePath = \"\";\n public lastModified = 0;\n public hash = \"\";\n public source = \"\";\n\n /** Files this file imports (relative paths). */\n public dependencies = new Set<string>();\n\n /**\n * Subset of `dependencies` reached only through type-only imports — a single\n * runtime occurrence makes the whole edge runtime.\n */\n public typeOnlyDependencies = new Set<string>();\n\n /** Original specifier → resolved absolute path. */\n public importMap = new Map<string, string>();\n\n /** Files that import this file (relative paths). */\n public dependents = new Set<string>();\n\n public type: FileType | undefined;\n\n /** Cleanup hooks the loaded module registered for itself. */\n public cleanup: CleanupFunction[] = [];\n\n /** Whether the source has zero runtime imports/exports. */\n public isTypeOnlyFile = false;\n\n public state: FileState = \"idle\";\n\n public constructor(\n public readonly absolutePath: string,\n public files: Map<string, FileManager>,\n public fileOperations: FileOperations,\n ) {}\n\n public addCleanup(cleanup: CleanupFunction | CleanupFunction[]) {\n const next = Array.isArray(cleanup) ? cleanup : [cleanup];\n this.cleanup.push(...next);\n this.cleanup = [...new Set(this.cleanup)];\n }\n\n public resetCleanup() {\n this.cleanup = [];\n }\n\n /**\n * Initial setup. Restores from manifest if the hash still matches,\n * otherwise re-processes from disk.\n */\n public async init(fileManifest?: Partial<FileManifest>): Promise<void> {\n this.relativePath = Path.toRelative(this.absolutePath);\n this.detectFileType();\n\n if (fileManifest) {\n await this.initFromManifest(fileManifest);\n } else {\n await this.process();\n }\n }\n\n /**\n * Read source, hash, parse imports, emit ready. The only file-system\n * touch happens here; transpilation is the loader hook's job.\n *\n * Both the read and the stat happen up front, before anything on this\n * instance is mutated. A path that disappears between the watcher event\n * and either of those calls (`ENOENT`) — typically a rename or move\n * racing the filesystem — is treated as a deletion rather than a thrown\n * error, and never leaves `source`/`hash` pointing at content that then\n * vanished. A real failure (`EACCES`, a permissions error, etc.) still\n * throws.\n *\n * @param force - re-parse even when the hash matches.\n * @returns true if the file was (re)parsed.\n */\n public async process({ force = false }: { force?: boolean } = {}): Promise<boolean> {\n if (!this.relativePath) this.relativePath = Path.toRelative(this.absolutePath);\n if (!this.type) this.detectFileType();\n\n this.state = \"loading\";\n\n let newSource: string;\n let modifiedTime: Date;\n\n try {\n newSource = await getFileAsync(this.absolutePath);\n modifiedTime = await lastModifiedAsync(this.absolutePath);\n } catch (error) {\n if (isEnoentError(error)) {\n this.state = \"deleted\";\n return false;\n }\n\n throw error;\n }\n\n const newHash = crypto.createHash(\"sha256\").update(newSource).digest(\"hex\");\n\n if (!force && newHash === this.hash) {\n this.state = \"ready\";\n return false;\n }\n\n this.source = newSource;\n this.hash = newHash;\n this.lastModified = modifiedTime.getTime();\n\n await this.rebuildImportMetadata();\n this.isTypeOnlyFile = isTypeOnlyFile(this.source);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n\n return true;\n }\n\n /**\n * Parse the current source and refresh `importMap`, `dependencies`, and\n * `typeOnlyDependencies`. A dependency is classified type-only iff every\n * import/export statement that resolves to it is type-only — a single\n * runtime reference makes the whole edge runtime.\n */\n protected async rebuildImportMetadata(): Promise<void> {\n const resolved = await parseImports(this.source, this.absolutePath);\n\n this.importMap = new Map();\n this.dependencies = new Set();\n this.typeOnlyDependencies = new Set();\n\n const runtimePaths = new Set<string>();\n\n for (const [originalPath, { absolutePath, isTypeOnly }] of resolved) {\n this.importMap.set(originalPath, absolutePath);\n\n const relativePath = Path.toRelative(absolutePath);\n this.dependencies.add(relativePath);\n\n if (!isTypeOnly) {\n runtimePaths.add(relativePath);\n }\n }\n\n for (const dependency of this.dependencies) {\n if (!runtimePaths.has(dependency)) {\n this.typeOnlyDependencies.add(dependency);\n }\n }\n }\n\n /**\n * Restore from a cached manifest entry. If the on-disk hash matches the\n * manifest hash, dep-graph metadata is restored from the manifest without\n * re-parsing. Otherwise the file is reprocessed.\n */\n protected async initFromManifest(fileManifest: Partial<FileManifest>): Promise<void> {\n this.type = fileManifest.type;\n\n this.state = \"loading\";\n\n try {\n this.source = await getFileAsync(this.absolutePath);\n } catch {\n this.state = \"deleted\";\n return;\n }\n\n const currentHash = crypto.createHash(\"sha256\").update(this.source).digest(\"hex\");\n\n if (currentHash !== fileManifest.hash) {\n await this.process({ force: true });\n return;\n }\n\n this.hash = fileManifest.hash!;\n this.lastModified = fileManifest.lastModified!;\n this.dependencies = new Set(fileManifest.dependencies || []);\n this.typeOnlyDependencies = new Set(fileManifest.typeOnlyDependencies || []);\n this.dependents = new Set(fileManifest.dependents || []);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n }\n\n protected detectFileType(): void {\n const path = this.relativePath;\n\n if (/(^|\\/)main\\.tsx?$/.test(path)) {\n this.type = \"main\";\n } else if (/\\.model\\.tsx?$/.test(path)) {\n // Before the folder-name checks below: `events/models/x.model.ts` is a model.\n this.type = \"model\";\n } else if (path.startsWith(\"src/config/\")) {\n this.type = \"config\";\n } else if (path.endsWith(\"routes.ts\") || path.endsWith(\"routes.tsx\")) {\n this.type = \"route\";\n } else if (path.includes(\"/events/\")) {\n this.type = \"event\";\n } else if (path.includes(\"controller\")) {\n this.type = \"controller\";\n } else if (path.includes(\"service\")) {\n this.type = \"service\";\n } else {\n this.type = \"other\";\n }\n }\n\n public toManifest(): FileManifest {\n return {\n absolutePath: this.absolutePath,\n relativePath: this.relativePath,\n lastModified: this.lastModified,\n hash: this.hash,\n dependencies: Array.from(this.dependencies),\n typeOnlyDependencies: Array.from(this.typeOnlyDependencies),\n dependents: Array.from(this.dependents),\n type: this.type!,\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -66,6 +66,20 @@ var FilesOrchestrator = class {
|
|
|
66
66
|
this.fileOperations = new FileOperations(this.files, this.dependencyGraph, this.manifest, this.specialFilesCollector);
|
|
67
67
|
this.eventHandler = new FileEventHandler(this.fileOperations, this.manifest, this.dependencyGraph, this.files);
|
|
68
68
|
}
|
|
69
|
+
/** Release resources owned by a test-server boot, including module timers/listeners. */
|
|
70
|
+
async dispose() {
|
|
71
|
+
const results = await Promise.allSettled([this.filesWatcher.close()]);
|
|
72
|
+
results.push(...await Promise.allSettled([this.eventHandler.dispose()]));
|
|
73
|
+
results.push(...await Promise.allSettled([this.healthCheckerManager.shutdown(), this.moduleLoader.dispose(this.files.values())]));
|
|
74
|
+
this.files.clear();
|
|
75
|
+
this.specialFilesCollector.collect(this.files);
|
|
76
|
+
this.loaderAcks?.dispose();
|
|
77
|
+
this.loaderAcks = null;
|
|
78
|
+
this.loaderPort = null;
|
|
79
|
+
this.pendingBumpedPaths = [];
|
|
80
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
81
|
+
if (failures.length) throw new AggregateError(failures.map((result) => result.reason), "Test runtime disposal failed");
|
|
82
|
+
}
|
|
69
83
|
async add(relativePath) {
|
|
70
84
|
return this.fileOperations.addFile(relativePath);
|
|
71
85
|
}
|
|
@@ -90,12 +104,13 @@ var FilesOrchestrator = class {
|
|
|
90
104
|
* Initialise managers and register the ESM loader hook. Must be called
|
|
91
105
|
* before any user `src/` module is dynamically imported.
|
|
92
106
|
*/
|
|
93
|
-
async init() {
|
|
107
|
+
async init(options = {}) {
|
|
94
108
|
if (this.isInitialized) return;
|
|
95
109
|
this.isInitialized = true;
|
|
96
110
|
await init;
|
|
97
111
|
await Promise.all([tsconfigManager.init(), packageJsonManager.init()]);
|
|
98
112
|
await ensureWarlockDirectory();
|
|
113
|
+
if (options.skipLoader) return;
|
|
99
114
|
const devServerConfig = await warlockConfigManager.lazyGet("devServer");
|
|
100
115
|
const transpileInit = buildTranspileInit(tsconfigManager.tsconfig?.compilerOptions, devServerConfig?.transpileCacheDebug === true);
|
|
101
116
|
this.loaderPort = await registerLoader(transpileInit);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files-orchestrator.mjs","names":[],"sources":["../../../../../../../../packages/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 { getDevelopmentAppModuleRegistry } from \"./app-module-registry\";\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 { defaultHealthCheckers } from \"./health-checker/default-health-checkers\";\nimport type {\n BuiltInHealthCheckerOptions,\n FileHealthCheckerContract,\n} from \"./health-checker/file-health-checker.contract\";\nimport { FilesHealthcareManager } from \"./health-checker/files-healthcare.manager\";\nimport { buildTranspileInit } from \"./loader/build-transpile-init.js\";\nimport { trackLoaderPortAcks, type LoaderPortAcks } from \"./loader/loader-port-acks\";\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/** Module dictionaries, relative to `src/`; matches what `isLocaleFile` accepts as JSON. */\nexport const MODULE_LOCALES_JSON_GLOB = \"app/*/utils/locales.json\";\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 /** `sync`/`sync-ack` round trips on {@link loaderPort}; never holds the process open. */\n private loaderAcks: LoaderPortAcks | null = null;\n\n /**\n * Files bumped since the last `flushVersionBumps()`. They are published to\n * the development app-module carrier only once the hook thread acknowledged\n * the bump, so a subscriber that imports the published URL cannot get the\n * previous instance.\n */\n private pendingBumpedPaths: string[] = [];\n\n public isInitialized = false;\n\n public constructor() {\n // Web creates its Vite bridge after this orchestrator exists; create the\n // carrier now so it can subscribe before the first bump happens.\n getDevelopmentAppModuleRegistry();\n\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 this.loaderAcks = trackLoaderPortAcks(this.loaderPort);\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 if (!this.loaderPort) return;\n\n this.loaderPort.postMessage({ type: \"bump\", absolutePath });\n this.pendingBumpedPaths.push(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.loaderAcks) return Promise.resolve();\n\n // Everything posted before this `sync` message is processed by the hook\n // thread before it answers, so exactly this batch is safe to publish on ack.\n const bumpedPaths = this.pendingBumpedPaths;\n this.pendingBumpedPaths = [];\n\n return this.loaderAcks.sync().then(() => {\n const appModules = getDevelopmentAppModuleRegistry();\n\n for (const absolutePath of bumpedPaths) appModules.bump(absolutePath);\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(\n healthCheckers?: FileHealthCheckerContract[] | BuiltInHealthCheckerOptions,\n ): Promise<void> {\n devLogDim(\"Started File Health Checks in the background.\");\n\n const checkers = Array.isArray(healthCheckers)\n ? healthCheckers\n : defaultHealthCheckers(healthCheckers);\n await this.healthCheckerManager.setHealthCheckers(checkers).initialize();\n await this.healthCheckerManager.validateAllFiles();\n }\n\n /**\n * Glob the src directory, returning relative paths: every `.ts`/`.tsx`\n * file plus each module's `utils/locales.json`.\n *\n * The JSON dictionaries must be in the boot set: the module loader\n * registers them like any other locale file, but a code-only glob left them\n * out, so `t()` printed raw keys until the watcher reported an edit.\n */\n public async getAllFilesFromFilesystem(): Promise<string[]> {\n const [codePaths, localePaths] = await Promise.all([\n getFilesFromDirectory(),\n getFilesFromDirectory(undefined, MODULE_LOCALES_JSON_GLOB),\n ]);\n\n return Array.from(\n new Set([...codePaths, ...localePaths].map((absPath) => Path.toRelative(absPath))),\n );\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) =>\n this.eventHandler.handleFileChange(p, settleMs),\n );\n this.filesWatcher.onFileAdd((p, settleMs) => this.eventHandler.handleFileAdd(p, settleMs));\n this.filesWatcher.onFileDelete((p, settleMs) =>\n this.eventHandler.handleFileDelete(p, settleMs),\n );\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
|
+
{"version":3,"file":"files-orchestrator.mjs","names":[],"sources":["../../../../../../../../packages/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 { getDevelopmentAppModuleRegistry } from \"./app-module-registry\";\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 { defaultHealthCheckers } from \"./health-checker/default-health-checkers\";\nimport type {\n BuiltInHealthCheckerOptions,\n FileHealthCheckerContract,\n} from \"./health-checker/file-health-checker.contract\";\nimport { FilesHealthcareManager } from \"./health-checker/files-healthcare.manager\";\nimport { buildTranspileInit } from \"./loader/build-transpile-init.js\";\nimport { trackLoaderPortAcks, type LoaderPortAcks } from \"./loader/loader-port-acks\";\nimport { registerLoader } from \"./loader/register-loader\";\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/** Module dictionaries, relative to `src/`; matches what `isLocaleFile` accepts as JSON. */\nexport const MODULE_LOCALES_JSON_GLOB = \"app/*/utils/locales.json\";\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 /** `sync`/`sync-ack` round trips on {@link loaderPort}; never holds the process open. */\n private loaderAcks: LoaderPortAcks | null = null;\n\n /**\n * Files bumped since the last `flushVersionBumps()`. They are published to\n * the development app-module carrier only once the hook thread acknowledged\n * the bump, so a subscriber that imports the published URL cannot get the\n * previous instance.\n */\n private pendingBumpedPaths: string[] = [];\n\n public isInitialized = false;\n\n public constructor() {\n // Web creates its Vite bridge after this orchestrator exists; create the\n // carrier now so it can subscribe before the first bump happens.\n getDevelopmentAppModuleRegistry();\n\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 /** Release resources owned by a test-server boot, including module timers/listeners. */\n public async dispose(): Promise<void> {\n const results = await Promise.allSettled([this.filesWatcher.close()]);\n results.push(...(await Promise.allSettled([this.eventHandler.dispose()])));\n results.push(\n ...(await Promise.allSettled([\n this.healthCheckerManager.shutdown(),\n this.moduleLoader.dispose(this.files.values()),\n ])),\n );\n this.files.clear();\n this.specialFilesCollector.collect(this.files);\n this.loaderAcks?.dispose();\n this.loaderAcks = null;\n this.loaderPort = null;\n this.pendingBumpedPaths = [];\n const failures = results.filter((result) => result.status === \"rejected\");\n if (failures.length)\n throw new AggregateError(\n failures.map((result) => result.reason),\n \"Test runtime disposal failed\",\n );\n // Node's registered loader hook is process-scoped and cannot be unregistered.\n // Keep isInitialized true so another boot does not stack another hook.\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(options: { skipLoader?: boolean } = {}) {\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 if (options.skipLoader) return;\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 this.loaderAcks = trackLoaderPortAcks(this.loaderPort);\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 if (!this.loaderPort) return;\n\n this.loaderPort.postMessage({ type: \"bump\", absolutePath });\n this.pendingBumpedPaths.push(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.loaderAcks) return Promise.resolve();\n\n // Everything posted before this `sync` message is processed by the hook\n // thread before it answers, so exactly this batch is safe to publish on ack.\n const bumpedPaths = this.pendingBumpedPaths;\n this.pendingBumpedPaths = [];\n\n return this.loaderAcks.sync().then(() => {\n const appModules = getDevelopmentAppModuleRegistry();\n\n for (const absolutePath of bumpedPaths) appModules.bump(absolutePath);\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(\n healthCheckers?: FileHealthCheckerContract[] | BuiltInHealthCheckerOptions,\n ): Promise<void> {\n devLogDim(\"Started File Health Checks in the background.\");\n\n const checkers = Array.isArray(healthCheckers)\n ? healthCheckers\n : defaultHealthCheckers(healthCheckers);\n await this.healthCheckerManager.setHealthCheckers(checkers).initialize();\n await this.healthCheckerManager.validateAllFiles();\n }\n\n /**\n * Glob the src directory, returning relative paths: every `.ts`/`.tsx`\n * file plus each module's `utils/locales.json`.\n *\n * The JSON dictionaries must be in the boot set: the module loader\n * registers them like any other locale file, but a code-only glob left them\n * out, so `t()` printed raw keys until the watcher reported an edit.\n */\n public async getAllFilesFromFilesystem(): Promise<string[]> {\n const [codePaths, localePaths] = await Promise.all([\n getFilesFromDirectory(),\n getFilesFromDirectory(undefined, MODULE_LOCALES_JSON_GLOB),\n ]);\n\n return Array.from(\n new Set([...codePaths, ...localePaths].map((absPath) => Path.toRelative(absPath))),\n );\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) =>\n this.eventHandler.handleFileChange(p, settleMs),\n );\n this.filesWatcher.onFileAdd((p, settleMs) => this.eventHandler.handleFileAdd(p, settleMs));\n this.filesWatcher.onFileDelete((p, settleMs) =>\n this.eventHandler.handleFileDelete(p, settleMs),\n );\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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"}
|
|
@@ -87,9 +87,19 @@ const ENV_FILES = [
|
|
|
87
87
|
];
|
|
88
88
|
var FilesWatcher = class {
|
|
89
89
|
constructor() {
|
|
90
|
+
this.onInterrupt = () => {
|
|
91
|
+
this.close();
|
|
92
|
+
};
|
|
90
93
|
this.id = Random.string();
|
|
91
94
|
this.rawEventTimestamps = /* @__PURE__ */ new Map();
|
|
92
95
|
}
|
|
96
|
+
async close() {
|
|
97
|
+
process.off("SIGINT", this.onInterrupt);
|
|
98
|
+
const watcher = this.watcher;
|
|
99
|
+
this.watcher = void 0;
|
|
100
|
+
await watcher?.close();
|
|
101
|
+
this.rawEventTimestamps.clear();
|
|
102
|
+
}
|
|
93
103
|
/**
|
|
94
104
|
* Watch for files changes
|
|
95
105
|
* @param config Optional watch configuration
|
|
@@ -115,7 +125,8 @@ var FilesWatcher = class {
|
|
|
115
125
|
...config?.exclude || []
|
|
116
126
|
]);
|
|
117
127
|
const timingsEnabled = devServerConfig?.timings === true;
|
|
118
|
-
|
|
128
|
+
await this.close();
|
|
129
|
+
const watcher = this.watcher = chokidar.watch(paths, {
|
|
119
130
|
ignoreInitial: true,
|
|
120
131
|
ignored,
|
|
121
132
|
persistent: true,
|
|
@@ -139,9 +150,7 @@ var FilesWatcher = class {
|
|
|
139
150
|
watcher.on("unlink", (filePath) => this.triggerEvent("delete", filePath));
|
|
140
151
|
watcher.on("addDir", (filePath) => this.triggerEvent("addDir", filePath));
|
|
141
152
|
watcher.on("unlinkDir", (filePath) => this.triggerEvent("unlinkDir", filePath));
|
|
142
|
-
process.on("SIGINT",
|
|
143
|
-
await watcher.close();
|
|
144
|
-
});
|
|
153
|
+
process.on("SIGINT", this.onInterrupt);
|
|
145
154
|
}
|
|
146
155
|
/**
|
|
147
156
|
* Trigger event immediately (no debouncing here)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files-watcher.mjs","names":["fg","fs","nodePath"],"sources":["../../../../../../../../packages/core/src/dev-server/files-watcher.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { Random } from \"@mongez/reinforcements\";\nimport chokidar from \"chokidar\";\nimport fg from \"fast-glob\";\nimport fs from \"node:fs\";\nimport nodePath from \"node:path\";\nimport { rootPath, srcPath } from \"../utils\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { Path } from \"../utils/normalized-path\";\n\ntype FileWatcherEvent = \"change\" | \"delete\" | \"add\" | \"error\" | \"addDir\" | \"unlinkDir\";\n\ntype FileChangeCallback = (filePath: string, settleMs?: number) => void;\ntype FileDeleteCallback = (filePath: string, settleMs?: number) => void;\ntype FileAddCallback = (filePath: string, settleMs?: number) => void;\ntype FileErrorCallback = (filePath: string, error: Error) => void;\ntype FileAddDirCallback = (filePath: string) => void;\ntype FileUnlinkDirCallback = (filePath: string) => void;\ntype OnFileEventCallback =\n | FileChangeCallback\n | FileDeleteCallback\n | FileAddCallback\n | FileErrorCallback\n | FileAddDirCallback\n | FileUnlinkDirCallback;\n\n/**\n * Watch configuration options\n */\nexport type WatchConfig = {\n /**\n * Glob patterns to include\n */\n include?: string[];\n /**\n * Glob patterns to exclude\n */\n exclude?: string[];\n};\n\n/**\n * Default patterns to exclude from watching\n */\nconst DEFAULT_EXCLUDE = [\"**/node_modules/**\", \"**/dist/**\", \"**/.warlock/**\", \"**/.git/**\"];\n\nconst GLOB_CHARS = /[*?{}[\\]]/;\n\n/**\n * Convert a glob (`**`, `*`, `?`, `{a,b}`) to a RegExp over a forward-slash\n * path. chokidar >= 4 has no glob support, so patterns are matched here\n * instead of being handed to it.\n */\nexport function globToRegExp(glob: string): RegExp {\n let source = \"\";\n let braceDepth = 0;\n\n for (let i = 0; i < glob.length; i++) {\n const char = glob[i]!;\n\n if (char === \"*\") {\n if (glob[i + 1] === \"*\") {\n i++;\n if (glob[i + 1] === \"/\") {\n i++;\n source += \"(?:.*/)?\";\n } else {\n source += \".*\";\n }\n } else {\n source += \"[^/]*\";\n }\n } else if (char === \"?\") {\n source += \"[^/]\";\n } else if (char === \"{\") {\n braceDepth++;\n source += \"(?:\";\n } else if (char === \"}\" && braceDepth > 0) {\n braceDepth--;\n source += \")\";\n } else if (char === \",\" && braceDepth > 0) {\n source += \"|\";\n } else {\n source += /[.+^$()|[\\]\\\\]/.test(char) ? `\\\\${char}` : char;\n }\n }\n\n return new RegExp(`^${source}$`);\n}\n\n/**\n * Build a chokidar `ignored` predicate from glob and literal patterns. A\n * pattern matches the absolute path or the project-relative path.\n */\nexport function createIgnoredMatcher(patterns: string[]): (path: string) => boolean {\n const matchers = patterns.map((pattern) => globToRegExp(Path.normalize(pattern)));\n\n return (path: string) => {\n const absolute = Path.normalize(path);\n const relative = Path.toRelative(absolute);\n\n return matchers.some((matcher) => matcher.test(absolute) || matcher.test(relative));\n };\n}\n\n/**\n * Expand glob `include` entries to concrete files (chokidar watches literal\n * paths only); literal entries pass through unchanged.\n */\nfunction expandIncludes(include: string[]): string[] {\n return include.flatMap((entry) =>\n GLOB_CHARS.test(entry)\n ? fg.sync(entry.replace(/\\\\/g, \"/\"), { cwd: rootPath(), absolute: true })\n : [entry],\n );\n}\n\n/**\n * All .env file variants to watch\n */\nconst ENV_FILES = [\n \".env\",\n \".env.local\",\n \".env.development\",\n \".env.development.local\",\n \".env.test\",\n \".env.test.local\",\n \".env.production\",\n \".env.production.local\",\n];\n\nexport class FilesWatcher {\n /**\n * File watcher id\n */\n private id = Random.string();\n\n /**\n * First-seen timestamp per path from chokidar's `raw` event, which fires\n * BEFORE `awaitWriteFinish` stabilises the change. Diffing it against the\n * stabilised `add`/`change` timestamp gives the actual watcher-settle\n * duration for the phase-timing line. Only populated when\n * `devServer.timings` is on — see `watch()` — so a disabled flag costs\n * nothing here beyond the `undefined` checks below.\n */\n private readonly rawEventTimestamps = new Map<string, number>();\n\n /**\n * Watch for files changes\n * @param config Optional watch configuration\n */\n public async watch(config?: WatchConfig) {\n // Get user config from warlock.config.ts\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\n const userWatchConfig = devServerConfig?.watch;\n\n // Build paths to watch:\n // 1. All .env variants that exist\n // 2. warlock.config.ts (project-level settings; restart-required on change)\n // 3. src directory\n // 4. Any additional paths from user config\n // Also any other `.env*` at the root (`.env.shared`, `.env.staging`, …).\n const rootEnvFiles = fs\n .readdirSync(rootPath(), { withFileTypes: true })\n .filter((entry) => entry.isFile() && entry.name.startsWith(\".env\"))\n .map((entry) => entry.name);\n const envPaths = [...new Set([...ENV_FILES, \".env.shared\", ...rootEnvFiles])].map((file) =>\n rootPath(file),\n );\n const basePaths = [...envPaths, rootPath(\"warlock.config.ts\"), srcPath()];\n const additionalPaths = expandIncludes(userWatchConfig?.include || config?.include || []);\n\n const paths = [...basePaths, ...additionalPaths].map((path) => Path.normalize(path));\n\n // Merge default exclude with config exclude\n const ignored = createIgnoredMatcher([\n ...DEFAULT_EXCLUDE,\n ...(userWatchConfig?.exclude || []),\n ...(config?.exclude || []),\n ]);\n\n const timingsEnabled = devServerConfig?.timings === true;\n\n const watcher = chokidar.watch(paths, {\n ignoreInitial: true,\n ignored,\n persistent: true,\n usePolling: false, // Try native first, will fallback if needed\n interval: 100,\n binaryInterval: 300,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n // On Windows, explicitly enable recursive watching\n depth: 99,\n });\n\n if (timingsEnabled) {\n watcher.on(\"raw\", (_event, evPath, details) => {\n // On the native (non-fsevents) handler chokidar uses on Windows/Linux,\n // `evPath` is only the changed entry's path *relative to the watched\n // directory* (see chokidar's `handler.js` `handleEvent`/`emitRaw`),\n // not the absolute path the later `add`/`change` event carries. Only\n // fsevents (macOS) hands back an already-absolute path. Reconstruct\n // the absolute path from `details.watchedPath` so the timestamp is\n // keyed the same way the stabilised event looks it up below —\n // otherwise the lookup always misses and the phase silently reads 0.\n const watchedPath = (details as { watchedPath?: string } | undefined)?.watchedPath;\n const absolutePath = watchedPath ? nodePath.resolve(watchedPath, evPath) : evPath;\n const normalized = Path.normalize(absolutePath);\n if (!this.rawEventTimestamps.has(normalized)) {\n this.rawEventTimestamps.set(normalized, performance.now());\n }\n });\n }\n\n watcher.on(\"add\", (filePath) => this.triggerEvent(\"add\", filePath));\n watcher.on(\"change\", (filePath) => this.triggerEvent(\"change\", filePath));\n watcher.on(\"unlink\", (filePath) => this.triggerEvent(\"delete\", filePath));\n watcher.on(\"addDir\", (filePath) => this.triggerEvent(\"addDir\", filePath));\n watcher.on(\"unlinkDir\", (filePath) => this.triggerEvent(\"unlinkDir\", filePath));\n // watcher.on(\"error\", (error: Error, filePath: string) =>\n // this.triggerEvent(\"error\", filePath, error),\n // );\n\n // Cleanup on process exit\n process.on(\"SIGINT\", async () => {\n await watcher.close();\n });\n }\n\n /**\n * Trigger event immediately (no debouncing here)\n * Debouncing is handled at the orchestrator level for batch processing\n */\n private triggerEvent(event: FileWatcherEvent, filePath: string, error?: Error) {\n const normalized = Path.normalize(filePath);\n const rawEventAt = this.rawEventTimestamps.get(normalized);\n let settleMs: number | undefined;\n\n if (rawEventAt !== undefined) {\n settleMs = performance.now() - rawEventAt;\n this.rawEventTimestamps.delete(normalized);\n }\n\n // `error` and `settleMs` share the same second-argument slot: an error\n // event's subscriber reads it as `Error`, every other event's subscriber\n // reads it as the watcher-settle duration (see the `On*Callback` types\n // above). Passing both positionally (`normalized, error, settleMs`) used\n // to leave `settleMs` in an unread third slot, so every change/add/delete\n // subscriber always received `undefined` in its `settleMs` parameter —\n // the watcher phase's timing was silently discarded here even when the\n // raw-event timestamp above resolved correctly.\n events.trigger(`file-watcher.${this.id}.${event}`, normalized, error ?? settleMs);\n }\n\n /**\n * On file change event\n */\n public onFileChange(callback: FileChangeCallback) {\n return this.on(\"change\", callback);\n }\n\n /**\n * On file delete event\n */\n public onFileDelete(callback: FileDeleteCallback) {\n return this.on(\"delete\", callback);\n }\n\n /**\n * On file add event\n */\n public onFileAdd(callback: FileAddCallback) {\n return this.on(\"add\", callback);\n }\n\n /**\n * On file error event\n */\n public onFileError(callback: FileErrorCallback) {\n return this.on(\"error\", callback);\n }\n\n /**\n * On file add dir event\n */\n public onDirectoryAdd(callback: FileAddDirCallback) {\n return this.on(\"addDir\", callback);\n }\n\n /**\n * On file unlink dir event\n */\n public onDirectoryRemove(callback: FileUnlinkDirCallback) {\n return this.on(\"unlinkDir\", callback);\n }\n\n /**\n * On file event\n */\n public on(event: FileWatcherEvent, callback: OnFileEventCallback) {\n return events.subscribe(`file-watcher.${this.id}.${event}`, callback);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"files-watcher.mjs","names":["fg","fs","nodePath"],"sources":["../../../../../../../../packages/core/src/dev-server/files-watcher.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { Random } from \"@mongez/reinforcements\";\nimport chokidar from \"chokidar\";\nimport fg from \"fast-glob\";\nimport fs from \"node:fs\";\nimport nodePath from \"node:path\";\nimport { rootPath, srcPath } from \"../utils\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { Path } from \"../utils/normalized-path\";\n\ntype FileWatcherEvent = \"change\" | \"delete\" | \"add\" | \"error\" | \"addDir\" | \"unlinkDir\";\n\ntype FileChangeCallback = (filePath: string, settleMs?: number) => void;\ntype FileDeleteCallback = (filePath: string, settleMs?: number) => void;\ntype FileAddCallback = (filePath: string, settleMs?: number) => void;\ntype FileErrorCallback = (filePath: string, error: Error) => void;\ntype FileAddDirCallback = (filePath: string) => void;\ntype FileUnlinkDirCallback = (filePath: string) => void;\ntype OnFileEventCallback =\n | FileChangeCallback\n | FileDeleteCallback\n | FileAddCallback\n | FileErrorCallback\n | FileAddDirCallback\n | FileUnlinkDirCallback;\n\n/**\n * Watch configuration options\n */\nexport type WatchConfig = {\n /**\n * Glob patterns to include\n */\n include?: string[];\n /**\n * Glob patterns to exclude\n */\n exclude?: string[];\n};\n\n/**\n * Default patterns to exclude from watching\n */\nconst DEFAULT_EXCLUDE = [\"**/node_modules/**\", \"**/dist/**\", \"**/.warlock/**\", \"**/.git/**\"];\n\nconst GLOB_CHARS = /[*?{}[\\]]/;\n\n/**\n * Convert a glob (`**`, `*`, `?`, `{a,b}`) to a RegExp over a forward-slash\n * path. chokidar >= 4 has no glob support, so patterns are matched here\n * instead of being handed to it.\n */\nexport function globToRegExp(glob: string): RegExp {\n let source = \"\";\n let braceDepth = 0;\n\n for (let i = 0; i < glob.length; i++) {\n const char = glob[i]!;\n\n if (char === \"*\") {\n if (glob[i + 1] === \"*\") {\n i++;\n if (glob[i + 1] === \"/\") {\n i++;\n source += \"(?:.*/)?\";\n } else {\n source += \".*\";\n }\n } else {\n source += \"[^/]*\";\n }\n } else if (char === \"?\") {\n source += \"[^/]\";\n } else if (char === \"{\") {\n braceDepth++;\n source += \"(?:\";\n } else if (char === \"}\" && braceDepth > 0) {\n braceDepth--;\n source += \")\";\n } else if (char === \",\" && braceDepth > 0) {\n source += \"|\";\n } else {\n source += /[.+^$()|[\\]\\\\]/.test(char) ? `\\\\${char}` : char;\n }\n }\n\n return new RegExp(`^${source}$`);\n}\n\n/**\n * Build a chokidar `ignored` predicate from glob and literal patterns. A\n * pattern matches the absolute path or the project-relative path.\n */\nexport function createIgnoredMatcher(patterns: string[]): (path: string) => boolean {\n const matchers = patterns.map((pattern) => globToRegExp(Path.normalize(pattern)));\n\n return (path: string) => {\n const absolute = Path.normalize(path);\n const relative = Path.toRelative(absolute);\n\n return matchers.some((matcher) => matcher.test(absolute) || matcher.test(relative));\n };\n}\n\n/**\n * Expand glob `include` entries to concrete files (chokidar watches literal\n * paths only); literal entries pass through unchanged.\n */\nfunction expandIncludes(include: string[]): string[] {\n return include.flatMap((entry) =>\n GLOB_CHARS.test(entry)\n ? fg.sync(entry.replace(/\\\\/g, \"/\"), { cwd: rootPath(), absolute: true })\n : [entry],\n );\n}\n\n/**\n * All .env file variants to watch\n */\nconst ENV_FILES = [\n \".env\",\n \".env.local\",\n \".env.development\",\n \".env.development.local\",\n \".env.test\",\n \".env.test.local\",\n \".env.production\",\n \".env.production.local\",\n];\n\nexport class FilesWatcher {\n private watcher?: ReturnType<typeof chokidar.watch>;\n private readonly onInterrupt = () => {\n void this.close();\n };\n\n public async close(): Promise<void> {\n process.off(\"SIGINT\", this.onInterrupt);\n const watcher = this.watcher;\n this.watcher = undefined;\n await watcher?.close();\n this.rawEventTimestamps.clear();\n }\n\n /**\n * File watcher id\n */\n private id = Random.string();\n\n /**\n * First-seen timestamp per path from chokidar's `raw` event, which fires\n * BEFORE `awaitWriteFinish` stabilises the change. Diffing it against the\n * stabilised `add`/`change` timestamp gives the actual watcher-settle\n * duration for the phase-timing line. Only populated when\n * `devServer.timings` is on — see `watch()` — so a disabled flag costs\n * nothing here beyond the `undefined` checks below.\n */\n private readonly rawEventTimestamps = new Map<string, number>();\n\n /**\n * Watch for files changes\n * @param config Optional watch configuration\n */\n public async watch(config?: WatchConfig) {\n // Get user config from warlock.config.ts\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\n const userWatchConfig = devServerConfig?.watch;\n\n // Build paths to watch:\n // 1. All .env variants that exist\n // 2. warlock.config.ts (project-level settings; restart-required on change)\n // 3. src directory\n // 4. Any additional paths from user config\n // Also any other `.env*` at the root (`.env.shared`, `.env.staging`, …).\n const rootEnvFiles = fs\n .readdirSync(rootPath(), { withFileTypes: true })\n .filter((entry) => entry.isFile() && entry.name.startsWith(\".env\"))\n .map((entry) => entry.name);\n const envPaths = [...new Set([...ENV_FILES, \".env.shared\", ...rootEnvFiles])].map((file) =>\n rootPath(file),\n );\n const basePaths = [...envPaths, rootPath(\"warlock.config.ts\"), srcPath()];\n const additionalPaths = expandIncludes(userWatchConfig?.include || config?.include || []);\n\n const paths = [...basePaths, ...additionalPaths].map((path) => Path.normalize(path));\n\n // Merge default exclude with config exclude\n const ignored = createIgnoredMatcher([\n ...DEFAULT_EXCLUDE,\n ...(userWatchConfig?.exclude || []),\n ...(config?.exclude || []),\n ]);\n\n const timingsEnabled = devServerConfig?.timings === true;\n\n await this.close();\n const watcher = (this.watcher = chokidar.watch(paths, {\n ignoreInitial: true,\n ignored,\n persistent: true,\n usePolling: false, // Try native first, will fallback if needed\n interval: 100,\n binaryInterval: 300,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n // On Windows, explicitly enable recursive watching\n depth: 99,\n }));\n\n if (timingsEnabled) {\n watcher.on(\"raw\", (_event, evPath, details) => {\n // On the native (non-fsevents) handler chokidar uses on Windows/Linux,\n // `evPath` is only the changed entry's path *relative to the watched\n // directory* (see chokidar's `handler.js` `handleEvent`/`emitRaw`),\n // not the absolute path the later `add`/`change` event carries. Only\n // fsevents (macOS) hands back an already-absolute path. Reconstruct\n // the absolute path from `details.watchedPath` so the timestamp is\n // keyed the same way the stabilised event looks it up below —\n // otherwise the lookup always misses and the phase silently reads 0.\n const watchedPath = (details as { watchedPath?: string } | undefined)?.watchedPath;\n const absolutePath = watchedPath ? nodePath.resolve(watchedPath, evPath) : evPath;\n const normalized = Path.normalize(absolutePath);\n if (!this.rawEventTimestamps.has(normalized)) {\n this.rawEventTimestamps.set(normalized, performance.now());\n }\n });\n }\n\n watcher.on(\"add\", (filePath) => this.triggerEvent(\"add\", filePath));\n watcher.on(\"change\", (filePath) => this.triggerEvent(\"change\", filePath));\n watcher.on(\"unlink\", (filePath) => this.triggerEvent(\"delete\", filePath));\n watcher.on(\"addDir\", (filePath) => this.triggerEvent(\"addDir\", filePath));\n watcher.on(\"unlinkDir\", (filePath) => this.triggerEvent(\"unlinkDir\", filePath));\n // watcher.on(\"error\", (error: Error, filePath: string) =>\n // this.triggerEvent(\"error\", filePath, error),\n // );\n\n // Cleanup on process exit\n process.on(\"SIGINT\", this.onInterrupt);\n }\n\n /**\n * Trigger event immediately (no debouncing here)\n * Debouncing is handled at the orchestrator level for batch processing\n */\n private triggerEvent(event: FileWatcherEvent, filePath: string, error?: Error) {\n const normalized = Path.normalize(filePath);\n const rawEventAt = this.rawEventTimestamps.get(normalized);\n let settleMs: number | undefined;\n\n if (rawEventAt !== undefined) {\n settleMs = performance.now() - rawEventAt;\n this.rawEventTimestamps.delete(normalized);\n }\n\n // `error` and `settleMs` share the same second-argument slot: an error\n // event's subscriber reads it as `Error`, every other event's subscriber\n // reads it as the watcher-settle duration (see the `On*Callback` types\n // above). Passing both positionally (`normalized, error, settleMs`) used\n // to leave `settleMs` in an unread third slot, so every change/add/delete\n // subscriber always received `undefined` in its `settleMs` parameter —\n // the watcher phase's timing was silently discarded here even when the\n // raw-event timestamp above resolved correctly.\n events.trigger(`file-watcher.${this.id}.${event}`, normalized, error ?? settleMs);\n }\n\n /**\n * On file change event\n */\n public onFileChange(callback: FileChangeCallback) {\n return this.on(\"change\", callback);\n }\n\n /**\n * On file delete event\n */\n public onFileDelete(callback: FileDeleteCallback) {\n return this.on(\"delete\", callback);\n }\n\n /**\n * On file add event\n */\n public onFileAdd(callback: FileAddCallback) {\n return this.on(\"add\", callback);\n }\n\n /**\n * On file error event\n */\n public onFileError(callback: FileErrorCallback) {\n return this.on(\"error\", callback);\n }\n\n /**\n * On file add dir event\n */\n public onDirectoryAdd(callback: FileAddDirCallback) {\n return this.on(\"addDir\", callback);\n }\n\n /**\n * On file unlink dir event\n */\n public onDirectoryRemove(callback: FileUnlinkDirCallback) {\n return this.on(\"unlinkDir\", callback);\n }\n\n /**\n * On file event\n */\n public on(event: FileWatcherEvent, callback: OnFileEventCallback) {\n return events.subscribe(`file-watcher.${this.id}.${event}`, callback);\n }\n}\n"],"mappings":"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"}
|
|
@@ -14,19 +14,31 @@
|
|
|
14
14
|
*/
|
|
15
15
|
function trackLoaderPortAcks(port) {
|
|
16
16
|
const waiters = [];
|
|
17
|
-
|
|
17
|
+
let disposed = false;
|
|
18
|
+
const onMessage = (message) => {
|
|
18
19
|
if (message?.type !== "sync-ack") return;
|
|
19
20
|
waiters.shift()?.();
|
|
20
21
|
if (waiters.length === 0) port.unref();
|
|
21
|
-
}
|
|
22
|
+
};
|
|
23
|
+
port.on("message", onMessage);
|
|
22
24
|
port.unref();
|
|
23
|
-
return {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
port.
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
return {
|
|
26
|
+
dispose() {
|
|
27
|
+
if (disposed) return;
|
|
28
|
+
disposed = true;
|
|
29
|
+
port.off("message", onMessage);
|
|
30
|
+
for (const resolve of waiters.splice(0)) resolve();
|
|
31
|
+
port.close();
|
|
32
|
+
},
|
|
33
|
+
sync() {
|
|
34
|
+
if (disposed) return Promise.resolve();
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
waiters.push(resolve);
|
|
37
|
+
port.ref();
|
|
38
|
+
port.postMessage({ type: "sync" });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader-port-acks.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/dev-server/loader/loader-port-acks.ts"],"sourcesContent":["import type { MessagePort } from \"node:worker_threads\";\n\nexport type LoaderPortAcks = {\n /**\n * Post a `sync` message and resolve once the hook thread answers with\n * `sync-ack`. Every message posted before it has been processed by then.\n */\n sync(): Promise<void>;\n};\n\n/**\n * Wire the main-thread end of the loader-hook channel so it never keeps the\n * process alive on its own.\n *\n * A `message` listener refs a `MessagePort`, and the hook thread lives for the\n * whole process, so a referenced port holds the event loop open after\n * everything else has closed: the HTTP test server's teardown finished, yet\n * Vitest's main process could not exit. The port is unreferenced as\n * soon as the listener is attached and referenced again only while a `sync`\n * waits for its acknowledgement, so a pending flush still completes.\n *\n * Acks arrive in the order the syncs were posted, so the waiters form a queue.\n */\nexport function trackLoaderPortAcks(port: MessagePort): LoaderPortAcks {\n const waiters: Array<() => void> = [];\n\n
|
|
1
|
+
{"version":3,"file":"loader-port-acks.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/dev-server/loader/loader-port-acks.ts"],"sourcesContent":["import type { MessagePort } from \"node:worker_threads\";\n\nexport type LoaderPortAcks = {\n /**\n * Post a `sync` message and resolve once the hook thread answers with\n * `sync-ack`. Every message posted before it has been processed by then.\n */\n sync(): Promise<void>;\n dispose(): void;\n};\n\n/**\n * Wire the main-thread end of the loader-hook channel so it never keeps the\n * process alive on its own.\n *\n * A `message` listener refs a `MessagePort`, and the hook thread lives for the\n * whole process, so a referenced port holds the event loop open after\n * everything else has closed: the HTTP test server's teardown finished, yet\n * Vitest's main process could not exit. The port is unreferenced as\n * soon as the listener is attached and referenced again only while a `sync`\n * waits for its acknowledgement, so a pending flush still completes.\n *\n * Acks arrive in the order the syncs were posted, so the waiters form a queue.\n */\nexport function trackLoaderPortAcks(port: MessagePort): LoaderPortAcks {\n const waiters: Array<() => void> = [];\n\n let disposed = false;\n const onMessage = (message: { type?: string }) => {\n if (message?.type !== \"sync-ack\") return;\n\n waiters.shift()?.();\n\n if (waiters.length === 0) port.unref();\n };\n port.on(\"message\", onMessage);\n\n port.unref();\n\n return {\n dispose() {\n if (disposed) return;\n disposed = true;\n port.off(\"message\", onMessage);\n for (const resolve of waiters.splice(0)) resolve();\n port.close();\n },\n sync() {\n if (disposed) return Promise.resolve();\n return new Promise<void>((resolve) => {\n waiters.push(resolve);\n port.ref();\n port.postMessage({ type: \"sync\" });\n });\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,SAAgB,oBAAoB,MAAmC;CACrE,MAAM,UAA6B,CAAC;CAEpC,IAAI,WAAW;CACf,MAAM,aAAa,YAA+B;EAChD,IAAI,SAAS,SAAS,YAAY;EAElC,QAAQ,MAAM,CAAC,GAAG;EAElB,IAAI,QAAQ,WAAW,GAAG,KAAK,MAAM;CACvC;CACA,KAAK,GAAG,WAAW,SAAS;CAE5B,KAAK,MAAM;CAEX,OAAO;EACL,UAAU;GACR,IAAI,UAAU;GACd,WAAW;GACX,KAAK,IAAI,WAAW,SAAS;GAC7B,KAAK,MAAM,WAAW,QAAQ,OAAO,CAAC,GAAG,QAAQ;GACjD,KAAK,MAAM;EACb;EACA,OAAO;GACL,IAAI,UAAU,OAAO,QAAQ,QAAQ;GACrC,OAAO,IAAI,SAAe,YAAY;IACpC,QAAQ,KAAK,OAAO;IACpB,KAAK,IAAI;IACT,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;GACnC,CAAC;EACH;CACF;AACF"}
|
|
@@ -42,6 +42,7 @@ const resolveExternalsFromCore = {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
+
const LOADER_KEY = Symbol.for("@warlock.js/core:esm-loader");
|
|
45
46
|
/**
|
|
46
47
|
* Bundle, write, and register the ESM loader hook.
|
|
47
48
|
*
|
|
@@ -74,11 +75,20 @@ const resolveExternalsFromCore = {
|
|
|
74
75
|
* // Later, when a file changes:
|
|
75
76
|
* port.postMessage({ type: "bump", absolutePath: "/abs/path/to/user.model.ts" });
|
|
76
77
|
*/
|
|
77
|
-
|
|
78
|
-
const
|
|
78
|
+
function registerLoader(transpile) {
|
|
79
|
+
const host = globalThis;
|
|
80
|
+
if (host[LOADER_KEY]) return host[LOADER_KEY];
|
|
81
|
+
const attempt = createLoader(transpile);
|
|
82
|
+
host[LOADER_KEY] = attempt;
|
|
83
|
+
attempt.catch(() => {
|
|
84
|
+
delete host[LOADER_KEY];
|
|
85
|
+
});
|
|
86
|
+
return attempt;
|
|
87
|
+
}
|
|
88
|
+
async function createLoader(transpile) {
|
|
79
89
|
const selfPath = fileURLToPath(import.meta.url);
|
|
80
90
|
const hookThreadPath = path.join(path.dirname(selfPath), `hook-thread${path.extname(selfPath)}`);
|
|
81
|
-
const
|
|
91
|
+
const output = (await build({
|
|
82
92
|
entryPoints: [hookThreadPath],
|
|
83
93
|
bundle: true,
|
|
84
94
|
format: "esm",
|
|
@@ -87,18 +97,27 @@ async function registerLoader(transpile) {
|
|
|
87
97
|
target: "node20",
|
|
88
98
|
packages: "external",
|
|
89
99
|
plugins: [resolveExternalsFromCore]
|
|
90
|
-
})).outputFiles[0]
|
|
100
|
+
})).outputFiles[0];
|
|
101
|
+
if (!output) throw new Error("The ESM loader bundle produced no output.");
|
|
102
|
+
const bundledCode = output.text;
|
|
91
103
|
const hookBundlePath = path.join(process.cwd(), ".warlock", "loader-hook.mjs");
|
|
92
104
|
await putFileAsync(hookBundlePath, bundledCode);
|
|
93
105
|
const srcRoot = path.join(process.cwd(), "src");
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
const { port1, port2 } = new MessageChannel();
|
|
107
|
+
try {
|
|
108
|
+
register(pathToFileURL(hookBundlePath).href, import.meta.url, {
|
|
109
|
+
data: {
|
|
110
|
+
port: port2,
|
|
111
|
+
srcRoot,
|
|
112
|
+
transpile
|
|
113
|
+
},
|
|
114
|
+
transferList: [port2]
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
port1.close();
|
|
118
|
+
port2.close();
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
102
121
|
return port1;
|
|
103
122
|
}
|
|
104
123
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register-loader.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/dev-server/loader/register-loader.ts"],"sourcesContent":["import { putFileAsync } from \"@warlock.js/fs\";\nimport { build, type Plugin } from \"esbuild\";\nimport { register } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { type MessagePort, MessageChannel } from \"node:worker_threads\";\nimport type { TranspileInit } from \"./load-hook
|
|
1
|
+
{"version":3,"file":"register-loader.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/dev-server/loader/register-loader.ts"],"sourcesContent":["import { putFileAsync } from \"@warlock.js/fs\";\nimport { build, type Plugin } from \"esbuild\";\nimport { register } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { type MessagePort, MessageChannel } from \"node:worker_threads\";\nimport type { TranspileInit } from \"./load-hook\";\n\n/**\n * Rewrite the hook bundle's remaining npm imports to absolute paths resolved\n * from **core's own** location.\n *\n * The bundle is written into the consuming app's `.warlock/` directory, so a\n * bare `import \"esbuild\"` in it resolves starting from the *app*, walking up\n * the app's `node_modules`. `esbuild` and `get-tsconfig` are core's\n * dependencies, not the app's — under npm/yarn's flat hoisting they happen to\n * be reachable anyway, but under pnpm's strict layout they are not, and the\n * dev server dies with `ERR_MODULE_NOT_FOUND` for a package the app never\n * imported. That is a phantom dependency baked into generated code.\n *\n * Bundling them in instead is not an option for `esbuild`: its JS API is a\n * thin wrapper that spawns a platform-specific native binary, so inlining the\n * JavaScript would not remove the need for the package on disk. Resolving to\n * an absolute `file://` URL at generation time does — the generated file then\n * points straight at the copy core itself is using, whatever the installer's\n * layout.\n */\nexport const resolveExternalsFromCore: Plugin = {\n name: \"warlock-resolve-externals-from-core\",\n setup(build) {\n // The filter is deliberately loose — anything not starting `.` or `/` —\n // so the guards below carry the real logic.\n build.onResolve({ filter: /^[^./]/ }, (args) => {\n // The entry point comes through here too, and on Windows its absolute\n // path (\"D:\\\\…\") is not `.` or `/`. Marking it external fails the build.\n if (args.kind === \"entry-point\" || path.isAbsolute(args.path)) {\n return null;\n }\n\n if (args.path.startsWith(\"node:\")) {\n return { external: true };\n }\n\n try {\n return { path: import.meta.resolve(args.path), external: true };\n } catch {\n // Unresolvable from here — leave it bare and let Node try, which is\n // exactly the previous behaviour rather than a hard failure.\n return { external: true };\n }\n });\n },\n};\n\nconst LOADER_KEY = Symbol.for(\"@warlock.js/core:esm-loader\");\ntype LoaderHost = typeof globalThis & { [LOADER_KEY]?: Promise<MessagePort> };\n\n/**\n * Bundle, write, and register the ESM loader hook.\n *\n * **Why bundle?**\n * `module.register()` runs the hook file in a fresh Node.js worker thread that\n * has no tsx hook of its own. The hook source is TypeScript, so we must produce\n * a plain ESM bundle before registering. esbuild inline-bundles all three hook\n * modules (`hook-thread`, `resolve-hook`, `load-hook`, `version-registry`)\n * into a single `.mjs` file written to `.warlock/`. External npm packages\n * (`esbuild`, `node:*`) are kept external — the hook thread can resolve those\n * from `node_modules` normally.\n *\n * **Why a file and not a `data:` URL?**\n * Some Node versions have issues resolving `import.meta.url` inside `data:`\n * modules. A real file under `.warlock/` is simpler and debuggable.\n *\n * **Timing**\n * Called from `FilesOrchestrator.init()` before any user `src/` module is\n * dynamically imported. `module.register()` takes effect for all subsequent\n * `import()` calls, which is exactly the window we need.\n *\n * @param transpile - Transpile-cache config to ship into the hook worker,\n * or `null` to keep the hook in tsx-passthrough mode.\n *\n * @returns The main-thread side of the MessageChannel. Callers post\n * `{ type: \"bump\", absolutePath }` messages on it to invalidate modules.\n *\n * @example\n * const port = await registerLoader(transpileInit);\n * // Later, when a file changes:\n * port.postMessage({ type: \"bump\", absolutePath: \"/abs/path/to/user.model.ts\" });\n */\nexport function registerLoader(transpile: TranspileInit): Promise<MessagePort> {\n const host = globalThis as LoaderHost;\n if (host[LOADER_KEY]) return host[LOADER_KEY];\n const attempt = createLoader(transpile);\n host[LOADER_KEY] = attempt;\n void attempt.catch(() => {\n delete host[LOADER_KEY];\n });\n return attempt;\n}\n\nasync function createLoader(transpile: TranspileInit): Promise<MessagePort> {\n // hook-thread is a sibling module, so it shares THIS file's extension:\n // `.ts` when core runs from source (tsx), `.mjs` when published.\n const selfPath = fileURLToPath(import.meta.url);\n const hookThreadPath = path.join(path.dirname(selfPath), `hook-thread${path.extname(selfPath)}`);\n\n const bundleResult = await build({\n entryPoints: [hookThreadPath],\n bundle: true,\n format: \"esm\",\n write: false,\n platform: \"node\",\n target: \"node20\",\n // npm packages and Node built-ins stay external, but the plugin rewrites\n // each remaining npm specifier to an absolute path resolved from core's\n // own install — see `resolveExternalsFromCore` for why bare imports break\n // under pnpm's strict layout.\n packages: \"external\",\n plugins: [resolveExternalsFromCore],\n });\n\n const output = bundleResult.outputFiles[0];\n if (!output) throw new Error(\"The ESM loader bundle produced no output.\");\n const bundledCode = output.text;\n // Caller (filesOrchestrator.init) guarantees .warlock/ exists before this runs.\n const hookBundlePath = path.join(process.cwd(), \".warlock\", \"loader-hook.mjs\");\n await putFileAsync(hookBundlePath, bundledCode);\n\n const srcRoot = path.join(process.cwd(), \"src\");\n\n // No tsx registration: our hook owns resolution (own-resolver) and the\n // transpile of every `.ts`/`.tsx` (esbuild, in the load hook). The chain\n // is simply [our hook] → [Node default] for non-TS (npm/.js, node:).\n // In this monorepo tsx is still the *launcher* (`tsx start.ts`) so its\n // loader is present anyway, but it is never consulted for TypeScript —\n // our hook short-circuits first. A released `node bin/warlock.js` has no\n // tsx at all and relies entirely on this hook.\n\n const { port1, port2 } = new MessageChannel();\n try {\n register(pathToFileURL(hookBundlePath).href, import.meta.url, {\n data: { port: port2, srcRoot, transpile },\n transferList: [port2],\n });\n } catch (error) {\n port1.close();\n port2.close();\n throw error;\n }\n\n return port1;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,2BAAmC;CAC9C,MAAM;CACN,MAAM,OAAO;EAGX,MAAM,UAAU,EAAE,QAAQ,SAAS,IAAI,SAAS;GAG9C,IAAI,KAAK,SAAS,iBAAiB,KAAK,WAAW,KAAK,IAAI,GAC1D,OAAO;GAGT,IAAI,KAAK,KAAK,WAAW,OAAO,GAC9B,OAAO,EAAE,UAAU,KAAK;GAG1B,IAAI;IACF,OAAO;KAAE,MAAM,YAAY,QAAQ,KAAK,IAAI;KAAG,UAAU;IAAK;GAChE,QAAQ;IAGN,OAAO,EAAE,UAAU,KAAK;GAC1B;EACF,CAAC;CACH;AACF;AAEA,MAAM,aAAa,OAAO,IAAI,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmC3D,SAAgB,eAAe,WAAgD;CAC7E,MAAM,OAAO;CACb,IAAI,KAAK,aAAa,OAAO,KAAK;CAClC,MAAM,UAAU,aAAa,SAAS;CACtC,KAAK,cAAc;CACnB,AAAK,QAAQ,YAAY;EACvB,OAAO,KAAK;CACd,CAAC;CACD,OAAO;AACT;AAEA,eAAe,aAAa,WAAgD;CAG1E,MAAM,WAAW,cAAc,YAAY,GAAG;CAC9C,MAAM,iBAAiB,KAAK,KAAK,KAAK,QAAQ,QAAQ,GAAG,cAAc,KAAK,QAAQ,QAAQ,GAAG;CAiB/F,MAAM,UAAS,MAfY,MAAM;EAC/B,aAAa,CAAC,cAAc;EAC5B,QAAQ;EACR,QAAQ;EACR,OAAO;EACP,UAAU;EACV,QAAQ;EAKR,UAAU;EACV,SAAS,CAAC,wBAAwB;CACpC,CAAC,EAE0B,CAAC,YAAY;CACxC,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,2CAA2C;CACxE,MAAM,cAAc,OAAO;CAE3B,MAAM,iBAAiB,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY,iBAAiB;CAC7E,MAAM,aAAa,gBAAgB,WAAW;CAE9C,MAAM,UAAU,KAAK,KAAK,QAAQ,IAAI,GAAG,KAAK;CAU9C,MAAM,EAAE,OAAO,UAAU,IAAI,eAAe;CAC5C,IAAI;EACF,SAAS,cAAc,cAAc,CAAC,CAAC,MAAM,YAAY,KAAK;GAC5D,MAAM;IAAE,MAAM;IAAO;IAAS;GAAU;GACxC,cAAc,CAAC,KAAK;EACtB,CAAC;CACH,SAAS,OAAO;EACd,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,MAAM;CACR;CAEA,OAAO;AACT"}
|
|
@@ -132,6 +132,20 @@ var ModuleLoader = class {
|
|
|
132
132
|
} catch {}
|
|
133
133
|
file.resetCleanup();
|
|
134
134
|
}
|
|
135
|
+
/** Await module-owned handles during test runtime shutdown, attempting every cleanup. */
|
|
136
|
+
async dispose(files) {
|
|
137
|
+
const failures = [];
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
for (const hook of file.cleanup) try {
|
|
140
|
+
if (typeof hook === "function") await hook();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
failures.push(error);
|
|
143
|
+
}
|
|
144
|
+
file.resetCleanup();
|
|
145
|
+
this.cleanupDeletedModule(file);
|
|
146
|
+
}
|
|
147
|
+
if (failures.length) throw new AggregateError(failures, "Module cleanup failed");
|
|
148
|
+
}
|
|
135
149
|
/**
|
|
136
150
|
* Clean up after a file whose source was deleted from disk.
|
|
137
151
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-loader.mjs","names":[],"sources":["../../../../../../../../packages/core/src/dev-server/module-loader.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { parseSourceFile } from \"../tooling/syntax/ast\";\nimport { importAppModule } from \"../loader/app-module-importer\";\nimport { registerGroupedTranslationKeys, registerLocaleDictionary } from \"../localization\";\nimport { router } from \"../router/router\";\nimport { devLogError, formatModuleNotFoundError } from \"./dev-logger\";\nimport { extractTranslationKeys } from \"./extract-translation-keys\";\nimport type { CleanupFunction, FileManager } from \"./file-manager\";\nimport { warnForLegacyLocales } from \"./legacy-locales-warning\";\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 /** Stable Warlock error code; `warlock explain WL2001` prints the fix. */\n public readonly code = \"WL2001\";\n\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 if (type === \"locale\" && /\\/utils\\/locales\\.(ts|tsx)$/.test(file.relativePath)) {\n warnForLegacyLocales(\n this.specialFilesCollector.getFilesByType(\"locale\").map((locale) => locale.relativePath),\n );\n }\n\n if (type === \"locale\" && file.relativePath.endsWith(\".json\")) {\n const cleanup = registerLocaleDictionary({\n sourceFile: file.relativePath,\n source: file.source,\n defaultNamespace: file.relativePath.split(\"/\")[2]!,\n });\n file.addCleanup(cleanup);\n return undefined;\n }\n\n if (type === \"locale\") {\n registerGroupedTranslationKeys(\n file.relativePath,\n extractTranslationKeys(parseSourceFile(file.relativePath, file.source)),\n );\n }\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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"}
|
|
1
|
+
{"version":3,"file":"module-loader.mjs","names":[],"sources":["../../../../../../../../packages/core/src/dev-server/module-loader.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { parseSourceFile } from \"../tooling/syntax/ast\";\nimport { importAppModule } from \"../loader/app-module-importer\";\nimport { registerGroupedTranslationKeys, registerLocaleDictionary } from \"../localization\";\nimport { router } from \"../router/router\";\nimport { devLogError, formatModuleNotFoundError } from \"./dev-logger\";\nimport { extractTranslationKeys } from \"./extract-translation-keys\";\nimport type { CleanupFunction, FileManager } from \"./file-manager\";\nimport { warnForLegacyLocales } from \"./legacy-locales-warning\";\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 /** Stable Warlock error code; `warlock explain WL2001` prints the fix. */\n public readonly code = \"WL2001\";\n\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 if (type === \"locale\" && /\\/utils\\/locales\\.(ts|tsx)$/.test(file.relativePath)) {\n warnForLegacyLocales(\n this.specialFilesCollector.getFilesByType(\"locale\").map((locale) => locale.relativePath),\n );\n }\n\n if (type === \"locale\" && file.relativePath.endsWith(\".json\")) {\n const cleanup = registerLocaleDictionary({\n sourceFile: file.relativePath,\n source: file.source,\n defaultNamespace: file.relativePath.split(\"/\")[2]!,\n });\n file.addCleanup(cleanup);\n return undefined;\n }\n\n if (type === \"locale\") {\n registerGroupedTranslationKeys(\n file.relativePath,\n extractTranslationKeys(parseSourceFile(file.relativePath, file.source)),\n );\n }\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 /** Await module-owned handles during test runtime shutdown, attempting every cleanup. */\n public async dispose(files: Iterable<FileManager>): Promise<void> {\n const failures: unknown[] = [];\n for (const file of files) {\n for (const hook of file.cleanup) {\n try {\n if (typeof hook === \"function\") await hook();\n } catch (error) {\n failures.push(error);\n }\n }\n file.resetCleanup();\n this.cleanupDeletedModule(file);\n }\n if (failures.length) throw new AggregateError(failures, \"Module cleanup failed\");\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,7 +1,10 @@
|
|
|
1
1
|
//#region ../../packages/core/src/dev-server/special-file-patterns.ts
|
|
2
2
|
const normalize = (path) => path.replace(/\\/g, "/");
|
|
3
3
|
/** `src/config/**` — any depth, the config key is the raw path */
|
|
4
|
-
const isConfigFile = (path) =>
|
|
4
|
+
const isConfigFile = (path) => {
|
|
5
|
+
const normalized = normalize(path);
|
|
6
|
+
return /^src\/config\/.*\.(ts|tsx)$/.test(normalized) && !/\.(spec|test)\.[^/]+$/.test(normalized) && !/\/(?:__tests__|__mocks__|__fixtures__|tests?|fixtures)\//.test(normalized);
|
|
7
|
+
};
|
|
5
8
|
/** `src/app/main.ts` or `src/app/<module>/main.ts` */
|
|
6
9
|
const isMainFile = (path) => /^src\/app\/([^/]+\/)?main\.(ts|tsx)$/.test(normalize(path));
|
|
7
10
|
/** `src/app/<module>/routes.ts` — one level deep */
|