@warlock.js/core 5.20.0 → 5.22.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 +1130 -1086
- package/esm/benchmark/benchmark.mjs +34 -25
- package/esm/benchmark/benchmark.mjs.map +1 -1
- package/esm/benchmark/profiler.d.mts.map +1 -1
- package/esm/benchmark/profiler.mjs +8 -3
- package/esm/benchmark/profiler.mjs.map +1 -1
- package/esm/bootstrap.d.mts.map +1 -1
- package/esm/bootstrap.mjs +3 -0
- package/esm/bootstrap.mjs.map +1 -1
- package/esm/cache/database-cache-driver.d.mts +2 -2
- package/esm/cache/database-cache-driver.d.mts.map +1 -1
- package/esm/cache/database-cache-driver.mjs +3 -2
- package/esm/cache/database-cache-driver.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs +16 -14
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -1
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +3 -2
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +20 -0
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs +4 -2
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/controller.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/controller.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs +22 -6
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/model.generator.mjs +22 -7
- package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +8 -6
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/templates/stubs.mjs +7 -7
- package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/writer.mjs +25 -1
- package/esm/cli/commands/generate/utils/writer.mjs.map +1 -1
- package/esm/cli/commands/routes/routes-command.action.mjs +5 -1
- package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
- package/esm/cli/commands/routes/with-console-on-stderr.mjs +24 -0
- package/esm/cli/commands/routes/with-console-on-stderr.mjs.map +1 -0
- package/esm/cli/commands/routes.command.mjs +3 -4
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/commands/start-production.command.mjs +5 -5
- package/esm/cli/commands/start-production.command.mjs.map +1 -1
- package/esm/cli/commands/storage-put.action.mjs +2 -1
- package/esm/cli/commands/storage-put.action.mjs.map +1 -1
- package/esm/cli/commands/update.command.mjs +1 -0
- package/esm/cli/commands/update.command.mjs.map +1 -1
- package/esm/cli/parse-cli-args.mjs +13 -1
- package/esm/cli/parse-cli-args.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs +4 -2
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/commands/index.mjs +3 -0
- package/esm/config/config-key-from-path.mjs +20 -0
- package/esm/config/config-key-from-path.mjs.map +1 -0
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +2 -3
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/connectors/connectors-manager.d.mts +4 -0
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs +30 -2
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/socket-connector.d.mts +29 -0
- package/esm/connectors/socket-connector.d.mts.map +1 -1
- package/esm/connectors/socket-connector.mjs +73 -9
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/connectors/types.d.mts +1 -1
- package/esm/connectors/types.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.d.mts +47 -24
- package/esm/database/models/database-log/database-log.d.mts.map +1 -1
- package/esm/database/models/database-log/database-log.mjs +36 -4
- package/esm/database/models/database-log/database-log.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs +10 -2
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +11 -4
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +26 -6
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs +2 -2
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +3 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +64 -5
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs +31 -9
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs.map +1 -1
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs +11 -7
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs +1 -1
- package/esm/dev-server/loader/register-loader.mjs +1 -1
- package/esm/dev-server/manifest-manager.d.mts.map +1 -1
- package/esm/dev-server/manifest-manager.mjs +12 -6
- package/esm/dev-server/manifest-manager.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +9 -0
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/dev-server/register-dev-shortcuts.mjs +2 -1
- package/esm/dev-server/register-dev-shortcuts.mjs.map +1 -1
- package/esm/dev-server/shortcuts.mjs +2 -2
- package/esm/dev-server/shortcuts.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +18 -0
- package/esm/dev-server/special-file-patterns.mjs.map +1 -0
- package/esm/dev-server/special-files-collector.d.mts.map +1 -1
- package/esm/dev-server/special-files-collector.mjs +2 -5
- package/esm/dev-server/special-files-collector.mjs.map +1 -1
- package/esm/dev-server/start-development-server.mjs +1 -2
- package/esm/dev-server/start-development-server.mjs.map +1 -1
- package/esm/dev-server/supervisor.mjs +23 -3
- package/esm/dev-server/supervisor.mjs.map +1 -1
- package/esm/dev-server/tsconfig-manager.mjs +22 -12
- package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +10 -9
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/encryption/encrypt.d.mts.map +1 -1
- package/esm/encryption/encrypt.mjs +16 -5
- package/esm/encryption/encrypt.mjs.map +1 -1
- package/esm/encryption/hash.d.mts.map +1 -1
- package/esm/encryption/hash.mjs +2 -0
- package/esm/encryption/hash.mjs.map +1 -1
- package/esm/encryption/password.d.mts.map +1 -1
- package/esm/encryption/password.mjs +3 -1
- package/esm/encryption/password.mjs.map +1 -1
- package/esm/generations/features/index.mjs +1 -1
- package/esm/generations/features/mysql.feature.mjs +9 -2
- package/esm/generations/features/mysql.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +2 -1
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs +4 -2
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/http/csrf-origin-policy.d.mts.map +1 -1
- package/esm/http/csrf-origin-policy.mjs +5 -2
- package/esm/http/csrf-origin-policy.mjs.map +1 -1
- package/esm/http/flush-pending-cookies.mjs +52 -0
- package/esm/http/flush-pending-cookies.mjs.map +1 -0
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs +4 -2
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +14 -3
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
- package/esm/http/middleware/utils/cidr-match.mjs +11 -2
- package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/plugins.d.mts.map +1 -1
- package/esm/http/plugins.mjs +10 -1
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/request.d.mts +12 -1
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +35 -7
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +16 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +75 -37
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/types.d.mts +24 -0
- package/esm/http/types.d.mts.map +1 -1
- package/esm/http/uploads/uploaded-file.controller.mjs +1 -1
- package/esm/http/uploads-config.mjs +2 -1
- package/esm/http/uploads-config.mjs.map +1 -1
- package/esm/http/uploads-types.d.mts +3 -3
- package/esm/image/image.d.mts +1 -1
- package/esm/image/image.d.mts.map +1 -1
- package/esm/image/image.mjs +25 -15
- package/esm/image/image.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/index.mjs +5 -3
- package/esm/logger/logger.mjs +6 -1
- package/esm/logger/logger.mjs.map +1 -1
- package/esm/mail/config.d.mts.map +1 -1
- package/esm/mail/config.mjs +31 -21
- package/esm/mail/config.mjs.map +1 -1
- package/esm/mail/react-mail.mjs +6 -3
- package/esm/mail/react-mail.mjs.map +1 -1
- package/esm/mail/send-mail.d.mts.map +1 -1
- package/esm/mail/send-mail.mjs +40 -19
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/production/build-contributions.mjs +3 -0
- package/esm/production/build-contributions.mjs.map +1 -1
- package/esm/production/esbuild-plugins.mjs +3 -2
- package/esm/production/esbuild-plugins.mjs.map +1 -1
- package/esm/production/production-builder.mjs +32 -36
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs +12 -5
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.d.mts +3 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.d.mts.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.mjs +17 -5
- package/esm/repositories/adapters/cascade/cascade-adapter.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +3 -3
- package/esm/repositories/adapters/cascade/cascade-query-builder.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +88 -61
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/index.d.mts +1 -1
- package/esm/repositories/adapters/cascade/index.mjs +1 -1
- package/esm/repositories/contracts/query-builder.contract.d.mts +3 -3
- package/esm/repositories/contracts/types.d.mts +5 -0
- package/esm/repositories/contracts/types.d.mts.map +1 -1
- package/esm/repositories/index.d.mts +1 -1
- package/esm/repositories/index.mjs +1 -1
- package/esm/repositories/repository.manager.d.mts +65 -3
- package/esm/repositories/repository.manager.d.mts.map +1 -1
- package/esm/repositories/repository.manager.mjs +146 -57
- package/esm/repositories/repository.manager.mjs.map +1 -1
- package/esm/resource/resource-field-builder.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.mjs +1 -0
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/resource.d.mts +10 -3
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs +19 -5
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/restful/restful.d.mts +12 -5
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/restful/restful.mjs +47 -16
- package/esm/restful/restful.mjs.map +1 -1
- package/esm/router/build-route-rate-limit.mjs +22 -0
- package/esm/router/build-route-rate-limit.mjs.map +1 -0
- package/esm/router/dev-dispatch.mjs +60 -0
- package/esm/router/dev-dispatch.mjs.map +1 -0
- package/esm/router/route-builder.mjs +4 -1
- package/esm/router/route-builder.mjs.map +1 -1
- package/esm/router/route-registry.d.mts.map +1 -1
- package/esm/router/route-registry.mjs +2 -2
- package/esm/router/route-registry.mjs.map +1 -1
- package/esm/router/router.d.mts +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +57 -39
- package/esm/router/router.mjs.map +1 -1
- package/esm/socket/types.d.mts +9 -2
- package/esm/socket/types.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts +17 -0
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs +53 -17
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/storage/drivers/local-driver.d.mts +7 -5
- package/esm/storage/drivers/local-driver.d.mts.map +1 -1
- package/esm/storage/drivers/local-driver.mjs +36 -31
- package/esm/storage/drivers/local-driver.mjs.map +1 -1
- package/esm/storage/index.d.mts +2 -1
- package/esm/storage/index.mjs +2 -1
- package/esm/storage/scoped-storage.d.mts +13 -1
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +18 -8
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts +0 -5
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +7 -16
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/types.d.mts +22 -4
- package/esm/storage/types.d.mts.map +1 -1
- package/esm/storage/utils/classify-ip-address.d.mts +13 -0
- package/esm/storage/utils/classify-ip-address.d.mts.map +1 -0
- package/esm/storage/utils/classify-ip-address.mjs +98 -0
- package/esm/storage/utils/classify-ip-address.mjs.map +1 -0
- package/esm/storage/utils/safe-fetch.d.mts +3 -12
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +3 -48
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/updater/update-warlock-packages.mjs +11 -1
- package/esm/updater/update-warlock-packages.mjs.map +1 -1
- package/esm/use-cases/use-case-broadcast.mjs +7 -1
- package/esm/use-cases/use-case-broadcast.mjs.map +1 -1
- package/esm/use-cases/use-cases-registry.mjs +21 -2
- package/esm/use-cases/use-cases-registry.mjs.map +1 -1
- package/esm/utils/database-log.mjs +0 -3
- package/esm/utils/database-log.mjs.map +1 -1
- package/esm/utils/queue.d.mts +6 -4
- package/esm/utils/queue.d.mts.map +1 -1
- package/esm/utils/queue.mjs +20 -9
- package/esm/utils/queue.mjs.map +1 -1
- package/esm/validation/database/exists-except-current-id.mjs +6 -2
- package/esm/validation/database/exists-except-current-id.mjs.map +1 -1
- package/esm/validation/database/unique-except-current-id.mjs +6 -2
- package/esm/validation/database/unique-except-current-id.mjs.map +1 -1
- package/esm/validation/init.mjs +6 -2
- package/esm/validation/init.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +3 -2
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +14 -1
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.mjs +3 -1
- package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
- package/esm/warlock-config/warn-deprecated-config.mjs +25 -0
- package/esm/warlock-config/warn-deprecated-config.mjs.map +1 -0
- package/package.json +16 -16
- package/esm/_virtual/_rolldown/runtime.mjs +0 -7
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { TranspileCache, cacheKey } from "./transpile-cache.mjs";
|
|
2
2
|
import { sourceSlug } from "./source-slug.mjs";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
-
import { transformSync } from "esbuild";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { transformSync } from "esbuild";
|
|
6
6
|
|
|
7
7
|
//#region ../core/src/dev-server/loader/load-hook.ts
|
|
8
8
|
const VERSION_QUERY = /\?v=\d+$/;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { register } from "node:module";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { putFileAsync } from "@warlock.js/fs";
|
|
4
|
-
import { build } from "esbuild";
|
|
5
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { build } from "esbuild";
|
|
6
6
|
import { MessageChannel } from "node:worker_threads";
|
|
7
7
|
|
|
8
8
|
//#region ../core/src/dev-server/loader/register-loader.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-manager.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/manifest-manager.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"manifest-manager.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/manifest-manager.ts"],"mappings":";;;;cAqBa,eAAA;EAAA,iBAiByB,KAAA;EAAA;;;EAAA,QAb5B,QAAA;EAa+C;;;cAAnB,KAAA,EAAO,GAAA,SAAY,WAAA;EA6B1C;;;;EAvBA,IAAA,IAAQ,OAAA;EA0Cd;;;EAnBM,IAAA,IAAQ,OAAA;EA0B0B;;;EAdxC,OAAA,CAAQ,QAAA,WAAmB,YAAA;EA4B3B;;;EArBA,OAAA,CAAQ,QAAA;EAmCR;;;EA5BA,OAAA,CAAQ,QAAA,UAAkB,YAAA,EAAc,YAAA;;;;EAOxC,UAAA,CAAW,QAAA;EA2CX;;AAAK;EApCL,eAAA;;;;EAOA,WAAA,IAAe,MAAA,SAAe,YAAA;;;;EAO9B,WAAA;;;;;;;;;;;;UAYC,0BAAA;;;;EAUD,KAAA;AAAA"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { MANIFEST_PATH } from "./flags.mjs";
|
|
2
|
-
import { fileExistsAsync, getJsonFileAsync
|
|
2
|
+
import { atomicWriteAsync, fileExistsAsync, getJsonFileAsync } from "@warlock.js/fs";
|
|
3
3
|
|
|
4
4
|
//#region ../core/src/dev-server/manifest-manager.ts
|
|
5
|
+
const MANIFEST_VERSION = "1.0.0";
|
|
5
6
|
var ManifestManager = class {
|
|
6
7
|
/**
|
|
7
8
|
* Constructor
|
|
@@ -9,7 +10,7 @@ var ManifestManager = class {
|
|
|
9
10
|
constructor(files) {
|
|
10
11
|
this.files = files;
|
|
11
12
|
this.manifest = {
|
|
12
|
-
version:
|
|
13
|
+
version: MANIFEST_VERSION,
|
|
13
14
|
lastBuildTime: Date.now(),
|
|
14
15
|
stats: {
|
|
15
16
|
totalFiles: 0,
|
|
@@ -23,10 +24,15 @@ var ManifestManager = class {
|
|
|
23
24
|
* @returns true if manifest exists, false otherwise
|
|
24
25
|
*/
|
|
25
26
|
async init() {
|
|
26
|
-
if (await fileExistsAsync(MANIFEST_PATH)) {
|
|
27
|
-
|
|
27
|
+
if (await fileExistsAsync(MANIFEST_PATH)) try {
|
|
28
|
+
const loaded = await getJsonFileAsync(MANIFEST_PATH);
|
|
29
|
+
if (loaded?.version !== MANIFEST_VERSION || typeof loaded.files !== "object") return false;
|
|
30
|
+
this.manifest = loaded;
|
|
28
31
|
return true;
|
|
29
|
-
}
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
30
36
|
}
|
|
31
37
|
/**
|
|
32
38
|
* Save manifest to disk
|
|
@@ -35,7 +41,7 @@ var ManifestManager = class {
|
|
|
35
41
|
this.manifest.stats.totalFiles = Object.keys(this.manifest.files).length;
|
|
36
42
|
this.manifest.stats.totalDependencies = this.calculateTotalDependencies();
|
|
37
43
|
this.manifest.lastBuildTime = Date.now();
|
|
38
|
-
await
|
|
44
|
+
await atomicWriteAsync(MANIFEST_PATH, JSON.stringify(this.manifest, null, 2));
|
|
39
45
|
}
|
|
40
46
|
/**
|
|
41
47
|
* Get file manifest data
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-manager.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/manifest-manager.ts"],"sourcesContent":["import { fileExistsAsync, getJsonFileAsync
|
|
1
|
+
{"version":3,"file":"manifest-manager.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/manifest-manager.ts"],"sourcesContent":["import { atomicWriteAsync, fileExistsAsync, getJsonFileAsync } from \"@warlock.js/fs\";\r\nimport { FileManager } from \"./file-manager\";\r\nimport type { FileManifest } from \"./types\";\r\nimport { MANIFEST_PATH } from \"./flags\";\r\n\r\nconst MANIFEST_VERSION = \"1.0.0\";\r\n\r\n/**\r\n * Manifest structure with metadata and files\r\n */\r\nexport type Manifest = {\r\n version: string;\r\n lastBuildTime: number;\r\n projectHash?: string;\r\n stats: {\r\n totalFiles: number;\r\n totalDependencies: number;\r\n };\r\n files: Record<string, FileManifest>;\r\n};\r\n\r\nexport class ManifestManager {\r\n /**\r\n * Manifest data with metadata\r\n */\r\n private manifest: Manifest = {\r\n version: MANIFEST_VERSION,\r\n lastBuildTime: Date.now(),\r\n stats: {\r\n totalFiles: 0,\r\n totalDependencies: 0,\r\n },\r\n files: {},\r\n };\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor(private readonly files: Map<string, FileManager>) {}\r\n\r\n /**\r\n * Initialize manifest manager\r\n * @returns true if manifest exists, false otherwise\r\n */\r\n public async init(): Promise<boolean> {\r\n if (await fileExistsAsync(MANIFEST_PATH)) {\r\n try {\r\n const loaded = await getJsonFileAsync<Manifest>(MANIFEST_PATH);\r\n\r\n // A torn write or a format change across upgrades is treated as absent.\r\n if (loaded?.version !== MANIFEST_VERSION || typeof loaded.files !== \"object\") {\r\n return false;\r\n }\r\n\r\n this.manifest = loaded;\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n return false;\r\n }\r\n\r\n /**\r\n * Save manifest to disk\r\n */\r\n public async save(): Promise<void> {\r\n // Update stats before saving\r\n this.manifest.stats.totalFiles = Object.keys(this.manifest.files).length;\r\n this.manifest.stats.totalDependencies = this.calculateTotalDependencies();\r\n this.manifest.lastBuildTime = Date.now();\r\n\r\n await atomicWriteAsync(MANIFEST_PATH, JSON.stringify(this.manifest, null, 2));\r\n }\r\n\r\n /**\r\n * Get file manifest data\r\n */\r\n public getFile(filePath: string): FileManifest | undefined {\r\n return this.manifest.files[filePath];\r\n }\r\n\r\n /**\r\n * Check if file exists in manifest\r\n */\r\n public hasFile(filePath: string): boolean {\r\n return filePath in this.manifest.files;\r\n }\r\n\r\n /**\r\n * Set file manifest data\r\n */\r\n public setFile(filePath: string, fileManifest: FileManifest): void {\r\n this.manifest.files[filePath] = fileManifest;\r\n }\r\n\r\n /**\r\n * Remove file from manifest\r\n */\r\n public removeFile(filePath: string): void {\r\n delete this.manifest.files[filePath];\r\n }\r\n\r\n /**\r\n * Get all file paths in manifest\r\n */\r\n public getAllFilePaths(): string[] {\r\n return Object.keys(this.manifest.files);\r\n }\r\n\r\n /**\r\n * Get all file manifests\r\n */\r\n public getAllFiles(): Record<string, FileManifest> {\r\n return this.manifest.files;\r\n }\r\n\r\n /**\r\n * Get manifest metadata\r\n */\r\n public getMetadata() {\r\n return {\r\n version: this.manifest.version,\r\n lastBuildTime: this.manifest.lastBuildTime,\r\n projectHash: this.manifest.projectHash,\r\n stats: this.manifest.stats,\r\n };\r\n }\r\n\r\n /**\r\n * Calculate total dependencies across all files\r\n */\r\n private calculateTotalDependencies(): number {\r\n return Object.values(this.manifest.files).reduce(\r\n (total, file) => total + (file.dependencies?.length || 0),\r\n 0,\r\n );\r\n }\r\n\r\n /**\r\n * Clear all files from manifest\r\n */\r\n public clear(): void {\r\n this.manifest.files = {};\r\n this.manifest.stats = {\r\n totalFiles: 0,\r\n totalDependencies: 0,\r\n };\r\n }\r\n}\r\n"],"mappings":";;;;AAKA,MAAM,mBAAmB;AAgBzB,IAAa,kBAAb,MAA6B;;;;CAiB3B,AAAO,YAAY,AAAiB,OAAiC;EAAjC;kBAbP;GAC3B,SAAS;GACT,eAAe,KAAK,IAAI;GACxB,OAAO;IACL,YAAY;IACZ,mBAAmB;GACrB;GACA,OAAO,CAAC;EACV;CAKsE;;;;;CAMtE,MAAa,OAAyB;EACpC,IAAI,MAAM,gBAAgB,aAAa,GACrC,IAAI;GACF,MAAM,SAAS,MAAM,iBAA2B,aAAa;GAG7D,IAAI,QAAQ,YAAY,oBAAoB,OAAO,OAAO,UAAU,UAClE,OAAO;GAGT,KAAK,WAAW;GAChB,OAAO;EACT,QAAQ;GACN,OAAO;EACT;EAGF,OAAO;CACT;;;;CAKA,MAAa,OAAsB;EAEjC,KAAK,SAAS,MAAM,aAAa,OAAO,KAAK,KAAK,SAAS,KAAK,CAAC,CAAC;EAClE,KAAK,SAAS,MAAM,oBAAoB,KAAK,2BAA2B;EACxE,KAAK,SAAS,gBAAgB,KAAK,IAAI;EAEvC,MAAM,iBAAiB,eAAe,KAAK,UAAU,KAAK,UAAU,MAAM,CAAC,CAAC;CAC9E;;;;CAKA,AAAO,QAAQ,UAA4C;EACzD,OAAO,KAAK,SAAS,MAAM;CAC7B;;;;CAKA,AAAO,QAAQ,UAA2B;EACxC,OAAO,YAAY,KAAK,SAAS;CACnC;;;;CAKA,AAAO,QAAQ,UAAkB,cAAkC;EACjE,KAAK,SAAS,MAAM,YAAY;CAClC;;;;CAKA,AAAO,WAAW,UAAwB;EACxC,OAAO,KAAK,SAAS,MAAM;CAC7B;;;;CAKA,AAAO,kBAA4B;EACjC,OAAO,OAAO,KAAK,KAAK,SAAS,KAAK;CACxC;;;;CAKA,AAAO,cAA4C;EACjD,OAAO,KAAK,SAAS;CACvB;;;;CAKA,AAAO,cAAc;EACnB,OAAO;GACL,SAAS,KAAK,SAAS;GACvB,eAAe,KAAK,SAAS;GAC7B,aAAa,KAAK,SAAS;GAC3B,OAAO,KAAK,SAAS;EACvB;CACF;;;;CAKA,AAAQ,6BAAqC;EAC3C,OAAO,OAAO,OAAO,KAAK,SAAS,KAAK,CAAC,CAAC,QACvC,OAAO,SAAS,SAAS,KAAK,cAAc,UAAU,IACvD,CACF;CACF;;;;CAKA,AAAO,QAAc;EACnB,KAAK,SAAS,QAAQ,CAAC;EACvB,KAAK,SAAS,QAAQ;GACpB,YAAY;GACZ,mBAAmB;EACrB;CACF;AACF"}
|
|
@@ -297,6 +297,15 @@ async function tryResolveWithExtensions(basePath) {
|
|
|
297
297
|
const validExtensions = new Set(extensions);
|
|
298
298
|
const ext = path.extname(normalizedBase);
|
|
299
299
|
if (ext && validExtensions.has(ext)) {
|
|
300
|
+
if ([
|
|
301
|
+
".js",
|
|
302
|
+
".jsx",
|
|
303
|
+
".mjs",
|
|
304
|
+
".cjs"
|
|
305
|
+
].includes(ext)) {
|
|
306
|
+
const stem = normalizedBase.slice(0, -ext.length);
|
|
307
|
+
for (const sourceExt of [".ts", ".tsx"]) if (await cachedFileExists(stem + sourceExt)) return stem + sourceExt;
|
|
308
|
+
}
|
|
300
309
|
if (await cachedFileExists(normalizedBase)) return normalizedBase;
|
|
301
310
|
return null;
|
|
302
311
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-imports.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/parse-imports.ts"],"sourcesContent":["import { fileExistsAsync, directoryExistsAsync } from \"@warlock.js/fs\";\nimport { type ImportSpecifier, parse } from \"es-module-lexer\";\nimport path from \"node:path\";\nimport { Path } from \"../utils/normalized-path\";\nimport { tsconfigManager } from \"./tsconfig-manager\";\n\n/**\n * Detect if a file contains only type definitions (no runtime code)\n *\n * A file is considered type-only if ALL its exports are:\n * - interface declarations\n * - type alias declarations\n * - export type { ... } statements\n * - export type { ... } from \"...\" statements\n *\n * Files with any of these are NOT type-only:\n * - export const/let/var\n * - export function\n * - export class\n * - export default (non-type)\n * - export { ... } (without type keyword)\n * - export * from (without type keyword)\n *\n * @param source - The source code to analyze\n * @returns true if the file exports only types\n */\nexport function isTypeOnlyFile(source: string): boolean {\n // Remove comments to avoid false positives\n const withoutComments = source\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\") // block comments\n .replace(/\\/\\/.*$/gm, \"\"); // line comments\n\n // Remove string literals to avoid false positives\n const withoutStrings = withoutComments\n .replace(/'(?:[^'\\\\]|\\\\.)*'/g, \"''\")\n .replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, '\"\"')\n .replace(/`(?:[^`\\\\]|\\\\.)*`/g, \"``\");\n\n // Patterns for type-only exports (these are safe)\n const typeOnlyPatterns = [\n /export\\s+type\\s+\\{[^}]*\\}/g, // export type { Foo, Bar }\n /export\\s+type\\s+\\{[^}]*\\}\\s+from\\s+['\"]/g, // export type { Foo } from \"...\"\n /export\\s+interface\\s+\\w+/g, // export interface Foo\n /export\\s+type\\s+\\w+\\s*=/g, // export type Foo =\n ];\n\n // Remove all type-only exports from consideration\n let remaining = withoutStrings;\n for (const pattern of typeOnlyPatterns) {\n remaining = remaining.replace(pattern, \"\");\n }\n\n // Patterns for runtime exports (these make the file NOT type-only)\n const runtimeExportPatterns = [\n /export\\s+(?:const|let|var)\\s+\\w+/g, // export const/let/var foo\n /export\\s+function\\s+\\w+/g, // export function foo\n /export\\s+async\\s+function\\s+\\w+/g, // export async function foo\n /export\\s+class\\s+\\w+/g, // export class Foo\n /export\\s+enum\\s+\\w+/g, // export enum Foo (enums have runtime value)\n /export\\s+default\\s+(?!type\\s)/g, // export default (not type)\n /export\\s+\\{[^}]*\\}(?!\\s+from)/g, // export { foo } (local re-export without type)\n /export\\s+\\{[^}]*\\}\\s+from\\s+['\"][^'\"]+['\"]/g, // export { foo } from (without type)\n /export\\s+\\*\\s+from\\s+['\"][^'\"]+['\"]/g, // export * from (re-exports everything)\n /export\\s+\\*\\s+as\\s+\\w+/g, // export * as namespace\n ];\n\n for (const pattern of runtimeExportPatterns) {\n if (pattern.test(remaining)) {\n // Reset regex lastIndex for subsequent tests\n pattern.lastIndex = 0;\n\n // Special case: check if export { } contains only type exports\n if (pattern.source.includes(\"export\\\\s+\\\\{\")) {\n const matches = remaining.match(/export\\s+\\{([^}]*)\\}(?:\\s+from\\s+['\"][^'\"]+['\"])?/g);\n if (matches) {\n for (const match of matches) {\n // Skip if it's already a type export\n if (/^export\\s+type\\s+/.test(match)) continue;\n\n // Extract the specifiers\n const specifiersMatch = match.match(/export\\s+\\{([^}]*)\\}/);\n if (specifiersMatch) {\n // Capture group 1 exists whenever the pattern matched. `?? \"\"`\n // yields an empty specifier list, which `some()` below reads as\n // \"no runtime specifier\" — the same answer an empty `{}` gives.\n const specifiers = specifiersMatch[1] ?? \"\";\n const items = specifiers\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n // If any specifier is NOT prefixed with \"type \", this is a runtime export\n const hasRuntimeSpecifier = items.some((item) => !item.startsWith(\"type \"));\n if (hasRuntimeSpecifier) {\n return false;\n }\n }\n }\n }\n continue;\n }\n\n return false;\n }\n }\n\n // If we get here, no runtime exports were found\n // But we should also verify the file has at least some type exports\n // (an empty file or file with only imports is not really \"type-only\")\n const hasTypeExports = typeOnlyPatterns.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(withoutStrings);\n });\n\n return hasTypeExports;\n}\n\n/**\n * Check if an import statement is a pure `import type` (the keyword sits\n * immediately after `import`, covering `import type { X }`, `import type Foo`,\n * and `import type * as Ns`).\n */\nfunction isTypeOnlyImport(line: string): boolean {\n const trimmed = line.trim();\n\n return trimmed.startsWith(\"import type \") || !!trimmed.match(/^import\\s+type\\s+[\\{\\*]/);\n}\n\n/**\n * Decide whether an `import ... from \"m\"` statement contributes any runtime\n * binding. Returns false for pure `import type` and for destructured imports\n * where every specifier is prefixed with the `type` keyword.\n */\nfunction hasRuntimeImports(line: string): boolean {\n const trimmed = line.trim();\n\n if (isTypeOnlyImport(trimmed)) {\n return false;\n }\n\n const specifiersMatch = trimmed.match(/import\\s+(?:type\\s+)?\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return true;\n }\n\n const items = (specifiersMatch[1] ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n return !items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Decide whether an `export ... from \"m\"` statement re-exports only types.\n * Covers `export type { X } from`, `export type * from`, and\n * `export { type X, type Y } from`.\n */\nfunction isExportTypeOnlyStatement(line: string): boolean {\n const trimmed = line.trim();\n\n if (/^export\\s+type\\s+/.test(trimmed)) {\n return true;\n }\n\n const specifiersMatch = trimmed.match(/export\\s+\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return false;\n }\n\n const items = (specifiersMatch[1] ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n return items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Extract import paths using regex (more reliable for TypeScript).\n * Each entry carries an `isTypeOnly` flag: true when every occurrence of the\n * path in the source is a type-only statement (no runtime binding).\n * A single runtime occurrence makes the path runtime.\n */\nfunction extractImportPathsWithRegex(\n source: string,\n): Array<{ path: string; originalLine: string; isTypeOnly: boolean }> {\n const imports: Array<{ path: string; originalLine: string; isTypeOnly: boolean }> = [];\n const seenPaths = new Map<string, number>();\n\n /**\n * Record an import path with its type-only flag.\n * If the same path appears in multiple statements, the edge is type-only\n * iff every occurrence is type-only — a single runtime statement makes it runtime.\n */\n const record = (importPath: string, originalLine: string, isTypeOnly: boolean): void => {\n if (!importPath) {\n return;\n }\n\n const existingIndex = seenPaths.get(importPath);\n\n if (existingIndex === undefined) {\n seenPaths.set(importPath, imports.length);\n imports.push({ path: importPath, originalLine, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n const existing = imports[existingIndex];\n\n // `existingIndex` came from findIndex on this same array, so it is in\n // range; the compiler does not carry that here.\n if (existing) existing.isTypeOnly = false;\n }\n };\n\n // Pattern 1: Standard ES module imports (handles multiline)\n // Matches: import { ... } from \"path\", import Foo from \"path\", import Foo, { ... } from \"path\"\n const importRegex =\n /import\\s+(?:type\\s+)?(\\{[\\s\\S]*?\\}|\\*\\s+as\\s+\\w+|\\w+(?:\\s*,\\s*\\{[\\s\\S]*?\\})?)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n let match;\n\n while ((match = importRegex.exec(source)) !== null) {\n const fullMatch = match[0];\n const importPath = match[2];\n\n // Each pattern here captures its path in a group, so a match always carries\n // one. Skipping rather than recording keeps a malformed match out of the\n // dependency graph entirely — recording `undefined` would add an import\n // edge to a module named \"undefined\", which the dev server would then\n // watch, fail to resolve, and report against the wrong file.\n if (importPath === undefined) continue;\n\n const isTypeOnly = !hasRuntimeImports(fullMatch);\n\n record(importPath, fullMatch, isTypeOnly);\n }\n\n // Pattern 1b: Side-effect imports - import \"path\" — always runtime\n const sideEffectRegex = /import\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = sideEffectRegex.exec(source)) !== null) {\n const sideEffectPath = match[1];\n\n if (sideEffectPath === undefined) continue;\n\n record(sideEffectPath, match[0], false);\n }\n\n // Pattern 2: Dynamic imports - import(\"path\") — always runtime\n const dynamicImportPattern = /import\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n\n while ((match = dynamicImportPattern.exec(source)) !== null) {\n const dynamicPath = match[1];\n\n if (dynamicPath === undefined) continue;\n\n record(dynamicPath, match[0], false);\n }\n\n // Pattern 3: Export from - export ... from \"path\"\n const exportFromPattern = /export\\s+(?:\\{[^}]*\\}|\\*|\\w+)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = exportFromPattern.exec(source)) !== null) {\n const fullMatch = match[0];\n const exportPath = match[1];\n\n if (exportPath === undefined) continue;\n\n record(exportPath, fullMatch, isExportTypeOnlyStatement(fullMatch));\n }\n\n return imports;\n}\n\n/**\n * Metadata for a single resolved import edge.\n * `isTypeOnly` is true iff every import/export statement that references this\n * path is type-only (pure `import type`, `export type`, or destructured lists\n * where every specifier carries the `type` keyword). A single runtime\n * occurrence flips the flag to false — that's what matters for cycle detection.\n */\nexport type ResolvedImport = {\n absolutePath: string;\n isTypeOnly: boolean;\n};\n\n/**\n * Parse import/export statements in a TS/JS source file and return the\n * resolved dependency edges keyed by the original import path. Includes\n * type-only edges (flagged) so downstream consumers can distinguish runtime\n * cycles from type-only ones.\n *\n * @example\n * const importMap = await parseImports(source, \"/abs/path/to/file.ts\");\n * for (const [importPath, { absolutePath, isTypeOnly }] of importMap) {\n * // ...\n * }\n */\nexport async function parseImports(\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n try {\n // Skip .d.ts files - they're type declarations, not runtime code\n if (filePath.endsWith(\".d.ts\")) {\n return new Map();\n }\n\n // Try es-module-lexer first (faster and more accurate for simple cases)\n try {\n const [imports] = await parse(source);\n\n if (imports && imports.length > 0) {\n return await resolveImports(imports as ImportSpecifier[], source, filePath);\n }\n } catch (lexerError) {\n // es-module-lexer failed, fall back to regex-based extraction\n // This is common with TypeScript files that have complex syntax\n }\n\n // Fallback: Use regex-based extraction (more forgiving with TypeScript)\n const regexImports = extractImportPathsWithRegex(source);\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const { path: importPath, isTypeOnly } of regexImports) {\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (resolvedPath) {\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n }\n\n return resolvedImports;\n } catch (error) {\n console.error(`Error parsing imports for ${filePath}:`, error);\n\n return new Map();\n }\n}\n\n/**\n * Merge a resolved import into the output map, preserving the type-only rule:\n * a path is type-only iff every statement that references it is type-only.\n */\nfunction mergeResolvedImport(\n target: Map<string, ResolvedImport>,\n importPath: string,\n absolutePath: string,\n isTypeOnly: boolean,\n): void {\n const existing = target.get(importPath);\n\n if (!existing) {\n target.set(importPath, { absolutePath, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n existing.isTypeOnly = false;\n }\n}\n\n/**\n * Resolve imports from `es-module-lexer` output. For each specifier, slice the\n * raw statement (`ss..se`) and re-run the type-only heuristic so we can flag\n * the edge correctly — the lexer itself does not distinguish `import type`.\n */\nasync function resolveImports(\n imports: ImportSpecifier[],\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const imp of imports) {\n const importPath = imp.n;\n\n if (!importPath) {\n continue;\n }\n\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (!resolvedPath) {\n continue;\n }\n\n const isTypeOnly = isLexerStatementTypeOnly(imp, source);\n\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n\n return resolvedImports;\n}\n\n/**\n * Classify a single lexer-reported statement as type-only or runtime.\n * Dynamic imports (`import(...)`) are always runtime. For static\n * imports/export-from we slice the raw source and apply the same heuristic\n * the regex fallback uses.\n */\nfunction isLexerStatementTypeOnly(imp: ImportSpecifier, source: string): boolean {\n // Dynamic imports always execute — they cannot be type-only.\n // es-module-lexer reports dynamic with a.n === undefined typically, but\n // we guard by checking the statement prefix too.\n const statementStart = imp.ss;\n const statementEnd = imp.se;\n\n if (\n typeof statementStart !== \"number\" ||\n typeof statementEnd !== \"number\" ||\n statementEnd <= statementStart\n ) {\n return false;\n }\n\n const statement = source.slice(statementStart, statementEnd);\n const trimmed = statement.trim();\n\n if (trimmed.startsWith(\"import(\") || /^\\bimport\\s*\\(/.test(trimmed)) {\n return false;\n }\n\n if (trimmed.startsWith(\"export\")) {\n return isExportTypeOnlyStatement(statement);\n }\n\n return !hasRuntimeImports(statement);\n}\n\n/**\n * Resolve alias imports to actual file paths with extensions\n * Example: app/users/services/get-users.service -> /absolute/path/to/src/app/users/services/get-users.service.ts\n */\nasync function resolveAliasImport(importPath: string): Promise<string | null> {\n // Use tsconfig manager to resolve the alias to an absolute path\n const resolvedBase = tsconfigManager.resolveAliasToAbsolute(importPath);\n\n if (!resolvedBase) return null;\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Resolve relative imports to actual file paths\n * Example: ./../services/get-user.service -> /absolute/path/to/services/get-user.service.ts\n */\nasync function resolveRelativeImport(\n importPath: string,\n currentFilePath: string,\n): Promise<string | null> {\n const dir = path.dirname(currentFilePath);\n // Use path.resolve to handle .. and . properly, then normalize to forward slashes\n const resolvedBase = Path.normalize(path.resolve(dir, importPath));\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Try to resolve a file path by checking different extensions\n * TypeScript/JavaScript files can be imported without extensions\n *\n * @TODO: For better performance, we need to check the files in files orchestrator\n * instead of using the file system as we will be fetching all project files anyway.\n */\n// Cache for file existence checks to avoid redundant filesystem calls\nconst fileExistsCache = new Map<string, boolean>();\n\n/**\n * Clear the file exists cache\n * Should be called when new files are created to ensure fresh lookups\n */\nexport function clearFileExistsCache(): void {\n fileExistsCache.clear();\n}\n\nasync function cachedFileExists(filePath: string): Promise<boolean> {\n if (fileExistsCache.has(filePath)) {\n return fileExistsCache.get(filePath)!;\n }\n const exists = (await fileExistsAsync(filePath)) as boolean;\n\n // Cache ONLY positive results. A negative (\"does not exist\") answer is the\n // exact case that flips moments later: the \"add a new controller/route\" flow\n // references a file, then creates it. Caching that negative for the whole\n // process lifetime (it was cleared only on a multi-file watcher batch) left\n // a routes.ts import unresolvable after its target was created — the dep\n // edge never formed and the route module import failed, so the new API route\n // 404'd until an unrelated multi-file batch cleared the cache. That is\n // finding 60721e35 (\"backend route HMR needs a second edit\"). A file that\n // exists does not stop existing mid-resolve, so caching the hit is safe;\n // re-probing a still-missing path costs only a handful of stat calls.\n if (exists) {\n fileExistsCache.set(filePath, exists);\n }\n\n return exists;\n}\n\nasync function tryResolveWithExtensions(basePath: string): Promise<string | null> {\n // Normalize the base path first (handle Windows paths)\n const normalizedBase = Path.normalize(basePath);\n\n // List of extensions to try, in order of preference\n const extensions = [\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\", \".cjs\"];\n const validExtensions = new Set(extensions);\n\n // If the path already has a VALID code file extension, check if it exists\n const ext = path.extname(normalizedBase);\n if (ext && validExtensions.has(ext)) {\n if (await cachedFileExists(normalizedBase)) {\n return normalizedBase;\n }\n // If explicit extension doesn't exist, return null\n return null;\n }\n\n // Try all extensions in parallel for better performance\n const pathsToCheck = extensions.map((extension) => normalizedBase + extension);\n const results = await Promise.all(\n pathsToCheck.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n // Return the first one that exists (in order of preference)\n for (const result of results) {\n if (result.exists) {\n return result.path;\n }\n }\n\n // Try index files in directory\n if (await directoryExistsAsync(normalizedBase)) {\n const indexPaths = extensions.map((extension) =>\n Path.join(normalizedBase, `index${extension}`),\n );\n const indexResults = await Promise.all(\n indexPaths.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n for (const result of indexResults) {\n if (result.exists) {\n return result.path;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Check if import is a Node.js built-in module\n */\nfunction isNodeBuiltin(importPath: string): boolean {\n const builtins = [\n \"fs\",\n \"path\",\n \"http\",\n \"https\",\n \"crypto\",\n \"stream\",\n \"util\",\n \"events\",\n \"buffer\",\n \"child_process\",\n \"os\",\n \"url\",\n \"querystring\",\n \"zlib\",\n \"net\",\n \"tls\",\n \"dns\",\n \"dgram\",\n \"cluster\",\n \"worker_threads\",\n \"perf_hooks\",\n \"async_hooks\",\n \"timers\",\n \"readline\",\n \"repl\",\n \"vm\",\n \"assert\",\n \"console\",\n \"process\",\n \"v8\",\n ];\n\n // Check for node: prefix or direct builtin name\n if (importPath.startsWith(\"node:\")) return true;\n\n // `split` always yields a first element. Falling back to the whole path\n // keeps the builtin check answering about a real name rather than undefined.\n const moduleName = importPath.split(\"/\")[0] ?? importPath;\n\n return builtins.includes(moduleName);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"parse-imports.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/parse-imports.ts"],"sourcesContent":["import { fileExistsAsync, directoryExistsAsync } from \"@warlock.js/fs\";\nimport { type ImportSpecifier, parse } from \"es-module-lexer\";\nimport path from \"node:path\";\nimport { Path } from \"../utils/normalized-path\";\nimport { tsconfigManager } from \"./tsconfig-manager\";\n\n/**\n * Detect if a file contains only type definitions (no runtime code)\n *\n * A file is considered type-only if ALL its exports are:\n * - interface declarations\n * - type alias declarations\n * - export type { ... } statements\n * - export type { ... } from \"...\" statements\n *\n * Files with any of these are NOT type-only:\n * - export const/let/var\n * - export function\n * - export class\n * - export default (non-type)\n * - export { ... } (without type keyword)\n * - export * from (without type keyword)\n *\n * @param source - The source code to analyze\n * @returns true if the file exports only types\n */\nexport function isTypeOnlyFile(source: string): boolean {\n // Remove comments to avoid false positives\n const withoutComments = source\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\") // block comments\n .replace(/\\/\\/.*$/gm, \"\"); // line comments\n\n // Remove string literals to avoid false positives\n const withoutStrings = withoutComments\n .replace(/'(?:[^'\\\\]|\\\\.)*'/g, \"''\")\n .replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, '\"\"')\n .replace(/`(?:[^`\\\\]|\\\\.)*`/g, \"``\");\n\n // Patterns for type-only exports (these are safe)\n const typeOnlyPatterns = [\n /export\\s+type\\s+\\{[^}]*\\}/g, // export type { Foo, Bar }\n /export\\s+type\\s+\\{[^}]*\\}\\s+from\\s+['\"]/g, // export type { Foo } from \"...\"\n /export\\s+interface\\s+\\w+/g, // export interface Foo\n /export\\s+type\\s+\\w+\\s*=/g, // export type Foo =\n ];\n\n // Remove all type-only exports from consideration\n let remaining = withoutStrings;\n for (const pattern of typeOnlyPatterns) {\n remaining = remaining.replace(pattern, \"\");\n }\n\n // Patterns for runtime exports (these make the file NOT type-only)\n const runtimeExportPatterns = [\n /export\\s+(?:const|let|var)\\s+\\w+/g, // export const/let/var foo\n /export\\s+function\\s+\\w+/g, // export function foo\n /export\\s+async\\s+function\\s+\\w+/g, // export async function foo\n /export\\s+class\\s+\\w+/g, // export class Foo\n /export\\s+enum\\s+\\w+/g, // export enum Foo (enums have runtime value)\n /export\\s+default\\s+(?!type\\s)/g, // export default (not type)\n /export\\s+\\{[^}]*\\}(?!\\s+from)/g, // export { foo } (local re-export without type)\n /export\\s+\\{[^}]*\\}\\s+from\\s+['\"][^'\"]+['\"]/g, // export { foo } from (without type)\n /export\\s+\\*\\s+from\\s+['\"][^'\"]+['\"]/g, // export * from (re-exports everything)\n /export\\s+\\*\\s+as\\s+\\w+/g, // export * as namespace\n ];\n\n for (const pattern of runtimeExportPatterns) {\n if (pattern.test(remaining)) {\n // Reset regex lastIndex for subsequent tests\n pattern.lastIndex = 0;\n\n // Special case: check if export { } contains only type exports\n if (pattern.source.includes(\"export\\\\s+\\\\{\")) {\n const matches = remaining.match(/export\\s+\\{([^}]*)\\}(?:\\s+from\\s+['\"][^'\"]+['\"])?/g);\n if (matches) {\n for (const match of matches) {\n // Skip if it's already a type export\n if (/^export\\s+type\\s+/.test(match)) continue;\n\n // Extract the specifiers\n const specifiersMatch = match.match(/export\\s+\\{([^}]*)\\}/);\n if (specifiersMatch) {\n // Capture group 1 exists whenever the pattern matched. `?? \"\"`\n // yields an empty specifier list, which `some()` below reads as\n // \"no runtime specifier\" — the same answer an empty `{}` gives.\n const specifiers = specifiersMatch[1] ?? \"\";\n const items = specifiers\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n // If any specifier is NOT prefixed with \"type \", this is a runtime export\n const hasRuntimeSpecifier = items.some((item) => !item.startsWith(\"type \"));\n if (hasRuntimeSpecifier) {\n return false;\n }\n }\n }\n }\n continue;\n }\n\n return false;\n }\n }\n\n // If we get here, no runtime exports were found\n // But we should also verify the file has at least some type exports\n // (an empty file or file with only imports is not really \"type-only\")\n const hasTypeExports = typeOnlyPatterns.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(withoutStrings);\n });\n\n return hasTypeExports;\n}\n\n/**\n * Check if an import statement is a pure `import type` (the keyword sits\n * immediately after `import`, covering `import type { X }`, `import type Foo`,\n * and `import type * as Ns`).\n */\nfunction isTypeOnlyImport(line: string): boolean {\n const trimmed = line.trim();\n\n return trimmed.startsWith(\"import type \") || !!trimmed.match(/^import\\s+type\\s+[\\{\\*]/);\n}\n\n/**\n * Decide whether an `import ... from \"m\"` statement contributes any runtime\n * binding. Returns false for pure `import type` and for destructured imports\n * where every specifier is prefixed with the `type` keyword.\n */\nfunction hasRuntimeImports(line: string): boolean {\n const trimmed = line.trim();\n\n if (isTypeOnlyImport(trimmed)) {\n return false;\n }\n\n const specifiersMatch = trimmed.match(/import\\s+(?:type\\s+)?\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return true;\n }\n\n const items = (specifiersMatch[1] ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n return !items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Decide whether an `export ... from \"m\"` statement re-exports only types.\n * Covers `export type { X } from`, `export type * from`, and\n * `export { type X, type Y } from`.\n */\nfunction isExportTypeOnlyStatement(line: string): boolean {\n const trimmed = line.trim();\n\n if (/^export\\s+type\\s+/.test(trimmed)) {\n return true;\n }\n\n const specifiersMatch = trimmed.match(/export\\s+\\{([^}]+)\\}/);\n\n if (!specifiersMatch) {\n return false;\n }\n\n const items = (specifiersMatch[1] ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n return items.every((item) => /^type\\s+\\w+/.test(item));\n}\n\n/**\n * Extract import paths using regex (more reliable for TypeScript).\n * Each entry carries an `isTypeOnly` flag: true when every occurrence of the\n * path in the source is a type-only statement (no runtime binding).\n * A single runtime occurrence makes the path runtime.\n */\nfunction extractImportPathsWithRegex(\n source: string,\n): Array<{ path: string; originalLine: string; isTypeOnly: boolean }> {\n const imports: Array<{ path: string; originalLine: string; isTypeOnly: boolean }> = [];\n const seenPaths = new Map<string, number>();\n\n /**\n * Record an import path with its type-only flag.\n * If the same path appears in multiple statements, the edge is type-only\n * iff every occurrence is type-only — a single runtime statement makes it runtime.\n */\n const record = (importPath: string, originalLine: string, isTypeOnly: boolean): void => {\n if (!importPath) {\n return;\n }\n\n const existingIndex = seenPaths.get(importPath);\n\n if (existingIndex === undefined) {\n seenPaths.set(importPath, imports.length);\n imports.push({ path: importPath, originalLine, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n const existing = imports[existingIndex];\n\n // `existingIndex` came from findIndex on this same array, so it is in\n // range; the compiler does not carry that here.\n if (existing) existing.isTypeOnly = false;\n }\n };\n\n // Pattern 1: Standard ES module imports (handles multiline)\n // Matches: import { ... } from \"path\", import Foo from \"path\", import Foo, { ... } from \"path\"\n const importRegex =\n /import\\s+(?:type\\s+)?(\\{[\\s\\S]*?\\}|\\*\\s+as\\s+\\w+|\\w+(?:\\s*,\\s*\\{[\\s\\S]*?\\})?)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n let match;\n\n while ((match = importRegex.exec(source)) !== null) {\n const fullMatch = match[0];\n const importPath = match[2];\n\n // Each pattern here captures its path in a group, so a match always carries\n // one. Skipping rather than recording keeps a malformed match out of the\n // dependency graph entirely — recording `undefined` would add an import\n // edge to a module named \"undefined\", which the dev server would then\n // watch, fail to resolve, and report against the wrong file.\n if (importPath === undefined) continue;\n\n const isTypeOnly = !hasRuntimeImports(fullMatch);\n\n record(importPath, fullMatch, isTypeOnly);\n }\n\n // Pattern 1b: Side-effect imports - import \"path\" — always runtime\n const sideEffectRegex = /import\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = sideEffectRegex.exec(source)) !== null) {\n const sideEffectPath = match[1];\n\n if (sideEffectPath === undefined) continue;\n\n record(sideEffectPath, match[0], false);\n }\n\n // Pattern 2: Dynamic imports - import(\"path\") — always runtime\n const dynamicImportPattern = /import\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n\n while ((match = dynamicImportPattern.exec(source)) !== null) {\n const dynamicPath = match[1];\n\n if (dynamicPath === undefined) continue;\n\n record(dynamicPath, match[0], false);\n }\n\n // Pattern 3: Export from - export ... from \"path\"\n const exportFromPattern = /export\\s+(?:\\{[^}]*\\}|\\*|\\w+)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\n\n while ((match = exportFromPattern.exec(source)) !== null) {\n const fullMatch = match[0];\n const exportPath = match[1];\n\n if (exportPath === undefined) continue;\n\n record(exportPath, fullMatch, isExportTypeOnlyStatement(fullMatch));\n }\n\n return imports;\n}\n\n/**\n * Metadata for a single resolved import edge.\n * `isTypeOnly` is true iff every import/export statement that references this\n * path is type-only (pure `import type`, `export type`, or destructured lists\n * where every specifier carries the `type` keyword). A single runtime\n * occurrence flips the flag to false — that's what matters for cycle detection.\n */\nexport type ResolvedImport = {\n absolutePath: string;\n isTypeOnly: boolean;\n};\n\n/**\n * Parse import/export statements in a TS/JS source file and return the\n * resolved dependency edges keyed by the original import path. Includes\n * type-only edges (flagged) so downstream consumers can distinguish runtime\n * cycles from type-only ones.\n *\n * @example\n * const importMap = await parseImports(source, \"/abs/path/to/file.ts\");\n * for (const [importPath, { absolutePath, isTypeOnly }] of importMap) {\n * // ...\n * }\n */\nexport async function parseImports(\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n try {\n // Skip .d.ts files - they're type declarations, not runtime code\n if (filePath.endsWith(\".d.ts\")) {\n return new Map();\n }\n\n // Try es-module-lexer first (faster and more accurate for simple cases)\n try {\n const [imports] = await parse(source);\n\n if (imports && imports.length > 0) {\n return await resolveImports(imports as ImportSpecifier[], source, filePath);\n }\n } catch (lexerError) {\n // es-module-lexer failed, fall back to regex-based extraction\n // This is common with TypeScript files that have complex syntax\n }\n\n // Fallback: Use regex-based extraction (more forgiving with TypeScript)\n const regexImports = extractImportPathsWithRegex(source);\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const { path: importPath, isTypeOnly } of regexImports) {\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (resolvedPath) {\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n }\n\n return resolvedImports;\n } catch (error) {\n console.error(`Error parsing imports for ${filePath}:`, error);\n\n return new Map();\n }\n}\n\n/**\n * Merge a resolved import into the output map, preserving the type-only rule:\n * a path is type-only iff every statement that references it is type-only.\n */\nfunction mergeResolvedImport(\n target: Map<string, ResolvedImport>,\n importPath: string,\n absolutePath: string,\n isTypeOnly: boolean,\n): void {\n const existing = target.get(importPath);\n\n if (!existing) {\n target.set(importPath, { absolutePath, isTypeOnly });\n\n return;\n }\n\n if (!isTypeOnly) {\n existing.isTypeOnly = false;\n }\n}\n\n/**\n * Resolve imports from `es-module-lexer` output. For each specifier, slice the\n * raw statement (`ss..se`) and re-run the type-only heuristic so we can flag\n * the edge correctly — the lexer itself does not distinguish `import type`.\n */\nasync function resolveImports(\n imports: ImportSpecifier[],\n source: string,\n filePath: string,\n): Promise<Map<string, ResolvedImport>> {\n const resolvedImports = new Map<string, ResolvedImport>();\n\n for (const imp of imports) {\n const importPath = imp.n;\n\n if (!importPath) {\n continue;\n }\n\n if (isNodeBuiltin(importPath)) {\n continue;\n }\n\n if (!importPath.startsWith(\".\") && !tsconfigManager.isAlias(importPath)) {\n continue;\n }\n\n let resolvedPath: string | null = null;\n\n if (tsconfigManager.isAlias(importPath)) {\n resolvedPath = await resolveAliasImport(importPath);\n } else if (importPath.startsWith(\".\")) {\n resolvedPath = await resolveRelativeImport(importPath, filePath);\n }\n\n if (!resolvedPath) {\n continue;\n }\n\n const isTypeOnly = isLexerStatementTypeOnly(imp, source);\n\n mergeResolvedImport(resolvedImports, importPath, resolvedPath, isTypeOnly);\n }\n\n return resolvedImports;\n}\n\n/**\n * Classify a single lexer-reported statement as type-only or runtime.\n * Dynamic imports (`import(...)`) are always runtime. For static\n * imports/export-from we slice the raw source and apply the same heuristic\n * the regex fallback uses.\n */\nfunction isLexerStatementTypeOnly(imp: ImportSpecifier, source: string): boolean {\n // Dynamic imports always execute — they cannot be type-only.\n // es-module-lexer reports dynamic with a.n === undefined typically, but\n // we guard by checking the statement prefix too.\n const statementStart = imp.ss;\n const statementEnd = imp.se;\n\n if (\n typeof statementStart !== \"number\" ||\n typeof statementEnd !== \"number\" ||\n statementEnd <= statementStart\n ) {\n return false;\n }\n\n const statement = source.slice(statementStart, statementEnd);\n const trimmed = statement.trim();\n\n if (trimmed.startsWith(\"import(\") || /^\\bimport\\s*\\(/.test(trimmed)) {\n return false;\n }\n\n if (trimmed.startsWith(\"export\")) {\n return isExportTypeOnlyStatement(statement);\n }\n\n return !hasRuntimeImports(statement);\n}\n\n/**\n * Resolve alias imports to actual file paths with extensions\n * Example: app/users/services/get-users.service -> /absolute/path/to/src/app/users/services/get-users.service.ts\n */\nasync function resolveAliasImport(importPath: string): Promise<string | null> {\n // Use tsconfig manager to resolve the alias to an absolute path\n const resolvedBase = tsconfigManager.resolveAliasToAbsolute(importPath);\n\n if (!resolvedBase) return null;\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Resolve relative imports to actual file paths\n * Example: ./../services/get-user.service -> /absolute/path/to/services/get-user.service.ts\n */\nasync function resolveRelativeImport(\n importPath: string,\n currentFilePath: string,\n): Promise<string | null> {\n const dir = path.dirname(currentFilePath);\n // Use path.resolve to handle .. and . properly, then normalize to forward slashes\n const resolvedBase = Path.normalize(path.resolve(dir, importPath));\n\n // Try to resolve with extensions\n const resolvedPath = await tryResolveWithExtensions(resolvedBase);\n\n return resolvedPath;\n}\n\n/**\n * Try to resolve a file path by checking different extensions\n * TypeScript/JavaScript files can be imported without extensions\n *\n * @TODO: For better performance, we need to check the files in files orchestrator\n * instead of using the file system as we will be fetching all project files anyway.\n */\n// Cache for file existence checks to avoid redundant filesystem calls\nconst fileExistsCache = new Map<string, boolean>();\n\n/**\n * Clear the file exists cache\n * Should be called when new files are created to ensure fresh lookups\n */\nexport function clearFileExistsCache(): void {\n fileExistsCache.clear();\n}\n\nasync function cachedFileExists(filePath: string): Promise<boolean> {\n if (fileExistsCache.has(filePath)) {\n return fileExistsCache.get(filePath)!;\n }\n const exists = (await fileExistsAsync(filePath)) as boolean;\n\n // Cache ONLY positive results. A negative (\"does not exist\") answer is the\n // exact case that flips moments later: the \"add a new controller/route\" flow\n // references a file, then creates it. Caching that negative for the whole\n // process lifetime (it was cleared only on a multi-file watcher batch) left\n // a routes.ts import unresolvable after its target was created — the dep\n // edge never formed and the route module import failed, so the new API route\n // 404'd until an unrelated multi-file batch cleared the cache. That is\n // finding 60721e35 (\"backend route HMR needs a second edit\"). A file that\n // exists does not stop existing mid-resolve, so caching the hit is safe;\n // re-probing a still-missing path costs only a handful of stat calls.\n if (exists) {\n fileExistsCache.set(filePath, exists);\n }\n\n return exists;\n}\n\nasync function tryResolveWithExtensions(basePath: string): Promise<string | null> {\n // Normalize the base path first (handle Windows paths)\n const normalizedBase = Path.normalize(basePath);\n\n // List of extensions to try, in order of preference\n const extensions = [\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\", \".cjs\"];\n const validExtensions = new Set(extensions);\n\n // If the path already has a VALID code file extension, check if it exists\n const ext = path.extname(normalizedBase);\n if (ext && validExtensions.has(ext)) {\n // NodeNext style: `./x.js` is written, `x.ts`/`x.tsx` is on disk. The\n // loader rewrites it the same way (own-resolver `probeFile`), and a\n // co-located source wins over the literal .js.\n if ([\".js\", \".jsx\", \".mjs\", \".cjs\"].includes(ext)) {\n const stem = normalizedBase.slice(0, -ext.length);\n for (const sourceExt of [\".ts\", \".tsx\"]) {\n if (await cachedFileExists(stem + sourceExt)) {\n return stem + sourceExt;\n }\n }\n }\n if (await cachedFileExists(normalizedBase)) {\n return normalizedBase;\n }\n // If explicit extension doesn't exist, return null\n return null;\n }\n\n // Try all extensions in parallel for better performance\n const pathsToCheck = extensions.map((extension) => normalizedBase + extension);\n const results = await Promise.all(\n pathsToCheck.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n // Return the first one that exists (in order of preference)\n for (const result of results) {\n if (result.exists) {\n return result.path;\n }\n }\n\n // Try index files in directory\n if (await directoryExistsAsync(normalizedBase)) {\n const indexPaths = extensions.map((extension) =>\n Path.join(normalizedBase, `index${extension}`),\n );\n const indexResults = await Promise.all(\n indexPaths.map(async (p) => ({ path: p, exists: await cachedFileExists(p) })),\n );\n\n for (const result of indexResults) {\n if (result.exists) {\n return result.path;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Check if import is a Node.js built-in module\n */\nfunction isNodeBuiltin(importPath: string): boolean {\n const builtins = [\n \"fs\",\n \"path\",\n \"http\",\n \"https\",\n \"crypto\",\n \"stream\",\n \"util\",\n \"events\",\n \"buffer\",\n \"child_process\",\n \"os\",\n \"url\",\n \"querystring\",\n \"zlib\",\n \"net\",\n \"tls\",\n \"dns\",\n \"dgram\",\n \"cluster\",\n \"worker_threads\",\n \"perf_hooks\",\n \"async_hooks\",\n \"timers\",\n \"readline\",\n \"repl\",\n \"vm\",\n \"assert\",\n \"console\",\n \"process\",\n \"v8\",\n ];\n\n // Check for node: prefix or direct builtin name\n if (importPath.startsWith(\"node:\")) return true;\n\n // `split` always yields a first element. Falling back to the whole path\n // keeps the builtin check answering about a real name rather than undefined.\n const moduleName = importPath.split(\"/\")[0] ?? importPath;\n\n return builtins.includes(moduleName);\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { devLogError } from "./dev-logger.mjs";
|
|
2
|
+
import "./supervisor.mjs";
|
|
2
3
|
import { restartDevServer } from "./restart-dev-server.mjs";
|
|
3
4
|
import { devServerShortcuts } from "./shortcuts.mjs";
|
|
4
5
|
import { colors } from "@mongez/copper";
|
|
@@ -53,7 +54,7 @@ async function quit(devServer) {
|
|
|
53
54
|
process.exit(0);
|
|
54
55
|
} catch (error) {
|
|
55
56
|
devLogError(`Shutdown failed: ${error.message}`);
|
|
56
|
-
process.exit(
|
|
57
|
+
process.exit(79);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
/** `h` — list whatever is armed right now, `u` included when it is. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register-dev-shortcuts.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/register-dev-shortcuts.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { devLogError } from \"./dev-logger\";\nimport type { DevelopmentServer } from \"./development-server\";\nimport { restartDevServer } from \"./restart-dev-server\";\nimport { devServerShortcuts } from \"./shortcuts\";\n\n/**\n * Arm the dev server's standing keyboard shortcuts and print the one-line\n * hint that tells the developer they exist.\n *\n * These stay available for the whole session. The update shortcut (`u`) is\n * deliberately not here — it is armed by the update notice only when there is\n * something to update, and dropped again once it has been used.\n *\n * Entirely TTY-gated: with piped stdin (CI, a supervisor, `warlock dev | tee`)\n * nothing is registered, no hint is printed, and stdin is never touched.\n */\nexport function registerDevShortcuts(devServer: DevelopmentServer): void {\n if (!devServerShortcuts.isSupported()) {\n return;\n }\n\n devServerShortcuts.register({\n key: \"r\",\n description: \"restart the server\",\n handler: () => restart(devServer),\n });\n\n devServerShortcuts.register({\n key: \"c\",\n description: \"clear the console\",\n handler: () => console.clear(),\n });\n\n devServerShortcuts.register({\n key: \"q\",\n description: \"quit\",\n handler: () => quit(devServer),\n });\n\n devServerShortcuts.register({\n key: \"h\",\n description: \"show this help\",\n handler: printShortcuts,\n });\n\n console.log(\n ` ${colors.dim(\"press\")} ${colors.bold(colors.cyan(\"h\"))} ` +\n `${colors.dim(\"for shortcuts\")}`,\n );\n}\n\n/** `r` — bring the server back up on a clean process. */\nasync function restart(devServer: DevelopmentServer): Promise<void> {\n const restarted = await restartDevServer(devServer);\n\n if (!restarted) {\n devServerShortcuts.resume();\n devLogError(\"Restart failed — start the dev server again manually.\");\n }\n}\n\n/** `q` — the same graceful shutdown Ctrl+C performs. */\nasync function quit(devServer: DevelopmentServer): Promise<void> {\n try {\n await devServer.shutdown();\n process.exit(0);\n } catch (error) {\n devLogError(`Shutdown failed: ${(error as Error).message}`);\n process.exit(
|
|
1
|
+
{"version":3,"file":"register-dev-shortcuts.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/register-dev-shortcuts.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { devLogError } from \"./dev-logger\";\nimport type { DevelopmentServer } from \"./development-server\";\nimport { restartDevServer } from \"./restart-dev-server\";\nimport { devServerShortcuts } from \"./shortcuts\";\nimport { QUIT_EXIT_CODE } from \"./supervisor\";\n\n/**\n * Arm the dev server's standing keyboard shortcuts and print the one-line\n * hint that tells the developer they exist.\n *\n * These stay available for the whole session. The update shortcut (`u`) is\n * deliberately not here — it is armed by the update notice only when there is\n * something to update, and dropped again once it has been used.\n *\n * Entirely TTY-gated: with piped stdin (CI, a supervisor, `warlock dev | tee`)\n * nothing is registered, no hint is printed, and stdin is never touched.\n */\nexport function registerDevShortcuts(devServer: DevelopmentServer): void {\n if (!devServerShortcuts.isSupported()) {\n return;\n }\n\n devServerShortcuts.register({\n key: \"r\",\n description: \"restart the server\",\n handler: () => restart(devServer),\n });\n\n devServerShortcuts.register({\n key: \"c\",\n description: \"clear the console\",\n handler: () => console.clear(),\n });\n\n devServerShortcuts.register({\n key: \"q\",\n description: \"quit\",\n handler: () => quit(devServer),\n });\n\n devServerShortcuts.register({\n key: \"h\",\n description: \"show this help\",\n handler: printShortcuts,\n });\n\n console.log(\n ` ${colors.dim(\"press\")} ${colors.bold(colors.cyan(\"h\"))} ` +\n `${colors.dim(\"for shortcuts\")}`,\n );\n}\n\n/** `r` — bring the server back up on a clean process. */\nasync function restart(devServer: DevelopmentServer): Promise<void> {\n const restarted = await restartDevServer(devServer);\n\n if (!restarted) {\n devServerShortcuts.resume();\n devLogError(\"Restart failed — start the dev server again manually.\");\n }\n}\n\n/** `q` — the same graceful shutdown Ctrl+C performs. */\nasync function quit(devServer: DevelopmentServer): Promise<void> {\n try {\n await devServer.shutdown();\n process.exit(0);\n } catch (error) {\n devLogError(`Shutdown failed: ${(error as Error).message}`);\n process.exit(QUIT_EXIT_CODE);\n }\n}\n\n/** `h` — list whatever is armed right now, `u` included when it is. */\nfunction printShortcuts(): void {\n console.log();\n console.log(` ${colors.bold(\"Shortcuts\")}`);\n\n for (const shortcut of devServerShortcuts.list()) {\n console.log(\n ` ${colors.bold(colors.cyan(shortcut.key))} ${colors.dim(shortcut.description)}`,\n );\n }\n\n console.log(` ${colors.bold(colors.cyan(\"Ctrl+C\"))} ${colors.dim(\"quit\")}`);\n console.log();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkBA,SAAgB,qBAAqB,WAAoC;CACvE,IAAI,CAAC,mBAAmB,YAAY,GAClC;CAGF,mBAAmB,SAAS;EAC1B,KAAK;EACL,aAAa;EACb,eAAe,QAAQ,SAAS;CAClC,CAAC;CAED,mBAAmB,SAAS;EAC1B,KAAK;EACL,aAAa;EACb,eAAe,QAAQ,MAAM;CAC/B,CAAC;CAED,mBAAmB,SAAS;EAC1B,KAAK;EACL,aAAa;EACb,eAAe,KAAK,SAAS;CAC/B,CAAC;CAED,mBAAmB,SAAS;EAC1B,KAAK;EACL,aAAa;EACb,SAAS;CACX,CAAC;CAED,QAAQ,IACN,MAAM,OAAO,IAAI,OAAO,EAAE,GAAG,OAAO,KAAK,OAAO,KAAK,GAAG,CAAC,EAAE,GACtD,OAAO,IAAI,eAAe,GACjC;AACF;;AAGA,eAAe,QAAQ,WAA6C;CAGlE,IAAI,CAAC,MAFmB,iBAAiB,SAAS,GAElC;EACd,mBAAmB,OAAO;EAC1B,YAAY,uDAAuD;CACrE;AACF;;AAGA,eAAe,KAAK,WAA6C;CAC/D,IAAI;EACF,MAAM,UAAU,SAAS;EACzB,QAAQ,KAAK,CAAC;CAChB,SAAS,OAAO;EACd,YAAY,oBAAqB,MAAgB,SAAS;EAC1D,QAAQ,OAAmB;CAC7B;AACF;;AAGA,SAAS,iBAAuB;CAC9B,QAAQ,IAAI;CACZ,QAAQ,IAAI,KAAK,OAAO,KAAK,WAAW,GAAG;CAE3C,KAAK,MAAM,YAAY,mBAAmB,KAAK,GAC7C,QAAQ,IACN,QAAQ,OAAO,KAAK,OAAO,KAAK,SAAS,GAAG,CAAC,EAAE,IAAI,OAAO,IAAI,SAAS,WAAW,GACpF;CAGF,QAAQ,IAAI,QAAQ,OAAO,KAAK,OAAO,KAAK,QAAQ,CAAC,EAAE,IAAI,OAAO,IAAI,MAAM,GAAG;CAC/E,QAAQ,IAAI;AACd"}
|
|
@@ -114,7 +114,7 @@ var DevServerShortcuts = class {
|
|
|
114
114
|
*/
|
|
115
115
|
async handleKeypress(key) {
|
|
116
116
|
if (!key) return;
|
|
117
|
-
if (key.ctrl && (key.name === "c" || key.name === "d")) {
|
|
117
|
+
if (key.ctrl && (key.name === "c" || key.name === "d") || key.sequence === "") {
|
|
118
118
|
this.release();
|
|
119
119
|
this.onInterrupt();
|
|
120
120
|
return;
|
|
@@ -132,7 +132,7 @@ var DevServerShortcuts = class {
|
|
|
132
132
|
};
|
|
133
133
|
/** Re-raise the interrupt the terminal would have sent outside raw mode. */
|
|
134
134
|
function raiseInterrupt() {
|
|
135
|
-
process.
|
|
135
|
+
process.emit("SIGINT");
|
|
136
136
|
}
|
|
137
137
|
/** Process-wide shortcuts for the running dev server. */
|
|
138
138
|
const devServerShortcuts = new DevServerShortcuts();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shortcuts.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/shortcuts.ts"],"sourcesContent":["import readline from \"node:readline\";\n\n/**\n * A single-keypress shortcut offered by the dev server while it runs.\n */\nexport type DevServerShortcut = {\n /** The key that triggers it, e.g. `\"u\"`. Matched case-insensitively. */\n key: string;\n /** Short human label, used when we print the available shortcuts. */\n description: string;\n /** What to run when the key is pressed. Rejections are swallowed. */\n handler: () => void | Promise<void>;\n};\n\n/**\n * Single-keypress shortcuts for the dev server (`press u to update`, …).\n *\n * Reading one key at a time requires putting stdin into raw mode, which\n * means this process — not the terminal driver — becomes responsible for\n * `Ctrl+C`. The manager therefore re-raises `SIGINT` itself so the existing\n * graceful-shutdown handlers keep working exactly as before.\n *\n * Everything is opt-in and self-guarding: with no TTY (piped output, CI,\n * a supervisor) `register()` reports `false` and stdin is never touched, so\n * callers can fall back to printing a plain \"run `npx warlock update`\" hint.\n */\nexport class DevServerShortcuts {\n private readonly shortcuts = new Map<string, DevServerShortcut>();\n\n /** Whether we currently hold stdin in raw mode. */\n private listening = false;\n\n /** Set while a handler runs, so a second keypress can't re-enter it. */\n private busy = false;\n\n private keypressListener?: (character: string, key: KeypressEvent) => void;\n\n /**\n * @param explicitInput The stream to read keys from. Tests pass a fake TTY;\n * production code omits it and gets `process.stdin`,\n * resolved lazily — see {@link input}.\n * @param onInterrupt What `Ctrl+C` does once raw mode has taken it away\n * from the terminal driver. Defaults to re-raising\n * `SIGINT`.\n */\n public constructor(\n private readonly explicitInput?: NodeJS.ReadStream,\n private readonly onInterrupt: () => void = raiseInterrupt,\n ) {}\n\n /**\n * The stream to read keys from, resolved on first USE rather than at\n * construction.\n *\n * `devServerShortcuts` below is a process-wide singleton, and every CLI\n * command loads it merely by importing `dev-server.command.ts` —\n * `framework-cli-commands.ts` statically imports every command module up\n * front, for `warlock add`/`migrate`/`routes`/etc. just as much as for\n * `warlock dev`. A constructor-default of `process.stdin` used to run at\n * THAT import, not at `register()`, so simply running any command touched\n * `process.stdin` and made Node construct a real stdin handle nobody asked\n * for — including one-shot commands that never call `register()` and have\n * no terminal to manage. A getter defers the touch to the methods that\n * actually need a stream (`isSupported`/`register`/`listen`/`release`),\n * none of which run for a command that never offers a shortcut.\n */\n private get input(): NodeJS.ReadStream {\n return this.explicitInput ?? process.stdin;\n }\n\n /**\n * Whether the current terminal can deliver individual keypresses. False in\n * CI, when stdin is a pipe, and in any non-interactive shell.\n */\n public isSupported(): boolean {\n return Boolean(this.input.isTTY && typeof this.input.setRawMode === \"function\");\n }\n\n /**\n * Offer a shortcut. Returns whether it was actually registered — `false`\n * means the terminal can't support keypresses and the caller should print\n * a copy-and-paste command instead.\n */\n public register(shortcut: DevServerShortcut): boolean {\n if (!this.isSupported()) {\n return false;\n }\n\n this.shortcuts.set(shortcut.key.toLowerCase(), shortcut);\n this.listen();\n\n return true;\n }\n\n /** Every armed shortcut, in the order it was registered. */\n public list(): DevServerShortcut[] {\n return [...this.shortcuts.values()];\n }\n\n /** Drop a shortcut, releasing the terminal once none are left. */\n public unregister(key: string): void {\n this.shortcuts.delete(key.toLowerCase());\n\n if (this.shortcuts.size === 0) {\n this.release();\n }\n }\n\n /**\n * Hand the terminal back: leave raw mode and stop listening. Call this\n * before spawning a child process that needs stdin (the package manager\n * install), and on shutdown. Registered shortcuts are kept, so `register()`\n * (or `resume()`) can take the terminal again afterwards.\n */\n public release(): void {\n if (!this.listening) {\n return;\n }\n\n this.listening = false;\n\n if (this.keypressListener) {\n this.input.off(\"keypress\", this.keypressListener);\n this.keypressListener = undefined;\n }\n\n if (this.input.isTTY) {\n this.input.setRawMode(false);\n }\n\n this.input.pause();\n }\n\n /** Re-take the terminal after a {@link release}, if shortcuts remain. */\n public resume(): void {\n if (this.shortcuts.size > 0) {\n this.listen();\n }\n }\n\n /** Start (or keep) listening for keypresses. */\n private listen(): void {\n if (this.listening || !this.isSupported()) {\n return;\n }\n\n this.listening = true;\n\n readline.emitKeypressEvents(this.input);\n this.input.setRawMode(true);\n this.input.resume();\n\n this.keypressListener = (_character, key) => {\n void this.handleKeypress(key);\n };\n\n this.input.on(\"keypress\", this.keypressListener);\n }\n\n /**\n * Route a keypress to its shortcut. In raw mode the terminal no longer\n * turns `Ctrl+C` into a signal for us, so we translate it back into a\n * `SIGINT` and let the dev server's shutdown handlers take it from there.\n */\n private async handleKeypress(key: KeypressEvent | undefined): Promise<void> {\n if (!key) {\n return;\n }\n\n if (key.ctrl && (key.name === \"c\" || key.name === \"d\")) {\n this.release();\n this.onInterrupt();\n return;\n }\n\n if (this.busy) {\n return;\n }\n\n const shortcut = key.name ? this.shortcuts.get(key.name.toLowerCase()) : undefined;\n\n if (!shortcut) {\n return;\n }\n\n this.busy = true;\n\n try {\n await shortcut.handler();\n } catch {\n // A shortcut is a convenience — never let it take the dev server down.\n } finally {\n this.busy = false;\n }\n }\n}\n\n/** Re-raise the interrupt the terminal would have sent outside raw mode. */\nfunction raiseInterrupt(): void {\n process.kill(
|
|
1
|
+
{"version":3,"file":"shortcuts.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/shortcuts.ts"],"sourcesContent":["import readline from \"node:readline\";\n\n/**\n * A single-keypress shortcut offered by the dev server while it runs.\n */\nexport type DevServerShortcut = {\n /** The key that triggers it, e.g. `\"u\"`. Matched case-insensitively. */\n key: string;\n /** Short human label, used when we print the available shortcuts. */\n description: string;\n /** What to run when the key is pressed. Rejections are swallowed. */\n handler: () => void | Promise<void>;\n};\n\n/**\n * Single-keypress shortcuts for the dev server (`press u to update`, …).\n *\n * Reading one key at a time requires putting stdin into raw mode, which\n * means this process — not the terminal driver — becomes responsible for\n * `Ctrl+C`. The manager therefore re-raises `SIGINT` itself so the existing\n * graceful-shutdown handlers keep working exactly as before.\n *\n * Everything is opt-in and self-guarding: with no TTY (piped output, CI,\n * a supervisor) `register()` reports `false` and stdin is never touched, so\n * callers can fall back to printing a plain \"run `npx warlock update`\" hint.\n */\nexport class DevServerShortcuts {\n private readonly shortcuts = new Map<string, DevServerShortcut>();\n\n /** Whether we currently hold stdin in raw mode. */\n private listening = false;\n\n /** Set while a handler runs, so a second keypress can't re-enter it. */\n private busy = false;\n\n private keypressListener?: (character: string, key: KeypressEvent) => void;\n\n /**\n * @param explicitInput The stream to read keys from. Tests pass a fake TTY;\n * production code omits it and gets `process.stdin`,\n * resolved lazily — see {@link input}.\n * @param onInterrupt What `Ctrl+C` does once raw mode has taken it away\n * from the terminal driver. Defaults to re-raising\n * `SIGINT`.\n */\n public constructor(\n private readonly explicitInput?: NodeJS.ReadStream,\n private readonly onInterrupt: () => void = raiseInterrupt,\n ) {}\n\n /**\n * The stream to read keys from, resolved on first USE rather than at\n * construction.\n *\n * `devServerShortcuts` below is a process-wide singleton, and every CLI\n * command loads it merely by importing `dev-server.command.ts` —\n * `framework-cli-commands.ts` statically imports every command module up\n * front, for `warlock add`/`migrate`/`routes`/etc. just as much as for\n * `warlock dev`. A constructor-default of `process.stdin` used to run at\n * THAT import, not at `register()`, so simply running any command touched\n * `process.stdin` and made Node construct a real stdin handle nobody asked\n * for — including one-shot commands that never call `register()` and have\n * no terminal to manage. A getter defers the touch to the methods that\n * actually need a stream (`isSupported`/`register`/`listen`/`release`),\n * none of which run for a command that never offers a shortcut.\n */\n private get input(): NodeJS.ReadStream {\n return this.explicitInput ?? process.stdin;\n }\n\n /**\n * Whether the current terminal can deliver individual keypresses. False in\n * CI, when stdin is a pipe, and in any non-interactive shell.\n */\n public isSupported(): boolean {\n return Boolean(this.input.isTTY && typeof this.input.setRawMode === \"function\");\n }\n\n /**\n * Offer a shortcut. Returns whether it was actually registered — `false`\n * means the terminal can't support keypresses and the caller should print\n * a copy-and-paste command instead.\n */\n public register(shortcut: DevServerShortcut): boolean {\n if (!this.isSupported()) {\n return false;\n }\n\n this.shortcuts.set(shortcut.key.toLowerCase(), shortcut);\n this.listen();\n\n return true;\n }\n\n /** Every armed shortcut, in the order it was registered. */\n public list(): DevServerShortcut[] {\n return [...this.shortcuts.values()];\n }\n\n /** Drop a shortcut, releasing the terminal once none are left. */\n public unregister(key: string): void {\n this.shortcuts.delete(key.toLowerCase());\n\n if (this.shortcuts.size === 0) {\n this.release();\n }\n }\n\n /**\n * Hand the terminal back: leave raw mode and stop listening. Call this\n * before spawning a child process that needs stdin (the package manager\n * install), and on shutdown. Registered shortcuts are kept, so `register()`\n * (or `resume()`) can take the terminal again afterwards.\n */\n public release(): void {\n if (!this.listening) {\n return;\n }\n\n this.listening = false;\n\n if (this.keypressListener) {\n this.input.off(\"keypress\", this.keypressListener);\n this.keypressListener = undefined;\n }\n\n if (this.input.isTTY) {\n this.input.setRawMode(false);\n }\n\n this.input.pause();\n }\n\n /** Re-take the terminal after a {@link release}, if shortcuts remain. */\n public resume(): void {\n if (this.shortcuts.size > 0) {\n this.listen();\n }\n }\n\n /** Start (or keep) listening for keypresses. */\n private listen(): void {\n if (this.listening || !this.isSupported()) {\n return;\n }\n\n this.listening = true;\n\n readline.emitKeypressEvents(this.input);\n this.input.setRawMode(true);\n this.input.resume();\n\n this.keypressListener = (_character, key) => {\n void this.handleKeypress(key);\n };\n\n this.input.on(\"keypress\", this.keypressListener);\n }\n\n /**\n * Route a keypress to its shortcut. In raw mode the terminal no longer\n * turns `Ctrl+C` into a signal for us, so we translate it back into a\n * `SIGINT` and let the dev server's shutdown handlers take it from there.\n */\n private async handleKeypress(key: KeypressEvent | undefined): Promise<void> {\n if (!key) {\n return;\n }\n\n if ((key.ctrl && (key.name === \"c\" || key.name === \"d\")) || key.sequence === \"\\u0003\") {\n this.release();\n this.onInterrupt();\n return;\n }\n\n if (this.busy) {\n return;\n }\n\n const shortcut = key.name ? this.shortcuts.get(key.name.toLowerCase()) : undefined;\n\n if (!shortcut) {\n return;\n }\n\n this.busy = true;\n\n try {\n await shortcut.handler();\n } catch {\n // A shortcut is a convenience — never let it take the dev server down.\n } finally {\n this.busy = false;\n }\n }\n}\n\n/** Re-raise the interrupt the terminal would have sent outside raw mode. */\nfunction raiseInterrupt(): void {\n // `process.kill(pid, \"SIGINT\")` is TerminateProcess on Windows, skipping the\n // graceful shutdown; emitting runs the registered handlers on every OS.\n process.emit(\"SIGINT\");\n}\n\n/** The keypress shape emitted by `readline.emitKeypressEvents`. */\ntype KeypressEvent = {\n name?: string;\n sequence?: string;\n ctrl?: boolean;\n meta?: boolean;\n shift?: boolean;\n};\n\n/** Process-wide shortcuts for the running dev server. */\nexport const devServerShortcuts = new DevServerShortcuts();\n"],"mappings":";;;;;;;;;;;;;;;AA0BA,IAAa,qBAAb,MAAgC;;;;;;;;;CAmB9B,AAAO,YACL,AAAiB,eACjB,AAAiB,cAA0B,gBAC3C;EAFiB;EACA;mCApBU,IAAI,IAA+B;mBAG5C;cAGL;CAeZ;;;;;;;;;;;;;;;;;CAkBH,IAAY,QAA2B;EACrC,OAAO,KAAK,iBAAiB,QAAQ;CACvC;;;;;CAMA,AAAO,cAAuB;EAC5B,OAAO,QAAQ,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM,eAAe,UAAU;CAChF;;;;;;CAOA,AAAO,SAAS,UAAsC;EACpD,IAAI,CAAC,KAAK,YAAY,GACpB,OAAO;EAGT,KAAK,UAAU,IAAI,SAAS,IAAI,YAAY,GAAG,QAAQ;EACvD,KAAK,OAAO;EAEZ,OAAO;CACT;;CAGA,AAAO,OAA4B;EACjC,OAAO,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC;CACpC;;CAGA,AAAO,WAAW,KAAmB;EACnC,KAAK,UAAU,OAAO,IAAI,YAAY,CAAC;EAEvC,IAAI,KAAK,UAAU,SAAS,GAC1B,KAAK,QAAQ;CAEjB;;;;;;;CAQA,AAAO,UAAgB;EACrB,IAAI,CAAC,KAAK,WACR;EAGF,KAAK,YAAY;EAEjB,IAAI,KAAK,kBAAkB;GACzB,KAAK,MAAM,IAAI,YAAY,KAAK,gBAAgB;GAChD,KAAK,mBAAmB;EAC1B;EAEA,IAAI,KAAK,MAAM,OACb,KAAK,MAAM,WAAW,KAAK;EAG7B,KAAK,MAAM,MAAM;CACnB;;CAGA,AAAO,SAAe;EACpB,IAAI,KAAK,UAAU,OAAO,GACxB,KAAK,OAAO;CAEhB;;CAGA,AAAQ,SAAe;EACrB,IAAI,KAAK,aAAa,CAAC,KAAK,YAAY,GACtC;EAGF,KAAK,YAAY;EAEjB,SAAS,mBAAmB,KAAK,KAAK;EACtC,KAAK,MAAM,WAAW,IAAI;EAC1B,KAAK,MAAM,OAAO;EAElB,KAAK,oBAAoB,YAAY,QAAQ;GAC3C,AAAK,KAAK,eAAe,GAAG;EAC9B;EAEA,KAAK,MAAM,GAAG,YAAY,KAAK,gBAAgB;CACjD;;;;;;CAOA,MAAc,eAAe,KAA+C;EAC1E,IAAI,CAAC,KACH;EAGF,IAAK,IAAI,SAAS,IAAI,SAAS,OAAO,IAAI,SAAS,QAAS,IAAI,aAAa,KAAU;GACrF,KAAK,QAAQ;GACb,KAAK,YAAY;GACjB;EACF;EAEA,IAAI,KAAK,MACP;EAGF,MAAM,WAAW,IAAI,OAAO,KAAK,UAAU,IAAI,IAAI,KAAK,YAAY,CAAC,IAAI;EAEzE,IAAI,CAAC,UACH;EAGF,KAAK,OAAO;EAEZ,IAAI;GACF,MAAM,SAAS,QAAQ;EACzB,QAAQ,CAER,UAAU;GACR,KAAK,OAAO;EACd;CACF;AACF;;AAGA,SAAS,iBAAuB;CAG9B,QAAQ,KAAK,QAAQ;AACvB;;AAYA,MAAa,qBAAqB,IAAI,mBAAmB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region ../core/src/dev-server/special-file-patterns.ts
|
|
2
|
+
const normalize = (path) => path.replace(/\\/g, "/");
|
|
3
|
+
/** `src/config/**` — any depth, the config key is the raw path */
|
|
4
|
+
const isConfigFile = (path) => /^src\/config\/.*\.(ts|tsx)$/.test(normalize(path));
|
|
5
|
+
/** `src/app/main.ts` or `src/app/<module>/main.ts` */
|
|
6
|
+
const isMainFile = (path) => /^src\/app\/([^/]+\/)?main\.(ts|tsx)$/.test(normalize(path));
|
|
7
|
+
/** `src/app/<module>/routes.ts` — one level deep */
|
|
8
|
+
const isRouteFile = (path) => /^src\/app\/[^/]+\/routes\.(ts|tsx)$/.test(normalize(path));
|
|
9
|
+
/** `src/app/<module>/events/<file>.ts` */
|
|
10
|
+
const isEventFile = (path) => /^src\/app\/[^/]+\/events\/[^/]+\.(ts|tsx)$/.test(normalize(path));
|
|
11
|
+
/** `src/app/<module>/utils/locales.ts` */
|
|
12
|
+
const isLocaleFile = (path) => /^src\/app\/[^/]+\/utils\/locales\.(ts|tsx)$/.test(normalize(path));
|
|
13
|
+
/** Source files the dev server can hot-reload; anything else is data (`.sql`, `.json`, …) */
|
|
14
|
+
const isCodeFile = (path) => /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/.test(normalize(path));
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
export { isCodeFile, isConfigFile, isEventFile, isLocaleFile, isMainFile, isRouteFile };
|
|
18
|
+
//# sourceMappingURL=special-file-patterns.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"special-file-patterns.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/special-file-patterns.ts"],"sourcesContent":["const normalize = (path: string) => path.replace(/\\\\/g, \"/\");\n\n/** `src/config/**` — any depth, the config key is the raw path */\nexport const isConfigFile = (path: string) => /^src\\/config\\/.*\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/main.ts` or `src/app/<module>/main.ts` */\nexport const isMainFile = (path: string) =>\n /^src\\/app\\/([^/]+\\/)?main\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/routes.ts` — one level deep */\nexport const isRouteFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/routes\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/events/<file>.ts` */\nexport const isEventFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/events\\/[^/]+\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/utils/locales.ts` */\nexport const isLocaleFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/utils\\/locales\\.(ts|tsx)$/.test(normalize(path));\n\n/** Source files the dev server can hot-reload; anything else is data (`.sql`, `.json`, …) */\nexport const isCodeFile = (path: string) =>\n /\\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/.test(normalize(path));\n"],"mappings":";AAAA,MAAM,aAAa,SAAiB,KAAK,QAAQ,OAAO,GAAG;;AAG3D,MAAa,gBAAgB,SAAiB,8BAA8B,KAAK,UAAU,IAAI,CAAC;;AAGhG,MAAa,cAAc,SACzB,uCAAuC,KAAK,UAAU,IAAI,CAAC;;AAG7D,MAAa,eAAe,SAC1B,sCAAsC,KAAK,UAAU,IAAI,CAAC;;AAG5D,MAAa,eAAe,SAC1B,6CAA6C,KAAK,UAAU,IAAI,CAAC;;AAGnE,MAAa,gBAAgB,SAC3B,8CAA8C,KAAK,UAAU,IAAI,CAAC;;AAGpE,MAAa,cAAc,SACzB,qCAAqC,KAAK,UAAU,IAAI,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"special-files-collector.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/special-files-collector.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"special-files-collector.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/special-files-collector.ts"],"mappings":";;;KASY,eAAA;;AAAZ;;;cAMa,qBAAA;EAAA,iBACM,OAAA;EAQV,OAAA,CAAQ,KAAA,EAAO,GAAA,SAAY,WAAA;EAO3B,OAAA,CAAQ,WAAA,EAAa,WAAA;EAIrB,UAAA,CAAW,YAAA;EAIX,UAAA,CAAW,WAAA,EAAa,WAAA;EAKxB,WAAA,CAAY,YAAA,WAAuB,eAAA;EAUnC,cAAA,CAAe,IAAA,EAAM,eAAA,GAAkB,WAAA;EAUvC,QAAA,IAAY,MAAA,CAAO,eAAA;EAUnB,KAAA;EAAA,QAIC,UAAA;AAAA"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isConfigFile, isEventFile, isLocaleFile, isMainFile, isRouteFile } from "./special-file-patterns.mjs";
|
|
2
|
+
|
|
1
3
|
//#region ../core/src/dev-server/special-files-collector.ts
|
|
2
4
|
/**
|
|
3
5
|
* Categorises files into the kinds the dev server treats specially
|
|
@@ -56,11 +58,6 @@ var SpecialFilesCollector = class {
|
|
|
56
58
|
else if (isLocaleFile(relativePath)) this.buckets.locale.set(relativePath, fileManager);
|
|
57
59
|
}
|
|
58
60
|
};
|
|
59
|
-
const isConfigFile = (path) => /^src\/config\/.*\.(ts|tsx)$/.test(path);
|
|
60
|
-
const isMainFile = (path) => /^src\/app\/[^/]+\/main\.(ts|tsx)$/.test(path) || path === "src/app/main.ts" || path === "src/app/main.tsx";
|
|
61
|
-
const isRouteFile = (path) => /^src\/app\/[^/]+\/routes\.(ts|tsx)$/.test(path);
|
|
62
|
-
const isEventFile = (path) => /^src\/app\/[^/]+\/events\/[^/]+\.(ts|tsx)$/.test(path);
|
|
63
|
-
const isLocaleFile = (path) => /^src\/app\/[^/]+\/utils\/locales\.(ts|tsx)$/.test(path);
|
|
64
61
|
|
|
65
62
|
//#endregion
|
|
66
63
|
export { SpecialFilesCollector };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"special-files-collector.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/special-files-collector.ts"],"sourcesContent":["import { FileManager } from \"./file-manager\";\r\n\r\nexport type SpecialFileType = \"config\" | \"main\" | \"route\" | \"event\" | \"locale\";\r\n\r\n/**\r\n * Categorises files into the kinds the dev server treats specially\r\n * (config / main / route / event / locale) and exposes typed accessors.\r\n */\r\nexport class SpecialFilesCollector {\r\n private readonly buckets: Record<SpecialFileType, Map<string, FileManager>> = {\r\n config: new Map(),\r\n main: new Map(),\r\n route: new Map(),\r\n event: new Map(),\r\n locale: new Map(),\r\n };\r\n\r\n public collect(files: Map<string, FileManager>): void {\r\n for (const bucket of Object.values(this.buckets)) bucket.clear();\r\n for (const [relativePath, fileManager] of files) {\r\n this.categorise(relativePath, fileManager);\r\n }\r\n }\r\n\r\n public addFile(fileManager: FileManager): void {\r\n this.categorise(fileManager.relativePath, fileManager);\r\n }\r\n\r\n public removeFile(relativePath: string): void {\r\n for (const bucket of Object.values(this.buckets)) bucket.delete(relativePath);\r\n }\r\n\r\n public updateFile(fileManager: FileManager): void {\r\n this.removeFile(fileManager.relativePath);\r\n this.categorise(fileManager.relativePath, fileManager);\r\n }\r\n\r\n public getFileType(relativePath: string): SpecialFileType | null {\r\n for (const [type, bucket] of Object.entries(this.buckets) as [\r\n SpecialFileType,\r\n Map<string, FileManager>,\r\n ][]) {\r\n if (bucket.has(relativePath)) return type;\r\n }\r\n return null;\r\n }\r\n\r\n public getFilesByType(type: SpecialFileType): FileManager[] {\r\n const files = Array.from(this.buckets[type].values());\r\n // Routes are loaded sequentially in alphabetical order so registration\r\n // is deterministic across runs.\r\n if (type === \"route\") {\r\n files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));\r\n }\r\n return files;\r\n }\r\n\r\n public getStats(): Record<SpecialFileType, number> {\r\n return {\r\n config: this.buckets.config.size,\r\n main: this.buckets.main.size,\r\n route: this.buckets.route.size,\r\n event: this.buckets.event.size,\r\n locale: this.buckets.locale.size,\r\n };\r\n }\r\n\r\n public clear(): void {\r\n for (const bucket of Object.values(this.buckets)) bucket.clear();\r\n }\r\n\r\n private categorise(relativePath: string, fileManager: FileManager): void {\r\n if (isConfigFile(relativePath)) {\r\n this.buckets.config.set(relativePath, fileManager);\r\n } else if (isMainFile(relativePath)) {\r\n this.buckets.main.set(relativePath, fileManager);\r\n } else if (isRouteFile(relativePath)) {\r\n this.buckets.route.set(relativePath, fileManager);\r\n } else if (isEventFile(relativePath)) {\r\n this.buckets.event.set(relativePath, fileManager);\r\n } else if (isLocaleFile(relativePath)) {\r\n this.buckets.locale.set(relativePath, fileManager);\r\n }\r\n }\r\n}\r\n\r\
|
|
1
|
+
{"version":3,"file":"special-files-collector.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/special-files-collector.ts"],"sourcesContent":["import { FileManager } from \"./file-manager\";\r\nimport {\r\n isConfigFile,\r\n isEventFile,\r\n isLocaleFile,\r\n isMainFile,\r\n isRouteFile,\r\n} from \"./special-file-patterns\";\r\n\r\nexport type SpecialFileType = \"config\" | \"main\" | \"route\" | \"event\" | \"locale\";\r\n\r\n/**\r\n * Categorises files into the kinds the dev server treats specially\r\n * (config / main / route / event / locale) and exposes typed accessors.\r\n */\r\nexport class SpecialFilesCollector {\r\n private readonly buckets: Record<SpecialFileType, Map<string, FileManager>> = {\r\n config: new Map(),\r\n main: new Map(),\r\n route: new Map(),\r\n event: new Map(),\r\n locale: new Map(),\r\n };\r\n\r\n public collect(files: Map<string, FileManager>): void {\r\n for (const bucket of Object.values(this.buckets)) bucket.clear();\r\n for (const [relativePath, fileManager] of files) {\r\n this.categorise(relativePath, fileManager);\r\n }\r\n }\r\n\r\n public addFile(fileManager: FileManager): void {\r\n this.categorise(fileManager.relativePath, fileManager);\r\n }\r\n\r\n public removeFile(relativePath: string): void {\r\n for (const bucket of Object.values(this.buckets)) bucket.delete(relativePath);\r\n }\r\n\r\n public updateFile(fileManager: FileManager): void {\r\n this.removeFile(fileManager.relativePath);\r\n this.categorise(fileManager.relativePath, fileManager);\r\n }\r\n\r\n public getFileType(relativePath: string): SpecialFileType | null {\r\n for (const [type, bucket] of Object.entries(this.buckets) as [\r\n SpecialFileType,\r\n Map<string, FileManager>,\r\n ][]) {\r\n if (bucket.has(relativePath)) return type;\r\n }\r\n return null;\r\n }\r\n\r\n public getFilesByType(type: SpecialFileType): FileManager[] {\r\n const files = Array.from(this.buckets[type].values());\r\n // Routes are loaded sequentially in alphabetical order so registration\r\n // is deterministic across runs.\r\n if (type === \"route\") {\r\n files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));\r\n }\r\n return files;\r\n }\r\n\r\n public getStats(): Record<SpecialFileType, number> {\r\n return {\r\n config: this.buckets.config.size,\r\n main: this.buckets.main.size,\r\n route: this.buckets.route.size,\r\n event: this.buckets.event.size,\r\n locale: this.buckets.locale.size,\r\n };\r\n }\r\n\r\n public clear(): void {\r\n for (const bucket of Object.values(this.buckets)) bucket.clear();\r\n }\r\n\r\n private categorise(relativePath: string, fileManager: FileManager): void {\r\n if (isConfigFile(relativePath)) {\r\n this.buckets.config.set(relativePath, fileManager);\r\n } else if (isMainFile(relativePath)) {\r\n this.buckets.main.set(relativePath, fileManager);\r\n } else if (isRouteFile(relativePath)) {\r\n this.buckets.route.set(relativePath, fileManager);\r\n } else if (isEventFile(relativePath)) {\r\n this.buckets.event.set(relativePath, fileManager);\r\n } else if (isLocaleFile(relativePath)) {\r\n this.buckets.locale.set(relativePath, fileManager);\r\n }\r\n }\r\n}\r\n\r\n"],"mappings":";;;;;;;AAeA,IAAa,wBAAb,MAAmC;;iBAC6C;GAC5E,wBAAQ,IAAI,IAAI;GAChB,sBAAM,IAAI,IAAI;GACd,uBAAO,IAAI,IAAI;GACf,uBAAO,IAAI,IAAI;GACf,wBAAQ,IAAI,IAAI;EAClB;;CAEA,AAAO,QAAQ,OAAuC;EACpD,KAAK,MAAM,UAAU,OAAO,OAAO,KAAK,OAAO,GAAG,OAAO,MAAM;EAC/D,KAAK,MAAM,CAAC,cAAc,gBAAgB,OACxC,KAAK,WAAW,cAAc,WAAW;CAE7C;CAEA,AAAO,QAAQ,aAAgC;EAC7C,KAAK,WAAW,YAAY,cAAc,WAAW;CACvD;CAEA,AAAO,WAAW,cAA4B;EAC5C,KAAK,MAAM,UAAU,OAAO,OAAO,KAAK,OAAO,GAAG,OAAO,OAAO,YAAY;CAC9E;CAEA,AAAO,WAAW,aAAgC;EAChD,KAAK,WAAW,YAAY,YAAY;EACxC,KAAK,WAAW,YAAY,cAAc,WAAW;CACvD;CAEA,AAAO,YAAY,cAA8C;EAC/D,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,KAAK,OAAO,GAItD,IAAI,OAAO,IAAI,YAAY,GAAG,OAAO;EAEvC,OAAO;CACT;CAEA,AAAO,eAAe,MAAsC;EAC1D,MAAM,QAAQ,MAAM,KAAK,KAAK,QAAQ,KAAK,CAAC,OAAO,CAAC;EAGpD,IAAI,SAAS,SACX,MAAM,MAAM,GAAG,MAAM,EAAE,aAAa,cAAc,EAAE,YAAY,CAAC;EAEnE,OAAO;CACT;CAEA,AAAO,WAA4C;EACjD,OAAO;GACL,QAAQ,KAAK,QAAQ,OAAO;GAC5B,MAAM,KAAK,QAAQ,KAAK;GACxB,OAAO,KAAK,QAAQ,MAAM;GAC1B,OAAO,KAAK,QAAQ,MAAM;GAC1B,QAAQ,KAAK,QAAQ,OAAO;EAC9B;CACF;CAEA,AAAO,QAAc;EACnB,KAAK,MAAM,UAAU,OAAO,OAAO,KAAK,OAAO,GAAG,OAAO,MAAM;CACjE;CAEA,AAAQ,WAAW,cAAsB,aAAgC;EACvE,IAAI,aAAa,YAAY,GAC3B,KAAK,QAAQ,OAAO,IAAI,cAAc,WAAW;OAC5C,IAAI,WAAW,YAAY,GAChC,KAAK,QAAQ,KAAK,IAAI,cAAc,WAAW;OAC1C,IAAI,YAAY,YAAY,GACjC,KAAK,QAAQ,MAAM,IAAI,cAAc,WAAW;OAC3C,IAAI,YAAY,YAAY,GACjC,KAAK,QAAQ,MAAM,IAAI,cAAc,WAAW;OAC3C,IAAI,aAAa,YAAY,GAClC,KAAK,QAAQ,OAAO,IAAI,cAAc,WAAW;CAErD;AACF"}
|
|
@@ -24,7 +24,6 @@ async function startDevelopmentServer(options = {}) {
|
|
|
24
24
|
try {
|
|
25
25
|
await devServer.start();
|
|
26
26
|
} catch (error) {
|
|
27
|
-
devLogError(`Failed to start Development Server: ${error.message}`);
|
|
28
27
|
await safeShutdown(devServer);
|
|
29
28
|
process.exit(isBootPreconditionError(error) ? 78 : 1);
|
|
30
29
|
}
|
|
@@ -42,7 +41,7 @@ function registerShutdownHandlers(devServer) {
|
|
|
42
41
|
const onSignal = (signal) => async () => {
|
|
43
42
|
devServeLog(colors.yellow(`📡 Received ${signal}`));
|
|
44
43
|
const ok = await safeShutdown(devServer);
|
|
45
|
-
process.exit(ok ? 0 :
|
|
44
|
+
process.exit(ok ? 0 : 79);
|
|
46
45
|
};
|
|
47
46
|
process.on("SIGINT", onSignal("SIGINT"));
|
|
48
47
|
process.on("SIGTERM", onSignal("SIGTERM"));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-development-server.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/start-development-server.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { isBootPreconditionError } from \"./boot-precondition-error\";\nimport { devLogError, devServeLog } from \"./dev-logger\";\nimport { DevelopmentServer } from \"./development-server\";\nimport { registerDevShortcuts } from \"./register-dev-shortcuts\";\nimport { BOOT_PRECONDITION_EXIT_CODE } from \"./supervisor\";\n\nlet handlersRegistered = false;\n\n/**\n * Options accepted by `startDevelopmentServer`. CLI flags translate to\n * these; programmatic callers (tests, scripts) can pass them directly.\n *\n * `undefined` for any field means \"fall through to `warlock.config.ts >\n * devServer.*` defaults.\" Pass an explicit value only to override.\n */\nexport type StartDevServerOptions = {\n /**\n * Delete `.warlock/manifest.json` before starting — forces every file\n * to be re-parsed from disk instead of restored from the cached graph.\n */\n fresh?: boolean;\n /** Run background type generation. Default from devServer config (true). */\n generateTypings?: boolean;\n /** Run file health checkers. Default from devServer config (true). */\n healthCheckers?: boolean;\n};\n\n/**\n * Start the development server.\n *\n * Boots the file orchestrator, loads app modules, starts late-phase\n * connectors, then wires graceful shutdown for SIGINT/SIGTERM (plus\n * SIGHUP on Windows where Ctrl+C in spawned children lands as SIGHUP).\n *\n * If startup throws, shuts down cleanly and exits 1 — dev should die\n * loudly rather than hang with a half-initialized state.\n */\nexport async function startDevelopmentServer(\n options: StartDevServerOptions = {},\n): Promise<DevelopmentServer> {\n // V8 honours inline source maps embedded by the transpile cache (and\n // tsx's own maps in passthrough mode), so thrown-error stack frames point\n // at the original `src/**.ts` line/column instead of transpiled output.\n process.setSourceMapsEnabled(true);\n\n const devServer = new DevelopmentServer(options);\n\n registerShutdownHandlers(devServer);\n\n try {\n await devServer.start();\n } catch (error) {\n
|
|
1
|
+
{"version":3,"file":"start-development-server.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/start-development-server.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { isBootPreconditionError } from \"./boot-precondition-error\";\nimport { devLogError, devServeLog } from \"./dev-logger\";\nimport { DevelopmentServer } from \"./development-server\";\nimport { registerDevShortcuts } from \"./register-dev-shortcuts\";\nimport { BOOT_PRECONDITION_EXIT_CODE, QUIT_EXIT_CODE } from \"./supervisor\";\n\nlet handlersRegistered = false;\n\n/**\n * Options accepted by `startDevelopmentServer`. CLI flags translate to\n * these; programmatic callers (tests, scripts) can pass them directly.\n *\n * `undefined` for any field means \"fall through to `warlock.config.ts >\n * devServer.*` defaults.\" Pass an explicit value only to override.\n */\nexport type StartDevServerOptions = {\n /**\n * Delete `.warlock/manifest.json` before starting — forces every file\n * to be re-parsed from disk instead of restored from the cached graph.\n */\n fresh?: boolean;\n /** Run background type generation. Default from devServer config (true). */\n generateTypings?: boolean;\n /** Run file health checkers. Default from devServer config (true). */\n healthCheckers?: boolean;\n};\n\n/**\n * Start the development server.\n *\n * Boots the file orchestrator, loads app modules, starts late-phase\n * connectors, then wires graceful shutdown for SIGINT/SIGTERM (plus\n * SIGHUP on Windows where Ctrl+C in spawned children lands as SIGHUP).\n *\n * If startup throws, shuts down cleanly and exits 1 — dev should die\n * loudly rather than hang with a half-initialized state.\n */\nexport async function startDevelopmentServer(\n options: StartDevServerOptions = {},\n): Promise<DevelopmentServer> {\n // V8 honours inline source maps embedded by the transpile cache (and\n // tsx's own maps in passthrough mode), so thrown-error stack frames point\n // at the original `src/**.ts` line/column instead of transpiled output.\n process.setSourceMapsEnabled(true);\n\n const devServer = new DevelopmentServer(options);\n\n registerShutdownHandlers(devServer);\n\n try {\n await devServer.start();\n } catch (error) {\n // `DevelopmentServer.start()` already logged the failure once.\n await safeShutdown(devServer);\n\n // A boot-precondition failure (busy port, rejected validator, ...) has\n // already printed a good message above — exit with the dedicated code so\n // the supervisor treats it as terminal instead of restart-looping into\n // the same failure. Every other error keeps exit 1, the ordinary\n // restart-worthy path.\n process.exit(isBootPreconditionError(error) ? BOOT_PRECONDITION_EXIT_CODE : 1);\n }\n\n // Only once the server is actually up — a half-started process has nothing\n // worth restarting, and taking raw mode before then would swallow the\n // Ctrl+C of a developer trying to escape a slow boot.\n registerDevShortcuts(devServer);\n\n return devServer;\n}\n\n/**\n * Register signal handlers once per process. Guards against handler\n * accumulation if `startDevelopmentServer` is invoked more than once\n * (tests, programmatic restart).\n */\nfunction registerShutdownHandlers(devServer: DevelopmentServer): void {\n if (handlersRegistered) return;\n handlersRegistered = true;\n\n const onSignal = (signal: NodeJS.Signals) => async () => {\n devServeLog(colors.yellow(`📡 Received ${signal}`));\n const ok = await safeShutdown(devServer);\n process.exit(ok ? 0 : QUIT_EXIT_CODE);\n };\n\n process.on(\"SIGINT\", onSignal(\"SIGINT\"));\n process.on(\"SIGTERM\", onSignal(\"SIGTERM\"));\n // Ctrl+C in spawned Windows children lands as SIGHUP, not SIGINT.\n if (process.platform === \"win32\") {\n process.on(\"SIGHUP\", onSignal(\"SIGHUP\"));\n }\n}\n\n/**\n * Shut down the dev server without swallowing async errors. Returns\n * whether shutdown completed cleanly so the caller picks the exit code.\n */\nasync function safeShutdown(devServer: DevelopmentServer): Promise<boolean> {\n try {\n await devServer.shutdown();\n return true;\n } catch (error) {\n devLogError(`Shutdown failed: ${(error as Error).message}`);\n return false;\n }\n}\n"],"mappings":";;;;;;;;AAOA,IAAI,qBAAqB;;;;;;;;;;;AA+BzB,eAAsB,uBACpB,UAAiC,CAAC,GACN;CAI5B,QAAQ,qBAAqB,IAAI;CAEjC,MAAM,YAAY,IAAI,kBAAkB,OAAO;CAE/C,yBAAyB,SAAS;CAElC,IAAI;EACF,MAAM,UAAU,MAAM;CACxB,SAAS,OAAO;EAEd,MAAM,aAAa,SAAS;EAO5B,QAAQ,KAAK,wBAAwB,KAAK,SAAkC,CAAC;CAC/E;CAKA,qBAAqB,SAAS;CAE9B,OAAO;AACT;;;;;;AAOA,SAAS,yBAAyB,WAAoC;CACpE,IAAI,oBAAoB;CACxB,qBAAqB;CAErB,MAAM,YAAY,WAA2B,YAAY;EACvD,YAAY,OAAO,OAAO,eAAe,QAAQ,CAAC;EAClD,MAAM,KAAK,MAAM,aAAa,SAAS;EACvC,QAAQ,KAAK,KAAK,MAAkB;CACtC;CAEA,QAAQ,GAAG,UAAU,SAAS,QAAQ,CAAC;CACvC,QAAQ,GAAG,WAAW,SAAS,SAAS,CAAC;CAEzC,IAAI,QAAQ,aAAa,SACvB,QAAQ,GAAG,UAAU,SAAS,QAAQ,CAAC;AAE3C;;;;;AAMA,eAAe,aAAa,WAAgD;CAC1E,IAAI;EACF,MAAM,UAAU,SAAS;EACzB,OAAO;CACT,SAAS,OAAO;EACd,YAAY,oBAAqB,MAAgB,SAAS;EAC1D,OAAO;CACT;AACF"}
|
|
@@ -25,6 +25,12 @@ const RESTART_EXIT_CODE = 75;
|
|
|
25
25
|
* "restarting" line and then reprint the same failure a moment later.
|
|
26
26
|
*/
|
|
27
27
|
const BOOT_PRECONDITION_EXIT_CODE = 78;
|
|
28
|
+
/**
|
|
29
|
+
* Exit code a worker uses when the user deliberately quit (Ctrl+C, `q`, a
|
|
30
|
+
* signal) but the connector shutdown failed. The supervisor honours it as
|
|
31
|
+
* final — never a crash to recover from — and exits 1.
|
|
32
|
+
*/
|
|
33
|
+
const QUIT_EXIT_CODE = 79;
|
|
28
34
|
/** Signals the supervisor must hand to the worker rather than act on itself. */
|
|
29
35
|
const FORWARDED_SIGNALS = ["SIGTERM", "SIGHUP"];
|
|
30
36
|
/**
|
|
@@ -32,6 +38,8 @@ const FORWARDED_SIGNALS = ["SIGTERM", "SIGHUP"];
|
|
|
32
38
|
* worth recovering from rather than a failed boot worth reporting once.
|
|
33
39
|
*/
|
|
34
40
|
const HEALTHY_UPTIME_MS = 5e3;
|
|
41
|
+
/** IPC message the worker sends once boot completes; uptime counts from it. */
|
|
42
|
+
const WORKER_BOOTED_MESSAGE = "warlock:booted";
|
|
35
43
|
/** Crashes allowed inside {@link CRASH_WINDOW_MS} before we stop restarting. */
|
|
36
44
|
const CRASH_RESTART_LIMIT = 3;
|
|
37
45
|
/** The rolling window flapping is measured over. */
|
|
@@ -65,17 +73,25 @@ function superviseDevServer(now = Date.now) {
|
|
|
65
73
|
/** Timestamps of recent crash-restarts, used to detect flapping. */
|
|
66
74
|
let crashes = [];
|
|
67
75
|
const spawnWorker = () => {
|
|
68
|
-
|
|
76
|
+
let bootedAt;
|
|
69
77
|
const spawnedWorker = spawn(process.execPath, [...process.execArgv, ...process.argv.slice(1)], {
|
|
70
78
|
cwd: process.cwd(),
|
|
71
79
|
env: {
|
|
72
80
|
...process.env,
|
|
73
81
|
[WORKER_ENV_FLAG]: "1"
|
|
74
82
|
},
|
|
75
|
-
stdio:
|
|
83
|
+
stdio: [
|
|
84
|
+
"inherit",
|
|
85
|
+
"inherit",
|
|
86
|
+
"inherit",
|
|
87
|
+
"ipc"
|
|
88
|
+
],
|
|
76
89
|
windowsHide: false
|
|
77
90
|
});
|
|
78
91
|
worker = spawnedWorker;
|
|
92
|
+
spawnedWorker.on("message", (message) => {
|
|
93
|
+
if (message === "warlock:booted") bootedAt = now();
|
|
94
|
+
});
|
|
79
95
|
spawnedWorker.on("error", (error) => {
|
|
80
96
|
if (worker !== spawnedWorker) return;
|
|
81
97
|
shuttingDown = true;
|
|
@@ -98,7 +114,11 @@ function superviseDevServer(now = Date.now) {
|
|
|
98
114
|
process.exit(code);
|
|
99
115
|
return;
|
|
100
116
|
}
|
|
101
|
-
if (
|
|
117
|
+
if (code === 79) {
|
|
118
|
+
process.exit(1);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if ((signal !== null || (code ?? 0) !== 0) && shouldRecoverFromCrash(bootedAt === void 0 ? 0 : now() - bootedAt)) {
|
|
102
122
|
devLogWarn(`Development server ${signal ? `was killed by ${signal}` : `exited with code ${code}`} — restarting.`);
|
|
103
123
|
spawnWorker();
|
|
104
124
|
return;
|