@warlock.js/core 5.26.0 → 5.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/esm/cli/commands/openapi-generator.action.mjs +43 -0
- package/esm/cli/commands/openapi-generator.action.mjs.map +1 -0
- package/esm/cli/commands/openapi-generator.command.mjs +44 -0
- package/esm/cli/commands/openapi-generator.command.mjs.map +1 -0
- package/esm/cli/commands/postman-generator.action.mjs +47 -0
- package/esm/cli/commands/postman-generator.action.mjs.map +1 -0
- package/esm/cli/commands/postman-generator.command.mjs +43 -0
- package/esm/cli/commands/postman-generator.command.mjs.map +1 -0
- package/esm/cli/framework-cli-commands.mjs +4 -0
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/dev-server/build-development-openapi-document.mjs +36 -0
- package/esm/dev-server/build-development-openapi-document.mjs.map +1 -0
- package/esm/dev-server/collect-development-api-routes.mjs +27 -0
- package/esm/dev-server/collect-development-api-routes.mjs.map +1 -0
- package/esm/dev-server/dev-logger.mjs +9 -1
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +6 -3
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +17 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/model-resource-typings.mjs +246 -0
- package/esm/dev-server/model-resource-typings.mjs.map +1 -0
- package/esm/dev-server/type-generator.mjs +46 -2
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/http/index.d.mts +2 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +29 -3
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/serialized.d.mts +114 -0
- package/esm/http/serialized.d.mts.map +1 -0
- package/esm/index.d.mts +11 -3
- package/esm/index.mjs +6 -1
- package/esm/openapi/build-openapi-document.d.mts +17 -0
- package/esm/openapi/build-openapi-document.d.mts.map +1 -0
- package/esm/openapi/build-openapi-document.mjs +124 -0
- package/esm/openapi/build-openapi-document.mjs.map +1 -0
- package/esm/openapi/cast-schema.mjs +89 -0
- package/esm/openapi/cast-schema.mjs.map +1 -0
- package/esm/openapi/collect-openapi-document.mjs +40 -0
- package/esm/openapi/collect-openapi-document.mjs.map +1 -0
- package/esm/openapi/development-openapi-provider.d.mts +28 -0
- package/esm/openapi/development-openapi-provider.d.mts.map +1 -0
- package/esm/openapi/development-openapi-provider.mjs +35 -0
- package/esm/openapi/development-openapi-provider.mjs.map +1 -0
- package/esm/openapi/error-responses.mjs +67 -0
- package/esm/openapi/error-responses.mjs.map +1 -0
- package/esm/openapi/index.d.mts +6 -0
- package/esm/openapi/index.mjs +6 -0
- package/esm/openapi/openapi-child-output.mjs +57 -0
- package/esm/openapi/openapi-child-output.mjs.map +1 -0
- package/esm/openapi/openapi-protocol.mjs +30 -0
- package/esm/openapi/openapi-protocol.mjs.map +1 -0
- package/esm/openapi/paths.mjs +63 -0
- package/esm/openapi/paths.mjs.map +1 -0
- package/esm/openapi/postman/build-request-body.mjs +86 -0
- package/esm/openapi/postman/build-request-body.mjs.map +1 -0
- package/esm/openapi/postman/build-request-url.mjs +85 -0
- package/esm/openapi/postman/build-request-url.mjs.map +1 -0
- package/esm/openapi/postman/build-saved-responses.mjs +60 -0
- package/esm/openapi/postman/build-saved-responses.mjs.map +1 -0
- package/esm/openapi/postman/example-from-schema.d.mts +17 -0
- package/esm/openapi/postman/example-from-schema.d.mts.map +1 -0
- package/esm/openapi/postman/example-from-schema.mjs +92 -0
- package/esm/openapi/postman/example-from-schema.mjs.map +1 -0
- package/esm/openapi/postman/http-status-text.mjs +31 -0
- package/esm/openapi/postman/http-status-text.mjs.map +1 -0
- package/esm/openapi/postman/index.d.mts +3 -0
- package/esm/openapi/postman/openapi-to-postman-collection.d.mts +22 -0
- package/esm/openapi/postman/openapi-to-postman-collection.d.mts.map +1 -0
- package/esm/openapi/postman/openapi-to-postman-collection.mjs +109 -0
- package/esm/openapi/postman/openapi-to-postman-collection.mjs.map +1 -0
- package/esm/openapi/postman/postman-types.d.mts +119 -0
- package/esm/openapi/postman/postman-types.d.mts.map +1 -0
- package/esm/openapi/postman/resolve-operation-auth.mjs +51 -0
- package/esm/openapi/postman/resolve-operation-auth.mjs.map +1 -0
- package/esm/openapi/postman/schema-refs.d.mts +7 -0
- package/esm/openapi/postman/schema-refs.d.mts.map +1 -0
- package/esm/openapi/postman/schema-refs.mjs +41 -0
- package/esm/openapi/postman/schema-refs.mjs.map +1 -0
- package/esm/openapi/postman/uuid-v5.mjs +29 -0
- package/esm/openapi/postman/uuid-v5.mjs.map +1 -0
- package/esm/openapi/request-schemas.mjs +138 -0
- package/esm/openapi/request-schemas.mjs.map +1 -0
- package/esm/openapi/response-schemas.mjs +85 -0
- package/esm/openapi/response-schemas.mjs.map +1 -0
- package/esm/openapi/schema-registry.mjs +143 -0
- package/esm/openapi/schema-registry.mjs.map +1 -0
- package/esm/openapi/security.mjs +98 -0
- package/esm/openapi/security.mjs.map +1 -0
- package/esm/openapi/types.d.mts +119 -0
- package/esm/openapi/types.d.mts.map +1 -0
- package/esm/production/registration-child-runner.mjs +139 -0
- package/esm/production/registration-child-runner.mjs.map +1 -0
- package/esm/production/route-registration-child.mjs +30 -2
- package/esm/production/route-registration-child.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs +22 -119
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/resource/define-resource.d.mts +44 -14
- package/esm/resource/define-resource.d.mts.map +1 -1
- package/esm/resource/define-resource.mjs +0 -40
- package/esm/resource/define-resource.mjs.map +1 -1
- package/esm/resource/index.d.mts +1 -1
- package/esm/resource/resource.d.mts +4 -4
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs +2 -4
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/resource/types.d.mts +89 -4
- package/esm/resource/types.d.mts.map +1 -1
- package/esm/router/index.d.mts +1 -1
- package/esm/router/named-api-routes-with-responses.mjs +59 -0
- package/esm/router/named-api-routes-with-responses.mjs.map +1 -0
- package/esm/router/response-schema-types.mjs +100 -0
- package/esm/router/response-schema-types.mjs.map +1 -0
- package/esm/router/router.d.mts +2 -2
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +38 -11
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +16 -1
- package/esm/router/types.d.mts.map +1 -1
- package/llms-full.txt +293 -5
- package/llms.txt +3 -2
- package/package.json +13 -13
- package/skills/README.md +3 -2
- package/skills/create-controller/SKILL.md +39 -2
- package/skills/define-resource/SKILL.md +100 -1
- package/skills/generate-openapi/SKILL.md +148 -0
- package/skills/index.md +3 -3
- package/skills/validate-input/SKILL.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
> ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
|
|
8
8
|
|
|
9
|
+
## 5.28.0 - 2026-10-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`warlock generate.postman`** writes a Postman Collection v2.1.0 (`storage/postman/collection.json`, or `--out`) derived from the same OpenAPI document as `generate.openapi` (options: `--title`, `--server`, `--include-pages`), so the two can never disagree. Each tag is a folder, each operation a request with an example body generated from its schema (JSON as raw, multipart as form-data), saved responses for every declared status, `{{baseUrl}}` and `{{token}}` collection variables, collection-level bearer auth for `authMiddleware()` routes and `noauth` on public ones. `openApiToPostmanCollection(document, options?)` is exported (pure, deterministic `_postman_id`), and `getDevelopmentPostmanCollection()` returns the collection for the running dev process.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Expected client errors no longer fill logs with stack traces: a thrown error that resolves to a 4xx (an `HttpError` such as `ConflictError` or `ResourceNotFoundError`, or a `DatabaseWriterValidationError` configured below 500 via `http.modelValidationErrorStatus`) is logged once at `warn` as `Name: message (status)`, without the stack. 5xx and unknown errors are logged at `error` with the stack, as before.
|
|
18
|
+
- A nested-resource field whose input is an empty list now outputs `[]`; it used to drop the key. `null`/`undefined` still omit it.
|
|
19
|
+
|
|
20
|
+
## 5.27.0 - 2026-10-01
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Typed resource output.** `defineResource()` infers the `toJSON()` output type from the schema's cast strings (`"string?"` is `string | null`, `"date"` is the default `{ iso; format; timestamp; humanTime }` object, nested and `"self"` resources resolve to their own output). Read it with `ResourceOutput<typeof UserResource>`. When `transform`/`boot`/builders change the shape, state it: `defineResource<ProductJson>({ ... })`. Builder fields are `unknown`.
|
|
25
|
+
- **`Serialized<T, W>`**: the type of what the client receives after Warlock serializes `T`, for API responses (`"json"`, default) and page loader data (`"devalue"`). Dates become strings on JSON, `toJSON()` results are used, functions are dropped, and models map to their resource through the augmentable `ModelResourceRegistry`.
|
|
26
|
+
- **Typed API responses from `handler.responseSchema`.** A response schema body now accepts cast strings with the resource suffixes (`"number?"`, `"string[]"`, `"date[]?"`), a resource, `[resource]` for an array, and nested plain objects. `ResponseBodyOutput<B>` and `ResponseSchemaOutput<S>` give the body type per status code with the same rules as `ResourceOutput`. `warlock generate.typings` and `warlock dev` read the declared schema at runtime and hand each route's response types to the web route-types generator, so nothing is annotated by hand.
|
|
27
|
+
- `NamedApiRoute` has an optional `response` map (status code to a TypeScript type expression), present only on routes that declare `responseSchema`; `router.getNamedApiRoutes()` accepts a `resolveResponse` option to attach it and still returns the plain `{ name, path, method }` records otherwise. Resources are mapped back to their `*.resource.ts(x)` export by identity; a resource defined inline or outside such a file is typed `unknown` with a warning naming the route and field. The typings child and the dev server share one serializer, so both emit identical strings.
|
|
28
|
+
- `responseSchema` is now kept when a route is bound from an array handler (`[controller, "action"]`) or a restful resource method; the bound function used to lose it.
|
|
29
|
+
- **`warlock generate.openapi`** writes an OpenAPI 3.1 document (`storage/openapi/openapi.json`, or `--out`) derived from the registered routes. Options: `--title`, `--server`, `--include-pages`. Paths use `{param}` templates and `all` routes expand to every verb; `validation.schema` becomes the request body (POST/PUT/PATCH) or query parameters (GET/HEAD/DELETE) according to `validating`, and `validation.params` becomes typed path parameters. Responses come from `handler.responseSchema` (casts, `"?"` as `type: [T, "null"]`, resources as `components.schemas` `$ref`s named after their export, `[Resource]` as an array); a route with no schema documents only a `200` description. A `422` (shaped by `validation.response`) is added when a route has `validation.schema`, and a `401` when it is guarded. Fields that cannot be described statically (field builders, resolver functions) are documented as `{}` and listed as warnings; the command exits non-zero only when the app cannot be loaded.
|
|
30
|
+
- **Security schemes from `authMiddleware`.** Core reads the `Symbol.for("warlock.auth")` descriptor (`{ sources: ("header" | { cookie: string })[], userTypes: string[] }`) that `authMiddleware()` attaches and emits `bearerAuth` / `cookieAuth` schemes only for the ones a route uses (header plus cookie is an OR). Middleware without a descriptor is ordinary middleware and produces no security entry.
|
|
31
|
+
- **`buildOpenApiDocument(routes, context)`** is exported: a pure function from `router.list()` routes to a plain-JSON OpenAPI document plus a `warnings` list, so a dev tool can build the same document in-process. The CLI builds it inside the route-registration child process, so Seal instances never cross IPC; that child's fork, IPC and timeout handling is now a shared helper with no change to `warlock build` (`WARLOCK_ROUTE_REGISTRATION_TIMEOUT_MS` applies to both).
|
|
32
|
+
- **`getDevelopmentOpenApiDocument()`** returns the same document for the routes registered in the running dev process (`warlock dev` registers the builder at startup; outside it the call throws), reusing the CLI child's builder (resource export map, `package.json` info, `http` server, `validation.response`). `@warlock.js/devtools` calls it to serve its API docs page at `/__warlock/docs`; warnings are logged once per process.
|
|
33
|
+
- **Models resolve to their resource in `Serialized`.** `warlock generate.typings` and `warlock dev` now write `.warlock/typings/model-resources.d.ts`, a `ModelResourceRegistry` augmentation with one entry per model class that declares `static resource = <Identifier>`, so `Serialized<User>` is the resource output instead of the raw model data. The identifier is followed through the model file's own imports (named, aliased, default, tsconfig aliases) or its own declarations, by reading the source only; a computed expression, `lazy(...)`, a generic or unexported model is left out and keeps the raw-data type. Duplicate class names get `_2`, `_3` keys, output is sorted, and the file is rewritten only when it changes. The app's `tsconfig.json` must include `.warlock/typings/*.d.ts` (the scaffold already does).
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Dependency: @mongez/supportive-is 3.0 (type-predicate checkers).
|
|
38
|
+
|
|
9
39
|
## 5.26.0 - 2026-09-30
|
|
10
40
|
|
|
11
41
|
### Upgrading
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { rootPath } from "../../utils/paths.mjs";
|
|
2
|
+
import { resolveBuildConfig } from "../../production/resolve-build-config.mjs";
|
|
3
|
+
import { collectOpenApiDocument } from "../../openapi/collect-openapi-document.mjs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { colors } from "@mongez/copper";
|
|
6
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
//#region ../core/src/cli/commands/openapi-generator.action.ts
|
|
9
|
+
/** Where the document goes unless `--out` says otherwise. */
|
|
10
|
+
const DEFAULT_OPENAPI_OUTPUT = "storage/openapi/openapi.json";
|
|
11
|
+
function stringOption(value) {
|
|
12
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Action behind `warlock generate.openapi`.
|
|
16
|
+
*
|
|
17
|
+
* Builds the OpenAPI 3.1 document in a fresh registration child (no connectors are booted),
|
|
18
|
+
* writes it as pretty JSON to `--out` (default `storage/openapi/openapi.json`) and prints the
|
|
19
|
+
* per-route gaps it found as warnings. A child failure is fatal and rejects; gaps never are.
|
|
20
|
+
*/
|
|
21
|
+
async function generateOpenApiAction({ options }) {
|
|
22
|
+
const cwd = rootPath();
|
|
23
|
+
const output = path.resolve(cwd, stringOption(options.out) ?? "storage/openapi/openapi.json");
|
|
24
|
+
const { document, warnings } = await collectOpenApiDocument({
|
|
25
|
+
cwd,
|
|
26
|
+
timeoutMs: resolveBuildConfig().routeRegistrationTimeoutMs,
|
|
27
|
+
title: stringOption(options.title),
|
|
28
|
+
server: stringOption(options.server),
|
|
29
|
+
includePages: options.includePages === true
|
|
30
|
+
});
|
|
31
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
32
|
+
await writeFile(output, `${JSON.stringify(document, null, 2)}\n`, "utf8");
|
|
33
|
+
const operations = Object.values(document.paths).reduce((total, item) => total + Object.keys(item).length, 0);
|
|
34
|
+
console.log(colors.greenBright(`OpenAPI ${document.openapi} written to ${output} (${Object.keys(document.paths).length} paths, ${operations} operations).`));
|
|
35
|
+
if (warnings.length > 0) {
|
|
36
|
+
console.warn(colors.yellowBright(`\n${warnings.length} warning${warnings.length === 1 ? "" : "s"}:`));
|
|
37
|
+
for (const warning of warnings) console.warn(` - ${warning}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { generateOpenApiAction, stringOption };
|
|
43
|
+
//# sourceMappingURL=openapi-generator.action.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-generator.action.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/openapi-generator.action.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { colors } from \"@mongez/copper\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { collectOpenApiDocument } from \"../../openapi/collect-openapi-document\";\nimport { resolveBuildConfig } from \"../../production/resolve-build-config\";\nimport { rootPath } from \"../../utils/paths\";\n\n/** Where the document goes unless `--out` says otherwise. */\nexport const DEFAULT_OPENAPI_OUTPUT = \"storage/openapi/openapi.json\";\n\ntype OptionValue = CommandActionData[\"options\"][string];\n\nexport function stringOption(value: OptionValue | undefined): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined;\n}\n\n/**\n * Action behind `warlock generate.openapi`.\n *\n * Builds the OpenAPI 3.1 document in a fresh registration child (no connectors are booted),\n * writes it as pretty JSON to `--out` (default `storage/openapi/openapi.json`) and prints the\n * per-route gaps it found as warnings. A child failure is fatal and rejects; gaps never are.\n */\nexport async function generateOpenApiAction({ options }: CommandActionData): Promise<void> {\n const cwd = rootPath();\n const output = path.resolve(cwd, stringOption(options.out) ?? DEFAULT_OPENAPI_OUTPUT);\n\n const { document, warnings } = await collectOpenApiDocument({\n cwd,\n timeoutMs: resolveBuildConfig().routeRegistrationTimeoutMs,\n title: stringOption(options.title),\n server: stringOption(options.server),\n includePages: options.includePages === true,\n });\n\n await mkdir(path.dirname(output), { recursive: true });\n await writeFile(output, `${JSON.stringify(document, null, 2)}\\n`, \"utf8\");\n\n const operations = Object.values(document.paths).reduce(\n (total, item) => total + Object.keys(item).length,\n 0,\n );\n\n console.log(\n colors.greenBright(\n `OpenAPI ${document.openapi} written to ${output} (${Object.keys(document.paths).length} paths, ${operations} operations).`,\n ),\n );\n\n if (warnings.length > 0) {\n console.warn(colors.yellowBright(`\\n${warnings.length} warning${warnings.length === 1 ? \"\" : \"s\"}:`));\n\n for (const warning of warnings) {\n console.warn(` - ${warning}`);\n }\n }\n}\n"],"mappings":";;;;;;;;;AASA,MAAa,yBAAyB;AAItC,SAAgB,aAAa,OAAoD;CAC/E,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,IAAI,MAAM,KAAK,IAAI;AAC/E;;;;;;;;AASA,eAAsB,sBAAsB,EAAE,WAA6C;CACzF,MAAM,MAAM,SAAS;CACrB,MAAM,SAAS,KAAK,QAAQ,KAAK,aAAa,QAAQ,GAAG,mCAA2B;CAEpF,MAAM,EAAE,UAAU,aAAa,MAAM,uBAAuB;EAC1D;EACA,WAAW,mBAAmB,CAAC,CAAC;EAChC,OAAO,aAAa,QAAQ,KAAK;EACjC,QAAQ,aAAa,QAAQ,MAAM;EACnC,cAAc,QAAQ,iBAAiB;CACzC,CAAC;CAED,MAAM,MAAM,KAAK,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;CACrD,MAAM,UAAU,QAAQ,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE,KAAK,MAAM;CAExE,MAAM,aAAa,OAAO,OAAO,SAAS,KAAK,CAAC,CAAC,QAC9C,OAAO,SAAS,QAAQ,OAAO,KAAK,IAAI,CAAC,CAAC,QAC3C,CACF;CAEA,QAAQ,IACN,OAAO,YACL,WAAW,SAAS,QAAQ,cAAc,OAAO,IAAI,OAAO,KAAK,SAAS,KAAK,CAAC,CAAC,OAAO,UAAU,WAAW,cAC/G,CACF;CAEA,IAAI,SAAS,SAAS,GAAG;EACvB,QAAQ,KAAK,OAAO,aAAa,KAAK,SAAS,OAAO,UAAU,SAAS,WAAW,IAAI,KAAK,IAAI,EAAE,CAAC;EAEpG,KAAK,MAAM,WAAW,UACpB,QAAQ,KAAK,OAAO,SAAS;CAEjC;AACF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { command } from "../../commands/cli-command.mjs";
|
|
2
|
+
import { generateOpenApiAction } from "./openapi-generator.action.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../core/src/cli/commands/openapi-generator.command.ts
|
|
5
|
+
/**
|
|
6
|
+
* `warlock generate.openapi` — write an OpenAPI 3.1 document derived from the registered
|
|
7
|
+
* routes, handler validation, declared `responseSchema` and auth middleware.
|
|
8
|
+
*
|
|
9
|
+
* Named with a dot like `generate.typings`; the `generate` command itself is the scaffolder.
|
|
10
|
+
* Routes are read in the same isolated child process `warlock build` uses, so no connector
|
|
11
|
+
* is started and the app keeps running untouched.
|
|
12
|
+
*/
|
|
13
|
+
const openApiGeneratorCommand = command({
|
|
14
|
+
name: "generate.openapi",
|
|
15
|
+
description: "Generate an OpenAPI 3.1 document (storage/openapi/openapi.json) from the routes",
|
|
16
|
+
action: generateOpenApiAction,
|
|
17
|
+
preload: { warlockConfig: true },
|
|
18
|
+
options: [
|
|
19
|
+
{
|
|
20
|
+
text: "--out, -o",
|
|
21
|
+
description: "Output file, relative to the project root (default storage/openapi/openapi.json)",
|
|
22
|
+
type: "string"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
text: "--title",
|
|
26
|
+
description: "info.title (default: the name in package.json)",
|
|
27
|
+
type: "string"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
text: "--server",
|
|
31
|
+
description: "servers[0].url (default: built from the http config)",
|
|
32
|
+
type: "string"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
text: "--include-pages",
|
|
36
|
+
description: "Also document page (SSR) routes",
|
|
37
|
+
type: "boolean"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { openApiGeneratorCommand };
|
|
44
|
+
//# sourceMappingURL=openapi-generator.command.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-generator.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/openapi-generator.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\nimport { generateOpenApiAction } from \"./openapi-generator.action\";\n\n/**\n * `warlock generate.openapi` — write an OpenAPI 3.1 document derived from the registered\n * routes, handler validation, declared `responseSchema` and auth middleware.\n *\n * Named with a dot like `generate.typings`; the `generate` command itself is the scaffolder.\n * Routes are read in the same isolated child process `warlock build` uses, so no connector\n * is started and the app keeps running untouched.\n */\nexport const openApiGeneratorCommand = command({\n name: \"generate.openapi\",\n description: \"Generate an OpenAPI 3.1 document (storage/openapi/openapi.json) from the routes\",\n action: generateOpenApiAction,\n preload: {\n warlockConfig: true,\n },\n options: [\n {\n text: \"--out, -o\",\n description: \"Output file, relative to the project root (default storage/openapi/openapi.json)\",\n type: \"string\",\n },\n {\n text: \"--title\",\n description: \"info.title (default: the name in package.json)\",\n type: \"string\",\n },\n {\n text: \"--server\",\n description: \"servers[0].url (default: built from the http config)\",\n type: \"string\",\n },\n {\n text: \"--include-pages\",\n description: \"Also document page (SSR) routes\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":";;;;;;;;;;;;AAWA,MAAa,0BAA0B,QAAQ;CAC7C,MAAM;CACN,aAAa;CACb,QAAQ;CACR,SAAS,EACP,eAAe,KACjB;CACA,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { rootPath } from "../../utils/paths.mjs";
|
|
2
|
+
import { openApiToPostmanCollection } from "../../openapi/postman/openapi-to-postman-collection.mjs";
|
|
3
|
+
import { resolveBuildConfig } from "../../production/resolve-build-config.mjs";
|
|
4
|
+
import { collectOpenApiDocument } from "../../openapi/collect-openapi-document.mjs";
|
|
5
|
+
import { stringOption } from "./openapi-generator.action.mjs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { colors } from "@mongez/copper";
|
|
8
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
9
|
+
|
|
10
|
+
//#region ../core/src/cli/commands/postman-generator.action.ts
|
|
11
|
+
/** Where the collection goes unless `--out` says otherwise. */
|
|
12
|
+
const DEFAULT_POSTMAN_OUTPUT = "storage/postman/collection.json";
|
|
13
|
+
function countRequests(items) {
|
|
14
|
+
return items.reduce((total, item) => total + ("item" in item ? countRequests(item.item) : 1), 0);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Action behind `warlock generate.postman`.
|
|
18
|
+
*
|
|
19
|
+
* Gets the OpenAPI document exactly as `generate.openapi` does (same registration child, same
|
|
20
|
+
* options), converts it to a Postman Collection v2.1.0 in this process and writes it as
|
|
21
|
+
* pretty JSON to `--out` (default `storage/postman/collection.json`). The per-route gaps the
|
|
22
|
+
* document builder found are printed as warnings. A child failure is fatal and rejects.
|
|
23
|
+
*/
|
|
24
|
+
async function generatePostmanAction({ options }) {
|
|
25
|
+
const cwd = rootPath();
|
|
26
|
+
const output = path.resolve(cwd, stringOption(options.out) ?? "storage/postman/collection.json");
|
|
27
|
+
const { document, warnings } = await collectOpenApiDocument({
|
|
28
|
+
cwd,
|
|
29
|
+
timeoutMs: resolveBuildConfig().routeRegistrationTimeoutMs,
|
|
30
|
+
title: stringOption(options.title),
|
|
31
|
+
server: stringOption(options.server),
|
|
32
|
+
includePages: options.includePages === true
|
|
33
|
+
});
|
|
34
|
+
const collection = openApiToPostmanCollection(document);
|
|
35
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
36
|
+
await writeFile(output, `${JSON.stringify(collection, null, 2)}\n`, "utf8");
|
|
37
|
+
const folders = collection.item.filter((item) => "item" in item).length;
|
|
38
|
+
console.log(colors.greenBright(`Postman collection written to ${output} (${folders} folder${folders === 1 ? "" : "s"}, ${countRequests(collection.item)} requests). Import it in Postman and set the {{token}} variable.`));
|
|
39
|
+
if (warnings.length > 0) {
|
|
40
|
+
console.warn(colors.yellowBright(`\n${warnings.length} warning${warnings.length === 1 ? "" : "s"}:`));
|
|
41
|
+
for (const warning of warnings) console.warn(` - ${warning}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
export { generatePostmanAction };
|
|
47
|
+
//# sourceMappingURL=postman-generator.action.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postman-generator.action.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/postman-generator.action.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { colors } from \"@mongez/copper\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { collectOpenApiDocument } from \"../../openapi/collect-openapi-document\";\nimport { openApiToPostmanCollection } from \"../../openapi/postman/openapi-to-postman-collection\";\nimport type { PostmanItem } from \"../../openapi/postman/postman-types\";\nimport { resolveBuildConfig } from \"../../production/resolve-build-config\";\nimport { rootPath } from \"../../utils/paths\";\nimport { stringOption } from \"./openapi-generator.action\";\n\n/** Where the collection goes unless `--out` says otherwise. */\nexport const DEFAULT_POSTMAN_OUTPUT = \"storage/postman/collection.json\";\n\nfunction countRequests(items: readonly PostmanItem[]): number {\n return items.reduce((total, item) => total + (\"item\" in item ? countRequests(item.item) : 1), 0);\n}\n\n/**\n * Action behind `warlock generate.postman`.\n *\n * Gets the OpenAPI document exactly as `generate.openapi` does (same registration child, same\n * options), converts it to a Postman Collection v2.1.0 in this process and writes it as\n * pretty JSON to `--out` (default `storage/postman/collection.json`). The per-route gaps the\n * document builder found are printed as warnings. A child failure is fatal and rejects.\n */\nexport async function generatePostmanAction({ options }: CommandActionData): Promise<void> {\n const cwd = rootPath();\n const output = path.resolve(cwd, stringOption(options.out) ?? DEFAULT_POSTMAN_OUTPUT);\n\n const { document, warnings } = await collectOpenApiDocument({\n cwd,\n timeoutMs: resolveBuildConfig().routeRegistrationTimeoutMs,\n title: stringOption(options.title),\n server: stringOption(options.server),\n includePages: options.includePages === true,\n });\n\n const collection = openApiToPostmanCollection(document);\n\n await mkdir(path.dirname(output), { recursive: true });\n await writeFile(output, `${JSON.stringify(collection, null, 2)}\\n`, \"utf8\");\n\n const folders = collection.item.filter((item) => \"item\" in item).length;\n\n console.log(\n colors.greenBright(\n `Postman collection written to ${output} (${folders} folder${folders === 1 ? \"\" : \"s\"}, ${countRequests(collection.item)} requests). Import it in Postman and set the {{token}} variable.`,\n ),\n );\n\n if (warnings.length > 0) {\n console.warn(colors.yellowBright(`\\n${warnings.length} warning${warnings.length === 1 ? \"\" : \"s\"}:`));\n\n for (const warning of warnings) {\n console.warn(` - ${warning}`);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAYA,MAAa,yBAAyB;AAEtC,SAAS,cAAc,OAAuC;CAC5D,OAAO,MAAM,QAAQ,OAAO,SAAS,SAAS,UAAU,OAAO,cAAc,KAAK,IAAI,IAAI,IAAI,CAAC;AACjG;;;;;;;;;AAUA,eAAsB,sBAAsB,EAAE,WAA6C;CACzF,MAAM,MAAM,SAAS;CACrB,MAAM,SAAS,KAAK,QAAQ,KAAK,aAAa,QAAQ,GAAG,sCAA2B;CAEpF,MAAM,EAAE,UAAU,aAAa,MAAM,uBAAuB;EAC1D;EACA,WAAW,mBAAmB,CAAC,CAAC;EAChC,OAAO,aAAa,QAAQ,KAAK;EACjC,QAAQ,aAAa,QAAQ,MAAM;EACnC,cAAc,QAAQ,iBAAiB;CACzC,CAAC;CAED,MAAM,aAAa,2BAA2B,QAAQ;CAEtD,MAAM,MAAM,KAAK,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;CACrD,MAAM,UAAU,QAAQ,GAAG,KAAK,UAAU,YAAY,MAAM,CAAC,EAAE,KAAK,MAAM;CAE1E,MAAM,UAAU,WAAW,KAAK,QAAQ,SAAS,UAAU,IAAI,CAAC,CAAC;CAEjE,QAAQ,IACN,OAAO,YACL,iCAAiC,OAAO,IAAI,QAAQ,SAAS,YAAY,IAAI,KAAK,IAAI,IAAI,cAAc,WAAW,IAAI,EAAE,iEAC3H,CACF;CAEA,IAAI,SAAS,SAAS,GAAG;EACvB,QAAQ,KAAK,OAAO,aAAa,KAAK,SAAS,OAAO,UAAU,SAAS,WAAW,IAAI,KAAK,IAAI,EAAE,CAAC;EAEpG,KAAK,MAAM,WAAW,UACpB,QAAQ,KAAK,OAAO,SAAS;CAEjC;AACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { command } from "../../commands/cli-command.mjs";
|
|
2
|
+
import { generatePostmanAction } from "./postman-generator.action.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../core/src/cli/commands/postman-generator.command.ts
|
|
5
|
+
/**
|
|
6
|
+
* `warlock generate.postman` — write a Postman Collection v2.1.0 derived from the same
|
|
7
|
+
* OpenAPI document `generate.openapi` produces, so the two never disagree.
|
|
8
|
+
*
|
|
9
|
+
* Routes are read in the same isolated child process `warlock build` uses; the conversion
|
|
10
|
+
* itself is pure and runs in the CLI process.
|
|
11
|
+
*/
|
|
12
|
+
const postmanGeneratorCommand = command({
|
|
13
|
+
name: "generate.postman",
|
|
14
|
+
description: "Generate a Postman collection (storage/postman/collection.json) from the routes",
|
|
15
|
+
action: generatePostmanAction,
|
|
16
|
+
preload: { warlockConfig: true },
|
|
17
|
+
options: [
|
|
18
|
+
{
|
|
19
|
+
text: "--out, -o",
|
|
20
|
+
description: "Output file, relative to the project root (default storage/postman/collection.json)",
|
|
21
|
+
type: "string"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
text: "--title",
|
|
25
|
+
description: "Collection name (default: the name in package.json)",
|
|
26
|
+
type: "string"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
text: "--server",
|
|
30
|
+
description: "Value of {{baseUrl}} (default: built from the http config)",
|
|
31
|
+
type: "string"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
text: "--include-pages",
|
|
35
|
+
description: "Also include page (SSR) routes",
|
|
36
|
+
type: "boolean"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { postmanGeneratorCommand };
|
|
43
|
+
//# sourceMappingURL=postman-generator.command.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postman-generator.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/postman-generator.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\nimport { generatePostmanAction } from \"./postman-generator.action\";\n\n/**\n * `warlock generate.postman` — write a Postman Collection v2.1.0 derived from the same\n * OpenAPI document `generate.openapi` produces, so the two never disagree.\n *\n * Routes are read in the same isolated child process `warlock build` uses; the conversion\n * itself is pure and runs in the CLI process.\n */\nexport const postmanGeneratorCommand = command({\n name: \"generate.postman\",\n description: \"Generate a Postman collection (storage/postman/collection.json) from the routes\",\n action: generatePostmanAction,\n preload: {\n warlockConfig: true,\n },\n options: [\n {\n text: \"--out, -o\",\n description: \"Output file, relative to the project root (default storage/postman/collection.json)\",\n type: \"string\",\n },\n {\n text: \"--title\",\n description: \"Collection name (default: the name in package.json)\",\n type: \"string\",\n },\n {\n text: \"--server\",\n description: \"Value of {{baseUrl}} (default: built from the http config)\",\n type: \"string\",\n },\n {\n text: \"--include-pages\",\n description: \"Also include page (SSR) routes\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":";;;;;;;;;;;AAUA,MAAa,0BAA0B,QAAQ;CAC7C,MAAM;CACN,aAAa;CACb,QAAQ;CACR,SAAS,EACP,eAAe,KACjB;CACA,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC"}
|
|
@@ -6,6 +6,8 @@ import { doctorCommand } from "./commands/doctor.command.mjs";
|
|
|
6
6
|
import { dropTablesCommand } from "./commands/drop-tables.command.mjs";
|
|
7
7
|
import { generateCommand, generateControllerCommand, generateMigrationCommand, generateModelCommand, generateModuleCommand, generateRepositoryCommand, generateResourceCommand, generateServiceCommand, generateUseCaseCommand } from "./commands/generate/generate.command.mjs";
|
|
8
8
|
import { migrateCommand } from "./commands/migrate.command.mjs";
|
|
9
|
+
import { openApiGeneratorCommand } from "./commands/openapi-generator.command.mjs";
|
|
10
|
+
import { postmanGeneratorCommand } from "./commands/postman-generator.command.mjs";
|
|
9
11
|
import { routesCommand, routesDiffCommand } from "./commands/routes.command.mjs";
|
|
10
12
|
import { seedCommand } from "./commands/seed.command.mjs";
|
|
11
13
|
import { startProductionCommand } from "./commands/start-production.command.mjs";
|
|
@@ -17,6 +19,8 @@ import { updateCommand } from "./commands/update.command.mjs";
|
|
|
17
19
|
const frameworkCommands = [
|
|
18
20
|
devServerCommand,
|
|
19
21
|
typingsGeneratorCommand,
|
|
22
|
+
openApiGeneratorCommand,
|
|
23
|
+
postmanGeneratorCommand,
|
|
20
24
|
doctorCommand,
|
|
21
25
|
routesCommand,
|
|
22
26
|
routesDiffCommand,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"framework-cli-commands.mjs","names":[],"sources":["../../../../../../../core/src/cli/framework-cli-commands.ts"],"sourcesContent":["import { addCommand } from \"./commands/add.command\";\nimport { buildCommand } from \"./commands/build.command\";\nimport { createDatabaseCommand } from \"./commands/create-database.command\";\nimport { devServerCommand } from \"./commands/dev-server.command\";\nimport { doctorCommand } from \"./commands/doctor.command\";\nimport { dropTablesCommand } from \"./commands/drop-tables.command\";\nimport {\n generateCommand,\n generateControllerCommand,\n generateMigrationCommand,\n generateModelCommand,\n generateModuleCommand,\n generateRepositoryCommand,\n generateResourceCommand,\n generateServiceCommand,\n generateUseCaseCommand,\n} from \"./commands/generate/generate.command\";\nimport { migrateCommand } from \"./commands/migrate.command\";\nimport { routesCommand, routesDiffCommand } from \"./commands/routes.command\";\nimport { seedCommand } from \"./commands/seed.command\";\nimport { startProductionCommand } from \"./commands/start-production.command\";\nimport { storagePutCommand } from \"./commands/storage-put.command\";\nimport { typingsGeneratorCommand } from \"./commands/typings-generator.command\";\nimport { updateCommand } from \"./commands/update.command\";\n\nexport const frameworkCommands = [\n // development commands\n devServerCommand,\n typingsGeneratorCommand,\n\n // diagnostics commands\n doctorCommand,\n routesCommand,\n routesDiffCommand,\n\n // production commands\n buildCommand,\n startProductionCommand,\n\n // database commands\n migrateCommand,\n seedCommand,\n createDatabaseCommand,\n dropTablesCommand,\n\n // generation/installation commands\n addCommand,\n updateCommand,\n\n // scaffolding commands\n generateCommand,\n generateModuleCommand,\n generateControllerCommand,\n generateServiceCommand,\n generateUseCaseCommand,\n generateModelCommand,\n generateRepositoryCommand,\n generateResourceCommand,\n generateMigrationCommand,\n\n // storage commands\n storagePutCommand,\n];\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"framework-cli-commands.mjs","names":[],"sources":["../../../../../../../core/src/cli/framework-cli-commands.ts"],"sourcesContent":["import { addCommand } from \"./commands/add.command\";\nimport { buildCommand } from \"./commands/build.command\";\nimport { createDatabaseCommand } from \"./commands/create-database.command\";\nimport { devServerCommand } from \"./commands/dev-server.command\";\nimport { doctorCommand } from \"./commands/doctor.command\";\nimport { dropTablesCommand } from \"./commands/drop-tables.command\";\nimport {\n generateCommand,\n generateControllerCommand,\n generateMigrationCommand,\n generateModelCommand,\n generateModuleCommand,\n generateRepositoryCommand,\n generateResourceCommand,\n generateServiceCommand,\n generateUseCaseCommand,\n} from \"./commands/generate/generate.command\";\nimport { migrateCommand } from \"./commands/migrate.command\";\nimport { openApiGeneratorCommand } from \"./commands/openapi-generator.command\";\nimport { postmanGeneratorCommand } from \"./commands/postman-generator.command\";\nimport { routesCommand, routesDiffCommand } from \"./commands/routes.command\";\nimport { seedCommand } from \"./commands/seed.command\";\nimport { startProductionCommand } from \"./commands/start-production.command\";\nimport { storagePutCommand } from \"./commands/storage-put.command\";\nimport { typingsGeneratorCommand } from \"./commands/typings-generator.command\";\nimport { updateCommand } from \"./commands/update.command\";\n\nexport const frameworkCommands = [\n // development commands\n devServerCommand,\n typingsGeneratorCommand,\n openApiGeneratorCommand,\n postmanGeneratorCommand,\n\n // diagnostics commands\n doctorCommand,\n routesCommand,\n routesDiffCommand,\n\n // production commands\n buildCommand,\n startProductionCommand,\n\n // database commands\n migrateCommand,\n seedCommand,\n createDatabaseCommand,\n dropTablesCommand,\n\n // generation/installation commands\n addCommand,\n updateCommand,\n\n // scaffolding commands\n generateCommand,\n generateModuleCommand,\n generateControllerCommand,\n generateServiceCommand,\n generateUseCaseCommand,\n generateModelCommand,\n generateRepositoryCommand,\n generateResourceCommand,\n generateMigrationCommand,\n\n // storage commands\n storagePutCommand,\n];\n"],"mappings":";;;;;;;;;;;;;;;;;;AA2BA,MAAa,oBAAoB;CAE/B;CACA;CACA;CACA;CAGA;CACA;CACA;CAGA;CACA;CAGA;CACA;CACA;CACA;CAGA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;AACF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region ../core/src/dev-server/build-development-openapi-document.ts
|
|
2
|
+
const warned = /* @__PURE__ */ new Set();
|
|
3
|
+
/**
|
|
4
|
+
* Build the OpenAPI document from the routes registered in THIS process, for the dev server.
|
|
5
|
+
*
|
|
6
|
+
* `warlock dev` has already loaded every route module, so no child process is needed: this
|
|
7
|
+
* runs the same `buildOpenApiForChild` the `generate.openapi` child runs (same resource
|
|
8
|
+
* export map, `package.json` info, `http` server and `validation.response` config), with the
|
|
9
|
+
* dev server's file list. Each distinct warning is logged once per process, not on every
|
|
10
|
+
* request to the docs page.
|
|
11
|
+
*
|
|
12
|
+
* Lives under `dev-server/`, off the production entry: the dev server registers it as the
|
|
13
|
+
* development OpenAPI provider (`registerDevelopmentOpenApiProvider`), and devtools reads it
|
|
14
|
+
* through `getDevelopmentOpenApiDocument()` from the root barrel.
|
|
15
|
+
*/
|
|
16
|
+
async function buildDevelopmentOpenApiDocument() {
|
|
17
|
+
const [{ filesOrchestrator }, { devLogWarn }, { buildOpenApiForChild }] = await Promise.all([
|
|
18
|
+
import("./files-orchestrator.mjs"),
|
|
19
|
+
import("./dev-logger.mjs"),
|
|
20
|
+
import("../openapi/openapi-child-output.mjs")
|
|
21
|
+
]);
|
|
22
|
+
return buildOpenApiForChild({
|
|
23
|
+
cwd: process.cwd(),
|
|
24
|
+
files: filesOrchestrator.getFiles().values(),
|
|
25
|
+
request: { version: 1 },
|
|
26
|
+
onWarn: (message) => {
|
|
27
|
+
if (warned.has(message)) return;
|
|
28
|
+
warned.add(message);
|
|
29
|
+
devLogWarn(message);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
export { buildDevelopmentOpenApiDocument };
|
|
36
|
+
//# sourceMappingURL=build-development-openapi-document.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-development-openapi-document.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/build-development-openapi-document.ts"],"sourcesContent":["import type { OpenApiBuildResult } from \"../openapi/types\";\n\nconst warned = new Set<string>();\n\n/**\n * Build the OpenAPI document from the routes registered in THIS process, for the dev server.\n *\n * `warlock dev` has already loaded every route module, so no child process is needed: this\n * runs the same `buildOpenApiForChild` the `generate.openapi` child runs (same resource\n * export map, `package.json` info, `http` server and `validation.response` config), with the\n * dev server's file list. Each distinct warning is logged once per process, not on every\n * request to the docs page.\n *\n * Lives under `dev-server/`, off the production entry: the dev server registers it as the\n * development OpenAPI provider (`registerDevelopmentOpenApiProvider`), and devtools reads it\n * through `getDevelopmentOpenApiDocument()` from the root barrel.\n */\nexport async function buildDevelopmentOpenApiDocument(): Promise<OpenApiBuildResult> {\n const [{ filesOrchestrator }, { devLogWarn }, { buildOpenApiForChild }] = await Promise.all([\n import(\"./files-orchestrator\"),\n import(\"./dev-logger\"),\n import(\"../openapi/openapi-child-output\"),\n ]);\n\n return buildOpenApiForChild({\n cwd: process.cwd(),\n files: filesOrchestrator.getFiles().values(),\n request: { version: 1 },\n onWarn: (message) => {\n if (warned.has(message)) return;\n\n warned.add(message);\n devLogWarn(message);\n },\n });\n}\n"],"mappings":";AAEA,MAAM,yBAAS,IAAI,IAAY;;;;;;;;;;;;;;AAe/B,eAAsB,kCAA+D;CACnF,MAAM,CAAC,EAAE,qBAAqB,EAAE,cAAc,EAAE,0BAA0B,MAAM,QAAQ,IAAI;EAC1F,OAAO;EACP,OAAO;EACP,OAAO;CACT,CAAC;CAED,OAAO,qBAAqB;EAC1B,KAAK,QAAQ,IAAI;EACjB,OAAO,kBAAkB,SAAS,CAAC,CAAC,OAAO;EAC3C,SAAS,EAAE,SAAS,EAAE;EACtB,SAAS,YAAY;GACnB,IAAI,OAAO,IAAI,OAAO,GAAG;GAEzB,OAAO,IAAI,OAAO;GAClB,WAAW,OAAO;EACpB;CACF,CAAC;AACH"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { devLogWarn } from "./dev-logger.mjs";
|
|
2
|
+
import { filesOrchestrator } from "./files-orchestrator.mjs";
|
|
3
|
+
import { collectNamedApiRoutesWithResponses } from "../router/named-api-routes-with-responses.mjs";
|
|
4
|
+
|
|
5
|
+
//#region ../core/src/dev-server/collect-development-api-routes.ts
|
|
6
|
+
const warned = /* @__PURE__ */ new Set();
|
|
7
|
+
/**
|
|
8
|
+
* Named API routes for the dev server's in-process route-type publisher. Uses the same
|
|
9
|
+
* serializer as the typings child process, so `response` strings never differ between
|
|
10
|
+
* `warlock dev` and `warlock generate.typings`. Each distinct warning is logged once per
|
|
11
|
+
* process, not on every hot reload.
|
|
12
|
+
*/
|
|
13
|
+
function collectDevelopmentApiRoutes() {
|
|
14
|
+
return collectNamedApiRoutesWithResponses({
|
|
15
|
+
appRoot: process.cwd(),
|
|
16
|
+
files: filesOrchestrator.getFiles().values(),
|
|
17
|
+
onWarn: (message) => {
|
|
18
|
+
if (warned.has(message)) return;
|
|
19
|
+
warned.add(message);
|
|
20
|
+
devLogWarn(message);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { collectDevelopmentApiRoutes };
|
|
27
|
+
//# sourceMappingURL=collect-development-api-routes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect-development-api-routes.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/collect-development-api-routes.ts"],"sourcesContent":["import { collectNamedApiRoutesWithResponses } from \"../router/named-api-routes-with-responses\";\nimport type { NamedApiRoute } from \"../router/types\";\nimport { devLogWarn } from \"./dev-logger\";\nimport { filesOrchestrator } from \"./files-orchestrator\";\n\nconst warned = new Set<string>();\n\n/**\n * Named API routes for the dev server's in-process route-type publisher. Uses the same\n * serializer as the typings child process, so `response` strings never differ between\n * `warlock dev` and `warlock generate.typings`. Each distinct warning is logged once per\n * process, not on every hot reload.\n */\nexport function collectDevelopmentApiRoutes(): Promise<readonly NamedApiRoute[]> {\n return collectNamedApiRoutesWithResponses({\n appRoot: process.cwd(),\n files: filesOrchestrator.getFiles().values(),\n onWarn: (message) => {\n if (warned.has(message)) return;\n\n warned.add(message);\n devLogWarn(message);\n },\n });\n}\n"],"mappings":";;;;;AAKA,MAAM,yBAAS,IAAI,IAAY;;;;;;;AAQ/B,SAAgB,8BAAiE;CAC/E,OAAO,mCAAmC;EACxC,SAAS,QAAQ,IAAI;EACrB,OAAO,kBAAkB,SAAS,CAAC,CAAC,OAAO;EAC3C,SAAS,YAAY;GACnB,IAAI,OAAO,IAAI,OAAO,GAAG;GAEzB,OAAO,IAAI,OAAO;GAClB,WAAW,OAAO;EACpB;CACF,CAAC;AACH"}
|
|
@@ -82,6 +82,14 @@ function devLogTimings(timings) {
|
|
|
82
82
|
].join(colors.dim(" · "));
|
|
83
83
|
console.log(`${timestamp()} ${colors.dim("⏱")} ${line}`);
|
|
84
84
|
}
|
|
85
|
+
function devLogConfig(file, connectors) {
|
|
86
|
+
const relativePath = Path.toRelative(file);
|
|
87
|
+
const connectorInfo = connectors && connectors.length > 0 ? colors.dim(` → restarting ${connectors.join(", ")}`) : "";
|
|
88
|
+
console.log(`${timestamp()} ${colors.cyan("config reload")} ${colors.dim(relativePath)}${connectorInfo}`);
|
|
89
|
+
}
|
|
90
|
+
function devLogReady(message) {
|
|
91
|
+
console.log(`\n${timestamp()} ${colors.green("➜")} ${colors.bold(message)}`);
|
|
92
|
+
}
|
|
85
93
|
function devLogSection(title) {
|
|
86
94
|
console.log(`\n${timestamp()} ${colors.bold(colors.cyan(title))}`);
|
|
87
95
|
}
|
|
@@ -117,5 +125,5 @@ function formatModuleNotFoundError(error, suggestions) {
|
|
|
117
125
|
const devServeLog = devLog;
|
|
118
126
|
|
|
119
127
|
//#endregion
|
|
120
|
-
export { devLogDim, devLogError, devLogHMR, devLogInfo, devLogSection, devLogSuccess, devLogTimings, devLogWarn, devServeLog, formatModuleNotFoundError };
|
|
128
|
+
export { devLog, devLogConfig, devLogDim, devLogError, devLogHMR, devLogInfo, devLogReady, devLogSection, devLogSuccess, devLogTimings, devLogWarn, devServeLog, formatErrorStack, formatModuleNotFoundError };
|
|
121
129
|
//# sourceMappingURL=dev-logger.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-logger.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/dev-logger.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport dayjs from \"dayjs\";\nimport { Path } from \"../utils/normalized-path\";\n\n/**\n * Dev server logger — Vite-style formatting helpers.\n */\n\nfunction timestamp(): string {\n return colors.dim(`${dayjs().format(\"HH:mm:ss A\")}`);\n}\n\nexport function devLog(message: string) {\n console.log(`${timestamp()} ${message}`);\n}\n\nexport function devLogSuccess(message: string) {\n console.log(`${timestamp()} ${colors.green(\"✓\")} ${colors.green(message)}`);\n}\n\n/**\n * Colourise a stack trace so the eye lands on *your* code.\n *\n * - Error header (`ReferenceError: x is not defined`) → bold red.\n * - Frames in project `src/` → highlighted: a green `›` pointer, yellow\n * function name, cyan relative path, dim `:line:col`. These are almost\n * always where the bug is.\n * - Framework (`@warlock.js`), `node_modules`, and `node:` internal frames\n * → dimmed and relativised. Still there for context, just out of the way.\n *\n * Source maps are enabled in dev, so the paths/lines here are already the\n * original `.ts` locations — this only changes how they're painted.\n */\nexport function formatErrorStack(stack: string): string {\n const frame = /^(\\s*)at (.+?) \\((.+):(\\d+):(\\d+)\\)$/;\n const bareFrame = /^(\\s*)at (.+):(\\d+):(\\d+)$/;\n let headerDone = false;\n\n return stack\n .split(\"\\n\")\n .map((line) => {\n const withFn = line.match(frame);\n const bare = line.match(bareFrame);\n\n if (!withFn && !bare) {\n // Header line(s) before the first frame.\n const painted = headerDone ? colors.dim(line) : colors.bold(colors.red(line));\n return painted;\n }\n\n headerDone = true;\n\n // Every read here is defaulted rather than asserted, and the reason is\n // what this function IS: a stack-trace formatter. If it throws while\n // formatting, it throws INSIDE error reporting — and what the developer\n // then sees is this function's failure instead of the error they were\n // actually chasing. An unreadable frame should degrade to a blank field,\n // never take the report down with it — every fatal needs an\n // unconditional floor.\n const fn = (withFn ? withFn[2] : \"\") ?? \"\";\n const file = (withFn ? withFn[3] : bare?.[2]) ?? \"\";\n const lineNo = (withFn ? withFn[4] : bare?.[3]) ?? \"\";\n const col = (withFn ? withFn[5] : bare?.[4]) ?? \"\";\n\n const isNodeInternal = file.startsWith(\"node:\");\n const isDep = file.includes(\"node_modules\");\n const isFramework = /[\\\\/]@warlock\\.js[\\\\/]/.test(file);\n const isUserCode = !isNodeInternal && !isDep && !isFramework;\n\n const rel = isNodeInternal ? file : Path.toRelative(file);\n const loc = `:${lineNo}:${col}`;\n\n if (isUserCode) {\n return (\n ` ${colors.green(\"›\")} ${colors.dim(\"at\")} ` +\n `${colors.yellow(fn || \"<anonymous>\")} ` +\n `${colors.cyan(rel)}${colors.dim(loc)}`\n );\n }\n\n const label = fn ? `at ${fn} ${rel}${loc}` : `at ${rel}${loc}`;\n return ` ${colors.dim(label)}`;\n })\n .join(\"\\n\");\n}\n\nexport function devLogError(message: string, error?: any) {\n console.log(`${timestamp()} ${colors.red(\"✗\")} ${colors.red(message)}`);\n if (error?.stack) console.log(formatErrorStack(error.stack));\n}\n\nexport function devLogWarn(message: string) {\n console.log(`${timestamp()} ${colors.yellow(\"⚠\")} ${colors.yellow(message)}`);\n}\n\nexport function devLogInfo(message: string) {\n console.log(`${timestamp()} ${colors.cyan(message)}`);\n}\n\nexport function devLogDim(message: string) {\n console.log(`${timestamp()} ${colors.dim(message)}`);\n}\n\nexport function devLogHMR(file: string, dependents?: number, durationMs?: number) {\n const relativePath = Path.toRelative(file);\n const depInfo = dependents\n ? colors.dim(` +${dependents} module${dependents > 1 ? \"s\" : \"\"}`)\n : \"\";\n const durationInfo = durationMs !== undefined ? colors.dim(` (${durationMs}ms)`) : \"\";\n console.log(\n `${timestamp()} 🔥 ${colors.green(\"hmr update\")} ${colors.dim(relativePath)}${depInfo}${durationInfo}`,\n );\n}\n\n/**\n * Per-phase breakdown for one hot reload, printed only when\n * `devServer.timings` is on. Phases run in this order: the watcher's\n * `awaitWriteFinish` settle wait, this handler's own debounce wait,\n * module-graph invalidation (version bumps), re-import of the changed\n * modules, and connector restart/rebind.\n */\nexport type ReloadPhaseTimings = {\n watcherSettleMs: number;\n debounceWaitMs: number;\n moduleGraphInvalidationMs: number;\n reimportMs: number;\n connectorRestartMs: number;\n};\n\nexport function devLogTimings(timings: ReloadPhaseTimings) {\n const phase = (label: string, ms: number) => `${colors.dim(label)} ${colors.dim(`${Math.round(ms)}ms`)}`;\n const line = [\n phase(\"watcher\", timings.watcherSettleMs),\n phase(\"debounce\", timings.debounceWaitMs),\n phase(\"graph\", timings.moduleGraphInvalidationMs),\n phase(\"reimport\", timings.reimportMs),\n phase(\"connectors\", timings.connectorRestartMs),\n ].join(colors.dim(\" · \"));\n console.log(`${timestamp()} ${colors.dim(\"⏱\")} ${line}`);\n}\n\nexport function devLogConfig(file: string, connectors?: string[]) {\n const relativePath = Path.toRelative(file);\n const connectorInfo =\n connectors && connectors.length > 0 ? colors.dim(` → restarting ${connectors.join(\", \")}`) : \"\";\n console.log(\n `${timestamp()} ${colors.cyan(\"config reload\")} ${colors.dim(relativePath)}${connectorInfo}`,\n );\n}\n\nexport function devLogReady(message: string) {\n console.log(`\\n${timestamp()} ${colors.green(\"➜\")} ${colors.bold(message)}`);\n}\n\nexport function devLogSection(title: string) {\n console.log(`\\n${timestamp()} ${colors.bold(colors.cyan(title))}`);\n}\n\n/**\n * Format ERR_MODULE_NOT_FOUND so the displayed paths are relative to the\n * project root. The loader hook keeps source paths in the URL so we only\n * need to strip the absolute prefix — no cache-path translation any more.\n */\nexport function formatModuleNotFoundError(error: Error, suggestions?: string[]): string {\n const match = error.message.match(/Cannot find module '([^']+)' imported from '([^']+)'/);\n if (!match) return error.message;\n\n // Same rule as the frame formatter above: this renders a MODULE NOT FOUND\n // report, so a missing capture must degrade the message, not replace the\n // user's error with a crash inside the reporter.\n const [, rawModulePath, rawImporterPath] = match;\n const modulePath = rawModulePath ?? \"\";\n const importerPath = rawImporterPath ?? \"\";\n const lines: string[] = [\n \"\",\n `${colors.red(\"❌ MODULE NOT FOUND\")}`,\n \"\",\n `${colors.dim(\"Cannot find:\")} ${colors.cyan(Path.toRelative(modulePath))}`,\n \"\",\n `${colors.dim(\"Imported by:\")}`,\n ` ${colors.yellow(\"→\")} ${colors.white(Path.toRelative(importerPath))}`,\n ];\n\n if (suggestions && suggestions.length > 0) {\n lines.push(\"\");\n lines.push(`${colors.dim(\"Did you mean?\")}`);\n suggestions.forEach((s) => lines.push(` ${colors.cyan(\"→\")} ${colors.green(s)}`));\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n/** @deprecated alias retained for older callers. Use `devLog` directly. */\nexport const devServeLog = devLog;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dev-logger.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/dev-logger.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport dayjs from \"dayjs\";\nimport { Path } from \"../utils/normalized-path\";\n\n/**\n * Dev server logger — Vite-style formatting helpers.\n */\n\nfunction timestamp(): string {\n return colors.dim(`${dayjs().format(\"HH:mm:ss A\")}`);\n}\n\nexport function devLog(message: string) {\n console.log(`${timestamp()} ${message}`);\n}\n\nexport function devLogSuccess(message: string) {\n console.log(`${timestamp()} ${colors.green(\"✓\")} ${colors.green(message)}`);\n}\n\n/**\n * Colourise a stack trace so the eye lands on *your* code.\n *\n * - Error header (`ReferenceError: x is not defined`) → bold red.\n * - Frames in project `src/` → highlighted: a green `›` pointer, yellow\n * function name, cyan relative path, dim `:line:col`. These are almost\n * always where the bug is.\n * - Framework (`@warlock.js`), `node_modules`, and `node:` internal frames\n * → dimmed and relativised. Still there for context, just out of the way.\n *\n * Source maps are enabled in dev, so the paths/lines here are already the\n * original `.ts` locations — this only changes how they're painted.\n */\nexport function formatErrorStack(stack: string): string {\n const frame = /^(\\s*)at (.+?) \\((.+):(\\d+):(\\d+)\\)$/;\n const bareFrame = /^(\\s*)at (.+):(\\d+):(\\d+)$/;\n let headerDone = false;\n\n return stack\n .split(\"\\n\")\n .map((line) => {\n const withFn = line.match(frame);\n const bare = line.match(bareFrame);\n\n if (!withFn && !bare) {\n // Header line(s) before the first frame.\n const painted = headerDone ? colors.dim(line) : colors.bold(colors.red(line));\n return painted;\n }\n\n headerDone = true;\n\n // Every read here is defaulted rather than asserted, and the reason is\n // what this function IS: a stack-trace formatter. If it throws while\n // formatting, it throws INSIDE error reporting — and what the developer\n // then sees is this function's failure instead of the error they were\n // actually chasing. An unreadable frame should degrade to a blank field,\n // never take the report down with it — every fatal needs an\n // unconditional floor.\n const fn = (withFn ? withFn[2] : \"\") ?? \"\";\n const file = (withFn ? withFn[3] : bare?.[2]) ?? \"\";\n const lineNo = (withFn ? withFn[4] : bare?.[3]) ?? \"\";\n const col = (withFn ? withFn[5] : bare?.[4]) ?? \"\";\n\n const isNodeInternal = file.startsWith(\"node:\");\n const isDep = file.includes(\"node_modules\");\n const isFramework = /[\\\\/]@warlock\\.js[\\\\/]/.test(file);\n const isUserCode = !isNodeInternal && !isDep && !isFramework;\n\n const rel = isNodeInternal ? file : Path.toRelative(file);\n const loc = `:${lineNo}:${col}`;\n\n if (isUserCode) {\n return (\n ` ${colors.green(\"›\")} ${colors.dim(\"at\")} ` +\n `${colors.yellow(fn || \"<anonymous>\")} ` +\n `${colors.cyan(rel)}${colors.dim(loc)}`\n );\n }\n\n const label = fn ? `at ${fn} ${rel}${loc}` : `at ${rel}${loc}`;\n return ` ${colors.dim(label)}`;\n })\n .join(\"\\n\");\n}\n\nexport function devLogError(message: string, error?: any) {\n console.log(`${timestamp()} ${colors.red(\"✗\")} ${colors.red(message)}`);\n if (error?.stack) console.log(formatErrorStack(error.stack));\n}\n\nexport function devLogWarn(message: string) {\n console.log(`${timestamp()} ${colors.yellow(\"⚠\")} ${colors.yellow(message)}`);\n}\n\nexport function devLogInfo(message: string) {\n console.log(`${timestamp()} ${colors.cyan(message)}`);\n}\n\nexport function devLogDim(message: string) {\n console.log(`${timestamp()} ${colors.dim(message)}`);\n}\n\nexport function devLogHMR(file: string, dependents?: number, durationMs?: number) {\n const relativePath = Path.toRelative(file);\n const depInfo = dependents\n ? colors.dim(` +${dependents} module${dependents > 1 ? \"s\" : \"\"}`)\n : \"\";\n const durationInfo = durationMs !== undefined ? colors.dim(` (${durationMs}ms)`) : \"\";\n console.log(\n `${timestamp()} 🔥 ${colors.green(\"hmr update\")} ${colors.dim(relativePath)}${depInfo}${durationInfo}`,\n );\n}\n\n/**\n * Per-phase breakdown for one hot reload, printed only when\n * `devServer.timings` is on. Phases run in this order: the watcher's\n * `awaitWriteFinish` settle wait, this handler's own debounce wait,\n * module-graph invalidation (version bumps), re-import of the changed\n * modules, and connector restart/rebind.\n */\nexport type ReloadPhaseTimings = {\n watcherSettleMs: number;\n debounceWaitMs: number;\n moduleGraphInvalidationMs: number;\n reimportMs: number;\n connectorRestartMs: number;\n};\n\nexport function devLogTimings(timings: ReloadPhaseTimings) {\n const phase = (label: string, ms: number) => `${colors.dim(label)} ${colors.dim(`${Math.round(ms)}ms`)}`;\n const line = [\n phase(\"watcher\", timings.watcherSettleMs),\n phase(\"debounce\", timings.debounceWaitMs),\n phase(\"graph\", timings.moduleGraphInvalidationMs),\n phase(\"reimport\", timings.reimportMs),\n phase(\"connectors\", timings.connectorRestartMs),\n ].join(colors.dim(\" · \"));\n console.log(`${timestamp()} ${colors.dim(\"⏱\")} ${line}`);\n}\n\nexport function devLogConfig(file: string, connectors?: string[]) {\n const relativePath = Path.toRelative(file);\n const connectorInfo =\n connectors && connectors.length > 0 ? colors.dim(` → restarting ${connectors.join(\", \")}`) : \"\";\n console.log(\n `${timestamp()} ${colors.cyan(\"config reload\")} ${colors.dim(relativePath)}${connectorInfo}`,\n );\n}\n\nexport function devLogReady(message: string) {\n console.log(`\\n${timestamp()} ${colors.green(\"➜\")} ${colors.bold(message)}`);\n}\n\nexport function devLogSection(title: string) {\n console.log(`\\n${timestamp()} ${colors.bold(colors.cyan(title))}`);\n}\n\n/**\n * Format ERR_MODULE_NOT_FOUND so the displayed paths are relative to the\n * project root. The loader hook keeps source paths in the URL so we only\n * need to strip the absolute prefix — no cache-path translation any more.\n */\nexport function formatModuleNotFoundError(error: Error, suggestions?: string[]): string {\n const match = error.message.match(/Cannot find module '([^']+)' imported from '([^']+)'/);\n if (!match) return error.message;\n\n // Same rule as the frame formatter above: this renders a MODULE NOT FOUND\n // report, so a missing capture must degrade the message, not replace the\n // user's error with a crash inside the reporter.\n const [, rawModulePath, rawImporterPath] = match;\n const modulePath = rawModulePath ?? \"\";\n const importerPath = rawImporterPath ?? \"\";\n const lines: string[] = [\n \"\",\n `${colors.red(\"❌ MODULE NOT FOUND\")}`,\n \"\",\n `${colors.dim(\"Cannot find:\")} ${colors.cyan(Path.toRelative(modulePath))}`,\n \"\",\n `${colors.dim(\"Imported by:\")}`,\n ` ${colors.yellow(\"→\")} ${colors.white(Path.toRelative(importerPath))}`,\n ];\n\n if (suggestions && suggestions.length > 0) {\n lines.push(\"\");\n lines.push(`${colors.dim(\"Did you mean?\")}`);\n suggestions.forEach((s) => lines.push(` ${colors.cyan(\"→\")} ${colors.green(s)}`));\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n/** @deprecated alias retained for older callers. Use `devLog` directly. */\nexport const devServeLog = devLog;\n"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Application } from "../application/application.mjs";
|
|
2
|
-
import { router } from "../router/router.mjs";
|
|
3
2
|
import "../application/index.mjs";
|
|
4
3
|
import { ConnectorLifecyclePhase } from "../connectors/types.mjs";
|
|
5
4
|
import { connectorsManager } from "../connectors/connectors-manager.mjs";
|
|
6
5
|
import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
|
|
6
|
+
import { registerDevelopmentOpenApiProvider } from "../openapi/development-openapi-provider.mjs";
|
|
7
7
|
import "../warlock-config/index.mjs";
|
|
8
8
|
import { devLogInfo, devLogSection, devLogWarn, devServeLog } from "./dev-logger.mjs";
|
|
9
9
|
import { MANIFEST_PATH } from "./flags.mjs";
|
|
@@ -12,9 +12,11 @@ import { publishCurrentRouteTypes } from "./route-types-publisher.mjs";
|
|
|
12
12
|
import { restartDevServer } from "./restart-dev-server.mjs";
|
|
13
13
|
import { devServerShortcuts } from "./shortcuts.mjs";
|
|
14
14
|
import { BootPreconditionError } from "./boot-precondition-error.mjs";
|
|
15
|
+
import { buildDevelopmentOpenApiDocument } from "./build-development-openapi-document.mjs";
|
|
15
16
|
import { LayerExecutor } from "./layer-executor.mjs";
|
|
16
17
|
import { printReadyBlock } from "./ready-block.mjs";
|
|
17
18
|
import { registerInstalledDevtools } from "./register-installed-devtools.mjs";
|
|
19
|
+
import { collectDevelopmentApiRoutes } from "./collect-development-api-routes.mjs";
|
|
18
20
|
import { typeGenerator } from "./type-generator.mjs";
|
|
19
21
|
import { colors } from "@mongez/copper";
|
|
20
22
|
import events from "@mongez/events";
|
|
@@ -38,6 +40,7 @@ var DevelopmentServer = class {
|
|
|
38
40
|
try {
|
|
39
41
|
this.connectorsStarted = true;
|
|
40
42
|
const startedAt = performance.now();
|
|
43
|
+
registerDevelopmentOpenApiProvider(buildDevelopmentOpenApiDocument);
|
|
41
44
|
if (this.options.fresh && await fileExistsAsync(MANIFEST_PATH)) {
|
|
42
45
|
await unlinkAsync(MANIFEST_PATH);
|
|
43
46
|
devServeLog(colors.cyanBright("Cleared manifest (--fresh)"));
|
|
@@ -56,7 +59,7 @@ var DevelopmentServer = class {
|
|
|
56
59
|
}
|
|
57
60
|
await registerInstalledDevtools();
|
|
58
61
|
await connectorsManager.startPhase("late");
|
|
59
|
-
await publishCurrentRouteTypes(process.cwd(),
|
|
62
|
+
await publishCurrentRouteTypes(process.cwd(), await collectDevelopmentApiRoutes());
|
|
60
63
|
this.layerExecutor = new LayerExecutor(filesOrchestrator.getDependencyGraph(), filesOrchestrator.specialFilesCollector, filesOrchestrator.moduleLoader, (absolutePath) => filesOrchestrator.bumpVersion(absolutePath), () => filesOrchestrator.flushVersionBumps());
|
|
61
64
|
this.running = true;
|
|
62
65
|
const duration = performance.now() - startedAt;
|
|
@@ -99,7 +102,7 @@ var DevelopmentServer = class {
|
|
|
99
102
|
const codeFiles = [...batch.added, ...batch.changed].filter((p) => !isEnvPath(p) && p !== "warlock.config.ts" && !batch.restartTriggers?.includes(p));
|
|
100
103
|
try {
|
|
101
104
|
await this.layerExecutor.executeBatchReload(codeFiles, filesOrchestrator.getFiles(), batch.deleted, batch.changed, batch.timings);
|
|
102
|
-
await publishCurrentRouteTypes(process.cwd(),
|
|
105
|
+
await publishCurrentRouteTypes(process.cwd(), await collectDevelopmentApiRoutes());
|
|
103
106
|
typeGenerator.executeTypingsGenerator([
|
|
104
107
|
...batch.added,
|
|
105
108
|
...batch.changed,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"development-server.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/development-server.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport events from \"@mongez/events\";\nimport { fileExistsAsync, unlinkAsync } from \"@warlock.js/fs\";\nimport { Application } from \"../application\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\nimport { router } from \"../router/router\";\nimport { warlockConfigManager } from \"../warlock-config\";\nimport { BootPreconditionError } from \"./boot-precondition-error\";\nimport { devLogInfo, devLogSection, devLogWarn, devServeLog } from \"./dev-logger\";\nimport { filesOrchestrator } from \"./files-orchestrator\";\nimport { MANIFEST_PATH } from \"./flags\";\nimport type { IncomingReloadTimings } from \"./layer-executor\";\nimport { LayerExecutor } from \"./layer-executor\";\nimport { printReadyBlock } from \"./ready-block\";\nimport { registerInstalledDevtools } from \"./register-installed-devtools\";\nimport { restartDevServer } from \"./restart-dev-server\";\nimport { publishCurrentRouteTypes } from \"./route-types-publisher\";\nimport { devServerShortcuts } from \"./shortcuts\";\nimport type { StartDevServerOptions } from \"./start-development-server\";\nimport { typeGenerator } from \"./type-generator\";\n\ntype Batch = {\n added: string[];\n changed: string[];\n deleted: string[];\n /** Non-code `watch.include` files that changed; only a restart applies them. */\n restartTriggers?: string[];\n timings?: IncomingReloadTimings;\n};\n\n/**\n * Top-level coordinator for `warlock dev`. Wires the file orchestrator, the\n * connectors, and the layer executor together, and listens for the watcher's\n * batched events to drive HMR.\n */\nexport class DevelopmentServer {\n private layerExecutor?: LayerExecutor;\n private running = false;\n\n /** Set once boot begins, so a failed boot still closes connectors that already started. */\n private connectorsStarted = false;\n private readonly options: StartDevServerOptions;\n\n public constructor(options: StartDevServerOptions = {}) {\n this.options = options;\n devLogSection(\"Starting Development Server...\");\n }\n\n public async start(): Promise<void> {\n try {\n this.connectorsStarted = true;\n const startedAt = performance.now();\n\n // --fresh deletes the manifest so reconciliation re-parses every file\n // from disk. Transpile caching is owned by the loader hook (in-memory).\n if (this.options.fresh && (await fileExistsAsync(MANIFEST_PATH))) {\n await unlinkAsync(MANIFEST_PATH);\n devServeLog(colors.cyanBright(\"Cleared manifest (--fresh)\"));\n }\n\n await filesOrchestrator.init();\n await filesOrchestrator.initializeAll();\n await filesOrchestrator.watchFiles();\n\n filesOrchestrator.specialFilesCollector.collect(filesOrchestrator.getFiles());\n\n this.setupEventListeners();\n\n // Decorator-driven registries (models, etc.) must be populated before\n // routes/services can resolve symbols by name.\n await this.autoDiscoverFiles();\n\n await filesOrchestrator.moduleLoader.loadAll();\n\n // A rejecting validator (Application.onValidateBoot) must abort boot\n // before late-phase connectors bind a port — this is what makes the\n // hook actually prevent serving instead of merely being defined.\n //\n // Wrapped and re-thrown as a BootPreconditionError: a rejected\n // validator is, by definition, a precondition whose cause cannot\n // change because the worker tried again — the supervisor must not\n // restart into the same rejection. See boot-precondition-error.ts.\n try {\n await Application.runStartupValidators();\n } catch (error) {\n throw new BootPreconditionError((error as Error).message, { cause: error });\n }\n\n // `@warlock.js/devtools` is a dev dependency the config never names;\n // when installed it joins the late phase here, and only here.\n await registerInstalledDevtools();\n\n // Late-phase connectors (http, socket) bind after app code has\n // registered routes/listeners.\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\n\n // Web installs its page table from its late connector. Generate only\n // after that installation so `href()` and the declaration table agree.\n await publishCurrentRouteTypes(process.cwd(), router.getNamedApiRoutes());\n\n this.layerExecutor = new LayerExecutor(\n filesOrchestrator.getDependencyGraph(),\n filesOrchestrator.specialFilesCollector,\n filesOrchestrator.moduleLoader,\n (absolutePath) => filesOrchestrator.bumpVersion(absolutePath),\n () => filesOrchestrator.flushVersionBumps(),\n );\n\n this.running = true;\n\n const duration = performance.now() - startedAt;\n\n // App modules are loaded and both connector phases are active — signal a\n // complete boot so `Application.onceBooted(...)` listeners fire.\n Application.markBooted({\n environment: Application.environment,\n runtimeStrategy: Application.runtimeStrategy,\n bootDurationMs: duration,\n });\n\n // ONE block, AFTER the boot it summarises, replacing the scattered\n // \"N route(s) registered\" / \"Server ready at …\" / \"ready in …\" lines that\n // used to arrive in completion order. Warnings logged during boot sit\n // above it and stay there — see `ready-block.ts`.\n printReadyBlock(duration);\n\n // Tell the supervisor (when there is one) that boot completed, so its\n // healthy-uptime window counts from here rather than from spawn.\n process.send?.(\"warlock:booted\");\n\n // Precedence: explicit CLI option > devServer.* config > default.\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\n const generateTypings =\n this.options.generateTypings ?? devServerConfig?.generateTypings ?? true;\n const healthCheckers = this.options.healthCheckers ?? devServerConfig?.healthCheckers ?? true;\n\n if (generateTypings) await typeGenerator.executeGenerateAllCommand();\n\n if (healthCheckers) {\n filesOrchestrator.startCheckingHealth(healthCheckers === true ? undefined : healthCheckers);\n }\n } catch (error) {\n devServeLog(colors.redBright(`Failed to start Development Server: ${error}`));\n await this.shutdown();\n throw error;\n }\n }\n\n /**\n * Eagerly import files whose decorators populate global registries so any\n * symbol-by-name resolution later in boot finds them.\n */\n private async autoDiscoverFiles(): Promise<void> {\n const discoveryTypes = [\"model\"] as const;\n for (const file of filesOrchestrator.files.values()) {\n if (file.type && (discoveryTypes as readonly string[]).includes(file.type)) {\n await filesOrchestrator.moduleLoader.loadModule(file, file.type);\n }\n }\n }\n\n /** Tail of the batch-reload chain (see `setupEventListeners`). */\n private reloadQueue: Promise<void> = Promise.resolve();\n\n private setupEventListeners(): void {\n // Reloads run one at a time: a second batch waits for the first to finish\n // so route re-registration and connector restarts never interleave.\n events.on(\"dev-server:batch-complete\", (batch: Batch) => {\n this.reloadQueue = this.reloadQueue\n .then(() => this.handleBatchComplete(batch))\n .catch((error) => devServeLog(colors.redBright(`Batch reload failed: ${error}`)));\n });\n }\n\n private async handleBatchComplete(batch: Batch): Promise<void> {\n if (!this.running || !this.layerExecutor) return;\n\n // warlock.config.ts holds settings read at boot (CLI commands, build\n // options, watch patterns, scheduled jobs) and .env feeds every config\n // that reads it. Neither can be hot-reloaded without leaving running\n // services configured with stale values, so the only honest response is\n // a full restart.\n const restartTrigger = this.findRestartTrigger(batch);\n\n if (restartTrigger) {\n // On success this never comes back — the process exits and the\n // supervisor replaces it. Reaching the next line means the restart was\n // declined by config or wasn't possible, so we fall through and still\n // hot-reload whatever ordinary code shared the batch.\n await this.restartForConfigChange(restartTrigger);\n }\n\n // No-op saves (an editor that fsyncs without a content write) are already\n // filtered upstream by the file hash in FileEventHandler, so batch.changed\n // only contains genuinely-changed paths by the time it reaches here.\n\n const total = batch.added.length + batch.changed.length + batch.deleted.length;\n if (total === 0) return;\n\n // Boot-time files still ride along in `changed` when a restart was\n // declined (`restartOnConfigChange: false`) or wasn't possible — they are\n // never hot-reloaded, so keep them out of the reload set either way.\n const codeFiles = [...batch.added, ...batch.changed].filter(\n (p) => !isEnvPath(p) && p !== \"warlock.config.ts\" && !batch.restartTriggers?.includes(p),\n );\n\n try {\n await this.layerExecutor.executeBatchReload(\n codeFiles,\n filesOrchestrator.getFiles(),\n batch.deleted,\n batch.changed,\n batch.timings,\n );\n\n // `executeBatchReload` includes route re-imports and affected connector\n // restarts. Publishing earlier could freeze a partial page/API view.\n // This also refreshes deletion-only batches after their cleanup/restart.\n await publishCurrentRouteTypes(process.cwd(), router.getNamedApiRoutes());\n\n typeGenerator.executeTypingsGenerator([...batch.added, ...batch.changed, ...batch.deleted]);\n\n filesOrchestrator.checkHealth(batch);\n } catch (error) {\n devServeLog(colors.redBright(`Failed to execute batch reload: ${error}`));\n }\n }\n\n /**\n * The boot-time file in this batch that can only be applied by restarting,\n * or `undefined` when the batch is ordinary application code.\n */\n private findRestartTrigger(batch: Batch): string | undefined {\n if (batch.changed.includes(\"warlock.config.ts\")) {\n return \"warlock.config.ts\";\n }\n\n return [...batch.added, ...batch.changed].find(isEnvPath) ?? batch.restartTriggers?.[0];\n }\n\n /**\n * Restart so a changed boot-time file actually takes effect.\n *\n * Opt out with `devServer.restartOnConfigChange: false` and the old\n * behaviour returns: a warning telling you to restart yourself. If the\n * restart can't be performed at all (no supervisor, a shutdown that threw),\n * `restartDevServer` reports it and we fall back to the same warning rather\n * than leaving the server running against config it isn't using.\n */\n private async restartForConfigChange(file: string): Promise<void> {\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\n\n if (devServerConfig?.restartOnConfigChange === false) {\n devLogWarn(`${file} changed — restart the dev server to apply.`);\n return;\n }\n\n devLogInfo(`${file} changed — restarting to apply.`);\n\n const restarted = await restartDevServer(this);\n\n if (!restarted) {\n devLogWarn(`${file} changed — restart the dev server to apply.`);\n }\n }\n\n public async shutdown(): Promise<void> {\n // Always hand the terminal back, even on a repeat/no-op shutdown — a\n // process that exits while stdin is still in raw mode leaves the user's\n // shell without line editing or Ctrl+C.\n devServerShortcuts.release();\n\n if (!this.running && !this.connectorsStarted) return;\n devServeLog(colors.redBright(\"Shutting down Development Server...\"));\n this.running = false;\n this.connectorsStarted = false;\n await connectorsManager.shutdown();\n devServeLog(colors.greenBright(\"Development Server stopped\"));\n }\n\n public isRunning(): boolean {\n return this.running;\n }\n}\n\nfunction isEnvPath(path: string): boolean {\n const basename = path.split(\"/\").pop() ?? path;\n return basename === \".env\" || basename.startsWith(\".env.\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"development-server.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/development-server.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport events from \"@mongez/events\";\nimport { fileExistsAsync, unlinkAsync } from \"@warlock.js/fs\";\nimport { Application } from \"../application\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\nimport { warlockConfigManager } from \"../warlock-config\";\nimport { registerDevelopmentOpenApiProvider } from \"../openapi/development-openapi-provider\";\nimport { BootPreconditionError } from \"./boot-precondition-error\";\nimport { buildDevelopmentOpenApiDocument } from \"./build-development-openapi-document\";\nimport { devLogInfo, devLogSection, devLogWarn, devServeLog } from \"./dev-logger\";\nimport { filesOrchestrator } from \"./files-orchestrator\";\nimport { MANIFEST_PATH } from \"./flags\";\nimport type { IncomingReloadTimings } from \"./layer-executor\";\nimport { LayerExecutor } from \"./layer-executor\";\nimport { printReadyBlock } from \"./ready-block\";\nimport { registerInstalledDevtools } from \"./register-installed-devtools\";\nimport { restartDevServer } from \"./restart-dev-server\";\nimport { collectDevelopmentApiRoutes } from \"./collect-development-api-routes\";\nimport { publishCurrentRouteTypes } from \"./route-types-publisher\";\nimport { devServerShortcuts } from \"./shortcuts\";\nimport type { StartDevServerOptions } from \"./start-development-server\";\nimport { typeGenerator } from \"./type-generator\";\n\ntype Batch = {\n added: string[];\n changed: string[];\n deleted: string[];\n /** Non-code `watch.include` files that changed; only a restart applies them. */\n restartTriggers?: string[];\n timings?: IncomingReloadTimings;\n};\n\n/**\n * Top-level coordinator for `warlock dev`. Wires the file orchestrator, the\n * connectors, and the layer executor together, and listens for the watcher's\n * batched events to drive HMR.\n */\nexport class DevelopmentServer {\n private layerExecutor?: LayerExecutor;\n private running = false;\n\n /** Set once boot begins, so a failed boot still closes connectors that already started. */\n private connectorsStarted = false;\n private readonly options: StartDevServerOptions;\n\n public constructor(options: StartDevServerOptions = {}) {\n this.options = options;\n devLogSection(\"Starting Development Server...\");\n }\n\n public async start(): Promise<void> {\n try {\n this.connectorsStarted = true;\n const startedAt = performance.now();\n\n // Devtools reads this through the root barrel, which must not reach dev-server code.\n registerDevelopmentOpenApiProvider(buildDevelopmentOpenApiDocument);\n\n // --fresh deletes the manifest so reconciliation re-parses every file\n // from disk. Transpile caching is owned by the loader hook (in-memory).\n if (this.options.fresh && (await fileExistsAsync(MANIFEST_PATH))) {\n await unlinkAsync(MANIFEST_PATH);\n devServeLog(colors.cyanBright(\"Cleared manifest (--fresh)\"));\n }\n\n await filesOrchestrator.init();\n await filesOrchestrator.initializeAll();\n await filesOrchestrator.watchFiles();\n\n filesOrchestrator.specialFilesCollector.collect(filesOrchestrator.getFiles());\n\n this.setupEventListeners();\n\n // Decorator-driven registries (models, etc.) must be populated before\n // routes/services can resolve symbols by name.\n await this.autoDiscoverFiles();\n\n await filesOrchestrator.moduleLoader.loadAll();\n\n // A rejecting validator (Application.onValidateBoot) must abort boot\n // before late-phase connectors bind a port — this is what makes the\n // hook actually prevent serving instead of merely being defined.\n //\n // Wrapped and re-thrown as a BootPreconditionError: a rejected\n // validator is, by definition, a precondition whose cause cannot\n // change because the worker tried again — the supervisor must not\n // restart into the same rejection. See boot-precondition-error.ts.\n try {\n await Application.runStartupValidators();\n } catch (error) {\n throw new BootPreconditionError((error as Error).message, { cause: error });\n }\n\n // `@warlock.js/devtools` is a dev dependency the config never names;\n // when installed it joins the late phase here, and only here.\n await registerInstalledDevtools();\n\n // Late-phase connectors (http, socket) bind after app code has\n // registered routes/listeners.\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\n\n // Web installs its page table from its late connector. Generate only\n // after that installation so `href()` and the declaration table agree.\n await publishCurrentRouteTypes(process.cwd(), await collectDevelopmentApiRoutes());\n\n this.layerExecutor = new LayerExecutor(\n filesOrchestrator.getDependencyGraph(),\n filesOrchestrator.specialFilesCollector,\n filesOrchestrator.moduleLoader,\n (absolutePath) => filesOrchestrator.bumpVersion(absolutePath),\n () => filesOrchestrator.flushVersionBumps(),\n );\n\n this.running = true;\n\n const duration = performance.now() - startedAt;\n\n // App modules are loaded and both connector phases are active — signal a\n // complete boot so `Application.onceBooted(...)` listeners fire.\n Application.markBooted({\n environment: Application.environment,\n runtimeStrategy: Application.runtimeStrategy,\n bootDurationMs: duration,\n });\n\n // ONE block, AFTER the boot it summarises, replacing the scattered\n // \"N route(s) registered\" / \"Server ready at …\" / \"ready in …\" lines that\n // used to arrive in completion order. Warnings logged during boot sit\n // above it and stay there — see `ready-block.ts`.\n printReadyBlock(duration);\n\n // Tell the supervisor (when there is one) that boot completed, so its\n // healthy-uptime window counts from here rather than from spawn.\n process.send?.(\"warlock:booted\");\n\n // Precedence: explicit CLI option > devServer.* config > default.\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\n const generateTypings =\n this.options.generateTypings ?? devServerConfig?.generateTypings ?? true;\n const healthCheckers = this.options.healthCheckers ?? devServerConfig?.healthCheckers ?? true;\n\n if (generateTypings) await typeGenerator.executeGenerateAllCommand();\n\n if (healthCheckers) {\n filesOrchestrator.startCheckingHealth(healthCheckers === true ? undefined : healthCheckers);\n }\n } catch (error) {\n devServeLog(colors.redBright(`Failed to start Development Server: ${error}`));\n await this.shutdown();\n throw error;\n }\n }\n\n /**\n * Eagerly import files whose decorators populate global registries so any\n * symbol-by-name resolution later in boot finds them.\n */\n private async autoDiscoverFiles(): Promise<void> {\n const discoveryTypes = [\"model\"] as const;\n for (const file of filesOrchestrator.files.values()) {\n if (file.type && (discoveryTypes as readonly string[]).includes(file.type)) {\n await filesOrchestrator.moduleLoader.loadModule(file, file.type);\n }\n }\n }\n\n /** Tail of the batch-reload chain (see `setupEventListeners`). */\n private reloadQueue: Promise<void> = Promise.resolve();\n\n private setupEventListeners(): void {\n // Reloads run one at a time: a second batch waits for the first to finish\n // so route re-registration and connector restarts never interleave.\n events.on(\"dev-server:batch-complete\", (batch: Batch) => {\n this.reloadQueue = this.reloadQueue\n .then(() => this.handleBatchComplete(batch))\n .catch((error) => devServeLog(colors.redBright(`Batch reload failed: ${error}`)));\n });\n }\n\n private async handleBatchComplete(batch: Batch): Promise<void> {\n if (!this.running || !this.layerExecutor) return;\n\n // warlock.config.ts holds settings read at boot (CLI commands, build\n // options, watch patterns, scheduled jobs) and .env feeds every config\n // that reads it. Neither can be hot-reloaded without leaving running\n // services configured with stale values, so the only honest response is\n // a full restart.\n const restartTrigger = this.findRestartTrigger(batch);\n\n if (restartTrigger) {\n // On success this never comes back — the process exits and the\n // supervisor replaces it. Reaching the next line means the restart was\n // declined by config or wasn't possible, so we fall through and still\n // hot-reload whatever ordinary code shared the batch.\n await this.restartForConfigChange(restartTrigger);\n }\n\n // No-op saves (an editor that fsyncs without a content write) are already\n // filtered upstream by the file hash in FileEventHandler, so batch.changed\n // only contains genuinely-changed paths by the time it reaches here.\n\n const total = batch.added.length + batch.changed.length + batch.deleted.length;\n if (total === 0) return;\n\n // Boot-time files still ride along in `changed` when a restart was\n // declined (`restartOnConfigChange: false`) or wasn't possible — they are\n // never hot-reloaded, so keep them out of the reload set either way.\n const codeFiles = [...batch.added, ...batch.changed].filter(\n (p) => !isEnvPath(p) && p !== \"warlock.config.ts\" && !batch.restartTriggers?.includes(p),\n );\n\n try {\n await this.layerExecutor.executeBatchReload(\n codeFiles,\n filesOrchestrator.getFiles(),\n batch.deleted,\n batch.changed,\n batch.timings,\n );\n\n // `executeBatchReload` includes route re-imports and affected connector\n // restarts. Publishing earlier could freeze a partial page/API view.\n // This also refreshes deletion-only batches after their cleanup/restart.\n await publishCurrentRouteTypes(process.cwd(), await collectDevelopmentApiRoutes());\n\n typeGenerator.executeTypingsGenerator([...batch.added, ...batch.changed, ...batch.deleted]);\n\n filesOrchestrator.checkHealth(batch);\n } catch (error) {\n devServeLog(colors.redBright(`Failed to execute batch reload: ${error}`));\n }\n }\n\n /**\n * The boot-time file in this batch that can only be applied by restarting,\n * or `undefined` when the batch is ordinary application code.\n */\n private findRestartTrigger(batch: Batch): string | undefined {\n if (batch.changed.includes(\"warlock.config.ts\")) {\n return \"warlock.config.ts\";\n }\n\n return [...batch.added, ...batch.changed].find(isEnvPath) ?? batch.restartTriggers?.[0];\n }\n\n /**\n * Restart so a changed boot-time file actually takes effect.\n *\n * Opt out with `devServer.restartOnConfigChange: false` and the old\n * behaviour returns: a warning telling you to restart yourself. If the\n * restart can't be performed at all (no supervisor, a shutdown that threw),\n * `restartDevServer` reports it and we fall back to the same warning rather\n * than leaving the server running against config it isn't using.\n */\n private async restartForConfigChange(file: string): Promise<void> {\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\n\n if (devServerConfig?.restartOnConfigChange === false) {\n devLogWarn(`${file} changed — restart the dev server to apply.`);\n return;\n }\n\n devLogInfo(`${file} changed — restarting to apply.`);\n\n const restarted = await restartDevServer(this);\n\n if (!restarted) {\n devLogWarn(`${file} changed — restart the dev server to apply.`);\n }\n }\n\n public async shutdown(): Promise<void> {\n // Always hand the terminal back, even on a repeat/no-op shutdown — a\n // process that exits while stdin is still in raw mode leaves the user's\n // shell without line editing or Ctrl+C.\n devServerShortcuts.release();\n\n if (!this.running && !this.connectorsStarted) return;\n devServeLog(colors.redBright(\"Shutting down Development Server...\"));\n this.running = false;\n this.connectorsStarted = false;\n await connectorsManager.shutdown();\n devServeLog(colors.greenBright(\"Development Server stopped\"));\n }\n\n public isRunning(): boolean {\n return this.running;\n }\n}\n\nfunction isEnvPath(path: string): boolean {\n const basename = path.split(\"/\").pop() ?? path;\n return basename === \".env\" || basename.startsWith(\".env.\");\n}\n"],"mappings":"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"}
|
|
@@ -24,6 +24,22 @@ import { fileURLToPath } from "node:url";
|
|
|
24
24
|
|
|
25
25
|
//#region ../core/src/dev-server/files-orchestrator.ts
|
|
26
26
|
/**
|
|
27
|
+
* Register a cleanup callback that fires before the current module is
|
|
28
|
+
* unloaded by HMR. Stack-inspects the caller so user code doesn't need to
|
|
29
|
+
* thread the FileManager through.
|
|
30
|
+
*/
|
|
31
|
+
function onCleanup(callback) {
|
|
32
|
+
if (globalThis.__currentModuleFile) {
|
|
33
|
+
globalThis.__currentModuleFile.addCleanup(callback);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const callerLine = (/* @__PURE__ */ new Error()).stack?.split("\n")[2];
|
|
37
|
+
const rawCaller = (callerLine?.match(/\((.+?):\d+:\d+\)/) || callerLine?.match(/at (.+?):\d+:\d+/))?.[1];
|
|
38
|
+
const callerFile = rawCaller?.startsWith("file://") ? fileURLToPath(rawCaller.replace(/\?.*$/, "")) : rawCaller;
|
|
39
|
+
if (!callerFile) return;
|
|
40
|
+
filesOrchestrator.files.get(Path.toRelative(callerFile))?.addCleanup(callback);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
27
43
|
* Top-level coordinator for the dev server's file system.
|
|
28
44
|
*
|
|
29
45
|
* Owns: file discovery, the dependency graph, the manifest, the special-files
|
|
@@ -185,5 +201,5 @@ async function runInBatches(items, size, fn) {
|
|
|
185
201
|
const filesOrchestrator = new FilesOrchestrator();
|
|
186
202
|
|
|
187
203
|
//#endregion
|
|
188
|
-
export { filesOrchestrator };
|
|
204
|
+
export { FilesOrchestrator, filesOrchestrator, onCleanup };
|
|
189
205
|
//# sourceMappingURL=files-orchestrator.mjs.map
|