@warlock.js/core 5.25.0 → 5.27.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.
Files changed (163) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/esm/cli/commands/doctor/checks/index.mjs +2 -0
  3. package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
  4. package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs +170 -0
  5. package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs.map +1 -0
  6. package/esm/cli/commands/doctor/doctor-command.action.mjs +7 -2
  7. package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
  8. package/esm/cli/commands/doctor.command.mjs +5 -0
  9. package/esm/cli/commands/doctor.command.mjs.map +1 -1
  10. package/esm/cli/commands/generate/generators/module.generator.mjs +1 -12
  11. package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
  12. package/esm/cli/commands/openapi-generator.action.mjs +43 -0
  13. package/esm/cli/commands/openapi-generator.action.mjs.map +1 -0
  14. package/esm/cli/commands/openapi-generator.command.mjs +44 -0
  15. package/esm/cli/commands/openapi-generator.command.mjs.map +1 -0
  16. package/esm/cli/commands/typings-generator.command.mjs +4 -1
  17. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  18. package/esm/cli/framework-cli-commands.mjs +2 -0
  19. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  20. package/esm/database/migrate-action.mjs +2 -2
  21. package/esm/database/migrate-action.mjs.map +1 -1
  22. package/esm/dev-server/build-development-openapi-document.mjs +36 -0
  23. package/esm/dev-server/build-development-openapi-document.mjs.map +1 -0
  24. package/esm/dev-server/collect-development-api-routes.mjs +27 -0
  25. package/esm/dev-server/collect-development-api-routes.mjs.map +1 -0
  26. package/esm/dev-server/dev-logger.mjs +9 -1
  27. package/esm/dev-server/dev-logger.mjs.map +1 -1
  28. package/esm/dev-server/development-server.mjs +6 -3
  29. package/esm/dev-server/development-server.mjs.map +1 -1
  30. package/esm/dev-server/files-orchestrator.mjs +17 -1
  31. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  32. package/esm/dev-server/legacy-locales-warning.mjs +16 -0
  33. package/esm/dev-server/legacy-locales-warning.mjs.map +1 -0
  34. package/esm/dev-server/model-resource-typings.mjs +246 -0
  35. package/esm/dev-server/model-resource-typings.mjs.map +1 -0
  36. package/esm/dev-server/module-loader.mjs +16 -0
  37. package/esm/dev-server/module-loader.mjs.map +1 -1
  38. package/esm/dev-server/special-file-patterns.mjs +2 -2
  39. package/esm/dev-server/special-file-patterns.mjs.map +1 -1
  40. package/esm/dev-server/translation-keys-sources.mjs +7 -3
  41. package/esm/dev-server/translation-keys-sources.mjs.map +1 -1
  42. package/esm/dev-server/type-generator.mjs +73 -6
  43. package/esm/dev-server/type-generator.mjs.map +1 -1
  44. package/esm/generations/features/access.feature.mjs +38 -18
  45. package/esm/generations/features/access.feature.mjs.map +1 -1
  46. package/esm/generations/features/bull-board.feature.mjs +2 -2
  47. package/esm/generations/features/bull-board.feature.mjs.map +1 -1
  48. package/esm/http/build-rate-limit-options.d.mts +8 -2
  49. package/esm/http/build-rate-limit-options.d.mts.map +1 -1
  50. package/esm/http/build-rate-limit-options.mjs +1 -0
  51. package/esm/http/build-rate-limit-options.mjs.map +1 -1
  52. package/esm/http/errors/errors.d.mts +1 -17
  53. package/esm/http/errors/errors.d.mts.map +1 -1
  54. package/esm/http/errors/errors.mjs +1 -20
  55. package/esm/http/errors/errors.mjs.map +1 -1
  56. package/esm/http/index.d.mts +4 -3
  57. package/esm/http/index.mjs +1 -1
  58. package/esm/http/middleware/inject-request-context.d.mts +1 -1
  59. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  60. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  61. package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
  62. package/esm/http/middleware/rate-limit.middleware.mjs +5 -1
  63. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  64. package/esm/http/request.d.mts +9 -26
  65. package/esm/http/request.d.mts.map +1 -1
  66. package/esm/http/request.mjs +1 -22
  67. package/esm/http/request.mjs.map +1 -1
  68. package/esm/http/response.d.mts.map +1 -1
  69. package/esm/http/response.mjs +4 -2
  70. package/esm/http/response.mjs.map +1 -1
  71. package/esm/http/serialized.d.mts +114 -0
  72. package/esm/http/serialized.d.mts.map +1 -0
  73. package/esm/index.d.mts +12 -5
  74. package/esm/index.mjs +8 -2
  75. package/esm/localization/index.mjs +4 -0
  76. package/esm/localization/parse-locale-dictionary.d.mts +23 -0
  77. package/esm/localization/parse-locale-dictionary.d.mts.map +1 -0
  78. package/esm/localization/parse-locale-dictionary.mjs +215 -0
  79. package/esm/localization/parse-locale-dictionary.mjs.map +1 -0
  80. package/esm/localization/register-locale-dictionary.d.mts +12 -0
  81. package/esm/localization/register-locale-dictionary.d.mts.map +1 -0
  82. package/esm/localization/register-locale-dictionary.mjs +58 -0
  83. package/esm/localization/register-locale-dictionary.mjs.map +1 -0
  84. package/esm/openapi/build-openapi-document.d.mts +17 -0
  85. package/esm/openapi/build-openapi-document.d.mts.map +1 -0
  86. package/esm/openapi/build-openapi-document.mjs +124 -0
  87. package/esm/openapi/build-openapi-document.mjs.map +1 -0
  88. package/esm/openapi/cast-schema.mjs +89 -0
  89. package/esm/openapi/cast-schema.mjs.map +1 -0
  90. package/esm/openapi/collect-openapi-document.mjs +40 -0
  91. package/esm/openapi/collect-openapi-document.mjs.map +1 -0
  92. package/esm/openapi/development-openapi-provider.d.mts +17 -0
  93. package/esm/openapi/development-openapi-provider.d.mts.map +1 -0
  94. package/esm/openapi/development-openapi-provider.mjs +21 -0
  95. package/esm/openapi/development-openapi-provider.mjs.map +1 -0
  96. package/esm/openapi/error-responses.mjs +67 -0
  97. package/esm/openapi/error-responses.mjs.map +1 -0
  98. package/esm/openapi/index.d.mts +3 -0
  99. package/esm/openapi/index.mjs +4 -0
  100. package/esm/openapi/openapi-child-output.mjs +57 -0
  101. package/esm/openapi/openapi-child-output.mjs.map +1 -0
  102. package/esm/openapi/openapi-protocol.mjs +30 -0
  103. package/esm/openapi/openapi-protocol.mjs.map +1 -0
  104. package/esm/openapi/paths.mjs +63 -0
  105. package/esm/openapi/paths.mjs.map +1 -0
  106. package/esm/openapi/request-schemas.mjs +138 -0
  107. package/esm/openapi/request-schemas.mjs.map +1 -0
  108. package/esm/openapi/response-schemas.mjs +85 -0
  109. package/esm/openapi/response-schemas.mjs.map +1 -0
  110. package/esm/openapi/schema-registry.mjs +143 -0
  111. package/esm/openapi/schema-registry.mjs.map +1 -0
  112. package/esm/openapi/security.mjs +98 -0
  113. package/esm/openapi/security.mjs.map +1 -0
  114. package/esm/openapi/types.d.mts +119 -0
  115. package/esm/openapi/types.d.mts.map +1 -0
  116. package/esm/production/production-builder.mjs +11 -2
  117. package/esm/production/production-builder.mjs.map +1 -1
  118. package/esm/production/registration-child-runner.mjs +139 -0
  119. package/esm/production/registration-child-runner.mjs.map +1 -0
  120. package/esm/production/route-registration-child.mjs +30 -2
  121. package/esm/production/route-registration-child.mjs.map +1 -1
  122. package/esm/production/route-registration-snapshot.mjs +22 -119
  123. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  124. package/esm/resource/define-resource.d.mts +44 -14
  125. package/esm/resource/define-resource.d.mts.map +1 -1
  126. package/esm/resource/define-resource.mjs +0 -40
  127. package/esm/resource/define-resource.mjs.map +1 -1
  128. package/esm/resource/index.d.mts +1 -1
  129. package/esm/resource/resource.d.mts +4 -4
  130. package/esm/resource/resource.d.mts.map +1 -1
  131. package/esm/resource/resource.mjs.map +1 -1
  132. package/esm/resource/types.d.mts +89 -4
  133. package/esm/resource/types.d.mts.map +1 -1
  134. package/esm/restful/restful.d.mts +5 -5
  135. package/esm/restful/restful.d.mts.map +1 -1
  136. package/esm/router/index.d.mts +1 -1
  137. package/esm/router/named-api-routes-with-responses.mjs +59 -0
  138. package/esm/router/named-api-routes-with-responses.mjs.map +1 -0
  139. package/esm/router/response-schema-types.mjs +100 -0
  140. package/esm/router/response-schema-types.mjs.map +1 -0
  141. package/esm/router/router.d.mts +2 -2
  142. package/esm/router/router.d.mts.map +1 -1
  143. package/esm/router/router.mjs +38 -11
  144. package/esm/router/router.mjs.map +1 -1
  145. package/esm/router/types.d.mts +24 -1
  146. package/esm/router/types.d.mts.map +1 -1
  147. package/esm/tests/start-http-development-server.d.mts +10 -0
  148. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  149. package/esm/tests/start-http-development-server.mjs +16 -2
  150. package/esm/tests/start-http-development-server.mjs.map +1 -1
  151. package/esm/validation/validateAll.mjs +48 -1
  152. package/esm/validation/validateAll.mjs.map +1 -1
  153. package/llms-full.txt +383 -80
  154. package/llms.txt +4 -3
  155. package/package.json +21 -21
  156. package/skills/README.md +4 -3
  157. package/skills/create-controller/SKILL.md +43 -2
  158. package/skills/define-resource/SKILL.md +100 -1
  159. package/skills/generate-openapi/SKILL.md +120 -0
  160. package/skills/index.md +3 -3
  161. package/skills/test-http/SKILL.md +9 -1
  162. package/skills/use-localization/SKILL.md +39 -26
  163. package/skills/validate-input/SKILL.md +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,59 @@ 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.27.0 - 2026-10-01
10
+
11
+ ### Added
12
+
13
+ - **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`.
14
+ - **`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`.
15
+ - **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.
16
+ - `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.
17
+ - `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.
18
+ - **`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.
19
+ - **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.
20
+ - **`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).
21
+ - **`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.
22
+ - **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).
23
+
24
+ ### Changed
25
+
26
+ - Dependency: @mongez/supportive-is 3.0 (type-predicate checkers).
27
+
28
+ ## 5.26.0 - 2026-09-30
29
+
30
+ ### Upgrading
31
+
32
+ - **Fastify plugins moved up a major:** `@fastify/static` 10, `@fastify/cookie` 11, `@fastify/rate-limit` 11 and `@fastify/multipart` 10, with `fastify` ^5.12.5 and `@fastify/http-proxy` ^11.6.3. This picks up the security fixes that those versions carry. Nothing changes unless you use one of the options below.
33
+ - **BREAKING (only if you pass `setHeaders` to `router.directory()`):** the callback now receives the Fastify reply, not the raw response. Replace `res.setHeader(name, value)` with `reply.header(name, value)`.
34
+ - On Windows and macOS, a static-folder URL with the wrong letter case (`/public/Logo.png` for `logo.png`) now answers 404, as it already did on Linux.
35
+
36
+ ### Added
37
+
38
+ - `http.rateLimit.ipv6Subnet` sets the IPv6 prefix length the global rate limiter groups clients by (default `64`).
39
+ - Module `src/app/<module>/utils/locales.json` dictionaries register translations with the module folder as their default group; root `$group` overrides it. Dictionaries strictly validate their JSON and translation shape, and reject duplicate key ownership. `utils/locales.ts` still works but is deprecated and will be removed in v6 (its warning ships separately).
40
+ - `parseLocaleDictionary()` is public for tooling that needs to validate and flatten a module locale dictionary without loading application code.
41
+ - `validation.params` validates route parameters with a Seal object schema. Number and boolean validators coerce the string segments, the parsed values replace `request.params`, and they merge into the validated data (params win over body keys). Failures answer like a body-schema failure. Routes without `params` still see raw strings.
42
+ - `warlock doctor` warns about each legacy `utils/locales.ts` file, and `warlock doctor --fix` converts the safe ones (a single `groupedTranslations` call with a literal object) to `locales.json`. It checks the JSON flattens to the same keys before deleting the `.ts` file. Anything else is reported as a manual migration with the reason. Comments in the converted file are not kept.
43
+
44
+ ### Changed
45
+
46
+ - Development now warns once when it auto-loads legacy module `utils/locales.ts` files, and `warlock add access` now writes its access translation to `src/app/access/utils/locales.json`.
47
+ - **Behaviour change:** rate limits group IPv6 clients by their `/64` network, and IPv4-mapped addresses (`::ffff:1.2.3.4`) count as the IPv4 address. This applies to the global limiter and to the per-route `rateLimit()` middleware. Before, a client could rotate through addresses in its own `/64` to get a fresh bucket each time. Users behind one IPv6 `/64`, such as one home or office, now share a bucket.
48
+ - `response.clearCookie()` now also sends `Max-Age=0` alongside the past `Expires` date.
49
+
50
+ ### Fixed
51
+
52
+ - `warlock generate.typings` honours `build.routeRegistrationTimeoutMs` / `WARLOCK_ROUTE_REGISTRATION_TIMEOUT_MS`, like `warlock build`. It always used 30 seconds, so `generate.typings && tsc && warlock build` could fail on a loaded machine even with a longer timeout configured.
53
+ - `request.t()`, `request.trans()` and `t()` are typed as returning `string` (they were `any`), so `title: request.t("...")` type-checks without a `String(...)` wrapper.
54
+ - `warlock migrate` runs package migrations (such as @warlock.js/auth's token tables) before your app's migrations, whatever their dates. On a fresh database, an app migration that alters a package table used to run before the table existed. The order inside each group is unchanged.
55
+ - `startHttpTestServer({ web: false })` now starts an API-only test server: it skips the optional web connector before its lazy lifecycle delegate constructs `WebConnector` or creates Vite. Omitting the option preserves the existing full-stack startup.
56
+ - `response.cookie()` truncates a fractional `maxAge` to whole seconds. `@fastify/cookie` 11 rejects a non-whole `maxAge`, so a computed value such as `ttl / 1000` used to fail the response.
57
+
58
+ ### Removed
59
+
60
+ - **BREAKING (only if something still reads `request.user`):** the `request.user` getter and `RequestUserMovedError`, the development-time diagnostic kept since the 5.12.0 move. The user lives at `request.locals.user` (set by `@warlock.js/auth`); TypeScript now reports any remaining `request.user`.
61
+
9
62
  ## 5.25.0 - 2026-09-28
10
63
 
11
64
  ### Added
@@ -3,6 +3,7 @@ import { connectorsCheck } from "./connectors.check.mjs";
3
3
  import { handlerSignatureCheck } from "./handler-signature.check.mjs";
4
4
  import { healthCheck } from "./health.check.mjs";
5
5
  import { jwtSecretCheck } from "./jwt-secret.check.mjs";
6
+ import { localesTsDeprecatedCheck } from "./locales-ts-deprecated.check.mjs";
6
7
  import { optionalPeersCheck } from "./optional-peers.check.mjs";
7
8
  import { releaseHygieneCheck } from "./release-hygiene.check.mjs";
8
9
  import { routesCheck } from "./routes.check.mjs";
@@ -26,6 +27,7 @@ const defaultDoctorChecks = [
26
27
  optionalPeersCheck,
27
28
  jwtSecretCheck,
28
29
  healthCheck,
30
+ localesTsDeprecatedCheck,
29
31
  releaseHygieneCheck
30
32
  ];
31
33
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/doctor/checks/index.ts"],"sourcesContent":["import type { DoctorCheck } from \"../check.types\";\nimport { configCheck } from \"./config.check\";\nimport { connectorsCheck } from \"./connectors.check\";\nimport { handlerSignatureCheck } from \"./handler-signature.check\";\nimport { healthCheck } from \"./health.check\";\nimport { jwtSecretCheck } from \"./jwt-secret.check\";\nimport { optionalPeersCheck } from \"./optional-peers.check\";\nimport { releaseHygieneCheck } from \"./release-hygiene.check\";\nimport { routesCheck } from \"./routes.check\";\n\n/**\n * The default, ordered set of checks `warlock doctor` runs. Ordering controls\n * the report layout: runtime-surface checks first (routes, handler signatures,\n * config, connectors, peers, health), release hygiene last.\n *\n * Every check is handed the same boot context, and any of them may return\n * `undefined` to opt out of a project it does not apply to — so this list is\n * the set of checks that COULD run, not the set of lines a given project will\n * print.\n */\nexport const defaultDoctorChecks: DoctorCheck[] = [\n routesCheck,\n handlerSignatureCheck,\n configCheck,\n connectorsCheck,\n optionalPeersCheck,\n jwtSecretCheck,\n healthCheck,\n releaseHygieneCheck,\n];\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,sBAAqC;CAChD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/doctor/checks/index.ts"],"sourcesContent":["import type { DoctorCheck } from \"../check.types\";\nimport { configCheck } from \"./config.check\";\nimport { connectorsCheck } from \"./connectors.check\";\nimport { handlerSignatureCheck } from \"./handler-signature.check\";\nimport { healthCheck } from \"./health.check\";\nimport { jwtSecretCheck } from \"./jwt-secret.check\";\nimport { localesTsDeprecatedCheck } from \"./locales-ts-deprecated.check\";\nimport { optionalPeersCheck } from \"./optional-peers.check\";\nimport { releaseHygieneCheck } from \"./release-hygiene.check\";\nimport { routesCheck } from \"./routes.check\";\n\n/**\n * The default, ordered set of checks `warlock doctor` runs. Ordering controls\n * the report layout: runtime-surface checks first (routes, handler signatures,\n * config, connectors, peers, health), release hygiene last.\n *\n * Every check is handed the same boot context, and any of them may return\n * `undefined` to opt out of a project it does not apply to — so this list is\n * the set of checks that COULD run, not the set of lines a given project will\n * print.\n */\nexport const defaultDoctorChecks: DoctorCheck[] = [\n routesCheck,\n handlerSignatureCheck,\n configCheck,\n connectorsCheck,\n optionalPeersCheck,\n jwtSecretCheck,\n healthCheck,\n localesTsDeprecatedCheck,\n releaseHygieneCheck,\n];\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,sBAAqC;CAChD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
@@ -0,0 +1,170 @@
1
+ import { config } from "../../../../config/config-getter.mjs";
2
+ import { appPath, rootPath } from "../../../../utils/paths.mjs";
3
+ import { parseLocaleDictionary } from "../../../../localization/parse-locale-dictionary.mjs";
4
+ import path from "node:path";
5
+ import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import ts from "typescript";
7
+
8
+ //#region ../core/src/cli/commands/doctor/checks/locales-ts-deprecated.check.ts
9
+ function localeSources() {
10
+ const appDirectory = appPath();
11
+ if (!existsSync(appDirectory)) return [];
12
+ return readdirSync(appDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => ({
13
+ file: path.join(appDirectory, entry.name, "utils", "locales.ts"),
14
+ module: entry.name
15
+ })).filter(({ file }) => existsSync(file));
16
+ }
17
+ function propertyName(property) {
18
+ if (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) return property.name.text;
19
+ }
20
+ function literalObject(node) {
21
+ if (!ts.isObjectLiteralExpression(node)) return void 0;
22
+ const value = Object.create(null);
23
+ for (const member of node.properties) {
24
+ if (!ts.isPropertyAssignment(member)) return void 0;
25
+ const key = propertyName(member);
26
+ if (key === void 0) return void 0;
27
+ if (ts.isStringLiteral(member.initializer) || ts.isNumericLiteral(member.initializer)) {
28
+ value[key] = ts.isNumericLiteral(member.initializer) ? Number(member.initializer.text) : member.initializer.text;
29
+ continue;
30
+ }
31
+ const nested = literalObject(member.initializer);
32
+ if (nested === void 0) return void 0;
33
+ value[key] = nested;
34
+ }
35
+ return value;
36
+ }
37
+ function flattenLiteral(value, group, entries) {
38
+ const values = Object.values(value);
39
+ if (values.every((item) => typeof item === "string" || typeof item === "number")) {
40
+ if (group === "") return false;
41
+ if (!values.every((item) => typeof item === "string")) return false;
42
+ entries[group] = value;
43
+ return true;
44
+ }
45
+ if (!values.every((item) => item !== null && typeof item === "object" && !Array.isArray(item))) return false;
46
+ return Object.entries(value).every(([key, nested]) => flattenLiteral(nested, group === "" ? key : `${group}.${key}`, entries));
47
+ }
48
+ function inspect(source) {
49
+ if (existsSync(path.join(path.dirname(source.file), "locales.json"))) return {
50
+ ...source,
51
+ convertible: false,
52
+ reason: "locales.json already exists"
53
+ };
54
+ const text = readFileSync(source.file, "utf8");
55
+ const program = ts.createSourceFile(source.file, text, ts.ScriptTarget.Latest, true);
56
+ const syntaxDiagnostics = ts.transpileModule(text, {
57
+ fileName: source.file,
58
+ reportDiagnostics: true
59
+ }).diagnostics;
60
+ if (syntaxDiagnostics !== void 0 && syntaxDiagnostics.length > 0) return {
61
+ ...source,
62
+ convertible: false,
63
+ reason: "TypeScript syntax is invalid"
64
+ };
65
+ const [importStatement, callStatement] = program.statements;
66
+ if (!importStatement || !ts.isImportDeclaration(importStatement) || !ts.isStringLiteral(importStatement.moduleSpecifier) || importStatement.moduleSpecifier.text !== "@warlock.js/core" || !importStatement.importClause || importStatement.importClause.isTypeOnly || importStatement.importClause.name || !importStatement.importClause.namedBindings || !ts.isNamedImports(importStatement.importClause.namedBindings) || importStatement.importClause.namedBindings.elements.length !== 1 || importStatement.importClause.namedBindings.elements[0]?.name.text !== "groupedTranslations" || importStatement.importClause.namedBindings.elements[0]?.propertyName) return {
67
+ ...source,
68
+ convertible: false,
69
+ reason: "must import only groupedTranslations from @warlock.js/core"
70
+ };
71
+ if (program.statements.length !== 2) return {
72
+ ...source,
73
+ convertible: false,
74
+ reason: "must contain only the groupedTranslations import and one call"
75
+ };
76
+ if (!callStatement || !ts.isExpressionStatement(callStatement) || !ts.isCallExpression(callStatement.expression)) return {
77
+ ...source,
78
+ convertible: false,
79
+ reason: "must contain one top-level groupedTranslations call"
80
+ };
81
+ const call = callStatement.expression;
82
+ if (!ts.isIdentifier(call.expression) || call.expression.text !== "groupedTranslations" || call.arguments.length !== 2 || !ts.isStringLiteral(call.arguments[0])) return {
83
+ ...source,
84
+ convertible: false,
85
+ reason: "must call groupedTranslations with a string group and object"
86
+ };
87
+ const dictionary = literalObject(call.arguments[1]);
88
+ if (!dictionary) return {
89
+ ...source,
90
+ convertible: false,
91
+ reason: "translation object must contain only literal keys, objects, strings, or numbers"
92
+ };
93
+ const group = call.arguments[0].text;
94
+ const entries = Object.create(null);
95
+ if (!flattenLiteral(dictionary, group, entries)) return {
96
+ ...source,
97
+ convertible: false,
98
+ reason: "translation object has no valid literal locale entries"
99
+ };
100
+ return {
101
+ ...source,
102
+ convertible: true,
103
+ reason: "pure groupedTranslations literal",
104
+ group,
105
+ dictionary,
106
+ entries
107
+ };
108
+ }
109
+ function inspectLocaleSources() {
110
+ return localeSources().map(inspect);
111
+ }
112
+ function localeCodes() {
113
+ const configured = config.key("app.localeCodes");
114
+ return Array.isArray(configured) && configured.every((code) => typeof code === "string") ? configured : void 0;
115
+ }
116
+ function sameEntries(left, right) {
117
+ return JSON.stringify(left) === JSON.stringify(right);
118
+ }
119
+ /** Convert safe legacy locale sources, retaining a TS file whenever verification cannot prove equivalence. */
120
+ function fixLocaleSources() {
121
+ return inspectLocaleSources().map((result) => {
122
+ if (!result.convertible || !result.group || !result.dictionary || !result.entries) return result;
123
+ const jsonFile = path.join(path.dirname(result.file), "locales.json");
124
+ const document = result.group === result.module ? result.dictionary : {
125
+ $group: result.group,
126
+ ...result.dictionary
127
+ };
128
+ const json = `${JSON.stringify(document, null, 2)}\n`;
129
+ writeFileSync(jsonFile, json);
130
+ try {
131
+ const parsed = parseLocaleDictionary({
132
+ sourceFile: rootPath(path.relative(process.cwd(), jsonFile)),
133
+ source: json,
134
+ defaultNamespace: result.module,
135
+ localeCodes: localeCodes()
136
+ });
137
+ if (!sameEntries(result.entries, parsed.entries)) throw new Error("flattened entries differ");
138
+ } catch (error) {
139
+ unlinkSync(jsonFile);
140
+ return {
141
+ ...result,
142
+ convertible: false,
143
+ reason: `verification failed: ${error instanceof Error ? error.message : String(error)}`
144
+ };
145
+ }
146
+ unlinkSync(result.file);
147
+ return {
148
+ ...result,
149
+ reason: "converted (comments in the TypeScript source are not retained)"
150
+ };
151
+ });
152
+ }
153
+ const localesTsDeprecatedCheck = {
154
+ name: "locales-ts-deprecated",
155
+ run: () => {
156
+ const sources = inspectLocaleSources();
157
+ if (sources.length === 0) return void 0;
158
+ return {
159
+ name: "locales-ts-deprecated",
160
+ status: "warn",
161
+ detail: `${sources.map((source) => {
162
+ return `${path.relative(process.cwd(), source.file).replaceAll("\\", "/")}: ${source.convertible ? "auto-convertible" : "manual"} (${source.reason})`;
163
+ }).join("; ")}. Auto-loaded locales.ts goes away in v6; run \`warlock doctor --fix\` to convert auto-convertible files.`
164
+ };
165
+ }
166
+ };
167
+
168
+ //#endregion
169
+ export { fixLocaleSources, localesTsDeprecatedCheck };
170
+ //# sourceMappingURL=locales-ts-deprecated.check.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locales-ts-deprecated.check.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/doctor/checks/locales-ts-deprecated.check.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport ts from \"typescript\";\nimport { config } from \"../../../../config/config-getter\";\nimport {\n parseLocaleDictionary,\n type LocaleDictionaryEntries,\n} from \"../../../../localization/parse-locale-dictionary\";\nimport { appPath, rootPath } from \"../../../../utils/paths\";\nimport type { DoctorCheck } from \"../check.types\";\n\ntype LocaleSource = {\n file: string;\n module: string;\n};\n\nexport type LocaleInspection = LocaleSource & {\n convertible: boolean;\n reason: string;\n group?: string;\n dictionary?: Record<string, unknown>;\n entries?: LocaleDictionaryEntries;\n};\n\nfunction localeSources(): LocaleSource[] {\n const appDirectory = appPath();\n if (!existsSync(appDirectory)) return [];\n\n return readdirSync(appDirectory, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => ({\n file: path.join(appDirectory, entry.name, \"utils\", \"locales.ts\"),\n module: entry.name,\n }))\n .filter(({ file }) => existsSync(file));\n}\n\nfunction propertyName(property: ts.PropertyAssignment): string | undefined {\n if (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))\n return property.name.text;\n return undefined;\n}\n\nfunction literalObject(node: ts.Expression): Record<string, unknown> | undefined {\n if (!ts.isObjectLiteralExpression(node)) return undefined;\n const value: Record<string, unknown> = Object.create(null);\n for (const member of node.properties) {\n if (!ts.isPropertyAssignment(member)) return undefined;\n const key = propertyName(member);\n if (key === undefined) return undefined;\n if (ts.isStringLiteral(member.initializer) || ts.isNumericLiteral(member.initializer)) {\n value[key] = ts.isNumericLiteral(member.initializer)\n ? Number(member.initializer.text)\n : member.initializer.text;\n continue;\n }\n const nested = literalObject(member.initializer);\n if (nested === undefined) return undefined;\n value[key] = nested;\n }\n return value;\n}\n\nfunction flattenLiteral(\n value: Record<string, unknown>,\n group: string,\n entries: LocaleDictionaryEntries,\n): boolean {\n const values = Object.values(value);\n if (values.every((item) => typeof item === \"string\" || typeof item === \"number\")) {\n if (group === \"\") return false;\n if (!values.every((item) => typeof item === \"string\")) return false;\n entries[group] = value as Record<string, string>;\n return true;\n }\n if (!values.every((item) => item !== null && typeof item === \"object\" && !Array.isArray(item)))\n return false;\n return Object.entries(value).every(([key, nested]) =>\n flattenLiteral(\n nested as Record<string, unknown>,\n group === \"\" ? key : `${group}.${key}`,\n entries,\n ),\n );\n}\n\nfunction inspect(source: LocaleSource): LocaleInspection {\n const jsonFile = path.join(path.dirname(source.file), \"locales.json\");\n if (existsSync(jsonFile))\n return { ...source, convertible: false, reason: \"locales.json already exists\" };\n\n const text = readFileSync(source.file, \"utf8\");\n const program = ts.createSourceFile(source.file, text, ts.ScriptTarget.Latest, true);\n const syntaxDiagnostics = ts.transpileModule(text, {\n fileName: source.file,\n reportDiagnostics: true,\n }).diagnostics;\n if (syntaxDiagnostics !== undefined && syntaxDiagnostics.length > 0)\n return { ...source, convertible: false, reason: \"TypeScript syntax is invalid\" };\n const [importStatement, callStatement] = program.statements;\n if (\n !importStatement ||\n !ts.isImportDeclaration(importStatement) ||\n !ts.isStringLiteral(importStatement.moduleSpecifier) ||\n importStatement.moduleSpecifier.text !== \"@warlock.js/core\" ||\n !importStatement.importClause ||\n importStatement.importClause.isTypeOnly ||\n importStatement.importClause.name ||\n !importStatement.importClause.namedBindings ||\n !ts.isNamedImports(importStatement.importClause.namedBindings) ||\n importStatement.importClause.namedBindings.elements.length !== 1 ||\n importStatement.importClause.namedBindings.elements[0]?.name.text !== \"groupedTranslations\" ||\n importStatement.importClause.namedBindings.elements[0]?.propertyName\n )\n return {\n ...source,\n convertible: false,\n reason: \"must import only groupedTranslations from @warlock.js/core\",\n };\n\n if (program.statements.length !== 2)\n return {\n ...source,\n convertible: false,\n reason: \"must contain only the groupedTranslations import and one call\",\n };\n\n if (\n !callStatement ||\n !ts.isExpressionStatement(callStatement) ||\n !ts.isCallExpression(callStatement.expression)\n )\n return {\n ...source,\n convertible: false,\n reason: \"must contain one top-level groupedTranslations call\",\n };\n const call = callStatement.expression;\n if (\n !ts.isIdentifier(call.expression) ||\n call.expression.text !== \"groupedTranslations\" ||\n call.arguments.length !== 2 ||\n !ts.isStringLiteral(call.arguments[0]!)\n )\n return {\n ...source,\n convertible: false,\n reason: \"must call groupedTranslations with a string group and object\",\n };\n const dictionary = literalObject(call.arguments[1]!);\n if (!dictionary)\n return {\n ...source,\n convertible: false,\n reason: \"translation object must contain only literal keys, objects, strings, or numbers\",\n };\n const group = call.arguments[0].text;\n const entries: LocaleDictionaryEntries = Object.create(null);\n if (!flattenLiteral(dictionary, group, entries))\n return {\n ...source,\n convertible: false,\n reason: \"translation object has no valid literal locale entries\",\n };\n return {\n ...source,\n convertible: true,\n reason: \"pure groupedTranslations literal\",\n group,\n dictionary,\n entries,\n };\n}\n\nexport function inspectLocaleSources(): LocaleInspection[] {\n return localeSources().map(inspect);\n}\n\nfunction localeCodes(): readonly string[] | undefined {\n const configured = config.key<unknown>(\"app.localeCodes\");\n return Array.isArray(configured) && configured.every((code) => typeof code === \"string\")\n ? configured\n : undefined;\n}\n\nfunction sameEntries(left: LocaleDictionaryEntries, right: LocaleDictionaryEntries): boolean {\n return JSON.stringify(left) === JSON.stringify(right);\n}\n\n/** Convert safe legacy locale sources, retaining a TS file whenever verification cannot prove equivalence. */\nexport function fixLocaleSources(): LocaleInspection[] {\n return inspectLocaleSources().map((result) => {\n if (!result.convertible || !result.group || !result.dictionary || !result.entries)\n return result;\n const jsonFile = path.join(path.dirname(result.file), \"locales.json\");\n const document: Record<string, unknown> =\n result.group === result.module\n ? result.dictionary\n : { $group: result.group, ...result.dictionary };\n const json = `${JSON.stringify(document, null, 2)}\\n`;\n writeFileSync(jsonFile, json);\n try {\n const parsed = parseLocaleDictionary({\n sourceFile: rootPath(path.relative(process.cwd(), jsonFile)),\n source: json,\n defaultNamespace: result.module,\n localeCodes: localeCodes(),\n });\n if (!sameEntries(result.entries, parsed.entries)) throw new Error(\"flattened entries differ\");\n } catch (error) {\n unlinkSync(jsonFile);\n return {\n ...result,\n convertible: false,\n reason: `verification failed: ${error instanceof Error ? error.message : String(error)}`,\n };\n }\n unlinkSync(result.file);\n return {\n ...result,\n reason: \"converted (comments in the TypeScript source are not retained)\",\n };\n });\n}\n\nexport const localesTsDeprecatedCheck: DoctorCheck = {\n name: \"locales-ts-deprecated\",\n run: () => {\n const sources = inspectLocaleSources();\n if (sources.length === 0) return undefined;\n const details = sources.map((source) => {\n const label = path.relative(process.cwd(), source.file).replaceAll(\"\\\\\", \"/\");\n return `${label}: ${source.convertible ? \"auto-convertible\" : \"manual\"} (${source.reason})`;\n });\n return {\n name: \"locales-ts-deprecated\",\n status: \"warn\",\n detail:\n `${details.join(\"; \")}. Auto-loaded locales.ts goes away in v6; ` +\n \"run `warlock doctor --fix` to convert auto-convertible files.\",\n };\n },\n};\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { bootForDiagnostics } from "./boot-for-diagnostics.mjs";
2
+ import { fixLocaleSources } from "./checks/locales-ts-deprecated.check.mjs";
2
3
  import { defaultDoctorChecks } from "./checks/index.mjs";
3
4
  import { printReport } from "./format-report.mjs";
4
5
  import { runChecks } from "./run-checks.mjs";
@@ -25,9 +26,13 @@ import { runChecks } from "./run-checks.mjs";
25
26
  * banner and exits zero — which also disposes of the handles the boot pass
26
27
  * left open (a Fastify instance, a Vite dev server for `web` apps).
27
28
  *
28
- * @param _data Parsed CLI args (unused — doctor takes no positional args).
29
+ * @param data Parsed CLI args, including the optional safe migration flag.
29
30
  */
30
- async function doctorCommandAction(_data) {
31
+ async function doctorCommandAction(data) {
32
+ if (data.options.fix === true) for (const result of fixLocaleSources()) {
33
+ const file = result.file.replace(process.cwd(), ".").replaceAll("\\", "/");
34
+ console.log(`${result.convertible ? "converted" : "manual"} ${file}: ${result.reason}`);
35
+ }
31
36
  const report = await runChecks(defaultDoctorChecks, await bootForDiagnostics());
32
37
  printReport(report);
33
38
  if (report.hasFailures) process.exit(report.exitCode);
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-command.action.mjs","names":[],"sources":["../../../../../../../../../core/src/cli/commands/doctor/doctor-command.action.ts"],"sourcesContent":["import type { CommandActionData } from \"../../../commands/types\";\nimport { bootForDiagnostics } from \"./boot-for-diagnostics\";\nimport { defaultDoctorChecks } from \"./checks\";\nimport { printReport } from \"./format-report\";\nimport { runChecks } from \"./run-checks\";\n\n/**\n * Action behind `warlock doctor`.\n *\n * Two steps, in this order and for a reason: ONE boot pass, then every check\n * against what that pass observed. The checks never boot anything themselves,\n * so the report cannot contain two lines that inspected the app at different\n * moments and contradicted each other — which is exactly what it used to\n * contain (`routes: 0 registered` beside `health: /health + /ready\n * registered`, in the same run, on an app serving fourteen routes).\n *\n * The pass boots every connector and starts none, so no database, cache or\n * socket connection is opened and no port is bound. See\n * `boot-for-diagnostics.ts`.\n *\n * A failing check is REPORTED (a line in the report + a non-zero exit), never\n * thrown: the runner converts any thrown check into a `fail` result first.\n *\n * On success this simply returns; the CLI manager then prints its own success\n * banner and exits zero — which also disposes of the handles the boot pass\n * left open (a Fastify instance, a Vite dev server for `web` apps).\n *\n * @param _data Parsed CLI args (unused — doctor takes no positional args).\n */\nexport async function doctorCommandAction(_data: CommandActionData): Promise<void> {\n const context = await bootForDiagnostics();\n\n const report = await runChecks(defaultDoctorChecks, context);\n\n printReport(report);\n\n if (report.hasFailures) {\n process.exit(report.exitCode);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,eAAsB,oBAAoB,OAAyC;CAGjF,MAAM,SAAS,MAAM,UAAU,qBAAqB,MAF9B,mBAAmB,CAEkB;CAE3D,YAAY,MAAM;CAElB,IAAI,OAAO,aACT,QAAQ,KAAK,OAAO,QAAQ;AAEhC"}
1
+ {"version":3,"file":"doctor-command.action.mjs","names":[],"sources":["../../../../../../../../../core/src/cli/commands/doctor/doctor-command.action.ts"],"sourcesContent":["import type { CommandActionData } from \"../../../commands/types\";\nimport { bootForDiagnostics } from \"./boot-for-diagnostics\";\nimport { defaultDoctorChecks } from \"./checks\";\nimport { fixLocaleSources } from \"./checks/locales-ts-deprecated.check\";\nimport { printReport } from \"./format-report\";\nimport { runChecks } from \"./run-checks\";\n\n/**\n * Action behind `warlock doctor`.\n *\n * Two steps, in this order and for a reason: ONE boot pass, then every check\n * against what that pass observed. The checks never boot anything themselves,\n * so the report cannot contain two lines that inspected the app at different\n * moments and contradicted each other — which is exactly what it used to\n * contain (`routes: 0 registered` beside `health: /health + /ready\n * registered`, in the same run, on an app serving fourteen routes).\n *\n * The pass boots every connector and starts none, so no database, cache or\n * socket connection is opened and no port is bound. See\n * `boot-for-diagnostics.ts`.\n *\n * A failing check is REPORTED (a line in the report + a non-zero exit), never\n * thrown: the runner converts any thrown check into a `fail` result first.\n *\n * On success this simply returns; the CLI manager then prints its own success\n * banner and exits zero — which also disposes of the handles the boot pass\n * left open (a Fastify instance, a Vite dev server for `web` apps).\n *\n * @param data Parsed CLI args, including the optional safe migration flag.\n */\nexport async function doctorCommandAction(data: CommandActionData): Promise<void> {\n if (data.options.fix === true) {\n for (const result of fixLocaleSources()) {\n const file = result.file.replace(process.cwd(), \".\").replaceAll(\"\\\\\", \"/\");\n console.log(`${result.convertible ? \"converted\" : \"manual\"} ${file}: ${result.reason}`);\n }\n }\n const context = await bootForDiagnostics();\n\n const report = await runChecks(defaultDoctorChecks, context);\n\n printReport(report);\n\n if (report.hasFailures) {\n process.exit(report.exitCode);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,eAAsB,oBAAoB,MAAwC;CAChF,IAAI,KAAK,QAAQ,QAAQ,MACvB,KAAK,MAAM,UAAU,iBAAiB,GAAG;EACvC,MAAM,OAAO,OAAO,KAAK,QAAQ,QAAQ,IAAI,GAAG,GAAG,CAAC,CAAC,WAAW,MAAM,GAAG;EACzE,QAAQ,IAAI,GAAG,OAAO,cAAc,cAAc,SAAS,GAAG,KAAK,IAAI,OAAO,QAAQ;CACxF;CAIF,MAAM,SAAS,MAAM,UAAU,qBAAqB,MAF9B,mBAAmB,CAEkB;CAE3D,YAAY,MAAM;CAElB,IAAI,OAAO,aACT,QAAQ,KAAK,OAAO,QAAQ;AAEhC"}
@@ -24,6 +24,11 @@ const doctorCommand = command({
24
24
  name: "doctor",
25
25
  description: "Boot the app read-only (no connections, no port) and report on its routes, config, connectors, drivers and health endpoints",
26
26
  action: doctorCommandAction,
27
+ options: [{
28
+ text: "--fix",
29
+ description: "Convert safely auto-convertible legacy utils/locales.ts files to locales.json",
30
+ type: "boolean"
31
+ }],
27
32
  preload: {
28
33
  runtimeStrategy: "development",
29
34
  config: true,
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/doctor.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\nimport { doctorCommandAction } from \"./doctor/doctor-command.action\";\n\n/**\n * `warlock doctor` — run a set of diagnostic checks against the real app and\n * print a grouped pass/warn/fail report. Exits non-zero if any check fails.\n *\n * Preload plan: config + env + framework bootstrap, and NO connectors. The\n * `connectors` preloader would START them (open the database, dial the cache,\n * bind the port); doctor's own boot pass BOOTS them instead — the phase where\n * routes and endpoints register — which is what gives the checks a real app to\n * read without opening a single connection. See\n * `doctor/boot-for-diagnostics.ts` for that seam.\n *\n * `runtimeStrategy: \"development\"` is declared, not defaulted: connectors branch\n * on it (`web` serves pages from a manifest in production and from Vite in\n * development, `http` picks its router scan the same way), and a diagnostic\n * command that left the strategy unset would take the production branch by\n * accident — refusing to boot a perfectly healthy app because no build manifest\n * exists. Doctor reads a working tree, so it says so.\n */\nexport const doctorCommand = command({\n name: \"doctor\",\n description:\n \"Boot the app read-only (no connections, no port) and report on its routes, config, connectors, drivers and health endpoints\",\n action: doctorCommandAction,\n preload: {\n runtimeStrategy: \"development\",\n config: true,\n env: true,\n bootstrap: true,\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,gBAAgB,QAAQ;CACnC,MAAM;CACN,aACE;CACF,QAAQ;CACR,SAAS;EACP,iBAAiB;EACjB,QAAQ;EACR,KAAK;EACL,WAAW;CACb;AACF,CAAC"}
1
+ {"version":3,"file":"doctor.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/doctor.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\nimport { doctorCommandAction } from \"./doctor/doctor-command.action\";\n\n/**\n * `warlock doctor` — run a set of diagnostic checks against the real app and\n * print a grouped pass/warn/fail report. Exits non-zero if any check fails.\n *\n * Preload plan: config + env + framework bootstrap, and NO connectors. The\n * `connectors` preloader would START them (open the database, dial the cache,\n * bind the port); doctor's own boot pass BOOTS them instead — the phase where\n * routes and endpoints register — which is what gives the checks a real app to\n * read without opening a single connection. See\n * `doctor/boot-for-diagnostics.ts` for that seam.\n *\n * `runtimeStrategy: \"development\"` is declared, not defaulted: connectors branch\n * on it (`web` serves pages from a manifest in production and from Vite in\n * development, `http` picks its router scan the same way), and a diagnostic\n * command that left the strategy unset would take the production branch by\n * accident — refusing to boot a perfectly healthy app because no build manifest\n * exists. Doctor reads a working tree, so it says so.\n */\nexport const doctorCommand = command({\n name: \"doctor\",\n description:\n \"Boot the app read-only (no connections, no port) and report on its routes, config, connectors, drivers and health endpoints\",\n action: doctorCommandAction,\n options: [\n {\n text: \"--fix\",\n description: \"Convert safely auto-convertible legacy utils/locales.ts files to locales.json\",\n type: \"boolean\",\n },\n ],\n preload: {\n runtimeStrategy: \"development\",\n config: true,\n env: true,\n bootstrap: true,\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,gBAAgB,QAAQ;CACnC,MAAM;CACN,aACE;CACF,QAAQ;CACR,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;CACA,SAAS;EACP,iBAAiB;EACjB,QAAQ;EACR,KAAK;EACL,WAAW;CACb;AACF,CAAC"}
@@ -52,18 +52,7 @@ import { guarded } from "app/shared/utils/router";
52
52
  // router.get("/${name.kebab}", listController);
53
53
  `;
54
54
  await putFileAsync(path.join(modulePath, "routes.ts"), routesContent);
55
- const localesContent = `import { groupedTranslations } from "@warlock.js/core";
56
-
57
- groupedTranslations("${name.camel}", {
58
- // Add your translations here
59
- // Example:
60
- // welcome: {
61
- // en: "Welcome",
62
- // ar: "مرحبا",
63
- // },
64
- });
65
- `;
66
- await putFileAsync(path.join(modulePath, "utils", "locales.ts"), localesContent);
55
+ await putFileAsync(path.join(modulePath, "utils", "locales.json"), "{}\n");
67
56
  if (withCrud) {
68
57
  const entity = singularName(moduleName);
69
58
  await putFileAsync(path.join(modulePath, "controllers", `create-${entity.kebab}.controller.ts`), crudCreateControllerStub(entity));
@@ -1 +1 @@
1
- {"version":3,"file":"module.generator.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/generate/generators/module.generator.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport path from \"node:path\";\nimport { appPath } from \"../../../../utils\";\nimport type { CommandActionData } from \"../../../../commands/types\";\nimport {\n crudCreateControllerStub,\n crudCreateSchemaStub,\n crudCreateServiceStub,\n crudDeleteControllerStub,\n crudDeleteServiceStub,\n crudGetServiceStub,\n crudListControllerStub,\n crudListServiceStub,\n crudModelStub,\n crudRepositoryStub,\n crudResourceStub,\n crudRoutesStub,\n crudSeedStub,\n crudShowControllerStub,\n crudUpdateControllerStub,\n crudUpdateSchemaStub,\n crudUpdateServiceStub,\n} from \"../templates/stubs\";\nimport { pluralName, singularName } from \"../utils/name-parser\";\nimport { moduleExists } from \"../utils/path-resolver\";\nimport {\n ensureDirectoryAsync,\n putFileAsync,\n reportSkippedFiles,\n setDryRun,\n setSkipExisting,\n} from \"../utils/writer\";\nimport { createMigrationFile } from \"./migration.generator\";\n\nexport async function generateModule(data: CommandActionData): Promise<void> {\n const moduleName = data.args[0];\n\n if (!moduleName) {\n console.log(colors.red(\"Error: Module name is required\"));\n console.log(colors.yellow(\"Usage: warlock generate.module <name> [options]\"));\n console.log(colors.yellow(\"Example: warlock generate.module products\"));\n console.log(colors.yellow(\" warlock generate.module products --minimal\"));\n process.exit(1);\n }\n\n const name = pluralName(moduleName);\n const force = data.options.force || data.options.f;\n setDryRun(Boolean(data.options.dryRun));\n // --force only creates what is missing; replacing edited files needs --overwrite too\n setSkipExisting(!data.options.overwrite);\n\n // Full CRUD is the default; --minimal (-m) opts down to a bare skeleton.\n const minimal = data.options.minimal || data.options.m;\n const withCrud = !minimal;\n\n // Check if module already exists\n if ((await moduleExists(name.kebab)) && !force) {\n console.log(colors.red(`Error: Module \"${name.kebab}\" already exists`));\n console.log(colors.yellow(\"Use --force to overwrite\"));\n process.exit(1);\n }\n\n const modulePath = appPath(name.kebab);\n\n // Create module directory structure\n const directories = [\n \"controllers\",\n \"services\",\n \"models\",\n \"repositories\",\n \"schema\",\n \"resources\",\n \"events\",\n \"types\",\n \"utils\",\n ];\n\n for (const dir of directories) {\n await ensureDirectoryAsync(path.join(modulePath, dir));\n }\n\n // Create main.ts\n const mainContent = `// You may use this file as a custom entry point to be executed when the app starts.\n// it runs only once, and you don't need to import the routes.ts file, it will be imported automatically.\n// Use it to regsiter or boot up any custom logic for this module.\n`;\n await putFileAsync(path.join(modulePath, \"main.ts\"), mainContent);\n\n // Create routes.ts\n const routesContent = withCrud\n ? crudRoutesStub(name)\n : `import { router } from \"@warlock.js/core\";\nimport { guarded } from \"app/shared/utils/router\";\n\n// Define your routes here\n// Example:\n// router.get(\"/${name.kebab}\", listController);\n`;\n await putFileAsync(path.join(modulePath, \"routes.ts\"), routesContent);\n\n // Create utils/locales.ts\n const localesContent = `import { groupedTranslations } from \"@warlock.js/core\";\n\ngroupedTranslations(\"${name.camel}\", {\n // Add your translations here\n // Example:\n // welcome: {\n // en: \"Welcome\",\n // ar: \"مرحبا\",\n // },\n});\n`;\n await putFileAsync(path.join(modulePath, \"utils\", \"locales.ts\"), localesContent);\n\n // Full CRUD scaffold (the default) unless --minimal was passed\n if (withCrud) {\n const entity = singularName(moduleName);\n\n // Create controllers\n await putFileAsync(\n path.join(modulePath, \"controllers\", `create-${entity.kebab}.controller.ts`),\n crudCreateControllerStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"controllers\", `update-${entity.kebab}.controller.ts`),\n crudUpdateControllerStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"controllers\", `list-${name.kebab}.controller.ts`),\n crudListControllerStub(name),\n );\n\n await putFileAsync(\n path.join(modulePath, \"controllers\", `get-${entity.kebab}.controller.ts`),\n crudShowControllerStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"controllers\", `delete-${entity.kebab}.controller.ts`),\n crudDeleteControllerStub(entity),\n );\n\n // Create schema files — each exports both the schema value and its\n // inferred type; controllers import them directly (no requests/ alias).\n await putFileAsync(\n path.join(modulePath, \"schema\", `create-${entity.kebab}.schema.ts`),\n crudCreateSchemaStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"schema\", `update-${entity.kebab}.schema.ts`),\n crudUpdateSchemaStub(entity),\n );\n\n // Create model\n await ensureDirectoryAsync(path.join(modulePath, \"models\", entity.kebab));\n await putFileAsync(\n path.join(modulePath, \"models\", entity.kebab, `${entity.kebab}.model.ts`),\n crudModelStub(entity),\n );\n\n // Create model index\n await putFileAsync(\n path.join(modulePath, \"models\", entity.kebab, \"index.ts\"),\n `export * from \"./${entity.kebab}.model\";\\n`,\n );\n\n // Create migrations folder\n await ensureDirectoryAsync(path.join(modulePath, \"models\", entity.kebab, \"migrations\"));\n\n // Create resource\n await putFileAsync(\n path.join(modulePath, \"resources\", `${entity.kebab}.resource.ts`),\n crudResourceStub(name),\n );\n\n // Create repository\n await putFileAsync(\n path.join(modulePath, \"repositories\", `${name.kebab}.repository.ts`),\n crudRepositoryStub(name),\n );\n\n // Create services\n await putFileAsync(\n path.join(modulePath, \"services\", `create-${entity.kebab}.service.ts`),\n crudCreateServiceStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"services\", `update-${entity.kebab}.service.ts`),\n crudUpdateServiceStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"services\", `list-${name.kebab}.service.ts`),\n crudListServiceStub(name),\n );\n\n await putFileAsync(\n path.join(modulePath, \"services\", `get-${entity.kebab}.service.ts`),\n crudGetServiceStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"services\", `delete-${entity.kebab}.service.ts`),\n crudDeleteServiceStub(entity),\n );\n\n // Create seeds\n await ensureDirectoryAsync(path.join(modulePath, \"seeds\"));\n await putFileAsync(path.join(modulePath, \"seeds\", `${name.kebab}.seed.ts`), crudSeedStub(name));\n\n // Create migration\n await createMigrationFile(name.kebab, entity.kebab);\n }\n\n reportSkippedFiles();\n\n console.log(colors.cyan(`\\n✨ Module \"${name.kebab}\" generated successfully!`));\n\n if (withCrud) {\n console.log(colors.gray(`\\n📦 CRUD scaffold created with:`));\n console.log(colors.gray(` - List, Get, Create, Update & Delete controllers`));\n console.log(colors.gray(` - Repository`));\n console.log(colors.gray(` - Validation schemas in schema/`));\n console.log(colors.gray(` - Model with resource`));\n console.log(colors.gray(` - Routes configured`));\n console.log(colors.gray(`\\nNext steps:`));\n const entity = singularName(moduleName);\n console.log(\n colors.gray(\n ` 1. Update model schema in models/${entity.kebab}/${entity.kebab}.model.ts`,\n ),\n );\n console.log(colors.gray(` 2. Update schema rules in schema/*.schema.ts`));\n } else {\n console.log(colors.gray(`\\nNext steps:`));\n console.log(colors.gray(` 1. Define routes in ${name.kebab}/routes.ts`));\n console.log(\n colors.gray(` 2. Create controllers: warlock generate.controller ${name.kebab}/<name>`),\n );\n console.log(colors.gray(` 3. Create models: warlock generate.model ${name.kebab}/<name>`));\n console.log(colors.gray(`\\nTip: omit --minimal to generate a full CRUD scaffold`));\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"module.generator.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/generate/generators/module.generator.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport path from \"node:path\";\nimport { appPath } from \"../../../../utils\";\nimport type { CommandActionData } from \"../../../../commands/types\";\nimport {\n crudCreateControllerStub,\n crudCreateSchemaStub,\n crudCreateServiceStub,\n crudDeleteControllerStub,\n crudDeleteServiceStub,\n crudGetServiceStub,\n crudListControllerStub,\n crudListServiceStub,\n crudModelStub,\n crudRepositoryStub,\n crudResourceStub,\n crudRoutesStub,\n crudSeedStub,\n crudShowControllerStub,\n crudUpdateControllerStub,\n crudUpdateSchemaStub,\n crudUpdateServiceStub,\n} from \"../templates/stubs\";\nimport { pluralName, singularName } from \"../utils/name-parser\";\nimport { moduleExists } from \"../utils/path-resolver\";\nimport {\n ensureDirectoryAsync,\n putFileAsync,\n reportSkippedFiles,\n setDryRun,\n setSkipExisting,\n} from \"../utils/writer\";\nimport { createMigrationFile } from \"./migration.generator\";\n\nexport async function generateModule(data: CommandActionData): Promise<void> {\n const moduleName = data.args[0];\n\n if (!moduleName) {\n console.log(colors.red(\"Error: Module name is required\"));\n console.log(colors.yellow(\"Usage: warlock generate.module <name> [options]\"));\n console.log(colors.yellow(\"Example: warlock generate.module products\"));\n console.log(colors.yellow(\" warlock generate.module products --minimal\"));\n process.exit(1);\n }\n\n const name = pluralName(moduleName);\n const force = data.options.force || data.options.f;\n setDryRun(Boolean(data.options.dryRun));\n // --force only creates what is missing; replacing edited files needs --overwrite too\n setSkipExisting(!data.options.overwrite);\n\n // Full CRUD is the default; --minimal (-m) opts down to a bare skeleton.\n const minimal = data.options.minimal || data.options.m;\n const withCrud = !minimal;\n\n // Check if module already exists\n if ((await moduleExists(name.kebab)) && !force) {\n console.log(colors.red(`Error: Module \"${name.kebab}\" already exists`));\n console.log(colors.yellow(\"Use --force to overwrite\"));\n process.exit(1);\n }\n\n const modulePath = appPath(name.kebab);\n\n // Create module directory structure\n const directories = [\n \"controllers\",\n \"services\",\n \"models\",\n \"repositories\",\n \"schema\",\n \"resources\",\n \"events\",\n \"types\",\n \"utils\",\n ];\n\n for (const dir of directories) {\n await ensureDirectoryAsync(path.join(modulePath, dir));\n }\n\n // Create main.ts\n const mainContent = `// You may use this file as a custom entry point to be executed when the app starts.\n// it runs only once, and you don't need to import the routes.ts file, it will be imported automatically.\n// Use it to regsiter or boot up any custom logic for this module.\n`;\n await putFileAsync(path.join(modulePath, \"main.ts\"), mainContent);\n\n // Create routes.ts\n const routesContent = withCrud\n ? crudRoutesStub(name)\n : `import { router } from \"@warlock.js/core\";\nimport { guarded } from \"app/shared/utils/router\";\n\n// Define your routes here\n// Example:\n// router.get(\"/${name.kebab}\", listController);\n`;\n await putFileAsync(path.join(modulePath, \"routes.ts\"), routesContent);\n\n /* Legacy TypeScript locale scaffold, retained here only until its removal can\n * be made in a dedicated cleanup. New modules use the JSON dictionary below.\n const localesContent = `import { groupedTranslations } from \"@warlock.js/core\";\n\ngroupedTranslations(\"${name.camel}\", {\n // Add your translations here\n // Example:\n // welcome: {\n // en: \"Welcome\",\n // ar: \"مرحبا\",\n // },\n});\n`;\n await putFileAsync(path.join(modulePath, \"utils\", \"locales.ts\"), localesContent); */\n\n // The generator cannot infer configured locale codes for a first dictionary.\n await putFileAsync(path.join(modulePath, \"utils\", \"locales.json\"), \"{}\\n\");\n\n // Full CRUD scaffold (the default) unless --minimal was passed\n if (withCrud) {\n const entity = singularName(moduleName);\n\n // Create controllers\n await putFileAsync(\n path.join(modulePath, \"controllers\", `create-${entity.kebab}.controller.ts`),\n crudCreateControllerStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"controllers\", `update-${entity.kebab}.controller.ts`),\n crudUpdateControllerStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"controllers\", `list-${name.kebab}.controller.ts`),\n crudListControllerStub(name),\n );\n\n await putFileAsync(\n path.join(modulePath, \"controllers\", `get-${entity.kebab}.controller.ts`),\n crudShowControllerStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"controllers\", `delete-${entity.kebab}.controller.ts`),\n crudDeleteControllerStub(entity),\n );\n\n // Create schema files — each exports both the schema value and its\n // inferred type; controllers import them directly (no requests/ alias).\n await putFileAsync(\n path.join(modulePath, \"schema\", `create-${entity.kebab}.schema.ts`),\n crudCreateSchemaStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"schema\", `update-${entity.kebab}.schema.ts`),\n crudUpdateSchemaStub(entity),\n );\n\n // Create model\n await ensureDirectoryAsync(path.join(modulePath, \"models\", entity.kebab));\n await putFileAsync(\n path.join(modulePath, \"models\", entity.kebab, `${entity.kebab}.model.ts`),\n crudModelStub(entity),\n );\n\n // Create model index\n await putFileAsync(\n path.join(modulePath, \"models\", entity.kebab, \"index.ts\"),\n `export * from \"./${entity.kebab}.model\";\\n`,\n );\n\n // Create migrations folder\n await ensureDirectoryAsync(path.join(modulePath, \"models\", entity.kebab, \"migrations\"));\n\n // Create resource\n await putFileAsync(\n path.join(modulePath, \"resources\", `${entity.kebab}.resource.ts`),\n crudResourceStub(name),\n );\n\n // Create repository\n await putFileAsync(\n path.join(modulePath, \"repositories\", `${name.kebab}.repository.ts`),\n crudRepositoryStub(name),\n );\n\n // Create services\n await putFileAsync(\n path.join(modulePath, \"services\", `create-${entity.kebab}.service.ts`),\n crudCreateServiceStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"services\", `update-${entity.kebab}.service.ts`),\n crudUpdateServiceStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"services\", `list-${name.kebab}.service.ts`),\n crudListServiceStub(name),\n );\n\n await putFileAsync(\n path.join(modulePath, \"services\", `get-${entity.kebab}.service.ts`),\n crudGetServiceStub(entity),\n );\n\n await putFileAsync(\n path.join(modulePath, \"services\", `delete-${entity.kebab}.service.ts`),\n crudDeleteServiceStub(entity),\n );\n\n // Create seeds\n await ensureDirectoryAsync(path.join(modulePath, \"seeds\"));\n await putFileAsync(path.join(modulePath, \"seeds\", `${name.kebab}.seed.ts`), crudSeedStub(name));\n\n // Create migration\n await createMigrationFile(name.kebab, entity.kebab);\n }\n\n reportSkippedFiles();\n\n console.log(colors.cyan(`\\n✨ Module \"${name.kebab}\" generated successfully!`));\n\n if (withCrud) {\n console.log(colors.gray(`\\n📦 CRUD scaffold created with:`));\n console.log(colors.gray(` - List, Get, Create, Update & Delete controllers`));\n console.log(colors.gray(` - Repository`));\n console.log(colors.gray(` - Validation schemas in schema/`));\n console.log(colors.gray(` - Model with resource`));\n console.log(colors.gray(` - Routes configured`));\n console.log(colors.gray(`\\nNext steps:`));\n const entity = singularName(moduleName);\n console.log(\n colors.gray(` 1. Update model schema in models/${entity.kebab}/${entity.kebab}.model.ts`),\n );\n console.log(colors.gray(` 2. Update schema rules in schema/*.schema.ts`));\n } else {\n console.log(colors.gray(`\\nNext steps:`));\n console.log(colors.gray(` 1. Define routes in ${name.kebab}/routes.ts`));\n console.log(\n colors.gray(` 2. Create controllers: warlock generate.controller ${name.kebab}/<name>`),\n );\n console.log(colors.gray(` 3. Create models: warlock generate.model ${name.kebab}/<name>`));\n console.log(colors.gray(`\\nTip: omit --minimal to generate a full CRUD scaffold`));\n }\n}\n"],"mappings":"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"}
@@ -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 };
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\nfunction 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,SAAS,aAAa,OAAoD;CACxE,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"}
@@ -36,7 +36,10 @@ const typingsGeneratorCommand = command({
36
36
  connectors: warlockConfigManager.isLoaded ? warlockConfigManager.get("connectors") ?? [] : [],
37
37
  appRoot: rootPath(),
38
38
  options: resolveBuildConfig,
39
- collectRoutes: async () => (await collectRouteRegistrationSnapshot({ cwd: rootPath() })).routes
39
+ collectRoutes: async () => (await collectRouteRegistrationSnapshot({
40
+ cwd: rootPath(),
41
+ timeoutMs: resolveBuildConfig().routeRegistrationTimeoutMs
42
+ })).routes
40
43
  });
41
44
  },
42
45
  preload: { warlockConfig: true }
@@ -1 +1 @@
1
- {"version":3,"file":"typings-generator.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/typings-generator.command.ts"],"sourcesContent":["import { filesOrchestrator } from \"../../dev-server/files-orchestrator\";\nimport { runConnectorTypings } from \"../../dev-server/run-connector-typings\";\nimport { typeGenerator } from \"../../dev-server/type-generator\";\nimport { command } from \"../../commands/cli-command\";\nimport { collectRouteRegistrationSnapshot } from \"../../production/route-registration-snapshot\";\nimport { resolveBuildConfig } from \"../../production/resolve-build-config\";\nimport { rootPath } from \"../../utils\";\nimport { warlockConfigManager } from \"../../warlock-config/warlock-config.manager\";\n\n/**\n * Standalone `warlock generate.typings`.\n *\n * Runs `filesOrchestrator.initializeAll()` before generating — the same\n * full-project discovery `warlock dev` performs at boot — so this produces\n * the identical registry from a fresh checkout instead of only whatever\n * config files a caller happened to list. A prior version added just the\n * config directory's files, which left `TranslationKeyRegistry` empty on any\n * app whose translation keys came from source outside `src/config`.\n *\n * Not `filesOrchestrator.init()` — that also registers the ESM loader hook\n * for dynamically importing project code, which typings generation, being\n * purely static AST inspection, never does.\n *\n * Connectors then write their own typings through the `build.typings` hook\n * (web's `web-routes.d.ts`), so a clean checkout typechecks before any build.\n */\nexport const typingsGeneratorCommand = command({\n name: \"generate.typings\",\n description: \"Generate type definitions for the project\",\n action: async () => {\n await filesOrchestrator.initializeAll();\n\n await typeGenerator.generateAll();\n\n await runConnectorTypings({\n connectors: warlockConfigManager.isLoaded ? (warlockConfigManager.get(\"connectors\") ?? []) : [],\n appRoot: rootPath(),\n options: resolveBuildConfig,\n collectRoutes: async () => (await collectRouteRegistrationSnapshot({ cwd: rootPath() })).routes,\n });\n },\n preload: {\n warlockConfig: true,\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAa,0BAA0B,QAAQ;CAC7C,MAAM;CACN,aAAa;CACb,QAAQ,YAAY;EAClB,MAAM,kBAAkB,cAAc;EAEtC,MAAM,cAAc,YAAY;EAEhC,MAAM,oBAAoB;GACxB,YAAY,qBAAqB,WAAY,qBAAqB,IAAI,YAAY,KAAK,CAAC,IAAK,CAAC;GAC9F,SAAS,SAAS;GAClB,SAAS;GACT,eAAe,aAAa,MAAM,iCAAiC,EAAE,KAAK,SAAS,EAAE,CAAC,EAAC,CAAE;EAC3F,CAAC;CACH;CACA,SAAS,EACP,eAAe,KACjB;AACF,CAAC"}
1
+ {"version":3,"file":"typings-generator.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/typings-generator.command.ts"],"sourcesContent":["import { filesOrchestrator } from \"../../dev-server/files-orchestrator\";\nimport { runConnectorTypings } from \"../../dev-server/run-connector-typings\";\nimport { typeGenerator } from \"../../dev-server/type-generator\";\nimport { command } from \"../../commands/cli-command\";\nimport { collectRouteRegistrationSnapshot } from \"../../production/route-registration-snapshot\";\nimport { resolveBuildConfig } from \"../../production/resolve-build-config\";\nimport { rootPath } from \"../../utils\";\nimport { warlockConfigManager } from \"../../warlock-config/warlock-config.manager\";\n\n/**\n * Standalone `warlock generate.typings`.\n *\n * Runs `filesOrchestrator.initializeAll()` before generating — the same\n * full-project discovery `warlock dev` performs at boot — so this produces\n * the identical registry from a fresh checkout instead of only whatever\n * config files a caller happened to list. A prior version added just the\n * config directory's files, which left `TranslationKeyRegistry` empty on any\n * app whose translation keys came from source outside `src/config`.\n *\n * Not `filesOrchestrator.init()` — that also registers the ESM loader hook\n * for dynamically importing project code, which typings generation, being\n * purely static AST inspection, never does.\n *\n * Connectors then write their own typings through the `build.typings` hook\n * (web's `web-routes.d.ts`), so a clean checkout typechecks before any build.\n */\nexport const typingsGeneratorCommand = command({\n name: \"generate.typings\",\n description: \"Generate type definitions for the project\",\n action: async () => {\n await filesOrchestrator.initializeAll();\n\n await typeGenerator.generateAll();\n\n await runConnectorTypings({\n connectors: warlockConfigManager.isLoaded\n ? (warlockConfigManager.get(\"connectors\") ?? [])\n : [],\n appRoot: rootPath(),\n options: resolveBuildConfig,\n // Same timeout as `warlock build` (build.routeRegistrationTimeoutMs or\n // WARLOCK_ROUTE_REGISTRATION_TIMEOUT_MS): `generate.typings` runs first in\n // the usual build script, so a slower default here fails the whole build.\n collectRoutes: async () =>\n (\n await collectRouteRegistrationSnapshot({\n cwd: rootPath(),\n timeoutMs: resolveBuildConfig().routeRegistrationTimeoutMs,\n })\n ).routes,\n });\n },\n preload: {\n warlockConfig: true,\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAa,0BAA0B,QAAQ;CAC7C,MAAM;CACN,aAAa;CACb,QAAQ,YAAY;EAClB,MAAM,kBAAkB,cAAc;EAEtC,MAAM,cAAc,YAAY;EAEhC,MAAM,oBAAoB;GACxB,YAAY,qBAAqB,WAC5B,qBAAqB,IAAI,YAAY,KAAK,CAAC,IAC5C,CAAC;GACL,SAAS,SAAS;GAClB,SAAS;GAIT,eAAe,aAEX,MAAM,iCAAiC;IACrC,KAAK,SAAS;IACd,WAAW,mBAAmB,CAAC,CAAC;GAClC,CAAC,EAAC,CACF;EACN,CAAC;CACH;CACA,SAAS,EACP,eAAe,KACjB;AACF,CAAC"}
@@ -6,6 +6,7 @@ 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";
9
10
  import { routesCommand, routesDiffCommand } from "./commands/routes.command.mjs";
10
11
  import { seedCommand } from "./commands/seed.command.mjs";
11
12
  import { startProductionCommand } from "./commands/start-production.command.mjs";
@@ -17,6 +18,7 @@ import { updateCommand } from "./commands/update.command.mjs";
17
18
  const frameworkCommands = [
18
19
  devServerCommand,
19
20
  typingsGeneratorCommand,
21
+ openApiGeneratorCommand,
20
22
  doctorCommand,
21
23
  routesCommand,
22
24
  routesDiffCommand,