@warlock.js/core 5.2.2 → 5.2.4
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 +13 -0
- package/esm/application/application.d.mts.map +1 -1
- package/esm/application/application.mjs.map +1 -1
- package/esm/benchmark/benchmark-snapshots.d.mts.map +1 -1
- package/esm/benchmark/benchmark.d.mts.map +1 -1
- package/esm/benchmark/channels/console.channel.mjs.map +1 -1
- package/esm/benchmark/channels/noop.channel.d.mts.map +1 -1
- package/esm/benchmark/profiler.d.mts.map +1 -1
- package/esm/benchmark/profiler.mjs.map +1 -1
- package/esm/bootstrap/setup.d.mts.map +1 -1
- package/esm/bootstrap/setup.mjs.map +1 -1
- package/esm/bootstrap.d.mts.map +1 -1
- package/esm/cache/database-cache-driver.d.mts.map +1 -1
- package/esm/cache/database-cache-driver.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/cli-commands.utils.mjs.map +1 -1
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/name-parser.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/prompt.mjs.map +1 -1
- package/esm/cli/commands/routes/diff-page-routes.mjs.map +1 -1
- package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -1
- package/esm/cli/commands/routes/route-row.mjs.map +1 -1
- package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
- package/esm/cli/commands/routes/routes-diff.action.mjs.map +1 -1
- package/esm/cli/commands/storage-put.action.mjs.map +1 -1
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/cli/commands-loader.mjs.map +1 -1
- package/esm/cli/start.mjs.map +1 -1
- package/esm/cli/string-similarity.mjs.map +1 -1
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/config-setter.d.mts.map +1 -1
- package/esm/config/load-config-files.mjs.map +1 -1
- package/esm/connectors/access-connector.d.mts.map +1 -1
- package/esm/connectors/ai-connector.d.mts.map +1 -1
- package/esm/connectors/assert-no-reserved-connector-names.mjs.map +1 -1
- package/esm/connectors/assert-unique-connector-names.mjs.map +1 -1
- package/esm/connectors/base-connector.d.mts.map +1 -1
- package/esm/connectors/base-connector.mjs.map +1 -1
- package/esm/connectors/cache-connector.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/database-connector.d.mts.map +1 -1
- package/esm/connectors/describe-server-address.mjs.map +1 -1
- package/esm/connectors/herald-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/logger-connector.d.mts.map +1 -1
- package/esm/connectors/mail-connector.d.mts.map +1 -1
- package/esm/connectors/notifications-connector.d.mts.map +1 -1
- package/esm/connectors/register-configured-connectors.mjs.map +1 -1
- package/esm/connectors/socket-connector.d.mts.map +1 -1
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/connectors/storage.connector.d.mts.map +1 -1
- package/esm/container/index.d.mts.map +1 -1
- package/esm/database/create-database-action.mjs.map +1 -1
- package/esm/database/drop-tables-action.mjs.map +1 -1
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/database/seeds/seed-records-table-migration.mjs.map +1 -1
- package/esm/database/seeds/seeders.manager.mjs.map +1 -1
- package/esm/database/seeds/seeds-table-migration.mjs.map +1 -1
- package/esm/dev-server/check-for-updates.mjs.map +1 -1
- package/esm/dev-server/dependency-graph.d.mts.map +1 -1
- package/esm/dev-server/dependency-graph.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs.map +1 -1
- 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.map +1 -1
- package/esm/dev-server/file-operations.d.mts.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs.map +1 -1
- package/esm/dev-server/health-checker/checkers/typescript-health-checker.mjs.map +1 -1
- package/esm/dev-server/health-checker/file-health-result.d.mts.map +1 -1
- package/esm/dev-server/health-checker/file-health-result.mjs.map +1 -1
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/dev-server/loader/hook-thread.d.mts.map +1 -1
- package/esm/dev-server/loader/own-resolver.mjs.map +1 -1
- package/esm/dev-server/loader/register-loader.mjs.map +1 -1
- package/esm/dev-server/loader/resolve-hook.mjs.map +1 -1
- package/esm/dev-server/loader/source-slug.mjs.map +1 -1
- package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
- package/esm/dev-server/loader/version-registry.mjs.map +1 -1
- package/esm/dev-server/manifest-manager.d.mts.map +1 -1
- package/esm/dev-server/manifest-manager.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/package-json-manager.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/dev-server/ready-block.mjs.map +1 -1
- package/esm/dev-server/special-files-collector.d.mts.map +1 -1
- package/esm/dev-server/special-files-collector.mjs.map +1 -1
- package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/dev-server/utils.mjs.map +1 -1
- package/esm/encryption/encrypt.mjs.map +1 -1
- package/esm/encryption/hash.mjs.map +1 -1
- package/esm/generations/add-command.action.mjs +28 -11
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/shadcn.feature.mjs.map +1 -1
- package/esm/generations/features/shared/migration-timestamp.mjs.map +1 -1
- package/esm/generations/features/tailwind.feature.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs +8 -7
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +49 -33
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/boot-port-preflight.d.mts.map +1 -1
- package/esm/http/context/request-context.d.mts.map +1 -1
- package/esm/http/context/request-context.mjs.map +1 -1
- package/esm/http/context/request-memo.d.mts.map +1 -1
- package/esm/http/context/request-memo.mjs.map +1 -1
- package/esm/http/createHttpApplication.d.mts.map +1 -1
- package/esm/http/createHttpApplication.mjs.map +1 -1
- package/esm/http/health.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/ready-report.d.mts.map +1 -1
- package/esm/http/request-controller.d.mts.map +1 -1
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server.d.mts.map +1 -1
- package/esm/http/server.mjs.map +1 -1
- package/esm/http/uploaded-file.d.mts.map +1 -1
- package/esm/http/uploaded-file.mjs.map +1 -1
- package/esm/http/uploads-config.d.mts.map +1 -1
- package/esm/image/image.d.mts.map +1 -1
- package/esm/image/image.mjs.map +1 -1
- package/esm/logger/logger.mjs.map +1 -1
- package/esm/mail/config.d.mts.map +1 -1
- package/esm/mail/events.d.mts.map +1 -1
- package/esm/mail/mail.d.mts.map +1 -1
- package/esm/mail/mail.mjs.map +1 -1
- package/esm/mail/mailer-pool.d.mts.map +1 -1
- package/esm/mail/mailer-pool.mjs.map +1 -1
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/mail/test-mailbox.d.mts.map +1 -1
- package/esm/mail/test-mailbox.mjs.map +1 -1
- package/esm/production/assert-generated-imports.mjs.map +1 -1
- package/esm/production/build-app-production.mjs.map +1 -1
- package/esm/production/dist-build-manifest.mjs.map +1 -1
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/production/promote-dist.mjs.map +1 -1
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.d.mts.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/repositories/repository.manager.d.mts.map +1 -1
- package/esm/repositories/repository.manager.mjs.map +1 -1
- package/esm/resource/register-resource.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/restful/restful.mjs.map +1 -1
- package/esm/router/log-request-lifecycle.mjs.map +1 -1
- package/esm/router/normalize-route-path.d.mts.map +1 -1
- package/esm/router/positional-handler-diagnostics.d.mts.map +1 -1
- package/esm/router/positional-handler-diagnostics.mjs.map +1 -1
- package/esm/router/route-registry.d.mts.map +1 -1
- package/esm/router/route-registry.mjs.map +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs.map +1 -1
- package/esm/socket/utils.d.mts.map +1 -1
- package/esm/storage/config.d.mts.map +1 -1
- package/esm/storage/context/storage-driver-context.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/storage/drivers/do-spaces-driver.d.mts.map +1 -1
- package/esm/storage/drivers/local-driver.d.mts.map +1 -1
- package/esm/storage/drivers/local-driver.mjs.map +1 -1
- package/esm/storage/drivers/r2-driver.d.mts.map +1 -1
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage-file.d.mts.map +1 -1
- package/esm/storage/storage-file.mjs.map +1 -1
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/test-connectors-selection.mjs.map +1 -1
- package/esm/tests/test-helpers.d.mts.map +1 -1
- package/esm/tests/test-setup-timeout.mjs.map +1 -1
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/updater/package-manager.mjs +15 -1
- package/esm/updater/package-manager.mjs.map +1 -1
- package/esm/updater/update-warlock-packages.mjs.map +1 -1
- package/esm/use-cases/use-case-broadcast.d.mts.map +1 -1
- package/esm/use-cases/use-case-events.d.mts.map +1 -1
- package/esm/use-cases/use-case-pipeline.d.mts.map +1 -1
- package/esm/use-cases/use-case.d.mts.map +1 -1
- package/esm/use-cases/use-cases-registry.d.mts.map +1 -1
- package/esm/use-cases/use-cases-registry.mjs.map +1 -1
- package/esm/utils/database-log.d.mts.map +1 -1
- package/esm/utils/database-log.mjs.map +1 -1
- package/esm/utils/environment.d.mts.map +1 -1
- package/esm/utils/framework-vesion.mjs.map +1 -1
- package/esm/utils/get-localized.mjs.map +1 -1
- package/esm/utils/load-environment.mjs.map +1 -1
- package/esm/utils/normalized-path.d.mts.map +1 -1
- package/esm/utils/paths.d.mts.map +1 -1
- package/esm/utils/promise-all-object.d.mts.map +1 -1
- package/esm/utils/promise-all-object.mjs.map +1 -1
- package/esm/utils/sluggable.mjs.map +1 -1
- package/esm/utils/version-compare.mjs.map +1 -1
- package/esm/validation/database/exists-except-current-id.mjs.map +1 -1
- package/esm/validation/database/exists-except-current-user.mjs.map +1 -1
- package/esm/validation/database/unique-except-current-id.mjs.map +1 -1
- package/esm/validation/plugins/localized-plugin.mjs.map +1 -1
- package/esm/validation/validators/file-validator.mjs.map +1 -1
- package/esm/vite/lower-stage3-decorators.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.map +1 -1
- package/llms-full.txt +4 -1
- package/llms.txt +1 -1
- package/package.json +12 -12
- package/skills/README.md +1 -1
- package/skills/write-cli-command/SKILL.md +4 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-file.mjs","names":[],"sources":["../../../../../../../core/src/storage/storage-file.ts"],"sourcesContent":["import { basename, dirname, extname } from \"path\";\nimport type { Readable } from \"stream\";\nimport type {\n CloudStorageDriverContract,\n CloudStorageFileData,\n FileVisibility,\n StorageDriverContract,\n StorageFileData,\n StorageFileInfo,\n} from \"./types\";\n\n/**\n * StorageFile class - OOP wrapper for storage file operations\n *\n * Provides a fluent interface for working with files in storage,\n * wrapping the underlying driver operations.\n *\n * @example\n * ```typescript\n * const file = await storage.put(buffer, \"uploads/image.jpg\");\n *\n * // Properties (sync, from cached data)\n * file.name // \"image.jpg\"\n * file.extension // \"jpg\"\n * file.path // \"uploads/image.jpg\"\n * file.hash // \"sha256:abc123...\"\n *\n * // Operations\n * await file.copy(\"uploads/backup.jpg\")\n * await file.move(\"archive/image.jpg\")\n * await file.delete()\n *\n * // Content\n * const buffer = await file.contents();\n * const stream = await file.stream();\n * ```\n */\nexport class StorageFile {\n /**\n * Relative file path\n */\n protected _path: string;\n\n /**\n * The driver instance\n */\n protected _driver: StorageDriverContract;\n\n /**\n * Cached file data (from put operations or lazy loaded)\n */\n protected _data?: StorageFileData;\n\n /**\n * Whether the file has been deleted\n */\n protected _deleted = false;\n\n /**\n * Create a new StorageFile instance\n *\n * @param path - Relative file path\n * @param driver - Driver instance\n * @param data - Optional initial data from put/copy operations\n */\n public constructor(path: string, driver: StorageDriverContract, data?: StorageFileData) {\n this._path = path;\n this._driver = driver;\n this._data = data;\n }\n\n // ============================================================\n // Properties\n // ============================================================\n\n /**\n * Get the relative file path\n */\n public get path(): string {\n return this._path;\n }\n\n /**\n * Get the file name (with extension)\n */\n public get name(): string {\n return basename(this._path);\n }\n\n /**\n * Get the file extension (without dot)\n */\n public get extension(): string {\n return extname(this._path).slice(1).toLowerCase();\n }\n\n /**\n * Get the directory path\n */\n public get directory(): string {\n return dirname(this._path);\n }\n\n /**\n * Get the driver name\n */\n public get driver(): string {\n return this._driver.name;\n }\n\n /**\n * Check if file has been deleted\n */\n public get isDeleted(): boolean {\n return this._deleted;\n }\n\n /**\n * Get public URL (sync if data cached, otherwise computed)\n */\n public get url(): string {\n this.ensureNotDeleted();\n return this._data?.url || this._driver.url(this._path);\n }\n\n /**\n * Get the absolute filesystem path (local driver only)\n */\n public get absolutePath(): string | undefined {\n this.ensureNotDeleted();\n if (\"path\" in this._driver && typeof this._driver.path === \"function\") {\n return this._driver.path(this._path);\n }\n\n return undefined;\n }\n\n /**\n * Get file hash (SHA-256, available from put operations)\n */\n public get hash(): string | undefined {\n return this._data?.hash;\n }\n\n // ============================================================\n // Data Methods (Lazy Loaded)\n // ============================================================\n\n /**\n * Get cached file data, or fetch it if not available\n */\n public async data(): Promise<StorageFileData> {\n this.ensureNotDeleted();\n if (!this._data) {\n // Fetch info and construct data\n const info = await this.metadata();\n this._data = {\n path: info.path,\n url: this._driver.url(this._path),\n size: info.size,\n hash: \"\", // Not available from metadata\n mimeType: info.mimeType || \"application/octet-stream\",\n driver: this._driver.name,\n };\n }\n return this._data;\n }\n\n /**\n * Get file size in bytes\n */\n public async size(): Promise<number> {\n const data = await this.data();\n return data.size;\n }\n\n /**\n * Get MIME type\n */\n public async mimeType(): Promise<string> {\n const data = await this.data();\n return data.mimeType;\n }\n\n /**\n * Get last modified date (fetches from driver)\n */\n public async lastModified(): Promise<Date | undefined> {\n this.ensureNotDeleted();\n const info = await this.metadata();\n return info.lastModified;\n }\n\n /**\n * Get ETag (cloud drivers, fetches from driver)\n */\n public async etag(): Promise<string | undefined> {\n this.ensureNotDeleted();\n const info = await this.metadata();\n return info.etag;\n }\n\n // ============================================================\n // Content Methods\n // ============================================================\n\n /**\n * Get file contents as Buffer\n */\n public async contents(): Promise<Buffer> {\n this.ensureNotDeleted();\n return this._driver.get(this._path);\n }\n\n /**\n * Get file contents as readable stream\n */\n public async stream(): Promise<Readable> {\n this.ensureNotDeleted();\n return this._driver.getStream(this._path);\n }\n\n /**\n * Get file contents as UTF-8 text\n */\n public async text(): Promise<string> {\n const buffer = await this.contents();\n return buffer.toString(\"utf-8\");\n }\n\n /**\n * Get file contents as base64 string\n */\n public async base64(): Promise<string> {\n const buffer = await this.contents();\n return buffer.toString(\"base64\");\n }\n\n /**\n * Get file contents as data URL\n */\n public async dataUrl(): Promise<string> {\n const [buffer, data] = await Promise.all([this.contents(), this.data()]);\n return `data:${data.mimeType};base64,${buffer.toString(\"base64\")}`;\n }\n\n // ============================================================\n // URL Methods\n // ============================================================\n\n /**\n * Get a temporary signed URL\n *\n * @param expiresIn - Seconds until expiration (default: 3600)\n */\n public async temporaryUrl(expiresIn = 3600): Promise<string> {\n this.ensureNotDeleted();\n return this._driver.temporaryUrl(this._path, expiresIn);\n }\n\n // ============================================================\n // File Operations\n // ============================================================\n\n /**\n * Check if the file exists\n */\n public async exists(): Promise<boolean> {\n if (this._deleted) return false;\n return this._driver.exists(this._path);\n }\n\n /**\n * Copy the file to a new location\n *\n * @param destination - Destination path\n * @returns New StorageFile instance at destination\n */\n public async copy(destination: string): Promise<StorageFile> {\n this.ensureNotDeleted();\n const result = await this._driver.copy(this._path, destination);\n return new StorageFile(destination, this._driver, result);\n }\n\n /**\n * Move the file to a new location\n *\n * @param destination - Destination path\n * @returns This StorageFile instance with updated path\n */\n public async move(destination: string): Promise<this> {\n this.ensureNotDeleted();\n const result = await this._driver.move(this._path, destination);\n this._path = destination;\n this._data = result; // Update cached data\n return this;\n }\n\n /**\n * Rename the file (move within same directory)\n *\n * @param newName - New file name\n * @returns This StorageFile instance with updated path\n */\n public async rename(newName: string): Promise<this> {\n const newPath = this.directory === \".\" ? newName : `${this.directory}/${newName}`;\n return this.move(newPath);\n }\n\n /**\n * Delete the file\n *\n * @returns true if deleted, false if not found\n */\n public async delete(): Promise<boolean> {\n this.ensureNotDeleted();\n const result = await this._driver.delete(this._path);\n this._deleted = true;\n return result;\n }\n\n // ============================================================\n // Cloud-Specific Methods\n // ============================================================\n\n /**\n * Set file visibility (cloud drivers only)\n *\n * @param visibility - \"public\" or \"private\"\n * @throws Error if driver doesn't support visibility\n */\n public async setVisibility(visibility: FileVisibility): Promise<this> {\n this.ensureNotDeleted();\n\n if (!(\"setVisibility\" in this._driver)) {\n throw new Error(\"setVisibility is only available for cloud storage drivers\");\n }\n\n await (this._driver as CloudStorageDriverContract).setVisibility(this._path, visibility);\n return this;\n }\n\n /**\n * Get file visibility (cloud drivers only)\n *\n * @throws Error if driver doesn't support visibility\n */\n public async getVisibility(): Promise<FileVisibility> {\n this.ensureNotDeleted();\n\n if (!(\"getVisibility\" in this._driver)) {\n throw new Error(\"getVisibility is only available for cloud storage drivers\");\n }\n\n return (this._driver as CloudStorageDriverContract).getVisibility(this._path);\n }\n\n /**\n * Set storage class (cloud drivers only)\n *\n * @param storageClass - Storage class (e.g., \"STANDARD\", \"GLACIER\")\n * @throws Error if driver doesn't support storage class\n */\n public async setStorageClass(storageClass: string): Promise<this> {\n this.ensureNotDeleted();\n\n if (!(\"setStorageClass\" in this._driver)) {\n throw new Error(\"setStorageClass is only available for cloud storage drivers\");\n }\n\n await (this._driver as CloudStorageDriverContract).setStorageClass(this._path, storageClass);\n return this;\n }\n\n // ============================================================\n // Utilities\n // ============================================================\n\n /**\n * Ensure the file has not been deleted\n */\n protected ensureNotDeleted(): void {\n if (this._deleted) {\n throw new Error(`File \"${this._path}\" has been deleted`);\n }\n }\n\n /**\n * Create a StorageFile instance from StorageFileData\n *\n * @param data - Storage file data from put/copy/move operations\n * @param driver - Driver instance\n */\n public static fromData(\n data: StorageFileData | CloudStorageFileData,\n driver: StorageDriverContract,\n ): StorageFile {\n return new StorageFile(data.path, driver, data);\n }\n\n /**\n * Get file metadata\n */\n public async metadata(): Promise<StorageFileInfo> {\n this.ensureNotDeleted();\n return this._driver.metadata(this._path);\n }\n\n /**\n * Determine if this file is an image type\n */\n public async isImage() {\n const metadata = await this.metadata();\n return metadata.mimeType!.startsWith(\"image/\");\n }\n\n /**\n * Determine if this file is a document type\n */\n public async isDocument() {\n const metadata = await this.metadata();\n return metadata.mimeType!.startsWith(\"application/\");\n }\n\n /**\n * Determine if this file is a pdf type\n */\n public async isPdf() {\n const metadata = await this.metadata();\n return metadata.mimeType!.startsWith(\"application/pdf\");\n }\n\n /**\n * Determine if this file is an excel file (any support excel file)\n */\n public async isExcel() {\n const metadata = await this.metadata();\n return (\n metadata.mimeType!.startsWith(\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n ) || metadata.mimeType!.startsWith(\"application/vnd.ms-excel\")\n );\n }\n\n /**\n * Determine if this file is a doc file\n */\n public async isDoc() {\n const metadata = await this.metadata();\n return (\n metadata.mimeType!.startsWith(\"application/msword\") ||\n metadata.mimeType!.startsWith(\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n )\n );\n }\n\n /**\n * Determine if this file is an audio type\n */\n public async isAudio() {\n const metadata = await this.metadata();\n return metadata.mimeType!.startsWith(\"audio/\");\n }\n\n /**\n * Determine if this file is a video type\n */\n public async isVideo() {\n const metadata = await this.metadata();\n return metadata.mimeType!.startsWith(\"video/\");\n }\n\n /**\n * Convert to plain object (returns cached data or constructs it)\n */\n public toJSON(): {\n path: string;\n name: string;\n extension: string;\n driver: string;\n url: string;\n hash?: string;\n size?: number;\n mimeType?: string;\n } {\n return {\n path: this._path,\n name: this.name,\n extension: this.extension,\n driver: this._driver.name,\n url: this._deleted ? \"\" : this.url,\n hash: this._data?.hash,\n size: this._data?.size,\n mimeType: this._data?.mimeType,\n };\n }\n\n /**\n * String representation\n */\n public toString(): string {\n return this._path;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"storage-file.mjs","names":[],"sources":["../../../../../../../core/src/storage/storage-file.ts"],"sourcesContent":["import { basename, dirname, extname } from \"path\";\nimport type { Readable } from \"stream\";\nimport type {\n CloudStorageDriverContract,\n CloudStorageFileData,\n FileVisibility,\n StorageDriverContract,\n StorageFileData,\n StorageFileInfo,\n} from \"./types\";\n\n/**\n * StorageFile class - OOP wrapper for storage file operations\n *\n * Provides a fluent interface for working with files in storage,\n * wrapping the underlying driver operations.\n *\n * @example\n * ```typescript\n * const file = await storage.put(buffer, \"uploads/image.jpg\");\n *\n * // Properties (sync, from cached data)\n * file.name // \"image.jpg\"\n * file.extension // \"jpg\"\n * file.path // \"uploads/image.jpg\"\n * file.hash // \"sha256:abc123...\"\n *\n * // Operations\n * await file.copy(\"uploads/backup.jpg\")\n * await file.move(\"archive/image.jpg\")\n * await file.delete()\n *\n * // Content\n * const buffer = await file.contents();\n * const stream = await file.stream();\n * ```\n */\nexport class StorageFile {\n /**\n * Relative file path\n */\n protected _path: string;\n\n /**\n * The driver instance\n */\n protected _driver: StorageDriverContract;\n\n /**\n * Cached file data (from put operations or lazy loaded)\n */\n protected _data?: StorageFileData;\n\n /**\n * Whether the file has been deleted\n */\n protected _deleted = false;\n\n /**\n * Create a new StorageFile instance\n *\n * @param path - Relative file path\n * @param driver - Driver instance\n * @param data - Optional initial data from put/copy operations\n */\n public constructor(path: string, driver: StorageDriverContract, data?: StorageFileData) {\n this._path = path;\n this._driver = driver;\n this._data = data;\n }\n\n // ============================================================\n // Properties\n // ============================================================\n\n /**\n * Get the relative file path\n */\n public get path(): string {\n return this._path;\n }\n\n /**\n * Get the file name (with extension)\n */\n public get name(): string {\n return basename(this._path);\n }\n\n /**\n * Get the file extension (without dot)\n */\n public get extension(): string {\n return extname(this._path).slice(1).toLowerCase();\n }\n\n /**\n * Get the directory path\n */\n public get directory(): string {\n return dirname(this._path);\n }\n\n /**\n * Get the driver name\n */\n public get driver(): string {\n return this._driver.name;\n }\n\n /**\n * Check if file has been deleted\n */\n public get isDeleted(): boolean {\n return this._deleted;\n }\n\n /**\n * Get public URL (sync if data cached, otherwise computed)\n */\n public get url(): string {\n this.ensureNotDeleted();\n return this._data?.url || this._driver.url(this._path);\n }\n\n /**\n * Get the absolute filesystem path (local driver only)\n */\n public get absolutePath(): string | undefined {\n this.ensureNotDeleted();\n if (\"path\" in this._driver && typeof this._driver.path === \"function\") {\n return this._driver.path(this._path);\n }\n\n return undefined;\n }\n\n /**\n * Get file hash (SHA-256, available from put operations)\n */\n public get hash(): string | undefined {\n return this._data?.hash;\n }\n\n // ============================================================\n // Data Methods (Lazy Loaded)\n // ============================================================\n\n /**\n * Get cached file data, or fetch it if not available\n */\n public async data(): Promise<StorageFileData> {\n this.ensureNotDeleted();\n if (!this._data) {\n // Fetch info and construct data\n const info = await this.metadata();\n this._data = {\n path: info.path,\n url: this._driver.url(this._path),\n size: info.size,\n hash: \"\", // Not available from metadata\n mimeType: info.mimeType || \"application/octet-stream\",\n driver: this._driver.name,\n };\n }\n return this._data;\n }\n\n /**\n * Get file size in bytes\n */\n public async size(): Promise<number> {\n const data = await this.data();\n return data.size;\n }\n\n /**\n * Get MIME type\n */\n public async mimeType(): Promise<string> {\n const data = await this.data();\n return data.mimeType;\n }\n\n /**\n * Get last modified date (fetches from driver)\n */\n public async lastModified(): Promise<Date | undefined> {\n this.ensureNotDeleted();\n const info = await this.metadata();\n return info.lastModified;\n }\n\n /**\n * Get ETag (cloud drivers, fetches from driver)\n */\n public async etag(): Promise<string | undefined> {\n this.ensureNotDeleted();\n const info = await this.metadata();\n return info.etag;\n }\n\n // ============================================================\n // Content Methods\n // ============================================================\n\n /**\n * Get file contents as Buffer\n */\n public async contents(): Promise<Buffer> {\n this.ensureNotDeleted();\n return this._driver.get(this._path);\n }\n\n /**\n * Get file contents as readable stream\n */\n public async stream(): Promise<Readable> {\n this.ensureNotDeleted();\n return this._driver.getStream(this._path);\n }\n\n /**\n * Get file contents as UTF-8 text\n */\n public async text(): Promise<string> {\n const buffer = await this.contents();\n return buffer.toString(\"utf-8\");\n }\n\n /**\n * Get file contents as base64 string\n */\n public async base64(): Promise<string> {\n const buffer = await this.contents();\n return buffer.toString(\"base64\");\n }\n\n /**\n * Get file contents as data URL\n */\n public async dataUrl(): Promise<string> {\n const [buffer, data] = await Promise.all([this.contents(), this.data()]);\n return `data:${data.mimeType};base64,${buffer.toString(\"base64\")}`;\n }\n\n // ============================================================\n // URL Methods\n // ============================================================\n\n /**\n * Get a temporary signed URL\n *\n * @param expiresIn - Seconds until expiration (default: 3600)\n */\n public async temporaryUrl(expiresIn = 3600): Promise<string> {\n this.ensureNotDeleted();\n return this._driver.temporaryUrl(this._path, expiresIn);\n }\n\n // ============================================================\n // File Operations\n // ============================================================\n\n /**\n * Check if the file exists\n */\n public async exists(): Promise<boolean> {\n if (this._deleted) return false;\n return this._driver.exists(this._path);\n }\n\n /**\n * Copy the file to a new location\n *\n * @param destination - Destination path\n * @returns New StorageFile instance at destination\n */\n public async copy(destination: string): Promise<StorageFile> {\n this.ensureNotDeleted();\n const result = await this._driver.copy(this._path, destination);\n return new StorageFile(destination, this._driver, result);\n }\n\n /**\n * Move the file to a new location\n *\n * @param destination - Destination path\n * @returns This StorageFile instance with updated path\n */\n public async move(destination: string): Promise<this> {\n this.ensureNotDeleted();\n const result = await this._driver.move(this._path, destination);\n this._path = destination;\n this._data = result; // Update cached data\n return this;\n }\n\n /**\n * Rename the file (move within same directory)\n *\n * @param newName - New file name\n * @returns This StorageFile instance with updated path\n */\n public async rename(newName: string): Promise<this> {\n const newPath = this.directory === \".\" ? newName : `${this.directory}/${newName}`;\n return this.move(newPath);\n }\n\n /**\n * Delete the file\n *\n * @returns true if deleted, false if not found\n */\n public async delete(): Promise<boolean> {\n this.ensureNotDeleted();\n const result = await this._driver.delete(this._path);\n this._deleted = true;\n return result;\n }\n\n // ============================================================\n // Cloud-Specific Methods\n // ============================================================\n\n /**\n * Set file visibility (cloud drivers only)\n *\n * @param visibility - \"public\" or \"private\"\n * @throws Error if driver doesn't support visibility\n */\n public async setVisibility(visibility: FileVisibility): Promise<this> {\n this.ensureNotDeleted();\n\n if (!(\"setVisibility\" in this._driver)) {\n throw new Error(\"setVisibility is only available for cloud storage drivers\");\n }\n\n await (this._driver as CloudStorageDriverContract).setVisibility(this._path, visibility);\n return this;\n }\n\n /**\n * Get file visibility (cloud drivers only)\n *\n * @throws Error if driver doesn't support visibility\n */\n public async getVisibility(): Promise<FileVisibility> {\n this.ensureNotDeleted();\n\n if (!(\"getVisibility\" in this._driver)) {\n throw new Error(\"getVisibility is only available for cloud storage drivers\");\n }\n\n return (this._driver as CloudStorageDriverContract).getVisibility(this._path);\n }\n\n /**\n * Set storage class (cloud drivers only)\n *\n * @param storageClass - Storage class (e.g., \"STANDARD\", \"GLACIER\")\n * @throws Error if driver doesn't support storage class\n */\n public async setStorageClass(storageClass: string): Promise<this> {\n this.ensureNotDeleted();\n\n if (!(\"setStorageClass\" in this._driver)) {\n throw new Error(\"setStorageClass is only available for cloud storage drivers\");\n }\n\n await (this._driver as CloudStorageDriverContract).setStorageClass(this._path, storageClass);\n return this;\n }\n\n // ============================================================\n // Utilities\n // ============================================================\n\n /**\n * Ensure the file has not been deleted\n */\n protected ensureNotDeleted(): void {\n if (this._deleted) {\n throw new Error(`File \"${this._path}\" has been deleted`);\n }\n }\n\n /**\n * Create a StorageFile instance from StorageFileData\n *\n * @param data - Storage file data from put/copy/move operations\n * @param driver - Driver instance\n */\n public static fromData(\n data: StorageFileData | CloudStorageFileData,\n driver: StorageDriverContract,\n ): StorageFile {\n return new StorageFile(data.path, driver, data);\n }\n\n /**\n * Get file metadata\n */\n public async metadata(): Promise<StorageFileInfo> {\n this.ensureNotDeleted();\n return this._driver.metadata(this._path);\n }\n\n /**\n * Determine if this file is an image type\n */\n public async isImage() {\n const metadata = await this.metadata();\n return metadata.mimeType!.startsWith(\"image/\");\n }\n\n /**\n * Determine if this file is a document type\n */\n public async isDocument() {\n const metadata = await this.metadata();\n return metadata.mimeType!.startsWith(\"application/\");\n }\n\n /**\n * Determine if this file is a pdf type\n */\n public async isPdf() {\n const metadata = await this.metadata();\n return metadata.mimeType!.startsWith(\"application/pdf\");\n }\n\n /**\n * Determine if this file is an excel file (any support excel file)\n */\n public async isExcel() {\n const metadata = await this.metadata();\n return (\n metadata.mimeType!.startsWith(\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n ) || metadata.mimeType!.startsWith(\"application/vnd.ms-excel\")\n );\n }\n\n /**\n * Determine if this file is a doc file\n */\n public async isDoc() {\n const metadata = await this.metadata();\n return (\n metadata.mimeType!.startsWith(\"application/msword\") ||\n metadata.mimeType!.startsWith(\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n )\n );\n }\n\n /**\n * Determine if this file is an audio type\n */\n public async isAudio() {\n const metadata = await this.metadata();\n return metadata.mimeType!.startsWith(\"audio/\");\n }\n\n /**\n * Determine if this file is a video type\n */\n public async isVideo() {\n const metadata = await this.metadata();\n return metadata.mimeType!.startsWith(\"video/\");\n }\n\n /**\n * Convert to plain object (returns cached data or constructs it)\n */\n public toJSON(): {\n path: string;\n name: string;\n extension: string;\n driver: string;\n url: string;\n hash?: string;\n size?: number;\n mimeType?: string;\n } {\n return {\n path: this._path,\n name: this.name,\n extension: this.extension,\n driver: this._driver.name,\n url: this._deleted ? \"\" : this.url,\n hash: this._data?.hash,\n size: this._data?.size,\n mimeType: this._data?.mimeType,\n };\n }\n\n /**\n * String representation\n */\n public toString(): string {\n return this._path;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.mts","names":[],"sources":["../../../../../../../core/src/storage/storage.ts"],"mappings":";;;;;;;;;;;AAiFA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,OAAA,SAAgB,aAAA,YAAyB,sBAAA;EAgWjD;;;;EAAA,UA3VO,OAAA,EAAO,GAAA,SAAA,qBAAA;EAwbL;;;;EAAA,UAlbF,OAAA,EAAO,GAAA,SAAA,mBAAA;EAqed;;;;EAAA,UA/dO,iBAAA,EAAoB,iBAAA;EAohBqB;;;;EAAA,QA9gB3C,WAAA;EA+jBwC;;;;;;EAgDoB;;;;;EA9lBvD,IAAA,
|
|
1
|
+
{"version":3,"file":"storage.d.mts","names":[],"sources":["../../../../../../../core/src/storage/storage.ts"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-fetch.mjs","names":[],"sources":["../../../../../../../../core/src/storage/utils/safe-fetch.ts"],"sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport { isIP } from \"@warlock.js/seal\";\nimport { StorageError } from \"./storage-error\";\n\n/**\n * SSRF / resource-exhaustion guard for storage's outbound downloads\n * (`putFromUrl`).\n *\n * NOTE: this MIRRORS `@warlock.js/ai`'s `src/security/private-ip.ts` and\n * `src/security/outbound-policy.ts`. It is duplicated here intentionally\n * because `@warlock.js/core` cannot depend on `@warlock.js/ai`. Pending\n * extraction of a shared security package, keep the two copies in sync.\n */\n\n/** 50 MiB — default cap on a downloaded body (storage files run larger than AI payloads). */\nconst DEFAULT_MAX_BYTES = 50 * 1024 * 1024;\n/** 30s — default per-request timeout. */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n/**\n * Cap on the number of re-validated redirect hops. Mirrors\n * `@warlock.js/ai`'s `DEFAULT_MAX_REDIRECTS`\n * (ai/src/security/outbound-policy.ts:15).\n */\nconst DEFAULT_MAX_REDIRECTS = 5;\n\n/** 3xx statuses whose `Location` a follow re-issues — mirrors ai/src/security/outbound-policy.ts:18. */\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/**\n * Options controlling a guarded outbound fetch. Every field is optional;\n * {@link resolveFetchPolicy} fills safe defaults. The defaults are\n * deliberately strict (https+http, private-IP deny ON, 30s timeout,\n * 50 MiB cap) so a caller that does not tune them still gets a hardened\n * request.\n */\nexport type SafeFetchOptions = {\n /**\n * Whether to ALLOW private / loopback / link-local / metadata hosts.\n * Default `false` — the SSRF guard is on (private hosts are denied).\n */\n allowPrivateHosts?: boolean;\n /**\n * Maximum response body size in bytes. A declared `content-length` over\n * this fails fast; otherwise the body is read with a running cap and the\n * stream is aborted on overflow. Default `52_428_800` (50 MiB).\n */\n maxBytes?: number;\n /** Per-request timeout in milliseconds. Default `30_000`. */\n timeoutMs?: number;\n /**\n * URL schemes permitted for the request, compared case-insensitively.\n * Default `[\"https\", \"http\"]`.\n */\n allowedSchemes?: string[];\n /**\n * Injected `fetch` implementation (for tests or proxies). Defaults to the\n * global `fetch`.\n */\n fetch?: typeof fetch;\n};\n\n/** {@link SafeFetchOptions} with every default resolved — never partial. */\ntype ResolvedFetchPolicy = {\n allowPrivateHosts: boolean;\n maxBytes: number;\n timeoutMs: number;\n allowedSchemes: string[];\n fetch: typeof fetch;\n};\n\n/**\n * Fill a {@link SafeFetchOptions} with strict defaults: https+http,\n * private-IP deny on, 30s timeout, 50 MiB cap, global `fetch`.\n */\nfunction resolveFetchPolicy(options: SafeFetchOptions = {}): ResolvedFetchPolicy {\n return {\n allowPrivateHosts: options.allowPrivateHosts ?? false,\n maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,\n timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n allowedSchemes: options.allowedSchemes ?? [\"https\", \"http\"],\n fetch: options.fetch ?? globalThis.fetch,\n };\n}\n\n/** Parse a dotted-quad into four octets, or `null` if malformed. */\nfunction parseIpv4(ip: string): [number, number, number, number] | null {\n const parts = ip.split(\".\");\n if (parts.length !== 4) {\n return null;\n }\n\n const octets = parts.map((part) => Number(part));\n if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {\n return null;\n }\n\n return octets as [number, number, number, number];\n}\n\nfunction isPrivateIpv4(ip: string): boolean {\n const octets = parseIpv4(ip);\n if (!octets) {\n return true; // unparseable → refuse, fail closed\n }\n\n const [a, b] = octets;\n\n return (\n a === 0 || // 0.0.0.0/8 \"this network\"\n a === 10 || // 10.0.0.0/8 private\n a === 127 || // 127.0.0.0/8 loopback\n (a === 100 && b >= 64 && b <= 127) || // 100.64.0.0/10 CGNAT\n (a === 169 && b === 254) || // 169.254.0.0/16 link-local + metadata\n (a === 172 && b >= 16 && b <= 31) || // 172.16.0.0/12 private\n (a === 192 && b === 168) || // 192.168.0.0/16 private\n (a === 192 && b === 0) || // 192.0.0.0/24 + 192.0.2.0/24 (IETF/test)\n (a === 198 && (b === 18 || b === 19)) || // 198.18.0.0/15 benchmarking\n (a === 198 && b === 51) || // 198.51.100.0/24 test-net-2\n (a === 203 && b === 0) || // 203.0.113.0/24 test-net-3\n a >= 224 // 224.0.0.0/4 multicast + 240.0.0.0/4 reserved + 255.255.255.255\n );\n}\n\nfunction isPrivateIpv6(ip: string): boolean {\n const normalized = ip.toLowerCase().split(\"%\")[0]; // drop zone id\n\n // IPv4-mapped / -embedded (::ffff:a.b.c.d, ::a.b.c.d) — defer to the v4\n // check on the trailing dotted-quad so an inward-mapped address is caught.\n const v4 = normalized.match(/(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$/);\n if (v4) {\n return isPrivateIpv4(v4[1]);\n }\n\n if (normalized === \"::1\" || normalized === \"::\") {\n return true; // loopback / unspecified\n }\n\n // Expand only the leading group enough to classify the reserved blocks.\n const firstGroup = normalized.split(\":\")[0];\n const head = firstGroup === \"\" ? 0 : Number.parseInt(firstGroup, 16);\n\n // fc00::/7 unique-local (fc.. / fd..)\n if ((head & 0xfe00) === 0xfc00) {\n return true;\n }\n // fe80::/10 link-local\n if ((head & 0xffc0) === 0xfe80) {\n return true;\n }\n\n return false;\n}\n\n/**\n * True when `ip` is a private, loopback, link-local, unique-local,\n * carrier-grade-NAT, unspecified, or otherwise non-public address — the set\n * an SSRF guard must refuse. Accepts IPv4 and IPv6 literals (including\n * IPv4-mapped IPv6 like `::ffff:169.254.169.254`). A non-IP string returns\n * `false` (the caller resolves hostnames via DNS first).\n *\n * The cloud-metadata endpoint `169.254.169.254` is covered by the IPv4\n * link-local range `169.254.0.0/16`.\n */\nexport function isPrivateOrReservedIp(ip: string): boolean {\n const family = isIP(ip);\n if (family === 4) {\n return isPrivateIpv4(ip);\n }\n if (family === 6) {\n return isPrivateIpv6(ip);\n }\n return false;\n}\n\n/** Strip the `[ ]` IPv6 brackets `URL.hostname` keeps. */\nfunction stripBrackets(host: string): string {\n return host.startsWith(\"[\") && host.endsWith(\"]\") ? host.slice(1, -1) : host;\n}\n\n/**\n * Reject when `host` is — or resolves to — a private / reserved address.\n * IP literals are checked directly; hostnames are resolved via DNS and every\n * returned address is checked (a public name pointing inward is caught). A\n * resolution failure fails closed.\n */\nasync function assertHostNotPrivate(host: string, rawUrl: string): Promise<void> {\n if (isIP(host) !== 0) {\n if (isPrivateOrReservedIp(host)) {\n throw new StorageError(\n `outbound request blocked — \"${host}\" is a private/reserved address`,\n { context: { url: rawUrl, address: host } },\n );\n }\n return;\n }\n\n let addresses: Array<{ address: string }>;\n try {\n addresses = await lookup(host, { all: true });\n } catch (cause) {\n throw new StorageError(\n `outbound request blocked — could not resolve host \"${host}\" to verify it is public`,\n { cause, context: { url: rawUrl, host } },\n );\n }\n\n for (const { address } of addresses) {\n if (isPrivateOrReservedIp(address)) {\n throw new StorageError(\n `outbound request blocked — host \"${host}\" resolves to a private/reserved address (${address})`,\n { context: { url: rawUrl, host, address } },\n );\n }\n }\n}\n\n/**\n * Validate a URL against the policy BEFORE any network call: scheme\n * allowlist and (when private hosts are not allowed) a DNS resolution that\n * rejects private / loopback / link-local / metadata addresses — the SSRF\n * guard. Returns the parsed `URL` on success; throws {@link StorageError}\n * otherwise.\n */\nasync function assertUrlAllowed(rawUrl: string, policy: ResolvedFetchPolicy): Promise<URL> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new StorageError(`outbound request blocked — invalid URL: ${rawUrl}`, {\n context: { url: rawUrl },\n });\n }\n\n const scheme = url.protocol.replace(/:$/, \"\").toLowerCase();\n if (!policy.allowedSchemes.some((s) => s.toLowerCase() === scheme)) {\n throw new StorageError(\n `outbound request blocked — scheme \"${scheme}\" is not allowed (allowed: ${policy.allowedSchemes.join(\", \")})`,\n { context: { url: rawUrl, scheme } },\n );\n }\n\n if (!policy.allowPrivateHosts) {\n const host = stripBrackets(url.hostname);\n await assertHostNotPrivate(host, rawUrl);\n }\n\n return url;\n}\n\n/**\n * Result of {@link safeFetchToBuffer}: the (capped) body plus the response's\n * content-type, so callers can derive a MIME type and enforce a non-empty\n * content-type check.\n */\nexport type SafeFetchResult = {\n buffer: Buffer;\n contentType: string | null;\n status: number;\n statusText: string;\n ok: boolean;\n};\n\n/**\n * SSRF-guarded download: validates the URL (scheme allowlist + post-DNS\n * private-IP deny), performs the request with a timeout via\n * `AbortController`, then streams the body with a hard byte cap — aborting\n * the moment the running total exceeds `maxBytes`. A declared\n * `content-length` over the cap fails fast.\n *\n * Redirects are NEVER delegated to the platform: every hop is issued with\n * `redirect: \"manual\"` and its `Location` is re-run through\n * {@link assertUrlAllowed} — including the DNS-resolution private-IP check\n * — before being followed (capped at {@link DEFAULT_MAX_REDIRECTS}), so a\n * 3xx from an allowed host cannot smuggle the request to a private /\n * metadata target. Mirrors `@warlock.js/ai`'s `guardedFetch`\n * (ai/src/security/outbound-policy.ts:188-284); duplicated per this\n * module's file-header note since core cannot depend on ai.\n *\n * @throws {StorageError} On a blocked URL (initial or redirect target),\n * a redirect-hop bound overflow, timeout, or oversized body.\n */\nexport async function safeFetchToBuffer(\n rawUrl: string,\n options: SafeFetchOptions = {},\n): Promise<SafeFetchResult> {\n const policy = resolveFetchPolicy(options);\n let url = await assertUrlAllowed(rawUrl, policy);\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(\n new StorageError(`outbound request timed out after ${policy.timeoutMs}ms`, {\n context: { url: rawUrl, timeoutMs: policy.timeoutMs },\n }),\n );\n }, policy.timeoutMs);\n\n try {\n let response: Response;\n\n for (let hop = 0; ; hop++) {\n response = await policy.fetch(url, { signal: controller.signal, redirect: \"manual\" });\n\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n break;\n }\n\n if (hop >= DEFAULT_MAX_REDIRECTS) {\n throw new StorageError(\n `outbound request blocked — more than ${DEFAULT_MAX_REDIRECTS} redirects`,\n { context: { url: rawUrl, maxRedirects: DEFAULT_MAX_REDIRECTS } },\n );\n }\n\n let target: URL;\n try {\n target = new URL(location, url);\n } catch {\n throw new StorageError(`outbound request blocked — invalid redirect Location: ${location}`, {\n context: { url: url.toString(), location },\n });\n }\n\n // Discard the interim body so the connection can be reused.\n if (response.body) {\n await response.body.cancel().catch(() => undefined);\n }\n\n // The redirect target gets the SAME scheme + private-IP validation\n // (incl. DNS resolution) as the initial URL — this is what closes\n // the redirect-based SSRF bypass: a 302 from an allowed host to\n // 169.254.169.254 is re-validated here, not blindly followed.\n url = await assertUrlAllowed(target.toString(), policy);\n }\n\n const declared = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > policy.maxBytes) {\n throw new StorageError(\n `outbound response body too large — declared ${declared} bytes exceeds the ${policy.maxBytes}-byte cap`,\n { context: { url: rawUrl, declared, maxBytes: policy.maxBytes } },\n );\n }\n\n const buffer = await readBodyCapped(response, policy.maxBytes, rawUrl);\n\n return {\n buffer,\n contentType: response.headers.get(\"content-type\"),\n status: response.status,\n statusText: response.statusText,\n ok: response.ok,\n };\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Read a response body into a Buffer with a hard byte cap. The stream is\n * read chunk-by-chunk and cancelled the moment the running total exceeds\n * `maxBytes`. Throws {@link StorageError} on overflow.\n */\nasync function readBodyCapped(\n response: Response,\n maxBytes: number,\n rawUrl: string,\n): Promise<Buffer> {\n if (!response.body) {\n const arrayBuffer = await response.arrayBuffer();\n if (arrayBuffer.byteLength > maxBytes) {\n throw new StorageError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { url: rawUrl, maxBytes } },\n );\n }\n return Buffer.from(arrayBuffer);\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (!value) {\n continue;\n }\n\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n throw new StorageError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { url: rawUrl, maxBytes } },\n );\n }\n chunks.push(value);\n }\n\n return Buffer.concat(chunks);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"safe-fetch.mjs","names":[],"sources":["../../../../../../../../core/src/storage/utils/safe-fetch.ts"],"sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport { isIP } from \"@warlock.js/seal\";\nimport { StorageError } from \"./storage-error\";\n\n/**\n * SSRF / resource-exhaustion guard for storage's outbound downloads\n * (`putFromUrl`).\n *\n * NOTE: this MIRRORS `@warlock.js/ai`'s `src/security/private-ip.ts` and\n * `src/security/outbound-policy.ts`. It is duplicated here intentionally\n * because `@warlock.js/core` cannot depend on `@warlock.js/ai`. Pending\n * extraction of a shared security package, keep the two copies in sync.\n */\n\n/** 50 MiB — default cap on a downloaded body (storage files run larger than AI payloads). */\nconst DEFAULT_MAX_BYTES = 50 * 1024 * 1024;\n/** 30s — default per-request timeout. */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n/**\n * Cap on the number of re-validated redirect hops. Mirrors\n * `@warlock.js/ai`'s `DEFAULT_MAX_REDIRECTS`\n * (ai/src/security/outbound-policy.ts:15).\n */\nconst DEFAULT_MAX_REDIRECTS = 5;\n\n/** 3xx statuses whose `Location` a follow re-issues — mirrors ai/src/security/outbound-policy.ts:18. */\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/**\n * Options controlling a guarded outbound fetch. Every field is optional;\n * {@link resolveFetchPolicy} fills safe defaults. The defaults are\n * deliberately strict (https+http, private-IP deny ON, 30s timeout,\n * 50 MiB cap) so a caller that does not tune them still gets a hardened\n * request.\n */\nexport type SafeFetchOptions = {\n /**\n * Whether to ALLOW private / loopback / link-local / metadata hosts.\n * Default `false` — the SSRF guard is on (private hosts are denied).\n */\n allowPrivateHosts?: boolean;\n /**\n * Maximum response body size in bytes. A declared `content-length` over\n * this fails fast; otherwise the body is read with a running cap and the\n * stream is aborted on overflow. Default `52_428_800` (50 MiB).\n */\n maxBytes?: number;\n /** Per-request timeout in milliseconds. Default `30_000`. */\n timeoutMs?: number;\n /**\n * URL schemes permitted for the request, compared case-insensitively.\n * Default `[\"https\", \"http\"]`.\n */\n allowedSchemes?: string[];\n /**\n * Injected `fetch` implementation (for tests or proxies). Defaults to the\n * global `fetch`.\n */\n fetch?: typeof fetch;\n};\n\n/** {@link SafeFetchOptions} with every default resolved — never partial. */\ntype ResolvedFetchPolicy = {\n allowPrivateHosts: boolean;\n maxBytes: number;\n timeoutMs: number;\n allowedSchemes: string[];\n fetch: typeof fetch;\n};\n\n/**\n * Fill a {@link SafeFetchOptions} with strict defaults: https+http,\n * private-IP deny on, 30s timeout, 50 MiB cap, global `fetch`.\n */\nfunction resolveFetchPolicy(options: SafeFetchOptions = {}): ResolvedFetchPolicy {\n return {\n allowPrivateHosts: options.allowPrivateHosts ?? false,\n maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,\n timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n allowedSchemes: options.allowedSchemes ?? [\"https\", \"http\"],\n fetch: options.fetch ?? globalThis.fetch,\n };\n}\n\n/** Parse a dotted-quad into four octets, or `null` if malformed. */\nfunction parseIpv4(ip: string): [number, number, number, number] | null {\n const parts = ip.split(\".\");\n if (parts.length !== 4) {\n return null;\n }\n\n const octets = parts.map((part) => Number(part));\n if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {\n return null;\n }\n\n return octets as [number, number, number, number];\n}\n\nfunction isPrivateIpv4(ip: string): boolean {\n const octets = parseIpv4(ip);\n if (!octets) {\n return true; // unparseable → refuse, fail closed\n }\n\n const [a, b] = octets;\n\n return (\n a === 0 || // 0.0.0.0/8 \"this network\"\n a === 10 || // 10.0.0.0/8 private\n a === 127 || // 127.0.0.0/8 loopback\n (a === 100 && b >= 64 && b <= 127) || // 100.64.0.0/10 CGNAT\n (a === 169 && b === 254) || // 169.254.0.0/16 link-local + metadata\n (a === 172 && b >= 16 && b <= 31) || // 172.16.0.0/12 private\n (a === 192 && b === 168) || // 192.168.0.0/16 private\n (a === 192 && b === 0) || // 192.0.0.0/24 + 192.0.2.0/24 (IETF/test)\n (a === 198 && (b === 18 || b === 19)) || // 198.18.0.0/15 benchmarking\n (a === 198 && b === 51) || // 198.51.100.0/24 test-net-2\n (a === 203 && b === 0) || // 203.0.113.0/24 test-net-3\n a >= 224 // 224.0.0.0/4 multicast + 240.0.0.0/4 reserved + 255.255.255.255\n );\n}\n\nfunction isPrivateIpv6(ip: string): boolean {\n const normalized = ip.toLowerCase().split(\"%\")[0]; // drop zone id\n\n // IPv4-mapped / -embedded (::ffff:a.b.c.d, ::a.b.c.d) — defer to the v4\n // check on the trailing dotted-quad so an inward-mapped address is caught.\n const v4 = normalized.match(/(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$/);\n if (v4) {\n return isPrivateIpv4(v4[1]);\n }\n\n if (normalized === \"::1\" || normalized === \"::\") {\n return true; // loopback / unspecified\n }\n\n // Expand only the leading group enough to classify the reserved blocks.\n const firstGroup = normalized.split(\":\")[0];\n const head = firstGroup === \"\" ? 0 : Number.parseInt(firstGroup, 16);\n\n // fc00::/7 unique-local (fc.. / fd..)\n if ((head & 0xfe00) === 0xfc00) {\n return true;\n }\n // fe80::/10 link-local\n if ((head & 0xffc0) === 0xfe80) {\n return true;\n }\n\n return false;\n}\n\n/**\n * True when `ip` is a private, loopback, link-local, unique-local,\n * carrier-grade-NAT, unspecified, or otherwise non-public address — the set\n * an SSRF guard must refuse. Accepts IPv4 and IPv6 literals (including\n * IPv4-mapped IPv6 like `::ffff:169.254.169.254`). A non-IP string returns\n * `false` (the caller resolves hostnames via DNS first).\n *\n * The cloud-metadata endpoint `169.254.169.254` is covered by the IPv4\n * link-local range `169.254.0.0/16`.\n */\nexport function isPrivateOrReservedIp(ip: string): boolean {\n const family = isIP(ip);\n if (family === 4) {\n return isPrivateIpv4(ip);\n }\n if (family === 6) {\n return isPrivateIpv6(ip);\n }\n return false;\n}\n\n/** Strip the `[ ]` IPv6 brackets `URL.hostname` keeps. */\nfunction stripBrackets(host: string): string {\n return host.startsWith(\"[\") && host.endsWith(\"]\") ? host.slice(1, -1) : host;\n}\n\n/**\n * Reject when `host` is — or resolves to — a private / reserved address.\n * IP literals are checked directly; hostnames are resolved via DNS and every\n * returned address is checked (a public name pointing inward is caught). A\n * resolution failure fails closed.\n */\nasync function assertHostNotPrivate(host: string, rawUrl: string): Promise<void> {\n if (isIP(host) !== 0) {\n if (isPrivateOrReservedIp(host)) {\n throw new StorageError(\n `outbound request blocked — \"${host}\" is a private/reserved address`,\n { context: { url: rawUrl, address: host } },\n );\n }\n return;\n }\n\n let addresses: Array<{ address: string }>;\n try {\n addresses = await lookup(host, { all: true });\n } catch (cause) {\n throw new StorageError(\n `outbound request blocked — could not resolve host \"${host}\" to verify it is public`,\n { cause, context: { url: rawUrl, host } },\n );\n }\n\n for (const { address } of addresses) {\n if (isPrivateOrReservedIp(address)) {\n throw new StorageError(\n `outbound request blocked — host \"${host}\" resolves to a private/reserved address (${address})`,\n { context: { url: rawUrl, host, address } },\n );\n }\n }\n}\n\n/**\n * Validate a URL against the policy BEFORE any network call: scheme\n * allowlist and (when private hosts are not allowed) a DNS resolution that\n * rejects private / loopback / link-local / metadata addresses — the SSRF\n * guard. Returns the parsed `URL` on success; throws {@link StorageError}\n * otherwise.\n */\nasync function assertUrlAllowed(rawUrl: string, policy: ResolvedFetchPolicy): Promise<URL> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new StorageError(`outbound request blocked — invalid URL: ${rawUrl}`, {\n context: { url: rawUrl },\n });\n }\n\n const scheme = url.protocol.replace(/:$/, \"\").toLowerCase();\n if (!policy.allowedSchemes.some((s) => s.toLowerCase() === scheme)) {\n throw new StorageError(\n `outbound request blocked — scheme \"${scheme}\" is not allowed (allowed: ${policy.allowedSchemes.join(\", \")})`,\n { context: { url: rawUrl, scheme } },\n );\n }\n\n if (!policy.allowPrivateHosts) {\n const host = stripBrackets(url.hostname);\n await assertHostNotPrivate(host, rawUrl);\n }\n\n return url;\n}\n\n/**\n * Result of {@link safeFetchToBuffer}: the (capped) body plus the response's\n * content-type, so callers can derive a MIME type and enforce a non-empty\n * content-type check.\n */\nexport type SafeFetchResult = {\n buffer: Buffer;\n contentType: string | null;\n status: number;\n statusText: string;\n ok: boolean;\n};\n\n/**\n * SSRF-guarded download: validates the URL (scheme allowlist + post-DNS\n * private-IP deny), performs the request with a timeout via\n * `AbortController`, then streams the body with a hard byte cap — aborting\n * the moment the running total exceeds `maxBytes`. A declared\n * `content-length` over the cap fails fast.\n *\n * Redirects are NEVER delegated to the platform: every hop is issued with\n * `redirect: \"manual\"` and its `Location` is re-run through\n * {@link assertUrlAllowed} — including the DNS-resolution private-IP check\n * — before being followed (capped at {@link DEFAULT_MAX_REDIRECTS}), so a\n * 3xx from an allowed host cannot smuggle the request to a private /\n * metadata target. Mirrors `@warlock.js/ai`'s `guardedFetch`\n * (ai/src/security/outbound-policy.ts:188-284); duplicated per this\n * module's file-header note since core cannot depend on ai.\n *\n * @throws {StorageError} On a blocked URL (initial or redirect target),\n * a redirect-hop bound overflow, timeout, or oversized body.\n */\nexport async function safeFetchToBuffer(\n rawUrl: string,\n options: SafeFetchOptions = {},\n): Promise<SafeFetchResult> {\n const policy = resolveFetchPolicy(options);\n let url = await assertUrlAllowed(rawUrl, policy);\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(\n new StorageError(`outbound request timed out after ${policy.timeoutMs}ms`, {\n context: { url: rawUrl, timeoutMs: policy.timeoutMs },\n }),\n );\n }, policy.timeoutMs);\n\n try {\n let response: Response;\n\n for (let hop = 0; ; hop++) {\n response = await policy.fetch(url, { signal: controller.signal, redirect: \"manual\" });\n\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n break;\n }\n\n if (hop >= DEFAULT_MAX_REDIRECTS) {\n throw new StorageError(\n `outbound request blocked — more than ${DEFAULT_MAX_REDIRECTS} redirects`,\n { context: { url: rawUrl, maxRedirects: DEFAULT_MAX_REDIRECTS } },\n );\n }\n\n let target: URL;\n try {\n target = new URL(location, url);\n } catch {\n throw new StorageError(`outbound request blocked — invalid redirect Location: ${location}`, {\n context: { url: url.toString(), location },\n });\n }\n\n // Discard the interim body so the connection can be reused.\n if (response.body) {\n await response.body.cancel().catch(() => undefined);\n }\n\n // The redirect target gets the SAME scheme + private-IP validation\n // (incl. DNS resolution) as the initial URL — this is what closes\n // the redirect-based SSRF bypass: a 302 from an allowed host to\n // 169.254.169.254 is re-validated here, not blindly followed.\n url = await assertUrlAllowed(target.toString(), policy);\n }\n\n const declared = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > policy.maxBytes) {\n throw new StorageError(\n `outbound response body too large — declared ${declared} bytes exceeds the ${policy.maxBytes}-byte cap`,\n { context: { url: rawUrl, declared, maxBytes: policy.maxBytes } },\n );\n }\n\n const buffer = await readBodyCapped(response, policy.maxBytes, rawUrl);\n\n return {\n buffer,\n contentType: response.headers.get(\"content-type\"),\n status: response.status,\n statusText: response.statusText,\n ok: response.ok,\n };\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Read a response body into a Buffer with a hard byte cap. The stream is\n * read chunk-by-chunk and cancelled the moment the running total exceeds\n * `maxBytes`. Throws {@link StorageError} on overflow.\n */\nasync function readBodyCapped(\n response: Response,\n maxBytes: number,\n rawUrl: string,\n): Promise<Buffer> {\n if (!response.body) {\n const arrayBuffer = await response.arrayBuffer();\n if (arrayBuffer.byteLength > maxBytes) {\n throw new StorageError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { url: rawUrl, maxBytes } },\n );\n }\n return Buffer.from(arrayBuffer);\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (!value) {\n continue;\n }\n\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n throw new StorageError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { url: rawUrl, maxBytes } },\n );\n }\n chunks.push(value);\n }\n\n return Buffer.concat(chunks);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-http-development-server.d.mts","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"mappings":";KAwBY,0BAAA;EAAA;;;;AAON;AA4EN;EA5EE,IAAI;AAAA;;;;;;AA8EI;AAoFV;;iBAtFsB,mBAAA,CACpB,OAAA,GAAS,0BAAA,GACR,OAAO;;AAoFyC;AA6BnD;;iBA7BsB,kBAAA,
|
|
1
|
+
{"version":3,"file":"start-http-development-server.d.mts","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"mappings":";KAwBY,0BAAA;EAAA;;;;AAON;AA4EN;EA5EE,IAAI;AAAA;;;;;;AA8EI;AAoFV;;iBAtFsB,mBAAA,CACpB,OAAA,GAAS,0BAAA,GACR,OAAO;;AAoFyC;AA6BnD;;iBA7BsB,kBAAA,IAAsB,OAAO;;AA6BhB;;iBAAnB,mBAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-connectors-selection.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-connectors-selection.ts"],"sourcesContent":["/**\n * Effective connector selection for the worker test lifecycle.\n *\n * Owns one decision: given what the caller passed and what the project\n * configured, which connectors does `setupTest` actually start? The precedence\n * is ratified in `contracts/2026-08-12-test-worker-lifecycle.md`:\n *\n * explicit non-`undefined` setupTest option > tests.connectors config > true\n */\nimport type { GenericObject } from \"@mongez/reinforcements\";\nimport { config } from \"../config\";\nimport type { ConnectorName } from \"../connectors\";\n\n/**\n * What a test runtime was asked to start.\n *\n * - `false` — bootstrap the runtime, start no connectors.\n * - `true` — start the framework's default connector set.\n * - an array — start exactly those. Order is not promised; lifecycle priority\n * decides the real boot/shutdown order.\n */\nexport type TestConnectorsSelection = boolean | ConnectorName[];\n\n/**\n * A caller's request, before the lower precedence layers are consulted.\n *\n * The distinction that matters is \"did the caller supply a value at all\", not\n * \"what value\" — `setupTest({ connectors: undefined })` must fall through to\n * project config exactly like `setupTest()` does, so an optional variable\n * holding `undefined` can never silently erase a project's configuration.\n */\nexport type RequestedTestConnectors =\n | { readonly isExplicit: true; readonly selection: TestConnectorsSelection }\n | { readonly isExplicit: false };\n\n/**\n * Classify the caller's `connectors` option as explicit or deferred.\n */\nexport function readRequestedConnectors(\n selection: TestConnectorsSelection | undefined,\n): RequestedTestConnectors {\n if (selection === undefined) {\n return { isExplicit: false };\n }\n\n return { isExplicit: true, selection };\n}\n\n/**\n * Read the `tests.connectors` config layer, or `undefined` when the project has\n * not configured one.\n *\n * Only meaningful once config files are loaded — before that, every project\n * looks like a project without a `tests` config.\n */\nexport function readConfiguredConnectors(): TestConnectorsSelection | undefined {\n // The default matters: `config.get` resolves an absent key to its default, and\n // ITS default is `null` — not `{}`. `warlock add test` does not generate\n // `src/config/tests.ts`, so reading `.connectors` off the result threw\n // \"Cannot read properties of null\" on the generated default path.\n const testsConfig = config.get<GenericObject>(\"tests\", {});\n\n // Cast at the config boundary: `GenericObject` values are untyped, and this is\n // the one place the untyped value becomes a typed selection.\n return testsConfig?.connectors as TestConnectorsSelection | undefined;\n}\n\n/**\n * Apply the ratified precedence:\n *\n * explicit non-`undefined` setupTest option > tests.connectors config > true\n *\n * ⚠ This is the reverse of 4.13, where config won over the parameter. Explicit\n * call-site intent beats a project default; a call that supplied nothing is not\n * intent, which is why \"omitted\" and \"explicitly `undefined`\" both defer.\n */\nexport function resolveEffectiveConnectors(\n requested: RequestedTestConnectors,\n): TestConnectorsSelection {\n if (requested.isExplicit) {\n return requested.selection;\n }\n\n const configured = readConfiguredConnectors();\n\n if (configured !== undefined) {\n return configured;\n }\n\n return true;\n}\n\n/**\n * Deduplicate a connector list and put it in a stable order.\n *\n * Two callers naming the same connectors in a different order asked for the same\n * thing, so the lifecycle must not treat them as a conflict.\n */\nexport function normalizeConnectorNames(names: ConnectorName[]): ConnectorName[] {\n return [...new Set(names)].sort();\n}\n\n/**\n * Compare two selections by normalized semantics — arrays as deduplicated sets,\n * never by caller order.\n */\nexport function isSameConnectorsSelection(\n left: TestConnectorsSelection,\n right: TestConnectorsSelection,\n): boolean {\n if (Array.isArray(left) && Array.isArray(right)) {\n const normalizedLeft = normalizeConnectorNames(left);\n const normalizedRight = normalizeConnectorNames(right);\n\n return (\n normalizedLeft.length === normalizedRight.length &&\n normalizedLeft.every((name, index) => name === normalizedRight[index])\n );\n }\n\n return left === right;\n}\n\n/**\n * Render a selection for an error message a caller can act on.\n */\nexport function describeConnectorsSelection(selection: TestConnectorsSelection): string {\n if (selection === true) {\n return \"the default connector set (`connectors: true`)\";\n }\n\n if (selection === false) {\n return \"no connectors (`connectors: false`)\";\n }\n\n const names = normalizeConnectorNames(selection)\n .map((name) => `\"${name}\"`)\n .join(\", \");\n\n return `\\`connectors: [${names}]\\``;\n}\n"],"mappings":";;;;;;;AAsCA,SAAgB,wBACd,WACyB;CACzB,IAAI,cAAc,QAChB,OAAO,EAAE,YAAY,MAAM;CAG7B,OAAO;EAAE,YAAY;EAAM;CAAU;AACvC;;;;;;;;AASA,SAAgB,2BAAgE;CAS9E,OAJoB,OAAO,IAAmB,SAAS,CAAC,CAIvC,
|
|
1
|
+
{"version":3,"file":"test-connectors-selection.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-connectors-selection.ts"],"sourcesContent":["/**\n * Effective connector selection for the worker test lifecycle.\n *\n * Owns one decision: given what the caller passed and what the project\n * configured, which connectors does `setupTest` actually start? The precedence\n * is ratified in `contracts/2026-08-12-test-worker-lifecycle.md`:\n *\n * explicit non-`undefined` setupTest option > tests.connectors config > true\n */\nimport type { GenericObject } from \"@mongez/reinforcements\";\nimport { config } from \"../config\";\nimport type { ConnectorName } from \"../connectors\";\n\n/**\n * What a test runtime was asked to start.\n *\n * - `false` — bootstrap the runtime, start no connectors.\n * - `true` — start the framework's default connector set.\n * - an array — start exactly those. Order is not promised; lifecycle priority\n * decides the real boot/shutdown order.\n */\nexport type TestConnectorsSelection = boolean | ConnectorName[];\n\n/**\n * A caller's request, before the lower precedence layers are consulted.\n *\n * The distinction that matters is \"did the caller supply a value at all\", not\n * \"what value\" — `setupTest({ connectors: undefined })` must fall through to\n * project config exactly like `setupTest()` does, so an optional variable\n * holding `undefined` can never silently erase a project's configuration.\n */\nexport type RequestedTestConnectors =\n | { readonly isExplicit: true; readonly selection: TestConnectorsSelection }\n | { readonly isExplicit: false };\n\n/**\n * Classify the caller's `connectors` option as explicit or deferred.\n */\nexport function readRequestedConnectors(\n selection: TestConnectorsSelection | undefined,\n): RequestedTestConnectors {\n if (selection === undefined) {\n return { isExplicit: false };\n }\n\n return { isExplicit: true, selection };\n}\n\n/**\n * Read the `tests.connectors` config layer, or `undefined` when the project has\n * not configured one.\n *\n * Only meaningful once config files are loaded — before that, every project\n * looks like a project without a `tests` config.\n */\nexport function readConfiguredConnectors(): TestConnectorsSelection | undefined {\n // The default matters: `config.get` resolves an absent key to its default, and\n // ITS default is `null` — not `{}`. `warlock add test` does not generate\n // `src/config/tests.ts`, so reading `.connectors` off the result threw\n // \"Cannot read properties of null\" on the generated default path.\n const testsConfig = config.get<GenericObject>(\"tests\", {});\n\n // Cast at the config boundary: `GenericObject` values are untyped, and this is\n // the one place the untyped value becomes a typed selection.\n return testsConfig?.connectors as TestConnectorsSelection | undefined;\n}\n\n/**\n * Apply the ratified precedence:\n *\n * explicit non-`undefined` setupTest option > tests.connectors config > true\n *\n * ⚠ This is the reverse of 4.13, where config won over the parameter. Explicit\n * call-site intent beats a project default; a call that supplied nothing is not\n * intent, which is why \"omitted\" and \"explicitly `undefined`\" both defer.\n */\nexport function resolveEffectiveConnectors(\n requested: RequestedTestConnectors,\n): TestConnectorsSelection {\n if (requested.isExplicit) {\n return requested.selection;\n }\n\n const configured = readConfiguredConnectors();\n\n if (configured !== undefined) {\n return configured;\n }\n\n return true;\n}\n\n/**\n * Deduplicate a connector list and put it in a stable order.\n *\n * Two callers naming the same connectors in a different order asked for the same\n * thing, so the lifecycle must not treat them as a conflict.\n */\nexport function normalizeConnectorNames(names: ConnectorName[]): ConnectorName[] {\n return [...new Set(names)].sort();\n}\n\n/**\n * Compare two selections by normalized semantics — arrays as deduplicated sets,\n * never by caller order.\n */\nexport function isSameConnectorsSelection(\n left: TestConnectorsSelection,\n right: TestConnectorsSelection,\n): boolean {\n if (Array.isArray(left) && Array.isArray(right)) {\n const normalizedLeft = normalizeConnectorNames(left);\n const normalizedRight = normalizeConnectorNames(right);\n\n return (\n normalizedLeft.length === normalizedRight.length &&\n normalizedLeft.every((name, index) => name === normalizedRight[index])\n );\n }\n\n return left === right;\n}\n\n/**\n * Render a selection for an error message a caller can act on.\n */\nexport function describeConnectorsSelection(selection: TestConnectorsSelection): string {\n if (selection === true) {\n return \"the default connector set (`connectors: true`)\";\n }\n\n if (selection === false) {\n return \"no connectors (`connectors: false`)\";\n }\n\n const names = normalizeConnectorNames(selection)\n .map((name) => `\"${name}\"`)\n .join(\", \");\n\n return `\\`connectors: [${names}]\\``;\n}\n"],"mappings":";;;;;;;AAsCA,SAAgB,wBACd,WACyB;CACzB,IAAI,cAAc,QAChB,OAAO,EAAE,YAAY,MAAM;CAG7B,OAAO;EAAE,YAAY;EAAM;CAAU;AACvC;;;;;;;;AASA,SAAgB,2BAAgE;CAS9E,OAJoB,OAAO,IAAmB,SAAS,CAAC,CAIvC,CAAC,EAAE;AACtB;;;;;;;;;;AAWA,SAAgB,2BACd,WACyB;CACzB,IAAI,UAAU,YACZ,OAAO,UAAU;CAGnB,MAAM,aAAa,yBAAyB;CAE5C,IAAI,eAAe,QACjB,OAAO;CAGT,OAAO;AACT;;;;;;;AAQA,SAAgB,wBAAwB,OAAyC;CAC/E,OAAO,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK;AAClC;;;;;AAMA,SAAgB,0BACd,MACA,OACS;CACT,IAAI,MAAM,QAAQ,IAAI,KAAK,MAAM,QAAQ,KAAK,GAAG;EAC/C,MAAM,iBAAiB,wBAAwB,IAAI;EACnD,MAAM,kBAAkB,wBAAwB,KAAK;EAErD,OACE,eAAe,WAAW,gBAAgB,UAC1C,eAAe,OAAO,MAAM,UAAU,SAAS,gBAAgB,MAAM;CAEzE;CAEA,OAAO,SAAS;AAClB;;;;AAKA,SAAgB,4BAA4B,WAA4C;CACtF,IAAI,cAAc,MAChB,OAAO;CAGT,IAAI,cAAc,OAChB,OAAO;CAOT,OAAO,kBAJO,wBAAwB,SAAS,CAAC,CAC7C,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAC1B,KAAK,IAEqB,EAAE;AACjC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.d.mts","names":[],"sources":["../../../../../../../core/src/tests/test-helpers.ts"],"mappings":";;AAYA;;;;AAAgC;AAehC;;AAfgC,iBAAhB,gBAAA
|
|
1
|
+
{"version":3,"file":"test-helpers.d.mts","names":[],"sources":["../../../../../../../core/src/tests/test-helpers.ts"],"mappings":";;AAYA;;;;AAAgC;AAehC;;AAfgC,iBAAhB,gBAAA;;;;;iBAeM,WAAA,CAAY,IAAA,UAAc,OAAA,GAAS,WAAA,GAAmB,OAAA,CAAQ,QAAA;;;;iBAgD9D,OAAA,CAAQ,IAAA,UAAc,OAAA,GAAS,WAAA,GAAmB,OAAA,CAAQ,QAAA;;;AAhDY;iBAuDtE,QAAA,CACpB,IAAA,UACA,IAAA,YACA,OAAA,GAAS,WAAA,GACR,OAAA,CAAQ,QAAA;;;;iBAOW,OAAA,CACpB,IAAA,UACA,IAAA,YACA,OAAA,GAAS,WAAA,GACR,OAAA,CAAQ,QAAA;;;;iBAOW,UAAA,CAAW,IAAA,UAAc,OAAA,GAAS,WAAA,GAAmB,OAAA,CAAQ,QAAA;;;;iBAO7D,SAAA,CACpB,IAAA,UACA,IAAA,YACA,OAAA,GAAS,WAAA,GACR,OAAA,CAAQ,QAAA;;AAxC6E;AAOxF;iBAwCsB,iBAAA,IAAqB,QAAA,EAAU,QAAA,GAAW,OAAA,CAAQ,CAAA;;;;iBAOlD,UAAA,IAAc,QAAA,EAAU,QAAA,EAAU,cAAA,YAAuB,OAAA,CAAQ,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-setup-timeout.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-setup-timeout.ts"],"sourcesContent":["/**\n * The setup bound for the worker test lifecycle.\n *\n * Owns one decision: how long a `setupTest` attempt may run before the\n * lifecycle gives up on it. `vitest-setup.ts` owns what giving up MEANS.\n *\n * This is a HANG GUARD, not a startup-performance deadline. A cold database, a\n * container that has just been started, a first-run migration — all of those are\n * legitimately slow and none of them are a stuck lifecycle. The default is\n * therefore generous on purpose: it exists so that a setup which will never\n * finish fails with a sentence a caller can act on, instead of stranding the\n * lifecycle in `starting` where `teardownTest`'s wait-then-re-enter path spins\n * until the worker dies of heap exhaustion.\n */\nimport type { GenericObject } from \"@mongez/reinforcements\";\nimport { config } from \"../config\";\n\n/**\n * Two minutes. Chosen to sit far above any healthy bootstrap and far below the\n * point where a human stops watching — a stuck worker should announce itself\n * while someone is still looking at the terminal.\n */\nexport const DEFAULT_TEST_SETUP_TIMEOUT = 120_000;\n\n/**\n * A scheduled expiry the lifecycle can cancel when the attempt settles first.\n *\n * @internal\n */\nexport type TestTimeoutHandle = {\n cancel: () => void;\n};\n\n/**\n * How the lifecycle schedules its expiry.\n *\n * Injectable so a spec can prove the bound in milliseconds instead of outliving\n * a real one — see {@link TestLifecycleRegistry.scheduleTimeout}. Deliberately\n * internal: the public API stays the `setupTest` / `teardownTest` pair.\n *\n * @internal\n */\nexport type TestTimeoutScheduler = (\n timeout: number,\n onExpiry: () => void,\n) => TestTimeoutHandle;\n\n/**\n * The real scheduler.\n *\n * `unref` matters: a pending hang guard must never be the reason a worker stays\n * alive after its tests have finished.\n *\n * @internal\n */\nexport const scheduleRealTimeout: TestTimeoutScheduler = (timeout, onExpiry) => {\n const timer = setTimeout(onExpiry, timeout);\n\n timer.unref?.();\n\n return { cancel: () => clearTimeout(timer) };\n};\n\n/**\n * Read `tests.setupTimeout`, or `undefined` when the project has not set one.\n *\n * Only meaningful once config files are loaded — before that, every project\n * looks like a project without a `tests` config.\n *\n * @throws when the key is present but is not a positive finite number of\n * milliseconds. Falling back to the default would silently erase a project's\n * configuration, which is the same defect the connector precedence rules out.\n */\nexport function readConfiguredSetupTimeout(): number | undefined {\n // `config.get` resolves an absent key to its default, and ITS default is\n // `null` — not `{}` — so the explicit `{}` is what keeps this from throwing\n // \"Cannot read properties of null\" on a project with no `src/config/tests.ts`.\n const testsConfig = config.get<GenericObject>(\"tests\", {});\n const configured = testsConfig?.setupTimeout as unknown;\n\n if (configured === undefined) {\n return undefined;\n }\n\n if (typeof configured !== \"number\" || !Number.isFinite(configured) || configured <= 0) {\n throw new Error(\n `tests.setupTimeout must be a positive number of milliseconds, but it is ${JSON.stringify(configured)}. Remove the key to use the default of ${DEFAULT_TEST_SETUP_TIMEOUT}ms.`,\n );\n }\n\n return configured;\n}\n\n/**\n * The message a caller sees when the bound expires.\n *\n * Names three things, because a hang guard that only says \"timed out\" sends the\n * reader to the framework's source: what state the lifecycle is stuck in, what\n * bound it exceeded, and how to raise that bound.\n */\nexport function describeExpiredSetup(timeout: number): string {\n return (\n `setupTest() did not finish within ${timeout}ms and is stuck in the \"starting\" state. ` +\n \"The lifecycle is now poisoned: whatever that attempt had already started is not known to be \" +\n \"closed, so later setupTest() calls refuse until the Vitest worker is recycled. \" +\n `If your cold start is legitimately slower than this, raise the bound with \\`tests.setupTimeout\\` ` +\n `in \\`src/config/tests.ts\\` — milliseconds, default ${DEFAULT_TEST_SETUP_TIMEOUT}.`\n );\n}\n"],"mappings":";;;;;;;;;AAsBA,MAAa,6BAA6B;;;;;;;;;AAiC1C,MAAa,uBAA6C,SAAS,aAAa;CAC9E,MAAM,QAAQ,WAAW,UAAU,OAAO;CAE1C,MAAM,QAAQ;CAEd,OAAO,EAAE,cAAc,aAAa,KAAK,EAAE;AAC7C;;;;;;;;;;;AAYA,SAAgB,6BAAiD;CAK/D,MAAM,aADc,OAAO,IAAmB,SAAS,CAAC,CAC3B,
|
|
1
|
+
{"version":3,"file":"test-setup-timeout.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-setup-timeout.ts"],"sourcesContent":["/**\n * The setup bound for the worker test lifecycle.\n *\n * Owns one decision: how long a `setupTest` attempt may run before the\n * lifecycle gives up on it. `vitest-setup.ts` owns what giving up MEANS.\n *\n * This is a HANG GUARD, not a startup-performance deadline. A cold database, a\n * container that has just been started, a first-run migration — all of those are\n * legitimately slow and none of them are a stuck lifecycle. The default is\n * therefore generous on purpose: it exists so that a setup which will never\n * finish fails with a sentence a caller can act on, instead of stranding the\n * lifecycle in `starting` where `teardownTest`'s wait-then-re-enter path spins\n * until the worker dies of heap exhaustion.\n */\nimport type { GenericObject } from \"@mongez/reinforcements\";\nimport { config } from \"../config\";\n\n/**\n * Two minutes. Chosen to sit far above any healthy bootstrap and far below the\n * point where a human stops watching — a stuck worker should announce itself\n * while someone is still looking at the terminal.\n */\nexport const DEFAULT_TEST_SETUP_TIMEOUT = 120_000;\n\n/**\n * A scheduled expiry the lifecycle can cancel when the attempt settles first.\n *\n * @internal\n */\nexport type TestTimeoutHandle = {\n cancel: () => void;\n};\n\n/**\n * How the lifecycle schedules its expiry.\n *\n * Injectable so a spec can prove the bound in milliseconds instead of outliving\n * a real one — see {@link TestLifecycleRegistry.scheduleTimeout}. Deliberately\n * internal: the public API stays the `setupTest` / `teardownTest` pair.\n *\n * @internal\n */\nexport type TestTimeoutScheduler = (\n timeout: number,\n onExpiry: () => void,\n) => TestTimeoutHandle;\n\n/**\n * The real scheduler.\n *\n * `unref` matters: a pending hang guard must never be the reason a worker stays\n * alive after its tests have finished.\n *\n * @internal\n */\nexport const scheduleRealTimeout: TestTimeoutScheduler = (timeout, onExpiry) => {\n const timer = setTimeout(onExpiry, timeout);\n\n timer.unref?.();\n\n return { cancel: () => clearTimeout(timer) };\n};\n\n/**\n * Read `tests.setupTimeout`, or `undefined` when the project has not set one.\n *\n * Only meaningful once config files are loaded — before that, every project\n * looks like a project without a `tests` config.\n *\n * @throws when the key is present but is not a positive finite number of\n * milliseconds. Falling back to the default would silently erase a project's\n * configuration, which is the same defect the connector precedence rules out.\n */\nexport function readConfiguredSetupTimeout(): number | undefined {\n // `config.get` resolves an absent key to its default, and ITS default is\n // `null` — not `{}` — so the explicit `{}` is what keeps this from throwing\n // \"Cannot read properties of null\" on a project with no `src/config/tests.ts`.\n const testsConfig = config.get<GenericObject>(\"tests\", {});\n const configured = testsConfig?.setupTimeout as unknown;\n\n if (configured === undefined) {\n return undefined;\n }\n\n if (typeof configured !== \"number\" || !Number.isFinite(configured) || configured <= 0) {\n throw new Error(\n `tests.setupTimeout must be a positive number of milliseconds, but it is ${JSON.stringify(configured)}. Remove the key to use the default of ${DEFAULT_TEST_SETUP_TIMEOUT}ms.`,\n );\n }\n\n return configured;\n}\n\n/**\n * The message a caller sees when the bound expires.\n *\n * Names three things, because a hang guard that only says \"timed out\" sends the\n * reader to the framework's source: what state the lifecycle is stuck in, what\n * bound it exceeded, and how to raise that bound.\n */\nexport function describeExpiredSetup(timeout: number): string {\n return (\n `setupTest() did not finish within ${timeout}ms and is stuck in the \"starting\" state. ` +\n \"The lifecycle is now poisoned: whatever that attempt had already started is not known to be \" +\n \"closed, so later setupTest() calls refuse until the Vitest worker is recycled. \" +\n `If your cold start is legitimately slower than this, raise the bound with \\`tests.setupTimeout\\` ` +\n `in \\`src/config/tests.ts\\` — milliseconds, default ${DEFAULT_TEST_SETUP_TIMEOUT}.`\n );\n}\n"],"mappings":";;;;;;;;;AAsBA,MAAa,6BAA6B;;;;;;;;;AAiC1C,MAAa,uBAA6C,SAAS,aAAa;CAC9E,MAAM,QAAQ,WAAW,UAAU,OAAO;CAE1C,MAAM,QAAQ;CAEd,OAAO,EAAE,cAAc,aAAa,KAAK,EAAE;AAC7C;;;;;;;;;;;AAYA,SAAgB,6BAAiD;CAK/D,MAAM,aADc,OAAO,IAAmB,SAAS,CAAC,CAC3B,CAAC,EAAE;CAEhC,IAAI,eAAe,QACjB;CAGF,IAAI,OAAO,eAAe,YAAY,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc,GAClF,MAAM,IAAI,MACR,2EAA2E,KAAK,UAAU,UAAU,EAAE,yCAAyC,2BAA2B,IAC5K;CAGF,OAAO;AACT;;;;;;;;AASA,SAAgB,qBAAqB,SAAyB;CAC5D,OACE,qCAAqC,QAAQ,0WAIS,2BAA2B;AAErF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest-setup.d.mts","names":[],"sources":["../../../../../../../core/src/tests/vitest-setup.ts"],"mappings":";;;KAoDY,gBAAA;EAAA;;;;AAS0B;AAOtC;;;EAPE,UAAA,GAAa,uBAAuB;AAAA;;;;AAQF;cADvB,kBAAA,SAA2B,KAAK;cACxB,OAAA;AAAA;;;;;;AAqC+C;AAgDpE;;;;AAA6C;;;;;;;;;;;;;;;;;iBAhDvB,SAAA,CAAU,OAAA,GAAU,gBAAA,GAAmB,OAAO;;;;;;;;;;;;;;;;;;iBAgD9C,YAAA,
|
|
1
|
+
{"version":3,"file":"vitest-setup.d.mts","names":[],"sources":["../../../../../../../core/src/tests/vitest-setup.ts"],"mappings":";;;KAoDY,gBAAA;EAAA;;;;AAS0B;AAOtC;;;EAPE,UAAA,GAAa,uBAAuB;AAAA;;;;AAQF;cADvB,kBAAA,SAA2B,KAAK;cACxB,OAAA;AAAA;;;;;;AAqC+C;AAgDpE;;;;AAA6C;;;;;;;;;;;;;;;;;iBAhDvB,SAAA,CAAU,OAAA,GAAU,gBAAA,GAAmB,OAAO;;;;;;;;;;;;;;;;;;iBAgD9C,YAAA,IAAgB,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest-setup.mjs","names":[],"sources":["../../../../../../../core/src/tests/vitest-setup.ts"],"sourcesContent":["/**\r\n * Worker test lifecycle — `setupTest` / `teardownTest`.\r\n *\r\n * Implements `contracts/2026-08-12-test-worker-lifecycle.md`. The pair is\r\n * context-scoped, not \"once per worker\": Vitest runs `setupFiles` before EVERY\r\n * test file and rebuilds the module registry each time, so the harness that\r\n * calls `setupTest` owns pairing it with `teardownTest` in the same runtime\r\n * context.\r\n *\r\n * Ownership, stated once because teardown is wider than it looks:\r\n *\r\n * - the runtime assumes exclusive ownership of the process-local\r\n * `connectorsManager` for its lifetime;\r\n * - it owns the framework bootstrap, the application shutdown hooks, and the\r\n * connectors it starts — including the `connectors: false` case, where\r\n * bootstrap still registers hooks;\r\n * - teardown is MANAGER-WIDE. `connectorsManager.shutdown()` has no selective\r\n * ownership handle, so a lifecycle that started two connectors still closes\r\n * every connector the manager holds. Mixing `setupTest` with manual connector\r\n * startup in the same process is unsupported for exactly that reason.\r\n * - HTTP global setup stays separately owned by `startHttpTestServer` /\r\n * `stopHttpTestServer`.\r\n */\r\nimport { Application } from \"../application/application\";\r\nimport { bootstrap } from \"../bootstrap\";\r\nimport { loadConfigFiles } from \"../config/load-config-files\";\r\nimport { connectorsManager } from \"../connectors\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport {\r\n describeConnectorsSelection,\r\n isSameConnectorsSelection,\r\n normalizeConnectorNames,\r\n readRequestedConnectors,\r\n resolveEffectiveConnectors,\r\n} from \"./test-connectors-selection\";\r\nimport type {\r\n RequestedTestConnectors,\r\n TestConnectorsSelection,\r\n} from \"./test-connectors-selection\";\r\nimport { getTestLifecycleRegistry } from \"./test-lifecycle-state\";\r\nimport type { TestLifecycleRegistry, TestSetupAttempt } from \"./test-lifecycle-state\";\r\nimport {\r\n DEFAULT_TEST_SETUP_TIMEOUT,\r\n describeExpiredSetup,\r\n readConfiguredSetupTimeout,\r\n scheduleRealTimeout,\r\n} from \"./test-setup-timeout\";\r\nimport type { TestTimeoutHandle } from \"./test-setup-timeout\";\r\n\r\nexport type { TestConnectorsSelection } from \"./test-connectors-selection\";\r\n\r\nexport type TestSetupOptions = {\r\n /**\r\n * Which connectors to start.\r\n *\r\n * Precedence is `explicit non-undefined option > tests.connectors config >\r\n * true`. Omitting the property, or passing `undefined`, lets project config\r\n * apply — so an optional variable that happens to be `undefined` cannot erase\r\n * it.\r\n */\r\n connectors?: TestConnectorsSelection;\r\n};\r\n\r\n/**\r\n * Raised by the lifecycle itself, never by the runtime it manages — a conflict\r\n * between two calls, or a refusal to reuse a runtime that did not close.\r\n */\r\nexport class TestLifecycleError extends Error {\r\n public constructor(message: string) {\r\n super(message);\r\n\r\n this.name = \"TestLifecycleError\";\r\n }\r\n}\r\n\r\nconst POISONED_MESSAGE =\r\n \"setupTest() refuses to start a test runtime: an earlier teardownTest() reported a shutdown failure, so the connectors, ports and pools it owned are not known to be closed. Restart the Vitest worker before setting up again, or call teardownTest() to retry the shutdown — the lifecycle only returns to idle on a fully successful retry.\";\r\n\r\n/**\r\n * Bootstrap the test runtime and start the selected connectors.\r\n *\r\n * ⚠ Runs once per TEST FILE, not once per worker: `setupFiles` is executed for\r\n * every file and its module registry is rebuilt with it, measured across both\r\n * pools with isolation on and off. The lifecycle state lives on the runtime\r\n * context rather than in this module precisely so that a rebuild cannot hide a\r\n * runtime that is still up.\r\n *\r\n * Repeated calls:\r\n *\r\n * - concurrent calls with the same effective selection share one startup;\r\n * - while ready, the same effective selection is a no-op;\r\n * - while starting or ready, a DIFFERENT effective selection rejects and leaves\r\n * the live runtime untouched;\r\n * - a failed setup unwinds and returns to idle, so a clean retry is allowed;\r\n * - after a successful `teardownTest`, a later setup may use a different\r\n * selection.\r\n *\r\n * @example\r\n * // let project config decide, falling back to the default set\r\n * await setupTest();\r\n *\r\n * @example\r\n * // bootstrap only — no connectors, whatever config says\r\n * await setupTest({ connectors: false });\r\n */\r\nexport async function setupTest(options?: TestSetupOptions): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n const requested = readRequestedConnectors(options?.connectors);\r\n\r\n if (registry.state === \"poisoned\") {\r\n throw new TestLifecycleError(POISONED_MESSAGE);\r\n }\r\n\r\n if (registry.state === \"stopping\" && registry.teardownAttempt) {\r\n await settled(registry.teardownAttempt);\r\n\r\n return setupTest(options);\r\n }\r\n\r\n if (registry.state === \"ready\") {\r\n assertReadySelectionMatches(registry.activeSelection, requested);\r\n\r\n return;\r\n }\r\n\r\n if (registry.state === \"starting\" && registry.setupAttempt) {\r\n const attempt = registry.setupAttempt;\r\n\r\n await assertPendingSetupMatches(attempt, requested);\r\n\r\n return attempt.completion;\r\n }\r\n\r\n return startTestRuntime(requested);\r\n}\r\n\r\n/**\r\n * Shut the test runtime down and release the lifecycle.\r\n *\r\n * - idle is a no-op;\r\n * - concurrent calls share one attempt;\r\n * - a call made while setup is still running waits for that attempt to settle,\r\n * then closes the runtime if it succeeded;\r\n * - the local \"ready\" state is always cleared, including when shutdown rejects;\r\n * - a shutdown rejection is surfaced, never swallowed, and poisons the\r\n * lifecycle — a reported close failure is not proof that anything closed, so\r\n * later `setupTest` calls refuse until the worker is restarted or a retry\r\n * fully succeeds.\r\n *\r\n * ⚠ Individual connector shutdown failures are caught and logged inside\r\n * `connectorsManager.shutdown()`, so they never reach this function and never\r\n * poison anything. This lifecycle can only surface what that layer reports.\r\n */\r\nexport async function teardownTest(): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n\r\n if (registry.state === \"stopping\" && registry.teardownAttempt) {\r\n return registry.teardownAttempt;\r\n }\r\n\r\n if (registry.state === \"starting\" && registry.setupAttempt) {\r\n // A runtime that is still opening cannot be closed halfway. Wait for the\r\n // attempt to settle: a failed one has already unwound itself and left the\r\n // lifecycle idle, which the re-entry below then reads as \"nothing to do\".\r\n await settled(registry.setupAttempt.completion);\r\n\r\n return teardownTest();\r\n }\r\n\r\n if (registry.state === \"idle\") {\r\n return;\r\n }\r\n\r\n registry.state = \"stopping\";\r\n\r\n // Deferred by a microtask so the registry is fully published before the\r\n // attempt's first line runs — a synchronous throw would otherwise reach the\r\n // `finally` below, and reset the state, before this function had recorded\r\n // that a teardown was in flight at all.\r\n const attempt = Promise.resolve().then(() => runTestRuntimeShutdown());\r\n\r\n registry.teardownAttempt = attempt;\r\n\r\n return attempt;\r\n}\r\n\r\n/**\r\n * The setup attempt's hang guard, as the lifecycle consumes it.\r\n *\r\n * The bound's POLICY — the default, the config key, the message — lives in\r\n * `test-setup-timeout.ts`. What expiry MEANS lives here.\r\n *\r\n * @internal\r\n */\r\ntype SetupBound = {\r\n /**\r\n * Rejects with the expiry error. Never resolves: the attempt is the only side\r\n * of the race that can succeed.\r\n */\r\n readonly expiry: Promise<never>;\r\n\r\n /**\r\n * Re-aim the bound at `next` ms measured from when the ATTEMPT STARTED, once\r\n * config has made `tests.setupTimeout` readable.\r\n */\r\n rearm(next: number): void;\r\n\r\n /** The race is over — cancel whatever timer is still pending. */\r\n settle(): void;\r\n\r\n /** Whether the bound, rather than the attempt, won the race. */\r\n hasExpired(): boolean;\r\n};\r\n\r\n/**\r\n * Arm the hang guard for one setup attempt.\r\n *\r\n * Armed at the default BEFORE the attempt runs, because `tests.setupTimeout` is\r\n * not readable until the attempt has loaded config — and loading config is\r\n * itself one of the steps that can hang. `rearm` then narrows or widens it the\r\n * moment config arrives.\r\n */\r\nfunction armSetupBound(registry: TestLifecycleRegistry): SetupBound {\r\n const schedule = registry.scheduleTimeout ?? scheduleRealTimeout;\r\n const armedAt = Date.now();\r\n\r\n let handle: TestTimeoutHandle | undefined;\r\n let expired = false;\r\n let closed = false;\r\n let expire: (error: unknown) => void = () => undefined;\r\n\r\n const expiry = new Promise<never>((_resolve, reject) => {\r\n expire = reject;\r\n });\r\n\r\n // Only `awaitSetupWithinBound` observes this, and only until the attempt wins\r\n // — after that nobody is listening, and an unobserved rejection surfaces as an\r\n // unhandled rejection that fails an unrelated test file. Same reason as the\r\n // `effectiveSelection` barrier below.\r\n expiry.catch(() => undefined);\r\n\r\n const expireNow = (bound: number): void => {\r\n if (closed) {\r\n return;\r\n }\r\n\r\n closed = true;\r\n expired = true;\r\n\r\n handle?.cancel();\r\n handle = undefined;\r\n\r\n expire(new TestLifecycleError(describeExpiredSetup(bound)));\r\n };\r\n\r\n const armFor = (bound: number, delay: number): void => {\r\n if (closed) {\r\n return;\r\n }\r\n\r\n handle?.cancel();\r\n\r\n // Already overdue. A bound the attempt has ALREADY exceeded must fire now,\r\n // not be rescheduled for time that was spent before it was known.\r\n if (delay <= 0) {\r\n expireNow(bound);\r\n\r\n return;\r\n }\r\n\r\n handle = schedule(delay, () => expireNow(bound));\r\n };\r\n\r\n const initialBound = registry.setupTimeoutOverride ?? DEFAULT_TEST_SETUP_TIMEOUT;\r\n\r\n armFor(initialBound, initialBound);\r\n\r\n return {\r\n expiry,\r\n\r\n // Measured from `armedAt`, never from now: adding a configured bound to the\r\n // time already spent would make the real bound default-plus-configured, and\r\n // a project that lowered the key would get a LONGER guard than the default.\r\n rearm: (next) => armFor(next, next - (Date.now() - armedAt)),\r\n\r\n settle: () => {\r\n closed = true;\r\n\r\n handle?.cancel();\r\n handle = undefined;\r\n },\r\n\r\n hasExpired: () => expired,\r\n };\r\n}\r\n\r\n/**\r\n * Race one setup attempt against its bound.\r\n *\r\n * The losing attempt is NOT cancellable — nothing here can interrupt a bootstrap\r\n * stuck in a socket connect — so when the bound wins, the lifecycle is poisoned\r\n * and every later state write from that abandoned attempt is dropped. That\r\n * dropping happens in `runTestRuntimeStartup`, guarded on `hasExpired()`;\r\n * without it the still-running attempt would quietly overwrite `poisoned` with\r\n * `ready` or `idle` and hand the next caller a runtime nobody owns.\r\n */\r\nasync function awaitSetupWithinBound(\r\n attempt: Promise<void>,\r\n bound: SetupBound,\r\n registry: TestLifecycleRegistry,\r\n): Promise<void> {\r\n try {\r\n await Promise.race([attempt, bound.expiry]);\r\n } catch (error) {\r\n if (bound.hasExpired()) {\r\n registry.state = \"poisoned\";\r\n registry.activeSelection = undefined;\r\n registry.setupAttempt = undefined;\r\n }\r\n\r\n throw error;\r\n } finally {\r\n bound.settle();\r\n }\r\n}\r\n\r\n/**\r\n * Open a new setup attempt and publish it before it runs.\r\n */\r\nfunction startTestRuntime(requested: RequestedTestConnectors): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n\r\n let publishEffectiveSelection: (selection: TestConnectorsSelection) => void = () => undefined;\r\n let failEffectiveSelection: (error: unknown) => void = () => undefined;\r\n\r\n const effectiveSelection = new Promise<TestConnectorsSelection>((resolve, reject) => {\r\n publishEffectiveSelection = resolve;\r\n failEffectiveSelection = reject;\r\n });\r\n\r\n // Only the mixed explicit/config comparison ever awaits this barrier, so its\r\n // rejection is usually unobserved — and an unobserved rejection would surface\r\n // as an unhandled rejection and fail an unrelated test file.\r\n effectiveSelection.catch(() => undefined);\r\n\r\n const bound = armSetupBound(registry);\r\n\r\n const attempt = Promise.resolve().then(() =>\r\n runTestRuntimeStartup(requested, publishEffectiveSelection, failEffectiveSelection, bound),\r\n );\r\n\r\n // The attempt outlives a bound that beat it, and by then `completion` has\r\n // already rejected with the expiry error — so the attempt's own late rejection\r\n // has no observer. Same unhandled-rejection hazard as above.\r\n attempt.catch(() => undefined);\r\n\r\n // `teardownTest` waits on this same promise, which is how a teardown requested\r\n // during setup inherits the bound. There is deliberately no second timer and\r\n // no separate teardown deadline.\r\n const completion = awaitSetupWithinBound(attempt, bound, registry);\r\n\r\n registry.state = \"starting\";\r\n registry.setupAttempt = { requested, effectiveSelection, completion };\r\n\r\n return completion;\r\n}\r\n\r\n/**\r\n * The startup attempt itself.\r\n *\r\n * On failure: best-effort unwind, reset bookkeeping, rethrow the ORIGINAL error\r\n * unchanged. A cleanup failure is a secondary diagnostic and never replaces the\r\n * cause the caller needs to read.\r\n */\r\nasync function runTestRuntimeStartup(\r\n requested: RequestedTestConnectors,\r\n publishEffectiveSelection: (selection: TestConnectorsSelection) => void,\r\n failEffectiveSelection: (error: unknown) => void,\r\n bound: SetupBound,\r\n): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n\r\n try {\r\n Application.setEnvironment(\"test\");\r\n\r\n await warlockConfigManager.load();\r\n await bootstrap();\r\n\r\n await filesOrchestrator.init();\r\n await loadConfigFiles(true);\r\n\r\n // The first point at which `tests.setupTimeout` is readable at all. An\r\n // invalid value throws from here and takes the normal failure path — falling\r\n // back to the default would silently erase what the project configured.\r\n const configuredTimeout = readConfiguredSetupTimeout();\r\n\r\n if (configuredTimeout !== undefined) {\r\n bound.rearm(configuredTimeout);\r\n }\r\n\r\n // Resolved here and not at call time: the config layer is only readable once\r\n // the four steps above have run.\r\n const effectiveSelection = resolveEffectiveConnectors(requested);\r\n\r\n // Every state write below is conditional on this attempt still being the one\r\n // the lifecycle is waiting for. Once the bound has expired the lifecycle is\r\n // poisoned and this attempt is abandoned — it may still be running, but it\r\n // no longer speaks for the runtime.\r\n if (!bound.hasExpired()) {\r\n registry.activeSelection = effectiveSelection;\r\n }\r\n\r\n publishEffectiveSelection(effectiveSelection);\r\n\r\n await startSelectedConnectors(effectiveSelection);\r\n\r\n if (!bound.hasExpired()) {\r\n registry.state = \"ready\";\r\n registry.setupAttempt = undefined;\r\n }\r\n } catch (error) {\r\n failEffectiveSelection(error);\r\n\r\n await unwindPartialStartup();\r\n\r\n if (!bound.hasExpired()) {\r\n registry.state = \"idle\";\r\n registry.activeSelection = undefined;\r\n registry.setupAttempt = undefined;\r\n }\r\n\r\n console.error(\"[vitest-setup] Failed to setup test environment:\", error);\r\n\r\n throw error;\r\n }\r\n}\r\n\r\n/**\r\n * `false` means none at all. Everything else starts something: an array starts\r\n * exactly those, and `true` starts the default set — all but http, which is the\r\n * global setup's job and shared across every worker.\r\n */\r\nasync function startSelectedConnectors(selection: TestConnectorsSelection): Promise<void> {\r\n if (selection === false) {\r\n return;\r\n }\r\n\r\n if (Array.isArray(selection)) {\r\n await connectorsManager.start(normalizeConnectorNames(selection));\r\n\r\n return;\r\n }\r\n\r\n await connectorsManager.startWithout([\"http\"]);\r\n}\r\n\r\n/**\r\n * Best-effort teardown of whatever a failed startup managed to start.\r\n *\r\n * Runs while an error is already in flight, so no step here may throw and every\r\n * step is attempted independently.\r\n */\r\nasync function unwindPartialStartup(): Promise<void> {\r\n // Two steps rather than one: `connectorsManager.shutdown()` runs the\r\n // application hooks itself, but it runs them FIRST and does not isolate their\r\n // rejection — so a hook that rejects would take the connector teardown down\r\n // with it and leave every connector up. Both calls are idempotent, so running\r\n // the hooks on their own first costs nothing and means neither failure can\r\n // skip the other step.\r\n await attemptCleanupStep(\"application shutdown hooks\", () => Application.runShutdownHooks());\r\n await attemptCleanupStep(\"connectors shutdown\", () => connectorsManager.shutdown());\r\n}\r\n\r\n/**\r\n * Run one cleanup step, isolating its failure so the next step still runs.\r\n */\r\nasync function attemptCleanupStep(step: string, run: () => Promise<void>): Promise<void> {\r\n try {\r\n await run();\r\n } catch (error) {\r\n // Reported, not thrown — it is secondary to the startup error about to be\r\n // rethrown, and losing that one to this would be the worse outcome.\r\n console.error(`[vitest-setup] Cleanup after a failed setup did not complete (${step}):`, error);\r\n }\r\n}\r\n\r\n/**\r\n * The teardown attempt itself.\r\n */\r\nasync function runTestRuntimeShutdown(): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n let shutdownSucceeded = false;\r\n\r\n try {\r\n // Manager-wide, and deliberately called whatever the selection was: with\r\n // `connectors: false` nothing was started, but bootstrap still registered\r\n // application shutdown hooks, and the manager runs those first (see\r\n // `connectors-manager.ts` — `Application.runShutdownHooks()` is its first\r\n // line). Skipping the call to \"match\" an empty selection would leak them.\r\n await connectorsManager.shutdown();\r\n\r\n shutdownSucceeded = true;\r\n } finally {\r\n // In a `finally`, not after the `await`: a rejected shutdown must not leave\r\n // the lifecycle claiming a runtime it no longer owns. Poisoned rather than\r\n // idle, because a cleared flag does not prove that ports, sockets, pools or\r\n // timers closed.\r\n registry.activeSelection = undefined;\r\n registry.setupAttempt = undefined;\r\n registry.teardownAttempt = undefined;\r\n registry.state = shutdownSucceeded ? \"idle\" : \"poisoned\";\r\n }\r\n}\r\n\r\n/**\r\n * Reject when a ready runtime was set up with a different selection.\r\n */\r\nfunction assertReadySelectionMatches(\r\n activeSelection: TestConnectorsSelection | undefined,\r\n requested: RequestedTestConnectors,\r\n): void {\r\n if (activeSelection === undefined) {\r\n return;\r\n }\r\n\r\n // Config is loaded by the time anything is ready, so the config-derived layer\r\n // is resolvable right here.\r\n const requestedSelection = resolveEffectiveConnectors(requested);\r\n\r\n assertSelectionsMatch(activeSelection, requestedSelection);\r\n}\r\n\r\n/**\r\n * Reject when an in-flight setup attempt is starting a different selection.\r\n */\r\nasync function assertPendingSetupMatches(\r\n attempt: TestSetupAttempt,\r\n requested: RequestedTestConnectors,\r\n): Promise<void> {\r\n const pending = attempt.requested;\r\n\r\n // Two calls that both defer to project config resolve to the same thing by\r\n // construction — knowable without waiting for config to load.\r\n if (!pending.isExplicit && !requested.isExplicit) {\r\n return;\r\n }\r\n\r\n if (pending.isExplicit && requested.isExplicit) {\r\n assertSelectionsMatch(pending.selection, requested.selection);\r\n\r\n return;\r\n }\r\n\r\n // Mixed: one side is config-derived and config is not readable until the\r\n // running attempt has loaded it, so wait for that barrier first.\r\n const activeSelection = await attempt.effectiveSelection;\r\n const requestedSelection = resolveEffectiveConnectors(requested);\r\n\r\n assertSelectionsMatch(activeSelection, requestedSelection);\r\n}\r\n\r\n/**\r\n * Name both selections: a caller looking at this message has to be able to tell\r\n * which call to change without reading the framework's source.\r\n */\r\nfunction assertSelectionsMatch(\r\n activeSelection: TestConnectorsSelection,\r\n requestedSelection: TestConnectorsSelection,\r\n): void {\r\n if (isSameConnectorsSelection(activeSelection, requestedSelection)) {\r\n return;\r\n }\r\n\r\n throw new TestLifecycleError(\r\n `setupTest() is already using ${describeConnectorsSelection(activeSelection)}, and this call asked for ${describeConnectorsSelection(requestedSelection)}. One test runtime serves one connector selection — call teardownTest() before setting up a different one.`,\r\n );\r\n}\r\n\r\n/**\r\n * Await an attempt for its state transition only, not its result.\r\n */\r\nfunction settled(attempt: Promise<void>): Promise<void> {\r\n return attempt.catch(() => undefined);\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vitest-setup.mjs","names":[],"sources":["../../../../../../../core/src/tests/vitest-setup.ts"],"sourcesContent":["/**\r\n * Worker test lifecycle — `setupTest` / `teardownTest`.\r\n *\r\n * Implements `contracts/2026-08-12-test-worker-lifecycle.md`. The pair is\r\n * context-scoped, not \"once per worker\": Vitest runs `setupFiles` before EVERY\r\n * test file and rebuilds the module registry each time, so the harness that\r\n * calls `setupTest` owns pairing it with `teardownTest` in the same runtime\r\n * context.\r\n *\r\n * Ownership, stated once because teardown is wider than it looks:\r\n *\r\n * - the runtime assumes exclusive ownership of the process-local\r\n * `connectorsManager` for its lifetime;\r\n * - it owns the framework bootstrap, the application shutdown hooks, and the\r\n * connectors it starts — including the `connectors: false` case, where\r\n * bootstrap still registers hooks;\r\n * - teardown is MANAGER-WIDE. `connectorsManager.shutdown()` has no selective\r\n * ownership handle, so a lifecycle that started two connectors still closes\r\n * every connector the manager holds. Mixing `setupTest` with manual connector\r\n * startup in the same process is unsupported for exactly that reason.\r\n * - HTTP global setup stays separately owned by `startHttpTestServer` /\r\n * `stopHttpTestServer`.\r\n */\r\nimport { Application } from \"../application/application\";\r\nimport { bootstrap } from \"../bootstrap\";\r\nimport { loadConfigFiles } from \"../config/load-config-files\";\r\nimport { connectorsManager } from \"../connectors\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport {\r\n describeConnectorsSelection,\r\n isSameConnectorsSelection,\r\n normalizeConnectorNames,\r\n readRequestedConnectors,\r\n resolveEffectiveConnectors,\r\n} from \"./test-connectors-selection\";\r\nimport type {\r\n RequestedTestConnectors,\r\n TestConnectorsSelection,\r\n} from \"./test-connectors-selection\";\r\nimport { getTestLifecycleRegistry } from \"./test-lifecycle-state\";\r\nimport type { TestLifecycleRegistry, TestSetupAttempt } from \"./test-lifecycle-state\";\r\nimport {\r\n DEFAULT_TEST_SETUP_TIMEOUT,\r\n describeExpiredSetup,\r\n readConfiguredSetupTimeout,\r\n scheduleRealTimeout,\r\n} from \"./test-setup-timeout\";\r\nimport type { TestTimeoutHandle } from \"./test-setup-timeout\";\r\n\r\nexport type { TestConnectorsSelection } from \"./test-connectors-selection\";\r\n\r\nexport type TestSetupOptions = {\r\n /**\r\n * Which connectors to start.\r\n *\r\n * Precedence is `explicit non-undefined option > tests.connectors config >\r\n * true`. Omitting the property, or passing `undefined`, lets project config\r\n * apply — so an optional variable that happens to be `undefined` cannot erase\r\n * it.\r\n */\r\n connectors?: TestConnectorsSelection;\r\n};\r\n\r\n/**\r\n * Raised by the lifecycle itself, never by the runtime it manages — a conflict\r\n * between two calls, or a refusal to reuse a runtime that did not close.\r\n */\r\nexport class TestLifecycleError extends Error {\r\n public constructor(message: string) {\r\n super(message);\r\n\r\n this.name = \"TestLifecycleError\";\r\n }\r\n}\r\n\r\nconst POISONED_MESSAGE =\r\n \"setupTest() refuses to start a test runtime: an earlier teardownTest() reported a shutdown failure, so the connectors, ports and pools it owned are not known to be closed. Restart the Vitest worker before setting up again, or call teardownTest() to retry the shutdown — the lifecycle only returns to idle on a fully successful retry.\";\r\n\r\n/**\r\n * Bootstrap the test runtime and start the selected connectors.\r\n *\r\n * ⚠ Runs once per TEST FILE, not once per worker: `setupFiles` is executed for\r\n * every file and its module registry is rebuilt with it, measured across both\r\n * pools with isolation on and off. The lifecycle state lives on the runtime\r\n * context rather than in this module precisely so that a rebuild cannot hide a\r\n * runtime that is still up.\r\n *\r\n * Repeated calls:\r\n *\r\n * - concurrent calls with the same effective selection share one startup;\r\n * - while ready, the same effective selection is a no-op;\r\n * - while starting or ready, a DIFFERENT effective selection rejects and leaves\r\n * the live runtime untouched;\r\n * - a failed setup unwinds and returns to idle, so a clean retry is allowed;\r\n * - after a successful `teardownTest`, a later setup may use a different\r\n * selection.\r\n *\r\n * @example\r\n * // let project config decide, falling back to the default set\r\n * await setupTest();\r\n *\r\n * @example\r\n * // bootstrap only — no connectors, whatever config says\r\n * await setupTest({ connectors: false });\r\n */\r\nexport async function setupTest(options?: TestSetupOptions): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n const requested = readRequestedConnectors(options?.connectors);\r\n\r\n if (registry.state === \"poisoned\") {\r\n throw new TestLifecycleError(POISONED_MESSAGE);\r\n }\r\n\r\n if (registry.state === \"stopping\" && registry.teardownAttempt) {\r\n await settled(registry.teardownAttempt);\r\n\r\n return setupTest(options);\r\n }\r\n\r\n if (registry.state === \"ready\") {\r\n assertReadySelectionMatches(registry.activeSelection, requested);\r\n\r\n return;\r\n }\r\n\r\n if (registry.state === \"starting\" && registry.setupAttempt) {\r\n const attempt = registry.setupAttempt;\r\n\r\n await assertPendingSetupMatches(attempt, requested);\r\n\r\n return attempt.completion;\r\n }\r\n\r\n return startTestRuntime(requested);\r\n}\r\n\r\n/**\r\n * Shut the test runtime down and release the lifecycle.\r\n *\r\n * - idle is a no-op;\r\n * - concurrent calls share one attempt;\r\n * - a call made while setup is still running waits for that attempt to settle,\r\n * then closes the runtime if it succeeded;\r\n * - the local \"ready\" state is always cleared, including when shutdown rejects;\r\n * - a shutdown rejection is surfaced, never swallowed, and poisons the\r\n * lifecycle — a reported close failure is not proof that anything closed, so\r\n * later `setupTest` calls refuse until the worker is restarted or a retry\r\n * fully succeeds.\r\n *\r\n * ⚠ Individual connector shutdown failures are caught and logged inside\r\n * `connectorsManager.shutdown()`, so they never reach this function and never\r\n * poison anything. This lifecycle can only surface what that layer reports.\r\n */\r\nexport async function teardownTest(): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n\r\n if (registry.state === \"stopping\" && registry.teardownAttempt) {\r\n return registry.teardownAttempt;\r\n }\r\n\r\n if (registry.state === \"starting\" && registry.setupAttempt) {\r\n // A runtime that is still opening cannot be closed halfway. Wait for the\r\n // attempt to settle: a failed one has already unwound itself and left the\r\n // lifecycle idle, which the re-entry below then reads as \"nothing to do\".\r\n await settled(registry.setupAttempt.completion);\r\n\r\n return teardownTest();\r\n }\r\n\r\n if (registry.state === \"idle\") {\r\n return;\r\n }\r\n\r\n registry.state = \"stopping\";\r\n\r\n // Deferred by a microtask so the registry is fully published before the\r\n // attempt's first line runs — a synchronous throw would otherwise reach the\r\n // `finally` below, and reset the state, before this function had recorded\r\n // that a teardown was in flight at all.\r\n const attempt = Promise.resolve().then(() => runTestRuntimeShutdown());\r\n\r\n registry.teardownAttempt = attempt;\r\n\r\n return attempt;\r\n}\r\n\r\n/**\r\n * The setup attempt's hang guard, as the lifecycle consumes it.\r\n *\r\n * The bound's POLICY — the default, the config key, the message — lives in\r\n * `test-setup-timeout.ts`. What expiry MEANS lives here.\r\n *\r\n * @internal\r\n */\r\ntype SetupBound = {\r\n /**\r\n * Rejects with the expiry error. Never resolves: the attempt is the only side\r\n * of the race that can succeed.\r\n */\r\n readonly expiry: Promise<never>;\r\n\r\n /**\r\n * Re-aim the bound at `next` ms measured from when the ATTEMPT STARTED, once\r\n * config has made `tests.setupTimeout` readable.\r\n */\r\n rearm(next: number): void;\r\n\r\n /** The race is over — cancel whatever timer is still pending. */\r\n settle(): void;\r\n\r\n /** Whether the bound, rather than the attempt, won the race. */\r\n hasExpired(): boolean;\r\n};\r\n\r\n/**\r\n * Arm the hang guard for one setup attempt.\r\n *\r\n * Armed at the default BEFORE the attempt runs, because `tests.setupTimeout` is\r\n * not readable until the attempt has loaded config — and loading config is\r\n * itself one of the steps that can hang. `rearm` then narrows or widens it the\r\n * moment config arrives.\r\n */\r\nfunction armSetupBound(registry: TestLifecycleRegistry): SetupBound {\r\n const schedule = registry.scheduleTimeout ?? scheduleRealTimeout;\r\n const armedAt = Date.now();\r\n\r\n let handle: TestTimeoutHandle | undefined;\r\n let expired = false;\r\n let closed = false;\r\n let expire: (error: unknown) => void = () => undefined;\r\n\r\n const expiry = new Promise<never>((_resolve, reject) => {\r\n expire = reject;\r\n });\r\n\r\n // Only `awaitSetupWithinBound` observes this, and only until the attempt wins\r\n // — after that nobody is listening, and an unobserved rejection surfaces as an\r\n // unhandled rejection that fails an unrelated test file. Same reason as the\r\n // `effectiveSelection` barrier below.\r\n expiry.catch(() => undefined);\r\n\r\n const expireNow = (bound: number): void => {\r\n if (closed) {\r\n return;\r\n }\r\n\r\n closed = true;\r\n expired = true;\r\n\r\n handle?.cancel();\r\n handle = undefined;\r\n\r\n expire(new TestLifecycleError(describeExpiredSetup(bound)));\r\n };\r\n\r\n const armFor = (bound: number, delay: number): void => {\r\n if (closed) {\r\n return;\r\n }\r\n\r\n handle?.cancel();\r\n\r\n // Already overdue. A bound the attempt has ALREADY exceeded must fire now,\r\n // not be rescheduled for time that was spent before it was known.\r\n if (delay <= 0) {\r\n expireNow(bound);\r\n\r\n return;\r\n }\r\n\r\n handle = schedule(delay, () => expireNow(bound));\r\n };\r\n\r\n const initialBound = registry.setupTimeoutOverride ?? DEFAULT_TEST_SETUP_TIMEOUT;\r\n\r\n armFor(initialBound, initialBound);\r\n\r\n return {\r\n expiry,\r\n\r\n // Measured from `armedAt`, never from now: adding a configured bound to the\r\n // time already spent would make the real bound default-plus-configured, and\r\n // a project that lowered the key would get a LONGER guard than the default.\r\n rearm: (next) => armFor(next, next - (Date.now() - armedAt)),\r\n\r\n settle: () => {\r\n closed = true;\r\n\r\n handle?.cancel();\r\n handle = undefined;\r\n },\r\n\r\n hasExpired: () => expired,\r\n };\r\n}\r\n\r\n/**\r\n * Race one setup attempt against its bound.\r\n *\r\n * The losing attempt is NOT cancellable — nothing here can interrupt a bootstrap\r\n * stuck in a socket connect — so when the bound wins, the lifecycle is poisoned\r\n * and every later state write from that abandoned attempt is dropped. That\r\n * dropping happens in `runTestRuntimeStartup`, guarded on `hasExpired()`;\r\n * without it the still-running attempt would quietly overwrite `poisoned` with\r\n * `ready` or `idle` and hand the next caller a runtime nobody owns.\r\n */\r\nasync function awaitSetupWithinBound(\r\n attempt: Promise<void>,\r\n bound: SetupBound,\r\n registry: TestLifecycleRegistry,\r\n): Promise<void> {\r\n try {\r\n await Promise.race([attempt, bound.expiry]);\r\n } catch (error) {\r\n if (bound.hasExpired()) {\r\n registry.state = \"poisoned\";\r\n registry.activeSelection = undefined;\r\n registry.setupAttempt = undefined;\r\n }\r\n\r\n throw error;\r\n } finally {\r\n bound.settle();\r\n }\r\n}\r\n\r\n/**\r\n * Open a new setup attempt and publish it before it runs.\r\n */\r\nfunction startTestRuntime(requested: RequestedTestConnectors): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n\r\n let publishEffectiveSelection: (selection: TestConnectorsSelection) => void = () => undefined;\r\n let failEffectiveSelection: (error: unknown) => void = () => undefined;\r\n\r\n const effectiveSelection = new Promise<TestConnectorsSelection>((resolve, reject) => {\r\n publishEffectiveSelection = resolve;\r\n failEffectiveSelection = reject;\r\n });\r\n\r\n // Only the mixed explicit/config comparison ever awaits this barrier, so its\r\n // rejection is usually unobserved — and an unobserved rejection would surface\r\n // as an unhandled rejection and fail an unrelated test file.\r\n effectiveSelection.catch(() => undefined);\r\n\r\n const bound = armSetupBound(registry);\r\n\r\n const attempt = Promise.resolve().then(() =>\r\n runTestRuntimeStartup(requested, publishEffectiveSelection, failEffectiveSelection, bound),\r\n );\r\n\r\n // The attempt outlives a bound that beat it, and by then `completion` has\r\n // already rejected with the expiry error — so the attempt's own late rejection\r\n // has no observer. Same unhandled-rejection hazard as above.\r\n attempt.catch(() => undefined);\r\n\r\n // `teardownTest` waits on this same promise, which is how a teardown requested\r\n // during setup inherits the bound. There is deliberately no second timer and\r\n // no separate teardown deadline.\r\n const completion = awaitSetupWithinBound(attempt, bound, registry);\r\n\r\n registry.state = \"starting\";\r\n registry.setupAttempt = { requested, effectiveSelection, completion };\r\n\r\n return completion;\r\n}\r\n\r\n/**\r\n * The startup attempt itself.\r\n *\r\n * On failure: best-effort unwind, reset bookkeeping, rethrow the ORIGINAL error\r\n * unchanged. A cleanup failure is a secondary diagnostic and never replaces the\r\n * cause the caller needs to read.\r\n */\r\nasync function runTestRuntimeStartup(\r\n requested: RequestedTestConnectors,\r\n publishEffectiveSelection: (selection: TestConnectorsSelection) => void,\r\n failEffectiveSelection: (error: unknown) => void,\r\n bound: SetupBound,\r\n): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n\r\n try {\r\n Application.setEnvironment(\"test\");\r\n\r\n await warlockConfigManager.load();\r\n await bootstrap();\r\n\r\n await filesOrchestrator.init();\r\n await loadConfigFiles(true);\r\n\r\n // The first point at which `tests.setupTimeout` is readable at all. An\r\n // invalid value throws from here and takes the normal failure path — falling\r\n // back to the default would silently erase what the project configured.\r\n const configuredTimeout = readConfiguredSetupTimeout();\r\n\r\n if (configuredTimeout !== undefined) {\r\n bound.rearm(configuredTimeout);\r\n }\r\n\r\n // Resolved here and not at call time: the config layer is only readable once\r\n // the four steps above have run.\r\n const effectiveSelection = resolveEffectiveConnectors(requested);\r\n\r\n // Every state write below is conditional on this attempt still being the one\r\n // the lifecycle is waiting for. Once the bound has expired the lifecycle is\r\n // poisoned and this attempt is abandoned — it may still be running, but it\r\n // no longer speaks for the runtime.\r\n if (!bound.hasExpired()) {\r\n registry.activeSelection = effectiveSelection;\r\n }\r\n\r\n publishEffectiveSelection(effectiveSelection);\r\n\r\n await startSelectedConnectors(effectiveSelection);\r\n\r\n if (!bound.hasExpired()) {\r\n registry.state = \"ready\";\r\n registry.setupAttempt = undefined;\r\n }\r\n } catch (error) {\r\n failEffectiveSelection(error);\r\n\r\n await unwindPartialStartup();\r\n\r\n if (!bound.hasExpired()) {\r\n registry.state = \"idle\";\r\n registry.activeSelection = undefined;\r\n registry.setupAttempt = undefined;\r\n }\r\n\r\n console.error(\"[vitest-setup] Failed to setup test environment:\", error);\r\n\r\n throw error;\r\n }\r\n}\r\n\r\n/**\r\n * `false` means none at all. Everything else starts something: an array starts\r\n * exactly those, and `true` starts the default set — all but http, which is the\r\n * global setup's job and shared across every worker.\r\n */\r\nasync function startSelectedConnectors(selection: TestConnectorsSelection): Promise<void> {\r\n if (selection === false) {\r\n return;\r\n }\r\n\r\n if (Array.isArray(selection)) {\r\n await connectorsManager.start(normalizeConnectorNames(selection));\r\n\r\n return;\r\n }\r\n\r\n await connectorsManager.startWithout([\"http\"]);\r\n}\r\n\r\n/**\r\n * Best-effort teardown of whatever a failed startup managed to start.\r\n *\r\n * Runs while an error is already in flight, so no step here may throw and every\r\n * step is attempted independently.\r\n */\r\nasync function unwindPartialStartup(): Promise<void> {\r\n // Two steps rather than one: `connectorsManager.shutdown()` runs the\r\n // application hooks itself, but it runs them FIRST and does not isolate their\r\n // rejection — so a hook that rejects would take the connector teardown down\r\n // with it and leave every connector up. Both calls are idempotent, so running\r\n // the hooks on their own first costs nothing and means neither failure can\r\n // skip the other step.\r\n await attemptCleanupStep(\"application shutdown hooks\", () => Application.runShutdownHooks());\r\n await attemptCleanupStep(\"connectors shutdown\", () => connectorsManager.shutdown());\r\n}\r\n\r\n/**\r\n * Run one cleanup step, isolating its failure so the next step still runs.\r\n */\r\nasync function attemptCleanupStep(step: string, run: () => Promise<void>): Promise<void> {\r\n try {\r\n await run();\r\n } catch (error) {\r\n // Reported, not thrown — it is secondary to the startup error about to be\r\n // rethrown, and losing that one to this would be the worse outcome.\r\n console.error(`[vitest-setup] Cleanup after a failed setup did not complete (${step}):`, error);\r\n }\r\n}\r\n\r\n/**\r\n * The teardown attempt itself.\r\n */\r\nasync function runTestRuntimeShutdown(): Promise<void> {\r\n const registry = getTestLifecycleRegistry();\r\n let shutdownSucceeded = false;\r\n\r\n try {\r\n // Manager-wide, and deliberately called whatever the selection was: with\r\n // `connectors: false` nothing was started, but bootstrap still registered\r\n // application shutdown hooks, and the manager runs those first (see\r\n // `connectors-manager.ts` — `Application.runShutdownHooks()` is its first\r\n // line). Skipping the call to \"match\" an empty selection would leak them.\r\n await connectorsManager.shutdown();\r\n\r\n shutdownSucceeded = true;\r\n } finally {\r\n // In a `finally`, not after the `await`: a rejected shutdown must not leave\r\n // the lifecycle claiming a runtime it no longer owns. Poisoned rather than\r\n // idle, because a cleared flag does not prove that ports, sockets, pools or\r\n // timers closed.\r\n registry.activeSelection = undefined;\r\n registry.setupAttempt = undefined;\r\n registry.teardownAttempt = undefined;\r\n registry.state = shutdownSucceeded ? \"idle\" : \"poisoned\";\r\n }\r\n}\r\n\r\n/**\r\n * Reject when a ready runtime was set up with a different selection.\r\n */\r\nfunction assertReadySelectionMatches(\r\n activeSelection: TestConnectorsSelection | undefined,\r\n requested: RequestedTestConnectors,\r\n): void {\r\n if (activeSelection === undefined) {\r\n return;\r\n }\r\n\r\n // Config is loaded by the time anything is ready, so the config-derived layer\r\n // is resolvable right here.\r\n const requestedSelection = resolveEffectiveConnectors(requested);\r\n\r\n assertSelectionsMatch(activeSelection, requestedSelection);\r\n}\r\n\r\n/**\r\n * Reject when an in-flight setup attempt is starting a different selection.\r\n */\r\nasync function assertPendingSetupMatches(\r\n attempt: TestSetupAttempt,\r\n requested: RequestedTestConnectors,\r\n): Promise<void> {\r\n const pending = attempt.requested;\r\n\r\n // Two calls that both defer to project config resolve to the same thing by\r\n // construction — knowable without waiting for config to load.\r\n if (!pending.isExplicit && !requested.isExplicit) {\r\n return;\r\n }\r\n\r\n if (pending.isExplicit && requested.isExplicit) {\r\n assertSelectionsMatch(pending.selection, requested.selection);\r\n\r\n return;\r\n }\r\n\r\n // Mixed: one side is config-derived and config is not readable until the\r\n // running attempt has loaded it, so wait for that barrier first.\r\n const activeSelection = await attempt.effectiveSelection;\r\n const requestedSelection = resolveEffectiveConnectors(requested);\r\n\r\n assertSelectionsMatch(activeSelection, requestedSelection);\r\n}\r\n\r\n/**\r\n * Name both selections: a caller looking at this message has to be able to tell\r\n * which call to change without reading the framework's source.\r\n */\r\nfunction assertSelectionsMatch(\r\n activeSelection: TestConnectorsSelection,\r\n requestedSelection: TestConnectorsSelection,\r\n): void {\r\n if (isSameConnectorsSelection(activeSelection, requestedSelection)) {\r\n return;\r\n }\r\n\r\n throw new TestLifecycleError(\r\n `setupTest() is already using ${describeConnectorsSelection(activeSelection)}, and this call asked for ${describeConnectorsSelection(requestedSelection)}. One test runtime serves one connector selection — call teardownTest() before setting up a different one.`,\r\n );\r\n}\r\n\r\n/**\r\n * Await an attempt for its state transition only, not its result.\r\n */\r\nfunction settled(attempt: Promise<void>): Promise<void> {\r\n return attempt.catch(() => undefined);\r\n}\r\n"],"mappings":"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"}
|
|
@@ -66,7 +66,21 @@ function getAddCommand(packageManager) {
|
|
|
66
66
|
default: return "npm install";
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The package-manager command that saves an exact version. Warlock family
|
|
71
|
+
* dependencies use this path so the version selected by the executing Core is
|
|
72
|
+
* preserved verbatim in package.json instead of being widened by a manager's
|
|
73
|
+
* default save prefix.
|
|
74
|
+
*/
|
|
75
|
+
function getExactAddCommand(packageManager) {
|
|
76
|
+
switch (packageManager) {
|
|
77
|
+
case "yarn": return "yarn add --exact";
|
|
78
|
+
case "pnpm": return "pnpm add --save-exact";
|
|
79
|
+
case "bun": return "bun add --exact";
|
|
80
|
+
default: return "npm install --save-exact";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
69
83
|
|
|
70
84
|
//#endregion
|
|
71
|
-
export { detectPackageManager, getAddCommand, getInstallCommand };
|
|
85
|
+
export { detectPackageManager, getAddCommand, getExactAddCommand, getInstallCommand };
|
|
72
86
|
//# sourceMappingURL=package-manager.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package-manager.mjs","names":[],"sources":["../../../../../../../core/src/updater/package-manager.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\nimport { rootPath } from \"../utils\";\n\n/** Package managers the framework knows how to drive. */\nexport type PackageManager = \"npm\" | \"yarn\" | \"pnpm\" | \"bun\";\n\n/**\n * The lockfile that identifies each package manager, in detection order.\n * Bun is checked first because a Bun project may also carry a `yarn.lock`\n * (Bun writes one for tooling compatibility) — matching yarn there would run\n * the wrong installer against the wrong lockfile.\n */\nconst LOCKFILES: ReadonlyArray<{ file: string; packageManager: PackageManager }> = [\n { file: \"bun.lock\", packageManager: \"bun\" },\n { file: \"bun.lockb\", packageManager: \"bun\" },\n { file: \"package-lock.json\", packageManager: \"npm\" },\n { file: \"yarn.lock\", packageManager: \"yarn\" },\n { file: \"pnpm-lock.yaml\", packageManager: \"pnpm\" },\n];\n\n/**\n * Detect the project's package manager from its lockfile, falling back to\n * npm when none is present. Shared by `warlock update` and `warlock add` so\n * both agree on a project that happens to carry more than one lockfile.\n */\nexport async function detectPackageManager(): Promise<PackageManager> {\n for (const { file, packageManager } of LOCKFILES) {\n if (await fileExistsAsync(rootPath(file))) {\n return packageManager;\n }\n }\n\n return \"npm\";\n}\n\n/**\n * The lockfile-syncing install command for the given manager. No package\n * arguments — `warlock update` rewrites the versions in package.json first,\n * then a plain install reconciles `node_modules` to match.\n */\nexport function getInstallCommand(packageManager: PackageManager): string {\n switch (packageManager) {\n case \"yarn\":\n return \"yarn install\";\n\n case \"pnpm\":\n return \"pnpm install\";\n\n case \"bun\":\n return \"bun install\";\n\n default:\n return \"npm install\";\n }\n}\n\n/**\n * The command that installs *specific* packages — what `warlock add` needs.\n * Distinct from {@link getInstallCommand}, which only reconciles what is\n * already written into package.json.\n */\nexport function getAddCommand(packageManager: PackageManager): string {\n switch (packageManager) {\n case \"yarn\":\n return \"yarn add\";\n\n case \"pnpm\":\n return \"pnpm add\";\n\n case \"bun\":\n return \"bun add\";\n\n default:\n return \"npm install\";\n }\n}\n"],"mappings":";;;;;;;;;;;AAYA,MAAM,YAA6E;CACjF;EAAE,MAAM;EAAY,gBAAgB;CAAM;CAC1C;EAAE,MAAM;EAAa,gBAAgB;CAAM;CAC3C;EAAE,MAAM;EAAqB,gBAAgB;CAAM;CACnD;EAAE,MAAM;EAAa,gBAAgB;CAAO;CAC5C;EAAE,MAAM;EAAkB,gBAAgB;CAAO;AACnD;;;;;;AAOA,eAAsB,uBAAgD;CACpE,KAAK,MAAM,EAAE,MAAM,oBAAoB,WACrC,IAAI,MAAM,gBAAgB,SAAS,IAAI,CAAC,GACtC,OAAO;CAIX,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAkB,gBAAwC;CACxE,QAAQ,gBAAR;EACE,KAAK,QACH,OAAO;EAET,KAAK,QACH,OAAO;EAET,KAAK,OACH,OAAO;EAET,SACE,OAAO;CACX;AACF;;;;;;AAOA,SAAgB,cAAc,gBAAwC;CACpE,QAAQ,gBAAR;EACE,KAAK,QACH,OAAO;EAET,KAAK,QACH,OAAO;EAET,KAAK,OACH,OAAO;EAET,SACE,OAAO;CACX;AACF"}
|
|
1
|
+
{"version":3,"file":"package-manager.mjs","names":[],"sources":["../../../../../../../core/src/updater/package-manager.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\nimport { rootPath } from \"../utils\";\n\n/** Package managers the framework knows how to drive. */\nexport type PackageManager = \"npm\" | \"yarn\" | \"pnpm\" | \"bun\";\n\n/**\n * The lockfile that identifies each package manager, in detection order.\n * Bun is checked first because a Bun project may also carry a `yarn.lock`\n * (Bun writes one for tooling compatibility) — matching yarn there would run\n * the wrong installer against the wrong lockfile.\n */\nconst LOCKFILES: ReadonlyArray<{ file: string; packageManager: PackageManager }> = [\n { file: \"bun.lock\", packageManager: \"bun\" },\n { file: \"bun.lockb\", packageManager: \"bun\" },\n { file: \"package-lock.json\", packageManager: \"npm\" },\n { file: \"yarn.lock\", packageManager: \"yarn\" },\n { file: \"pnpm-lock.yaml\", packageManager: \"pnpm\" },\n];\n\n/**\n * Detect the project's package manager from its lockfile, falling back to\n * npm when none is present. Shared by `warlock update` and `warlock add` so\n * both agree on a project that happens to carry more than one lockfile.\n */\nexport async function detectPackageManager(): Promise<PackageManager> {\n for (const { file, packageManager } of LOCKFILES) {\n if (await fileExistsAsync(rootPath(file))) {\n return packageManager;\n }\n }\n\n return \"npm\";\n}\n\n/**\n * The lockfile-syncing install command for the given manager. No package\n * arguments — `warlock update` rewrites the versions in package.json first,\n * then a plain install reconciles `node_modules` to match.\n */\nexport function getInstallCommand(packageManager: PackageManager): string {\n switch (packageManager) {\n case \"yarn\":\n return \"yarn install\";\n\n case \"pnpm\":\n return \"pnpm install\";\n\n case \"bun\":\n return \"bun install\";\n\n default:\n return \"npm install\";\n }\n}\n\n/**\n * The command that installs *specific* packages — what `warlock add` needs.\n * Distinct from {@link getInstallCommand}, which only reconciles what is\n * already written into package.json.\n */\nexport function getAddCommand(packageManager: PackageManager): string {\n switch (packageManager) {\n case \"yarn\":\n return \"yarn add\";\n\n case \"pnpm\":\n return \"pnpm add\";\n\n case \"bun\":\n return \"bun add\";\n\n default:\n return \"npm install\";\n }\n}\n\n/**\n * The package-manager command that saves an exact version. Warlock family\n * dependencies use this path so the version selected by the executing Core is\n * preserved verbatim in package.json instead of being widened by a manager's\n * default save prefix.\n */\nexport function getExactAddCommand(packageManager: PackageManager): string {\n switch (packageManager) {\n case \"yarn\":\n return \"yarn add --exact\";\n\n case \"pnpm\":\n return \"pnpm add --save-exact\";\n\n case \"bun\":\n return \"bun add --exact\";\n\n default:\n return \"npm install --save-exact\";\n }\n}\n"],"mappings":";;;;;;;;;;;AAYA,MAAM,YAA6E;CACjF;EAAE,MAAM;EAAY,gBAAgB;CAAM;CAC1C;EAAE,MAAM;EAAa,gBAAgB;CAAM;CAC3C;EAAE,MAAM;EAAqB,gBAAgB;CAAM;CACnD;EAAE,MAAM;EAAa,gBAAgB;CAAO;CAC5C;EAAE,MAAM;EAAkB,gBAAgB;CAAO;AACnD;;;;;;AAOA,eAAsB,uBAAgD;CACpE,KAAK,MAAM,EAAE,MAAM,oBAAoB,WACrC,IAAI,MAAM,gBAAgB,SAAS,IAAI,CAAC,GACtC,OAAO;CAIX,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAkB,gBAAwC;CACxE,QAAQ,gBAAR;EACE,KAAK,QACH,OAAO;EAET,KAAK,QACH,OAAO;EAET,KAAK,OACH,OAAO;EAET,SACE,OAAO;CACX;AACF;;;;;;AAOA,SAAgB,cAAc,gBAAwC;CACpE,QAAQ,gBAAR;EACE,KAAK,QACH,OAAO;EAET,KAAK,QACH,OAAO;EAET,KAAK,OACH,OAAO;EAET,SACE,OAAO;CACX;AACF;;;;;;;AAQA,SAAgB,mBAAmB,gBAAwC;CACzE,QAAQ,gBAAR;EACE,KAAK,QACH,OAAO;EAET,KAAK,QACH,OAAO;EAET,KAAK,OACH,OAAO;EAET,SACE,OAAO;CACX;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-warlock-packages.mjs","names":[],"sources":["../../../../../../../core/src/updater/update-warlock-packages.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getJsonFileAsync, putJsonFileAsync } from \"@warlock.js/fs\";\nimport { execSync } from \"node:child_process\";\nimport { rootPath } from \"../utils\";\nimport { fetchLatestVersion } from \"../utils/npm-registry\";\nimport { isNewerVersion } from \"../utils/version-compare\";\nimport { detectPackageManager, getInstallCommand } from \"./package-manager\";\n\n/** Scope prefix that identifies a Warlock framework package. */\nconst WARLOCK_SCOPE = \"@warlock.js/\";\n\n/** The dependency maps in package.json we update, in display order. */\nconst DEPENDENCY_SECTIONS = [\"dependencies\", \"devDependencies\"] as const;\n\ntype DependencySection = (typeof DEPENDENCY_SECTIONS)[number];\n\ntype DependencyMap = Record<string, string>;\n\ntype PackageJson = Record<string, unknown> & {\n dependencies?: DependencyMap;\n devDependencies?: DependencyMap;\n};\n\n/** A `@warlock.js/*` dependency found in package.json. */\ntype WarlockDependency = {\n name: string;\n section: DependencySection;\n /** The current spec exactly as written, e.g. `^4.2.0`. */\n current: string;\n};\n\n/** A dependency whose latest version has been resolved from the registry. */\nexport type ResolvedDependency = WarlockDependency & {\n /** Latest version from npm, or `undefined` when the lookup failed. */\n latest: string | undefined;\n};\n\n/** A concrete version bump to write back into package.json. */\nexport type PackageUpdate = {\n name: string;\n section: DependencySection;\n /** Existing spec, e.g. `^4.2.0`. */\n from: string;\n /** New spec with the original range operator preserved, e.g. `^4.3.0`. */\n to: string;\n};\n\n/** Options accepted by {@link updateWarlockPackages}. */\nexport type UpdateWarlockPackagesOptions = {\n /** Run the package manager install after rewriting versions (default true). */\n install?: boolean;\n /**\n * Report what would change without touching package.json or node_modules.\n * Resolves with the `outdated` outcome instead of `updated`.\n */\n dryRun?: boolean;\n};\n\n/**\n * How an update run ended. Callers branch on this instead of re-deriving the\n * situation from the printed output — the dev-server `u` shortcut needs to\n * know whether restarting is warranted, and whether it may offer a retry.\n */\nexport type UpdateOutcome =\n /** No package.json at the project root — nothing to do. */\n | \"no-package-json\"\n /** package.json carries no `@warlock.js/*` dependency. */\n | \"no-packages\"\n /** Every registry lookup failed — offline, DNS down, or npm unreachable. */\n | \"registry-unreachable\"\n /** Every package is already at (or ahead of) its latest published version. */\n | \"up-to-date\"\n /** Newer versions exist, and a dry run left everything untouched. */\n | \"outdated\"\n /** Versions were rewritten (and installed, unless `install: false`). */\n | \"updated\"\n /** Versions were rewritten but the package manager install failed. */\n | \"install-failed\";\n\n/** The result of an update run. */\nexport type UpdateWarlockPackagesResult = {\n outcome: UpdateOutcome;\n /** The bumps written to package.json — empty unless `outcome` wrote any. */\n updates: PackageUpdate[];\n /** The install failure, present only for `install-failed`. */\n error?: Error;\n};\n\n/**\n * Update every `@warlock.js/*` package listed in the project's root\n * package.json to its latest published version, then reconcile `node_modules`\n * via the detected package manager.\n *\n * The original range operator on each dependency (`^`, `~`, or an exact pin)\n * is preserved; specs that aren't a plain semver — `workspace:*`, `*`,\n * `latest`, git/file URLs — are left untouched. Only genuine upgrades are\n * written, so re-running on an already-current project is a no-op.\n *\n * Resolves rather than throws on the two failures a caller is expected to\n * survive — an unreachable registry and a failed install — so the dev-server\n * shortcut can report them and keep serving.\n */\nexport async function updateWarlockPackages(\n options: UpdateWarlockPackagesOptions = {},\n): Promise<UpdateWarlockPackagesResult> {\n const runInstall = options.install ?? true;\n const packageJsonPath = rootPath(\"package.json\");\n\n if (!(await fileExistsAsync(packageJsonPath))) {\n console.log(`${colors.red(\"✖\")} No package.json found at the project root.`);\n return { outcome: \"no-package-json\", updates: [] };\n }\n\n const packageJson = (await getJsonFileAsync(packageJsonPath)) as PackageJson;\n const dependencies = collectWarlockDependencies(packageJson);\n\n if (dependencies.length === 0) {\n console.log(`${colors.yellow(\"⚠\")} No @warlock.js packages found in package.json.`);\n return { outcome: \"no-packages\", updates: [] };\n }\n\n console.log(\n `${colors.cyan(\"›\")} Checking ${colors.bold(String(dependencies.length))} ` +\n `@warlock.js package(s) for updates…`,\n );\n\n const resolved = await resolveLatestVersions(dependencies);\n\n // Distinguish \"nothing newer\" from \"we never got an answer\". Without this,\n // an offline run reports every package as up to date, which is a lie.\n if (isRegistryUnreachable(resolved)) {\n console.log(\n `${colors.yellow(\"⚠\")} Could not reach the npm registry — ` +\n `no versions were changed. Check your connection and try again.`,\n );\n return { outcome: \"registry-unreachable\", updates: [] };\n }\n\n const updates = resolvePackageUpdates(resolved);\n\n if (updates.length === 0) {\n console.log(`${colors.green(\"✓\")} All @warlock.js packages are already up to date.`);\n return { outcome: \"up-to-date\", updates: [] };\n }\n\n if (options.dryRun) {\n printUpdates(updates, { pending: true });\n return { outcome: \"outdated\", updates };\n }\n\n applyUpdates(packageJson, updates);\n await putJsonFileAsync(packageJsonPath, packageJson);\n\n printUpdates(updates);\n\n if (!runInstall) {\n console.log(\n colors.dim(\"Skipped install (--no-install). Run your package manager to apply the changes.\"),\n );\n return { outcome: \"updated\", updates };\n }\n\n try {\n await installDependencies();\n } catch (error) {\n console.log(\n `${colors.red(\"✖\")} The install failed. package.json is already updated — ` +\n `re-run your package manager install to finish.`,\n );\n return { outcome: \"install-failed\", updates, error: error as Error };\n }\n\n return { outcome: \"updated\", updates };\n}\n\n/**\n * Whether *every* lookup came back empty, which in practice means the\n * registry was never reached. A single failed package (unpublished, renamed,\n * private) leaves the rest resolvable and is not treated as an outage.\n */\nexport function isRegistryUnreachable(resolved: ResolvedDependency[]): boolean {\n return resolved.length > 0 && resolved.every((dependency) => !dependency.latest);\n}\n\n/** Collect every `@warlock.js/*` dependency across the relevant sections. */\nfunction collectWarlockDependencies(packageJson: PackageJson): WarlockDependency[] {\n const dependencies: WarlockDependency[] = [];\n\n for (const section of DEPENDENCY_SECTIONS) {\n const map = packageJson[section];\n\n if (!map) {\n continue;\n }\n\n for (const [name, current] of Object.entries(map)) {\n if (name.startsWith(WARLOCK_SCOPE)) {\n dependencies.push({ name, section, current });\n }\n }\n }\n\n return dependencies;\n}\n\n/** Resolve each dependency's latest version from the registry in parallel. */\nasync function resolveLatestVersions(\n dependencies: WarlockDependency[],\n): Promise<ResolvedDependency[]> {\n return Promise.all(\n dependencies.map(async (dependency) => ({\n ...dependency,\n latest: await fetchLatestVersion(dependency.name),\n })),\n );\n}\n\n/**\n * Turn resolved dependencies into the concrete set of version bumps to write.\n * Pure (no I/O) so it can be unit-tested directly: skips lookups that failed,\n * non-semver specs, and anything already at (or ahead of) the latest version,\n * and preserves the original range operator on everything it does rewrite.\n */\nexport function resolvePackageUpdates(resolved: ResolvedDependency[]): PackageUpdate[] {\n const updates: PackageUpdate[] = [];\n\n for (const dependency of resolved) {\n if (!dependency.latest) {\n continue;\n }\n\n const parsed = parseSpec(dependency.current);\n\n if (!parsed) {\n continue;\n }\n\n if (!isNewerVersion(dependency.latest, parsed.version)) {\n continue;\n }\n\n updates.push({\n name: dependency.name,\n section: dependency.section,\n from: dependency.current,\n to: `${parsed.operator}${dependency.latest}`,\n });\n }\n\n return updates;\n}\n\n/**\n * Split a dependency spec into its range operator and concrete version.\n * Returns `undefined` for specs we won't touch — `workspace:*`, `*`,\n * `latest`, multi-part ranges, or git/file URLs — leaving them exactly as\n * the author wrote them.\n */\nfunction parseSpec(spec: string): { operator: string; version: string } | undefined {\n const match = spec.trim().match(/^(\\^|~)?(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)$/);\n\n if (!match) {\n return undefined;\n }\n\n return { operator: match[1] ?? \"\", version: match[2] };\n}\n\n/** Write the resolved bumps back into the package.json object in place. */\nfunction applyUpdates(packageJson: PackageJson, updates: PackageUpdate[]): void {\n for (const update of updates) {\n const map = packageJson[update.section];\n\n if (map) {\n map[update.name] = update.to;\n }\n }\n}\n\n/** Print the version bumps — applied, or pending on a dry run. */\nfunction printUpdates(updates: PackageUpdate[], { pending = false } = {}): void {\n const headline = pending\n ? `${colors.yellow(\"↑\")} ${colors.bold(String(updates.length))} package(s) can be updated:`\n : `${colors.green(\"✓\")} Updated ${colors.bold(String(updates.length))} package(s):`;\n\n console.log(headline);\n\n for (const update of updates) {\n console.log(\n ` ${colors.cyan(update.name)} ` +\n `${colors.dim(update.from)} ${colors.dim(\"→\")} ${colors.greenBright(update.to)}`,\n );\n }\n}\n\n/** Reconcile node_modules to the rewritten package.json via the project PM. */\nasync function installDependencies(): Promise<void> {\n const packageManager = await detectPackageManager();\n const installCommand = getInstallCommand(packageManager);\n\n console.log(`${colors.cyan(\"›\")} Running ${colors.bold(installCommand)}…`);\n\n execSync(installCommand, { cwd: process.cwd(), stdio: \"inherit\" });\n}\n"],"mappings":";;;;;;;;;;;AASA,MAAM,gBAAgB;;AAGtB,MAAM,sBAAsB,CAAC,gBAAgB,iBAAiB;;;;;;;;;;;;;;;AA0F9D,eAAsB,sBACpB,UAAwC,CAAC,GACH;CACtC,MAAM,aAAa,QAAQ,WAAW;CACtC,MAAM,kBAAkB,SAAS,cAAc;CAE/C,IAAI,CAAE,MAAM,gBAAgB,eAAe,GAAI;EAC7C,QAAQ,IAAI,GAAG,OAAO,IAAI,GAAG,EAAE,4CAA4C;EAC3E,OAAO;GAAE,SAAS;GAAmB,SAAS,CAAC;EAAE;CACnD;CAEA,MAAM,cAAe,MAAM,iBAAiB,eAAe;CAC3D,MAAM,eAAe,2BAA2B,WAAW;CAE3D,IAAI,aAAa,WAAW,GAAG;EAC7B,QAAQ,IAAI,GAAG,OAAO,OAAO,GAAG,EAAE,gDAAgD;EAClF,OAAO;GAAE,SAAS;GAAe,SAAS,CAAC;EAAE;CAC/C;CAEA,QAAQ,IACN,GAAG,OAAO,KAAK,GAAG,EAAE,YAAY,OAAO,KAAK,OAAO,aAAa,MAAM,CAAC,EAAE,qCAE3E;CAEA,MAAM,WAAW,MAAM,sBAAsB,YAAY;CAIzD,IAAI,sBAAsB,QAAQ,GAAG;EACnC,QAAQ,IACN,GAAG,OAAO,OAAO,GAAG,EAAE,mGAExB;EACA,OAAO;GAAE,SAAS;GAAwB,SAAS,CAAC;EAAE;CACxD;CAEA,MAAM,UAAU,sBAAsB,QAAQ;CAE9C,IAAI,QAAQ,WAAW,GAAG;EACxB,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,kDAAkD;EACnF,OAAO;GAAE,SAAS;GAAc,SAAS,CAAC;EAAE;CAC9C;CAEA,IAAI,QAAQ,QAAQ;EAClB,aAAa,SAAS,EAAE,SAAS,KAAK,CAAC;EACvC,OAAO;GAAE,SAAS;GAAY;EAAQ;CACxC;CAEA,aAAa,aAAa,OAAO;CACjC,MAAM,iBAAiB,iBAAiB,WAAW;CAEnD,aAAa,OAAO;CAEpB,IAAI,CAAC,YAAY;EACf,QAAQ,IACN,OAAO,IAAI,gFAAgF,CAC7F;EACA,OAAO;GAAE,SAAS;GAAW;EAAQ;CACvC;CAEA,IAAI;EACF,MAAM,oBAAoB;CAC5B,SAAS,OAAO;EACd,QAAQ,IACN,GAAG,OAAO,IAAI,GAAG,EAAE,sGAErB;EACA,OAAO;GAAE,SAAS;GAAkB;GAAgB;EAAe;CACrE;CAEA,OAAO;EAAE,SAAS;EAAW;CAAQ;AACvC;;;;;;AAOA,SAAgB,sBAAsB,UAAyC;CAC7E,OAAO,SAAS,SAAS,KAAK,SAAS,OAAO,eAAe,CAAC,WAAW,MAAM;AACjF;;AAGA,SAAS,2BAA2B,aAA+C;CACjF,MAAM,eAAoC,CAAC;CAE3C,KAAK,MAAM,WAAW,qBAAqB;EACzC,MAAM,MAAM,YAAY;EAExB,IAAI,CAAC,KACH;EAGF,KAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,GAAG,GAC9C,IAAI,KAAK,WAAW,aAAa,GAC/B,aAAa,KAAK;GAAE;GAAM;GAAS;EAAQ,CAAC;CAGlD;CAEA,OAAO;AACT;;AAGA,eAAe,sBACb,cAC+B;CAC/B,OAAO,QAAQ,IACb,aAAa,IAAI,OAAO,gBAAgB;EACtC,GAAG;EACH,QAAQ,MAAM,mBAAmB,WAAW,IAAI;CAClD,EAAE,CACJ;AACF;;;;;;;AAQA,SAAgB,sBAAsB,UAAiD;CACrF,MAAM,UAA2B,CAAC;CAElC,KAAK,MAAM,cAAc,UAAU;EACjC,IAAI,CAAC,WAAW,QACd;EAGF,MAAM,SAAS,UAAU,WAAW,OAAO;EAE3C,IAAI,CAAC,QACH;EAGF,IAAI,CAAC,eAAe,WAAW,QAAQ,OAAO,OAAO,GACnD;EAGF,QAAQ,KAAK;GACX,MAAM,WAAW;GACjB,SAAS,WAAW;GACpB,MAAM,WAAW;GACjB,IAAI,GAAG,OAAO,WAAW,WAAW;EACtC,CAAC;CACH;CAEA,OAAO;AACT;;;;;;;AAQA,SAAS,UAAU,MAAiE;CAClF,MAAM,QAAQ,KAAK,KAAK,EAAE,MAAM,8CAA8C;CAE9E,IAAI,CAAC,OACH;CAGF,OAAO;EAAE,UAAU,MAAM,MAAM;EAAI,SAAS,MAAM;CAAG;AACvD;;AAGA,SAAS,aAAa,aAA0B,SAAgC;CAC9E,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,MAAM,YAAY,OAAO;EAE/B,IAAI,KACF,IAAI,OAAO,QAAQ,OAAO;CAE9B;AACF;;AAGA,SAAS,aAAa,SAA0B,EAAE,UAAU,UAAU,CAAC,GAAS;CAC9E,MAAM,WAAW,UACb,GAAG,OAAO,OAAO,GAAG,EAAE,GAAG,OAAO,KAAK,OAAO,QAAQ,MAAM,CAAC,EAAE,+BAC7D,GAAG,OAAO,MAAM,GAAG,EAAE,WAAW,OAAO,KAAK,OAAO,QAAQ,MAAM,CAAC,EAAE;CAExE,QAAQ,IAAI,QAAQ;CAEpB,KAAK,MAAM,UAAU,SACnB,QAAQ,IACN,MAAM,OAAO,KAAK,OAAO,IAAI,EAAE,IAC1B,OAAO,IAAI,OAAO,IAAI,EAAE,GAAG,OAAO,IAAI,GAAG,EAAE,GAAG,OAAO,YAAY,OAAO,EAAE,GACjF;AAEJ;;AAGA,eAAe,sBAAqC;CAElD,MAAM,iBAAiB,kBAAkB,MADZ,qBAAqB,CACK;CAEvD,QAAQ,IAAI,GAAG,OAAO,KAAK,GAAG,EAAE,WAAW,OAAO,KAAK,cAAc,EAAE,EAAE;CAEzE,SAAS,gBAAgB;EAAE,KAAK,QAAQ,IAAI;EAAG,OAAO;CAAU,CAAC;AACnE"}
|
|
1
|
+
{"version":3,"file":"update-warlock-packages.mjs","names":[],"sources":["../../../../../../../core/src/updater/update-warlock-packages.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getJsonFileAsync, putJsonFileAsync } from \"@warlock.js/fs\";\nimport { execSync } from \"node:child_process\";\nimport { rootPath } from \"../utils\";\nimport { fetchLatestVersion } from \"../utils/npm-registry\";\nimport { isNewerVersion } from \"../utils/version-compare\";\nimport { detectPackageManager, getInstallCommand } from \"./package-manager\";\n\n/** Scope prefix that identifies a Warlock framework package. */\nconst WARLOCK_SCOPE = \"@warlock.js/\";\n\n/** The dependency maps in package.json we update, in display order. */\nconst DEPENDENCY_SECTIONS = [\"dependencies\", \"devDependencies\"] as const;\n\ntype DependencySection = (typeof DEPENDENCY_SECTIONS)[number];\n\ntype DependencyMap = Record<string, string>;\n\ntype PackageJson = Record<string, unknown> & {\n dependencies?: DependencyMap;\n devDependencies?: DependencyMap;\n};\n\n/** A `@warlock.js/*` dependency found in package.json. */\ntype WarlockDependency = {\n name: string;\n section: DependencySection;\n /** The current spec exactly as written, e.g. `^4.2.0`. */\n current: string;\n};\n\n/** A dependency whose latest version has been resolved from the registry. */\nexport type ResolvedDependency = WarlockDependency & {\n /** Latest version from npm, or `undefined` when the lookup failed. */\n latest: string | undefined;\n};\n\n/** A concrete version bump to write back into package.json. */\nexport type PackageUpdate = {\n name: string;\n section: DependencySection;\n /** Existing spec, e.g. `^4.2.0`. */\n from: string;\n /** New spec with the original range operator preserved, e.g. `^4.3.0`. */\n to: string;\n};\n\n/** Options accepted by {@link updateWarlockPackages}. */\nexport type UpdateWarlockPackagesOptions = {\n /** Run the package manager install after rewriting versions (default true). */\n install?: boolean;\n /**\n * Report what would change without touching package.json or node_modules.\n * Resolves with the `outdated` outcome instead of `updated`.\n */\n dryRun?: boolean;\n};\n\n/**\n * How an update run ended. Callers branch on this instead of re-deriving the\n * situation from the printed output — the dev-server `u` shortcut needs to\n * know whether restarting is warranted, and whether it may offer a retry.\n */\nexport type UpdateOutcome =\n /** No package.json at the project root — nothing to do. */\n | \"no-package-json\"\n /** package.json carries no `@warlock.js/*` dependency. */\n | \"no-packages\"\n /** Every registry lookup failed — offline, DNS down, or npm unreachable. */\n | \"registry-unreachable\"\n /** Every package is already at (or ahead of) its latest published version. */\n | \"up-to-date\"\n /** Newer versions exist, and a dry run left everything untouched. */\n | \"outdated\"\n /** Versions were rewritten (and installed, unless `install: false`). */\n | \"updated\"\n /** Versions were rewritten but the package manager install failed. */\n | \"install-failed\";\n\n/** The result of an update run. */\nexport type UpdateWarlockPackagesResult = {\n outcome: UpdateOutcome;\n /** The bumps written to package.json — empty unless `outcome` wrote any. */\n updates: PackageUpdate[];\n /** The install failure, present only for `install-failed`. */\n error?: Error;\n};\n\n/**\n * Update every `@warlock.js/*` package listed in the project's root\n * package.json to its latest published version, then reconcile `node_modules`\n * via the detected package manager.\n *\n * The original range operator on each dependency (`^`, `~`, or an exact pin)\n * is preserved; specs that aren't a plain semver — `workspace:*`, `*`,\n * `latest`, git/file URLs — are left untouched. Only genuine upgrades are\n * written, so re-running on an already-current project is a no-op.\n *\n * Resolves rather than throws on the two failures a caller is expected to\n * survive — an unreachable registry and a failed install — so the dev-server\n * shortcut can report them and keep serving.\n */\nexport async function updateWarlockPackages(\n options: UpdateWarlockPackagesOptions = {},\n): Promise<UpdateWarlockPackagesResult> {\n const runInstall = options.install ?? true;\n const packageJsonPath = rootPath(\"package.json\");\n\n if (!(await fileExistsAsync(packageJsonPath))) {\n console.log(`${colors.red(\"✖\")} No package.json found at the project root.`);\n return { outcome: \"no-package-json\", updates: [] };\n }\n\n const packageJson = (await getJsonFileAsync(packageJsonPath)) as PackageJson;\n const dependencies = collectWarlockDependencies(packageJson);\n\n if (dependencies.length === 0) {\n console.log(`${colors.yellow(\"⚠\")} No @warlock.js packages found in package.json.`);\n return { outcome: \"no-packages\", updates: [] };\n }\n\n console.log(\n `${colors.cyan(\"›\")} Checking ${colors.bold(String(dependencies.length))} ` +\n `@warlock.js package(s) for updates…`,\n );\n\n const resolved = await resolveLatestVersions(dependencies);\n\n // Distinguish \"nothing newer\" from \"we never got an answer\". Without this,\n // an offline run reports every package as up to date, which is a lie.\n if (isRegistryUnreachable(resolved)) {\n console.log(\n `${colors.yellow(\"⚠\")} Could not reach the npm registry — ` +\n `no versions were changed. Check your connection and try again.`,\n );\n return { outcome: \"registry-unreachable\", updates: [] };\n }\n\n const updates = resolvePackageUpdates(resolved);\n\n if (updates.length === 0) {\n console.log(`${colors.green(\"✓\")} All @warlock.js packages are already up to date.`);\n return { outcome: \"up-to-date\", updates: [] };\n }\n\n if (options.dryRun) {\n printUpdates(updates, { pending: true });\n return { outcome: \"outdated\", updates };\n }\n\n applyUpdates(packageJson, updates);\n await putJsonFileAsync(packageJsonPath, packageJson);\n\n printUpdates(updates);\n\n if (!runInstall) {\n console.log(\n colors.dim(\"Skipped install (--no-install). Run your package manager to apply the changes.\"),\n );\n return { outcome: \"updated\", updates };\n }\n\n try {\n await installDependencies();\n } catch (error) {\n console.log(\n `${colors.red(\"✖\")} The install failed. package.json is already updated — ` +\n `re-run your package manager install to finish.`,\n );\n return { outcome: \"install-failed\", updates, error: error as Error };\n }\n\n return { outcome: \"updated\", updates };\n}\n\n/**\n * Whether *every* lookup came back empty, which in practice means the\n * registry was never reached. A single failed package (unpublished, renamed,\n * private) leaves the rest resolvable and is not treated as an outage.\n */\nexport function isRegistryUnreachable(resolved: ResolvedDependency[]): boolean {\n return resolved.length > 0 && resolved.every((dependency) => !dependency.latest);\n}\n\n/** Collect every `@warlock.js/*` dependency across the relevant sections. */\nfunction collectWarlockDependencies(packageJson: PackageJson): WarlockDependency[] {\n const dependencies: WarlockDependency[] = [];\n\n for (const section of DEPENDENCY_SECTIONS) {\n const map = packageJson[section];\n\n if (!map) {\n continue;\n }\n\n for (const [name, current] of Object.entries(map)) {\n if (name.startsWith(WARLOCK_SCOPE)) {\n dependencies.push({ name, section, current });\n }\n }\n }\n\n return dependencies;\n}\n\n/** Resolve each dependency's latest version from the registry in parallel. */\nasync function resolveLatestVersions(\n dependencies: WarlockDependency[],\n): Promise<ResolvedDependency[]> {\n return Promise.all(\n dependencies.map(async (dependency) => ({\n ...dependency,\n latest: await fetchLatestVersion(dependency.name),\n })),\n );\n}\n\n/**\n * Turn resolved dependencies into the concrete set of version bumps to write.\n * Pure (no I/O) so it can be unit-tested directly: skips lookups that failed,\n * non-semver specs, and anything already at (or ahead of) the latest version,\n * and preserves the original range operator on everything it does rewrite.\n */\nexport function resolvePackageUpdates(resolved: ResolvedDependency[]): PackageUpdate[] {\n const updates: PackageUpdate[] = [];\n\n for (const dependency of resolved) {\n if (!dependency.latest) {\n continue;\n }\n\n const parsed = parseSpec(dependency.current);\n\n if (!parsed) {\n continue;\n }\n\n if (!isNewerVersion(dependency.latest, parsed.version)) {\n continue;\n }\n\n updates.push({\n name: dependency.name,\n section: dependency.section,\n from: dependency.current,\n to: `${parsed.operator}${dependency.latest}`,\n });\n }\n\n return updates;\n}\n\n/**\n * Split a dependency spec into its range operator and concrete version.\n * Returns `undefined` for specs we won't touch — `workspace:*`, `*`,\n * `latest`, multi-part ranges, or git/file URLs — leaving them exactly as\n * the author wrote them.\n */\nfunction parseSpec(spec: string): { operator: string; version: string } | undefined {\n const match = spec.trim().match(/^(\\^|~)?(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)$/);\n\n if (!match) {\n return undefined;\n }\n\n return { operator: match[1] ?? \"\", version: match[2] };\n}\n\n/** Write the resolved bumps back into the package.json object in place. */\nfunction applyUpdates(packageJson: PackageJson, updates: PackageUpdate[]): void {\n for (const update of updates) {\n const map = packageJson[update.section];\n\n if (map) {\n map[update.name] = update.to;\n }\n }\n}\n\n/** Print the version bumps — applied, or pending on a dry run. */\nfunction printUpdates(updates: PackageUpdate[], { pending = false } = {}): void {\n const headline = pending\n ? `${colors.yellow(\"↑\")} ${colors.bold(String(updates.length))} package(s) can be updated:`\n : `${colors.green(\"✓\")} Updated ${colors.bold(String(updates.length))} package(s):`;\n\n console.log(headline);\n\n for (const update of updates) {\n console.log(\n ` ${colors.cyan(update.name)} ` +\n `${colors.dim(update.from)} ${colors.dim(\"→\")} ${colors.greenBright(update.to)}`,\n );\n }\n}\n\n/** Reconcile node_modules to the rewritten package.json via the project PM. */\nasync function installDependencies(): Promise<void> {\n const packageManager = await detectPackageManager();\n const installCommand = getInstallCommand(packageManager);\n\n console.log(`${colors.cyan(\"›\")} Running ${colors.bold(installCommand)}…`);\n\n execSync(installCommand, { cwd: process.cwd(), stdio: \"inherit\" });\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-case-broadcast.d.mts","names":[],"sources":["../../../../../../../core/src/use-cases/use-case-broadcast.ts"],"mappings":";;;;;AAkBA;;;;;;;;iBAAsB,sBAAA,
|
|
1
|
+
{"version":3,"file":"use-case-broadcast.d.mts","names":[],"sources":["../../../../../../../core/src/use-cases/use-case-broadcast.ts"],"mappings":";;;;;AAkBA;;;;;;;;iBAAsB,sBAAA,SAA+B,MAAA;EACnD,IAAA;EACA,EAAA;EACA,MAAA,EAAQ,MAAA;EACR,MAAA,EAAQ,aAAA,CAAc,MAAA;EACtB,SAAA,GAAY,sBAAA,CAAuB,MAAA;EACnC,MAAA,GAAS,qBAAA;AAAA,IACP,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-case-events.d.mts","names":[],"sources":["../../../../../../../core/src/use-cases/use-case-events.ts"],"mappings":";;;;;AAWA;cAAa,wBAAA,EAA0B,yBAItC;;;AAAA;AASD;;;;cAAa,oBAAA;yBACY,GAAA,EAAK,yBAAA;;;sBASV,QAAA,GAAa,MAAA,EAAQ,aAAA,CAAc,MAAA;;;qBASlC,GAAA,EAAK,kBAAA;;;;;;;;;;;iBAkBJ,kBAAA,
|
|
1
|
+
{"version":3,"file":"use-case-events.d.mts","names":[],"sources":["../../../../../../../core/src/use-cases/use-case-events.ts"],"mappings":";;;;;AAWA;cAAa,wBAAA,EAA0B,yBAItC;;;AAAA;AASD;;;;cAAa,oBAAA;yBACY,GAAA,EAAK,yBAAA;;;sBASV,QAAA,GAAa,MAAA,EAAQ,aAAA,CAAc,MAAA;;;qBASlC,GAAA,EAAK,kBAAA;;;;;;;;;;;iBAkBJ,kBAAA,WACpB,GAAA,EAAK,QAAA,EACL,SAAA;EACE,UAAA,KAAe,GAAA,EAAK,QAAA,YAAoB,OAAA;EACxC,OAAA,KAAY,GAAA,EAAK,QAAA,YAAoB,OAAA;EACrC,MAAA,KAAW,GAAA,EAAK,QAAA,YAAoB,OAAA;AAAA,IACrC,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-case-pipeline.d.mts","names":[],"sources":["../../../../../../../core/src/use-cases/use-case-pipeline.ts"],"mappings":";;;;KAWY,eAAA;EACV,IAAA;EACA,EAAA;EACA,IAAA,EAAM,KAAA;EACN,GAAA,EAAK,cAAA;EACL,SAAA,EAAW,IAAA;EACX,MAAA,GAAS,eAAA;EACT,MAAA,GAAS,YAAA,CAAa,KAAA;EACtB,MAAA,GAAS,uBAAA,CAAwB,KAAA,KADX;EAGtB,WAAA,IAAe,GAAA,EAAK,yBAAA,WAFa;EAIjC,aAAA,IAAiB,GAAA,EAAK,yBAAA;AAAA;;;;;;;;;;;;;;iBAgBF,WAAA,
|
|
1
|
+
{"version":3,"file":"use-case-pipeline.d.mts","names":[],"sources":["../../../../../../../core/src/use-cases/use-case-pipeline.ts"],"mappings":";;;;KAWY,eAAA;EACV,IAAA;EACA,EAAA;EACA,IAAA,EAAM,KAAA;EACN,GAAA,EAAK,cAAA;EACL,SAAA,EAAW,IAAA;EACX,MAAA,GAAS,eAAA;EACT,MAAA,GAAS,YAAA,CAAa,KAAA;EACtB,MAAA,GAAS,uBAAA,CAAwB,KAAA,KADX;EAGtB,WAAA,IAAe,GAAA,EAAK,yBAAA,WAFa;EAIjC,aAAA,IAAiB,GAAA,EAAK,yBAAA;AAAA;;;;;;;;;;;;;;iBAgBF,WAAA,QAAmB,IAAA,EAAM,eAAA,CAAgB,KAAA,IAAS,OAAA,CAAQ,KAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-case.d.mts","names":[],"sources":["../../../../../../../core/src/use-cases/use-case.ts"],"mappings":";;;;;;AAwDA;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,OAAA,wBAEC,eAAA,cACH,cAAA,GAAiB,cAAA,
|
|
1
|
+
{"version":3,"file":"use-case.d.mts","names":[],"sources":["../../../../../../../core/src/use-cases/use-case.ts"],"mappings":";;;;;;AAwDA;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,OAAA,wBAEC,eAAA,cACH,cAAA,GAAiB,cAAA,EAC7B,OAAA,EAAS,iBAAA,CAAkB,MAAA,EAAQ,MAAA,EAAQ,GAAA,IAAO,cAAA,CAAe,MAAA,EAAQ,KAAA,CAAM,MAAA;AAAA,iBACjE,OAAA,wCAA+C,cAAA,GAAiB,cAAA,EAC9E,OAAA,EAAS,OAAA,CAAQ,MAAA,EAAQ,KAAA,EAAO,GAAA,IAC/B,cAAA,CAAe,MAAA,EAAQ,KAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-cases-registry.d.mts","names":[],"sources":["../../../../../../../core/src/use-cases/use-cases-registry.ts"],"mappings":";;;;;AAaA;iBAAgB,gBAAA,
|
|
1
|
+
{"version":3,"file":"use-cases-registry.d.mts","names":[],"sources":["../../../../../../../core/src/use-cases/use-cases-registry.ts"],"mappings":";;;;;AAaA;iBAAgB,gBAAA,gBACd,IAAA,UACA,OAAA,EAAS,iBAAA,CAAkB,MAAA,EAAQ,KAAA;;;;;;;;iBAoBrB,kBAAA,CAAmB,IAAY;;;;;;;AApBJ;iBAmC3B,UAAA,gBAA0B,IAAA,WACJ,iBAAA,CAAkB,MAAA,EAAQ,KAAA;;;;iBAMhD,WAAA,IAAW,GAAA,SAAA,iBAAA;AAP3B;;;AAAA,iBAcgB,2BAAA,CAA4B,IAAY;;;;iBAexC,0BAAA,CAA2B,IAAY;;;;;;iBA2BjC,iBAAA,CAAkB,IAAA,WAAe,OAAO,CAAC,aAAA;;AAvDM;AAMrE;;iBA+EsB,iBAAA,CAAkB,IAAA,UAAc,MAAA,EAAQ,aAAA,QAAkB,OAAA"}
|