@warlock.js/core 5.25.0 → 5.26.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 (80) hide show
  1. package/CHANGELOG.md +34 -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/typings-generator.command.mjs +4 -1
  13. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  14. package/esm/database/migrate-action.mjs +2 -2
  15. package/esm/database/migrate-action.mjs.map +1 -1
  16. package/esm/dev-server/legacy-locales-warning.mjs +16 -0
  17. package/esm/dev-server/legacy-locales-warning.mjs.map +1 -0
  18. package/esm/dev-server/module-loader.mjs +16 -0
  19. package/esm/dev-server/module-loader.mjs.map +1 -1
  20. package/esm/dev-server/special-file-patterns.mjs +2 -2
  21. package/esm/dev-server/special-file-patterns.mjs.map +1 -1
  22. package/esm/dev-server/translation-keys-sources.mjs +7 -3
  23. package/esm/dev-server/translation-keys-sources.mjs.map +1 -1
  24. package/esm/dev-server/type-generator.mjs +27 -4
  25. package/esm/dev-server/type-generator.mjs.map +1 -1
  26. package/esm/generations/features/access.feature.mjs +38 -18
  27. package/esm/generations/features/access.feature.mjs.map +1 -1
  28. package/esm/generations/features/bull-board.feature.mjs +2 -2
  29. package/esm/generations/features/bull-board.feature.mjs.map +1 -1
  30. package/esm/http/build-rate-limit-options.d.mts +8 -2
  31. package/esm/http/build-rate-limit-options.d.mts.map +1 -1
  32. package/esm/http/build-rate-limit-options.mjs +1 -0
  33. package/esm/http/build-rate-limit-options.mjs.map +1 -1
  34. package/esm/http/errors/errors.d.mts +1 -17
  35. package/esm/http/errors/errors.d.mts.map +1 -1
  36. package/esm/http/errors/errors.mjs +1 -20
  37. package/esm/http/errors/errors.mjs.map +1 -1
  38. package/esm/http/index.d.mts +2 -2
  39. package/esm/http/index.mjs +1 -1
  40. package/esm/http/middleware/inject-request-context.d.mts +1 -1
  41. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  42. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  43. package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
  44. package/esm/http/middleware/rate-limit.middleware.mjs +5 -1
  45. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  46. package/esm/http/request.d.mts +7 -24
  47. package/esm/http/request.d.mts.map +1 -1
  48. package/esm/http/request.mjs +1 -22
  49. package/esm/http/request.mjs.map +1 -1
  50. package/esm/http/response.d.mts.map +1 -1
  51. package/esm/http/response.mjs +4 -2
  52. package/esm/http/response.mjs.map +1 -1
  53. package/esm/index.d.mts +5 -3
  54. package/esm/index.mjs +5 -2
  55. package/esm/localization/index.mjs +4 -0
  56. package/esm/localization/parse-locale-dictionary.d.mts +23 -0
  57. package/esm/localization/parse-locale-dictionary.d.mts.map +1 -0
  58. package/esm/localization/parse-locale-dictionary.mjs +215 -0
  59. package/esm/localization/parse-locale-dictionary.mjs.map +1 -0
  60. package/esm/localization/register-locale-dictionary.d.mts +12 -0
  61. package/esm/localization/register-locale-dictionary.d.mts.map +1 -0
  62. package/esm/localization/register-locale-dictionary.mjs +58 -0
  63. package/esm/localization/register-locale-dictionary.mjs.map +1 -0
  64. package/esm/production/production-builder.mjs +11 -2
  65. package/esm/production/production-builder.mjs.map +1 -1
  66. package/esm/router/types.d.mts +8 -0
  67. package/esm/router/types.d.mts.map +1 -1
  68. package/esm/tests/start-http-development-server.d.mts +10 -0
  69. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  70. package/esm/tests/start-http-development-server.mjs +16 -2
  71. package/esm/tests/start-http-development-server.mjs.map +1 -1
  72. package/esm/validation/validateAll.mjs +48 -1
  73. package/esm/validation/validateAll.mjs.map +1 -1
  74. package/llms-full.txt +118 -75
  75. package/llms.txt +1 -1
  76. package/package.json +20 -20
  77. package/skills/README.md +1 -1
  78. package/skills/create-controller/SKILL.md +4 -0
  79. package/skills/test-http/SKILL.md +9 -1
  80. package/skills/use-localization/SKILL.md +39 -26
package/CHANGELOG.md CHANGED
@@ -6,6 +6,40 @@ 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.26.0 - 2026-09-30
10
+
11
+ ### Upgrading
12
+
13
+ - **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.
14
+ - **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)`.
15
+ - 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.
16
+
17
+ ### Added
18
+
19
+ - `http.rateLimit.ipv6Subnet` sets the IPv6 prefix length the global rate limiter groups clients by (default `64`).
20
+ - 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).
21
+ - `parseLocaleDictionary()` is public for tooling that needs to validate and flatten a module locale dictionary without loading application code.
22
+ - `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.
23
+ - `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.
24
+
25
+ ### Changed
26
+
27
+ - 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`.
28
+ - **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.
29
+ - `response.clearCookie()` now also sends `Max-Age=0` alongside the past `Expires` date.
30
+
31
+ ### Fixed
32
+
33
+ - `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.
34
+ - `request.t()`, `request.trans()` and `t()` are typed as returning `string` (they were `any`), so `title: request.t("...")` type-checks without a `String(...)` wrapper.
35
+ - `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.
36
+ - `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.
37
+ - `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.
38
+
39
+ ### Removed
40
+
41
+ - **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`.
42
+
9
43
  ## 5.25.0 - 2026-09-28
10
44
 
11
45
  ### 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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"}
@@ -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"}
@@ -126,7 +126,7 @@ async function loadMigrationFile(absPath) {
126
126
  const timestampMatch = path.basename(absPath).match(/^(\d{2}-\d{2}-\d{4}_\d{2}-\d{2}-\d{2})/);
127
127
  if (timestampMatch) MigrationClass.createdAt = timestampMatch[1];
128
128
  }
129
- migrationRunner.register(MigrationClass);
129
+ migrationRunner.register(MigrationClass, "app");
130
130
  }
131
131
  /**
132
132
  *
@@ -141,7 +141,7 @@ async function loadAllMigrations() {
141
141
  const configMigrations = warlockConfigManager.get("database")?.migrations || [];
142
142
  for (const MigrationClass of configMigrations) {
143
143
  if (!MigrationClass.migrationName) MigrationClass.migrationName = MigrationClass.name;
144
- migrationRunner.register(MigrationClass);
144
+ migrationRunner.register(MigrationClass, "package");
145
145
  }
146
146
  const migrations = await migrationFiles();
147
147
  for (const migrationFile of migrations) await loadMigrationFile(migrationFile);
@@ -1 +1 @@
1
- {"version":3,"file":"migrate-action.mjs","names":[],"sources":["../../../../../../../core/src/database/migrate-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport {\n exportMigrationsSQL,\n freshMigrate,\n listExecutedMigrations,\n Migration,\n type MigrationConstructor,\n migrationRunner,\n rollbackMigrations,\n runMigrations,\n} from \"@warlock.js/cascade\";\nimport dayjs from \"dayjs\";\nimport path from \"path\";\nimport type { CommandActionData } from \"../commands/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { Path } from \"../utils/normalized-path\";\nimport { getFilesFromDirectory } from \"../dev-server/utils\";\nimport { srcPath } from \"../utils\";\nimport { isTestFile } from \"../utils/is-test-file\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { exitCodeFor, PENDING_EXIT_CODE } from \"./pending-exit-code\";\nimport {\n resolvePendingMigrations,\n type PendingMigrationsResult,\n} from \"./resolve-pending-migrations\";\n\n/**\n * `--list` — print the migration state: what has run, then what will run next.\n *\n * The executed section is printed FIRST and unconditionally, before anything\n * touches disk or config. That ordering is the contract: reading migrations\n * from the table cannot fail because of a broken migration file, and `--list`\n * is the command an operator reaches for *during* an incident. The pending\n * section is best-effort on top of an answer that is already on screen.\n *\n * Always exits 0. It is a report; `--pending` is the gate.\n */\nasync function listMigrationsAction() {\n await printExecutedMigrations();\n\n const pending = await resolvePendingMigrations(loadAllMigrations);\n\n printPendingMigrations(pending);\n}\n\nasync function printExecutedMigrations() {\n const createdMigrations = await listExecutedMigrations();\n\n console.log(`\\nTotal Executed Migrations: ${colors.green(createdMigrations.length)}\\n`);\n\n if (createdMigrations.length === 0) {\n console.log(colors.gray(\" No migrations have been executed yet.\\n\"));\n return;\n }\n\n // Display each migration as a block\n for (const migration of createdMigrations) {\n const executedAt = dayjs(migration.executedAt).format(\"DD-MM-YYYY hh:mm:ss A\");\n const createdAt = migration.createdAt\n ? dayjs(migration.createdAt).format(\"DD-MM-YYYY hh:mm:ss A\")\n : null;\n\n // Migration name with checkmark icon\n console.log(` ${colors.green(\"✔\")} ${colors.cyanBright(migration.name)}`);\n\n // Executed date\n console.log(` ${colors.gray(\"Executed:\")} ${colors.white(executedAt)}`);\n\n // Created date (if available)\n if (createdAt) {\n console.log(` ${colors.gray(\"Created:\")} ${colors.yellow(createdAt)}`);\n }\n\n console.log(\"\"); // Empty line between migrations\n }\n}\n\n/**\n * Print the pending section shared by `--list` and `--pending`.\n *\n * An unavailable result prints an explicit line and NEVER a count. \"0 pending\"\n * over a tree we failed to read is the one output this whole feature exists to\n * prevent.\n */\nfunction printPendingMigrations(result: PendingMigrationsResult) {\n if (result.type === \"unavailable\") {\n console.log(\n `${colors.yellowBright(\"Pending: unavailable\")} — ${result.reason}\\n` +\n colors.gray(\" The executed list above is still accurate.\\n\"),\n );\n return;\n }\n\n const { migrations } = result;\n\n console.log(`Total Pending Migrations: ${colors.green(migrations.length)}\\n`);\n\n if (migrations.length === 0) {\n console.log(colors.gray(\" Nothing pending — the database is up to date.\\n\"));\n return;\n }\n\n // Numbered because the ORDER is the answer: this is what the next\n // `warlock migrate` will do, in the sequence it will do it.\n migrations.forEach((migration, index) => {\n console.log(` ${colors.gray(`${index + 1}.`)} ${colors.cyanBright(migration.name)}`);\n });\n\n console.log(\"\");\n}\n\n/**\n * `--pending` — the scriptable half. Same computation as `--list`'s pending\n * section, different exit contract: see {@link PENDING_EXIT_CODE}.\n */\nasync function pendingMigrationsAction() {\n const result = await resolvePendingMigrations(loadAllMigrations);\n\n printPendingMigrations(result);\n\n const exitCode = exitCodeFor(result);\n\n if (exitCode === PENDING_EXIT_CODE.unavailable) {\n console.error(\n colors.redBright(\"Could not determine the pending set — exiting 2 rather than reporting 0.\"),\n );\n }\n\n process.exit(exitCode);\n}\n\nasync function allMigrationsFilesAction() {\n // get all available migration files in the project\n const files = (await migrationFiles()).map((path) => Path.toRelative(path));\n console.log(`Total Migration Files: ${colors.green(files.length)}`);\n\n for (const file of files) {\n console.log(colors.yellowBright(file));\n }\n}\n\n/**\n * If path is provided, then run the migration runner against that file only\n * If fresh is provided, then rollback all migrations and run all migrations\n * If rollback is provided, then run the migration runner against all files in reverse order\n */\nexport async function migrateAction(options: CommandActionData) {\n const { fresh, path, rollback, all, list, pending, sql, pendingOnly, compact } = options.options;\n\n if (list) {\n return await listMigrationsAction();\n }\n\n if (pending) {\n return await pendingMigrationsAction();\n }\n\n if (all) {\n return await allMigrationsFilesAction();\n }\n\n if (path) {\n await loadMigrationFile(Path.toAbsolute(path as string));\n } else {\n await loadAllMigrations();\n }\n\n if (fresh && rollback) {\n console.log(colors.redBright(\"You can't use --fresh and --rollback together\"));\n process.exit(1);\n }\n\n if (rollback) {\n await rollbackMigrations({ all: true });\n return;\n }\n\n if (sql) {\n await exportMigrationsSQL({\n pendingOnly: pendingOnly as boolean,\n compact: compact as boolean,\n });\n return;\n }\n\n if (fresh) {\n await freshMigrate();\n return;\n }\n\n await runMigrations();\n}\n\nasync function loadMigrationFile(absPath: string) {\n const relativePath = Path.toRelative(absPath);\n\n // MigrationConstructor, not `typeof Migration`: a migration file default-exports a\n // CONCRETE subclass, while Migration itself is abstract and so cannot satisfy a\n // `new () => ...` parameter. The annotation described the base class rather than\n // what is actually loaded, and that is what migrationRunner.register() rejected.\n const loadedModule = await filesOrchestrator.load<{ default: MigrationConstructor }>(\n relativePath,\n );\n\n if (!loadedModule?.default) {\n throw new Error(`${Path.toRelative(absPath)} must have a default export`);\n }\n\n const MigrationClass = loadedModule.default;\n\n if (!MigrationClass.migrationName) {\n MigrationClass.migrationName = path\n .basename(absPath)\n .split(\".\")[0]\n .replace(\"-migration\", \"\")\n .replace(\"_migration\", \"\");\n }\n\n // Extract createdAt timestamp from filename if not already set\n // Expected format: MM-DD-YYYY_HH-MM-SS-name.migration.ts\n if (!MigrationClass.createdAt) {\n const filename = path.basename(absPath);\n const timestampMatch = filename.match(/^(\\d{2}-\\d{2}-\\d{4}_\\d{2}-\\d{2}-\\d{2})/);\n if (timestampMatch) {\n MigrationClass.createdAt = timestampMatch[1];\n }\n }\n\n migrationRunner.register(MigrationClass);\n}\n\n/**\n *\n * @returns List of absolute paths to migration files\n */\nasync function migrationFiles() {\n const migrationFiles = await getFilesFromDirectory(srcPath(\"app\"), \"*/models/*/migrations/*\");\n const separateMigrationsFolderFIles = await getFilesFromDirectory(\n srcPath(\"app\"),\n \"*/migrations/*\",\n );\n\n const migrations = [...migrationFiles, ...separateMigrationsFolderFIles].filter(\n (file) => !isTestFile(file),\n );\n\n return migrations;\n}\n\nasync function loadAllMigrations() {\n // Load config-registered migrations (from packages like @warlock.js/auth)\n const configMigrations = warlockConfigManager.get(\"database\")?.migrations || [];\n\n for (const MigrationClass of configMigrations) {\n // Use class name as migration name if not set\n if (!MigrationClass.migrationName) {\n MigrationClass.migrationName = MigrationClass.name;\n }\n migrationRunner.register(MigrationClass);\n }\n\n // Always load file-based migrations from src/app, regardless of config\n const migrations = await migrationFiles();\n for (const migrationFile of migrations) {\n await loadMigrationFile(migrationFile);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"migrate-action.mjs","names":[],"sources":["../../../../../../../core/src/database/migrate-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport {\n exportMigrationsSQL,\n freshMigrate,\n listExecutedMigrations,\n Migration,\n type MigrationConstructor,\n migrationRunner,\n rollbackMigrations,\n runMigrations,\n} from \"@warlock.js/cascade\";\nimport dayjs from \"dayjs\";\nimport path from \"path\";\nimport type { CommandActionData } from \"../commands/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { Path } from \"../utils/normalized-path\";\nimport { getFilesFromDirectory } from \"../dev-server/utils\";\nimport { srcPath } from \"../utils\";\nimport { isTestFile } from \"../utils/is-test-file\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { exitCodeFor, PENDING_EXIT_CODE } from \"./pending-exit-code\";\nimport {\n resolvePendingMigrations,\n type PendingMigrationsResult,\n} from \"./resolve-pending-migrations\";\n\n/**\n * `--list` — print the migration state: what has run, then what will run next.\n *\n * The executed section is printed FIRST and unconditionally, before anything\n * touches disk or config. That ordering is the contract: reading migrations\n * from the table cannot fail because of a broken migration file, and `--list`\n * is the command an operator reaches for *during* an incident. The pending\n * section is best-effort on top of an answer that is already on screen.\n *\n * Always exits 0. It is a report; `--pending` is the gate.\n */\nasync function listMigrationsAction() {\n await printExecutedMigrations();\n\n const pending = await resolvePendingMigrations(loadAllMigrations);\n\n printPendingMigrations(pending);\n}\n\nasync function printExecutedMigrations() {\n const createdMigrations = await listExecutedMigrations();\n\n console.log(`\\nTotal Executed Migrations: ${colors.green(createdMigrations.length)}\\n`);\n\n if (createdMigrations.length === 0) {\n console.log(colors.gray(\" No migrations have been executed yet.\\n\"));\n return;\n }\n\n // Display each migration as a block\n for (const migration of createdMigrations) {\n const executedAt = dayjs(migration.executedAt).format(\"DD-MM-YYYY hh:mm:ss A\");\n const createdAt = migration.createdAt\n ? dayjs(migration.createdAt).format(\"DD-MM-YYYY hh:mm:ss A\")\n : null;\n\n // Migration name with checkmark icon\n console.log(` ${colors.green(\"✔\")} ${colors.cyanBright(migration.name)}`);\n\n // Executed date\n console.log(` ${colors.gray(\"Executed:\")} ${colors.white(executedAt)}`);\n\n // Created date (if available)\n if (createdAt) {\n console.log(` ${colors.gray(\"Created:\")} ${colors.yellow(createdAt)}`);\n }\n\n console.log(\"\"); // Empty line between migrations\n }\n}\n\n/**\n * Print the pending section shared by `--list` and `--pending`.\n *\n * An unavailable result prints an explicit line and NEVER a count. \"0 pending\"\n * over a tree we failed to read is the one output this whole feature exists to\n * prevent.\n */\nfunction printPendingMigrations(result: PendingMigrationsResult) {\n if (result.type === \"unavailable\") {\n console.log(\n `${colors.yellowBright(\"Pending: unavailable\")} — ${result.reason}\\n` +\n colors.gray(\" The executed list above is still accurate.\\n\"),\n );\n return;\n }\n\n const { migrations } = result;\n\n console.log(`Total Pending Migrations: ${colors.green(migrations.length)}\\n`);\n\n if (migrations.length === 0) {\n console.log(colors.gray(\" Nothing pending — the database is up to date.\\n\"));\n return;\n }\n\n // Numbered because the ORDER is the answer: this is what the next\n // `warlock migrate` will do, in the sequence it will do it.\n migrations.forEach((migration, index) => {\n console.log(` ${colors.gray(`${index + 1}.`)} ${colors.cyanBright(migration.name)}`);\n });\n\n console.log(\"\");\n}\n\n/**\n * `--pending` — the scriptable half. Same computation as `--list`'s pending\n * section, different exit contract: see {@link PENDING_EXIT_CODE}.\n */\nasync function pendingMigrationsAction() {\n const result = await resolvePendingMigrations(loadAllMigrations);\n\n printPendingMigrations(result);\n\n const exitCode = exitCodeFor(result);\n\n if (exitCode === PENDING_EXIT_CODE.unavailable) {\n console.error(\n colors.redBright(\"Could not determine the pending set — exiting 2 rather than reporting 0.\"),\n );\n }\n\n process.exit(exitCode);\n}\n\nasync function allMigrationsFilesAction() {\n // get all available migration files in the project\n const files = (await migrationFiles()).map((path) => Path.toRelative(path));\n console.log(`Total Migration Files: ${colors.green(files.length)}`);\n\n for (const file of files) {\n console.log(colors.yellowBright(file));\n }\n}\n\n/**\n * If path is provided, then run the migration runner against that file only\n * If fresh is provided, then rollback all migrations and run all migrations\n * If rollback is provided, then run the migration runner against all files in reverse order\n */\nexport async function migrateAction(options: CommandActionData) {\n const { fresh, path, rollback, all, list, pending, sql, pendingOnly, compact } = options.options;\n\n if (list) {\n return await listMigrationsAction();\n }\n\n if (pending) {\n return await pendingMigrationsAction();\n }\n\n if (all) {\n return await allMigrationsFilesAction();\n }\n\n if (path) {\n await loadMigrationFile(Path.toAbsolute(path as string));\n } else {\n await loadAllMigrations();\n }\n\n if (fresh && rollback) {\n console.log(colors.redBright(\"You can't use --fresh and --rollback together\"));\n process.exit(1);\n }\n\n if (rollback) {\n await rollbackMigrations({ all: true });\n return;\n }\n\n if (sql) {\n await exportMigrationsSQL({\n pendingOnly: pendingOnly as boolean,\n compact: compact as boolean,\n });\n return;\n }\n\n if (fresh) {\n await freshMigrate();\n return;\n }\n\n await runMigrations();\n}\n\nexport async function loadMigrationFile(absPath: string) {\n const relativePath = Path.toRelative(absPath);\n\n // MigrationConstructor, not `typeof Migration`: a migration file default-exports a\n // CONCRETE subclass, while Migration itself is abstract and so cannot satisfy a\n // `new () => ...` parameter. The annotation described the base class rather than\n // what is actually loaded, and that is what migrationRunner.register() rejected.\n const loadedModule = await filesOrchestrator.load<{ default: MigrationConstructor }>(\n relativePath,\n );\n\n if (!loadedModule?.default) {\n throw new Error(`${Path.toRelative(absPath)} must have a default export`);\n }\n\n const MigrationClass = loadedModule.default;\n\n if (!MigrationClass.migrationName) {\n MigrationClass.migrationName = path\n .basename(absPath)\n .split(\".\")[0]\n .replace(\"-migration\", \"\")\n .replace(\"_migration\", \"\");\n }\n\n // Extract createdAt timestamp from filename if not already set\n // Expected format: MM-DD-YYYY_HH-MM-SS-name.migration.ts\n if (!MigrationClass.createdAt) {\n const filename = path.basename(absPath);\n const timestampMatch = filename.match(/^(\\d{2}-\\d{2}-\\d{4}_\\d{2}-\\d{2}-\\d{2})/);\n if (timestampMatch) {\n MigrationClass.createdAt = timestampMatch[1];\n }\n }\n\n migrationRunner.register(MigrationClass, \"app\");\n}\n\n/**\n *\n * @returns List of absolute paths to migration files\n */\nasync function migrationFiles() {\n const migrationFiles = await getFilesFromDirectory(srcPath(\"app\"), \"*/models/*/migrations/*\");\n const separateMigrationsFolderFIles = await getFilesFromDirectory(\n srcPath(\"app\"),\n \"*/migrations/*\",\n );\n\n const migrations = [...migrationFiles, ...separateMigrationsFolderFIles].filter(\n (file) => !isTestFile(file),\n );\n\n return migrations;\n}\n\nexport async function loadAllMigrations() {\n // Load config-registered migrations (from packages like @warlock.js/auth)\n const configMigrations = warlockConfigManager.get(\"database\")?.migrations || [];\n\n for (const MigrationClass of configMigrations) {\n // Use class name as migration name if not set\n if (!MigrationClass.migrationName) {\n MigrationClass.migrationName = MigrationClass.name;\n }\n migrationRunner.register(MigrationClass, \"package\");\n }\n\n // Always load file-based migrations from src/app, regardless of config\n const migrations = await migrationFiles();\n for (const migrationFile of migrations) {\n await loadMigrationFile(migrationFile);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { log } from "@warlock.js/logger";
2
+
3
+ //#region ../core/src/dev-server/legacy-locales-warning.ts
4
+ let warned = false;
5
+ /** Warn once per dev-server process when legacy locale modules are loaded. */
6
+ function warnForLegacyLocales(localeFiles) {
7
+ const legacyFiles = localeFiles.filter((file) => /\/utils\/locales\.(ts|tsx)$/.test(file));
8
+ if (warned || legacyFiles.length === 0) return;
9
+ warned = true;
10
+ const count = legacyFiles.length;
11
+ log.warn("localization", "legacy-locales", `${count} legacy utils/locales.ts ${count === 1 ? "file is" : "files are"} auto-loaded. Run \`warlock doctor --fix\` to migrate them; auto-loaded locales.ts is removed in v6.`);
12
+ }
13
+
14
+ //#endregion
15
+ export { warnForLegacyLocales };
16
+ //# sourceMappingURL=legacy-locales-warning.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"legacy-locales-warning.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/legacy-locales-warning.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\n\nlet warned = false;\n\n/** Warn once per dev-server process when legacy locale modules are loaded. */\nexport function warnForLegacyLocales(localeFiles: readonly string[]): void {\n const legacyFiles = localeFiles.filter((file) => /\\/utils\\/locales\\.(ts|tsx)$/.test(file));\n if (warned || legacyFiles.length === 0) return;\n\n warned = true;\n const count = legacyFiles.length;\n log.warn(\n \"localization\",\n \"legacy-locales\",\n `${count} legacy utils/locales.ts ${count === 1 ? \"file is\" : \"files are\"} auto-loaded. ` +\n \"Run `warlock doctor --fix` to migrate them; auto-loaded locales.ts is removed in v6.\",\n );\n}\n\n/** Test-only reset for the process-scoped warning guard. */\nexport function resetLegacyLocalesWarningForTests(): void {\n warned = false;\n}\n"],"mappings":";;;AAEA,IAAI,SAAS;;AAGb,SAAgB,qBAAqB,aAAsC;CACzE,MAAM,cAAc,YAAY,QAAQ,SAAS,8BAA8B,KAAK,IAAI,CAAC;CACzF,IAAI,UAAU,YAAY,WAAW,GAAG;CAExC,SAAS;CACT,MAAM,QAAQ,YAAY;CAC1B,IAAI,KACF,gBACA,kBACA,GAAG,MAAM,2BAA2B,UAAU,IAAI,YAAY,YAAY,qGAE5E;AACF"}
@@ -1,8 +1,13 @@
1
1
  import { router } from "../router/router.mjs";
2
+ import { registerGroupedTranslationKeys, registerLocaleDictionary } from "../localization/register-locale-dictionary.mjs";
3
+ import "../localization/index.mjs";
2
4
  import { devLogError, formatModuleNotFoundError } from "./dev-logger.mjs";
3
5
  import { importAppModule } from "../loader/app-module-importer.mjs";
6
+ import { extractTranslationKeys } from "./extract-translation-keys.mjs";
7
+ import { warnForLegacyLocales } from "./legacy-locales-warning.mjs";
4
8
  import { getDevelopmentModelModuleRegistry } from "./model-module-registry.mjs";
5
9
  import { pathToFileURL } from "node:url";
10
+ import ts from "typescript";
6
11
 
7
12
  //#region ../core/src/dev-server/module-loader.ts
8
13
  /**
@@ -72,6 +77,17 @@ var ModuleLoader = class {
72
77
  */
73
78
  async loadModule(file, type) {
74
79
  if (file.relativePath.endsWith(".env")) return void 0;
80
+ if (type === "locale" && /\/utils\/locales\.(ts|tsx)$/.test(file.relativePath)) warnForLegacyLocales(this.specialFilesCollector.getFilesByType("locale").map((locale) => locale.relativePath));
81
+ if (type === "locale" && file.relativePath.endsWith(".json")) {
82
+ const cleanup = registerLocaleDictionary({
83
+ sourceFile: file.relativePath,
84
+ source: file.source,
85
+ defaultNamespace: file.relativePath.split("/")[2]
86
+ });
87
+ file.addCleanup(cleanup);
88
+ return;
89
+ }
90
+ if (type === "locale") registerGroupedTranslationKeys(file.relativePath, extractTranslationKeys(ts.createSourceFile(file.relativePath, file.source, ts.ScriptTarget.Latest)));
75
91
  globalThis.__currentModuleFile = file;
76
92
  try {
77
93
  const fileUrl = pathToFileURL(file.absolutePath).href;