@warlock.js/core 5.34.3 → 5.36.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 (281) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/esm/cli/cli-commands.manager.mjs +1 -1
  3. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  4. package/esm/cli/commands/add.command.mjs +1 -0
  5. package/esm/cli/commands/add.command.mjs.map +1 -1
  6. package/esm/cli/commands/build.command.mjs +30 -3
  7. package/esm/cli/commands/build.command.mjs.map +1 -1
  8. package/esm/cli/commands/check/check-command.action.mjs +1 -0
  9. package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
  10. package/esm/cli/commands/check.command.mjs +5 -0
  11. package/esm/cli/commands/check.command.mjs.map +1 -1
  12. package/esm/cli/commands/explain/explain-command.action.mjs +21 -0
  13. package/esm/cli/commands/explain/explain-command.action.mjs.map +1 -1
  14. package/esm/cli/commands/explain.command.mjs +14 -4
  15. package/esm/cli/commands/explain.command.mjs.map +1 -1
  16. package/esm/cli/commands/generate/generate.command.mjs +19 -1
  17. package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
  18. package/esm/cli/commands/generate/generators/endpoint.generator.mjs +1 -1
  19. package/esm/cli/commands/generate/generators/endpoint.generator.mjs.map +1 -1
  20. package/esm/cli/commands/generate/generators/web.generator.mjs +169 -0
  21. package/esm/cli/commands/generate/generators/web.generator.mjs.map +1 -0
  22. package/esm/cli/commands/generate/templates/web-stubs.mjs +231 -0
  23. package/esm/cli/commands/generate/templates/web-stubs.mjs.map +1 -0
  24. package/esm/cli/commands/inspect/inspect-command.action.mjs +4 -3
  25. package/esm/cli/commands/inspect/inspect-command.action.mjs.map +1 -1
  26. package/esm/cli/commands/inspect.command.mjs +13 -8
  27. package/esm/cli/commands/inspect.command.mjs.map +1 -1
  28. package/esm/cli/commands/mcp/mcp-command.action.mjs +1 -1
  29. package/esm/cli/commands/mcp/mcp-command.action.mjs.map +1 -1
  30. package/esm/config/config-loader.d.mts.map +1 -1
  31. package/esm/config/config-loader.mjs +2 -0
  32. package/esm/config/config-loader.mjs.map +1 -1
  33. package/esm/config/load-config-files.mjs +4 -0
  34. package/esm/config/load-config-files.mjs.map +1 -1
  35. package/esm/dev-server/file-event-handler.mjs +11 -1
  36. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  37. package/esm/dev-server/file-manager.d.mts +1 -1
  38. package/esm/dev-server/file-manager.d.mts.map +1 -1
  39. package/esm/dev-server/file-manager.mjs.map +1 -1
  40. package/esm/dev-server/files-orchestrator.mjs +16 -1
  41. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  42. package/esm/dev-server/files-watcher.mjs +13 -4
  43. package/esm/dev-server/files-watcher.mjs.map +1 -1
  44. package/esm/dev-server/loader/loader-port-acks.mjs +21 -9
  45. package/esm/dev-server/loader/loader-port-acks.mjs.map +1 -1
  46. package/esm/dev-server/loader/register-loader.mjs +31 -12
  47. package/esm/dev-server/loader/register-loader.mjs.map +1 -1
  48. package/esm/dev-server/module-loader.mjs +15 -1
  49. package/esm/dev-server/module-loader.mjs.map +1 -1
  50. package/esm/dev-server/parse-imports.mjs +1 -1
  51. package/esm/dev-server/special-file-patterns.mjs +4 -1
  52. package/esm/dev-server/special-file-patterns.mjs.map +1 -1
  53. package/esm/dev-server/type-generator.mjs +1 -1
  54. package/esm/e2e/cdp.d.mts.map +1 -1
  55. package/esm/e2e/cdp.mjs +9 -2
  56. package/esm/e2e/cdp.mjs.map +1 -1
  57. package/esm/e2e/index.d.mts +2 -2
  58. package/esm/e2e/page.d.mts +14 -1
  59. package/esm/e2e/page.d.mts.map +1 -1
  60. package/esm/e2e/page.mjs +81 -8
  61. package/esm/e2e/page.mjs.map +1 -1
  62. package/esm/e2e/types.d.mts +49 -0
  63. package/esm/e2e/types.d.mts.map +1 -1
  64. package/esm/e2e/types.mjs.map +1 -1
  65. package/esm/errors/error-codes.d.mts +72 -0
  66. package/esm/errors/error-codes.d.mts.map +1 -1
  67. package/esm/errors/error-codes.mjs +72 -0
  68. package/esm/errors/error-codes.mjs.map +1 -1
  69. package/esm/errors/index.d.mts +2 -2
  70. package/esm/errors/index.mjs +2 -2
  71. package/esm/generations/add-command.action.mjs +2 -0
  72. package/esm/generations/add-command.action.mjs.map +1 -1
  73. package/esm/generations/design-system-payload.mjs +53 -0
  74. package/esm/generations/design-system-payload.mjs.map +1 -0
  75. package/esm/generations/features/design-system.feature.mjs +38 -0
  76. package/esm/generations/features/design-system.feature.mjs.map +1 -0
  77. package/esm/generations/features/index.mjs +3 -1
  78. package/esm/generations/features/index.mjs.map +1 -1
  79. package/esm/generations/features/notifications.feature.mjs +13 -2
  80. package/esm/generations/features/notifications.feature.mjs.map +1 -1
  81. package/esm/generations/features/test.feature.mjs +48 -13
  82. package/esm/generations/features/test.feature.mjs.map +1 -1
  83. package/esm/generations/features/web.feature.mjs +3 -3
  84. package/esm/generations/features/web.feature.mjs.map +1 -1
  85. package/esm/generations/stubs.mjs +9 -5
  86. package/esm/generations/stubs.mjs.map +1 -1
  87. package/esm/http/error-mapper.d.mts +1 -1
  88. package/esm/http/error-mapper.mjs +3 -3
  89. package/esm/http/error-mapper.mjs.map +1 -1
  90. package/esm/http/error-response.d.mts +3 -0
  91. package/esm/http/error-response.d.mts.map +1 -1
  92. package/esm/http/error-response.mjs +8 -2
  93. package/esm/http/error-response.mjs.map +1 -1
  94. package/esm/http/errors/errors.d.mts +8 -0
  95. package/esm/http/errors/errors.d.mts.map +1 -1
  96. package/esm/http/errors/errors.mjs +13 -1
  97. package/esm/http/errors/errors.mjs.map +1 -1
  98. package/esm/http/index.d.mts +3 -2
  99. package/esm/http/index.mjs +3 -2
  100. package/esm/http/list-endpoint.d.mts +35 -0
  101. package/esm/http/list-endpoint.d.mts.map +1 -0
  102. package/esm/http/list-endpoint.mjs +28 -0
  103. package/esm/http/list-endpoint.mjs.map +1 -0
  104. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  105. package/esm/http/middleware/inject-request-context.mjs +12 -2
  106. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  107. package/esm/http/server.mjs +1 -1
  108. package/esm/index.d.mts +10 -6
  109. package/esm/index.mjs +5 -2
  110. package/esm/localization/index.mjs +3 -1
  111. package/esm/localization/translation-usage-graph.d.mts +49 -0
  112. package/esm/localization/translation-usage-graph.d.mts.map +1 -0
  113. package/esm/localization/translation-usage-graph.mjs +128 -0
  114. package/esm/localization/translation-usage-graph.mjs.map +1 -0
  115. package/esm/localization/translation-usage.d.mts +85 -0
  116. package/esm/localization/translation-usage.d.mts.map +1 -0
  117. package/esm/localization/translation-usage.mjs +307 -0
  118. package/esm/localization/translation-usage.mjs.map +1 -0
  119. package/esm/openapi/security.mjs +12 -4
  120. package/esm/openapi/security.mjs.map +1 -1
  121. package/esm/openapi/types.d.mts +2 -0
  122. package/esm/openapi/types.d.mts.map +1 -1
  123. package/esm/production/build-budgets.d.mts +64 -0
  124. package/esm/production/build-budgets.d.mts.map +1 -0
  125. package/esm/production/build-budgets.mjs +109 -0
  126. package/esm/production/build-budgets.mjs.map +1 -0
  127. package/esm/production/production-builder.mjs +13 -1
  128. package/esm/production/production-builder.mjs.map +1 -1
  129. package/esm/production/resolve-build-config.d.mts +1 -1
  130. package/esm/production/resolve-build-config.d.mts.map +1 -1
  131. package/esm/production/resolve-build-config.mjs.map +1 -1
  132. package/esm/production/route-registration-snapshot.mjs +5 -3
  133. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  134. package/esm/restful/restful.d.mts +1 -1
  135. package/esm/router/router.d.mts +2 -0
  136. package/esm/router/router.d.mts.map +1 -1
  137. package/esm/router/router.mjs +4 -1
  138. package/esm/router/router.mjs.map +1 -1
  139. package/esm/router/types.d.mts +19 -0
  140. package/esm/router/types.d.mts.map +1 -1
  141. package/esm/tests/fakes/storage-fake.d.mts +3 -1
  142. package/esm/tests/fakes/storage-fake.d.mts.map +1 -1
  143. package/esm/tests/fakes/storage-fake.mjs +3 -3
  144. package/esm/tests/fakes/storage-fake.mjs.map +1 -1
  145. package/esm/tests/fakes/test-fakes.d.mts +3 -1
  146. package/esm/tests/fakes/test-fakes.d.mts.map +1 -1
  147. package/esm/tests/fakes/test-fakes.mjs +3 -2
  148. package/esm/tests/fakes/test-fakes.mjs.map +1 -1
  149. package/esm/tests/index.d.mts +4 -2
  150. package/esm/tests/index.mjs +3 -1
  151. package/esm/tests/load-unit-config.mjs +24 -0
  152. package/esm/tests/load-unit-config.mjs.map +1 -0
  153. package/esm/tests/response-contract.d.mts +32 -0
  154. package/esm/tests/response-contract.d.mts.map +1 -0
  155. package/esm/tests/response-contract.mjs +116 -0
  156. package/esm/tests/response-contract.mjs.map +1 -0
  157. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  158. package/esm/tests/start-http-development-server.mjs +10 -2
  159. package/esm/tests/start-http-development-server.mjs.map +1 -1
  160. package/esm/tests/test-lifecycle-state.mjs.map +1 -1
  161. package/esm/tests/test-locales.mjs +33 -0
  162. package/esm/tests/test-locales.mjs.map +1 -0
  163. package/esm/tests/test-runtime-scope.d.mts +15 -0
  164. package/esm/tests/test-runtime-scope.d.mts.map +1 -0
  165. package/esm/tests/test-runtime-scope.mjs +80 -0
  166. package/esm/tests/test-runtime-scope.mjs.map +1 -0
  167. package/esm/tests/vitest-setup.d.mts +13 -3
  168. package/esm/tests/vitest-setup.d.mts.map +1 -1
  169. package/esm/tests/vitest-setup.mjs +146 -18
  170. package/esm/tests/vitest-setup.mjs.map +1 -1
  171. package/esm/tooling/check/affected-files.d.mts +5 -0
  172. package/esm/tooling/check/affected-files.d.mts.map +1 -0
  173. package/esm/tooling/check/affected-files.mjs +56 -0
  174. package/esm/tooling/check/affected-files.mjs.map +1 -0
  175. package/esm/tooling/check/api-route-references.d.mts +16 -0
  176. package/esm/tooling/check/api-route-references.d.mts.map +1 -0
  177. package/esm/tooling/check/api-route-references.mjs +146 -0
  178. package/esm/tooling/check/api-route-references.mjs.map +1 -0
  179. package/esm/tooling/check/changed-files.d.mts +2 -0
  180. package/esm/tooling/check/changed-files.d.mts.map +1 -1
  181. package/esm/tooling/check/changed-files.mjs +2 -2
  182. package/esm/tooling/check/changed-files.mjs.map +1 -1
  183. package/esm/tooling/check/conventions-step.d.mts +7 -3
  184. package/esm/tooling/check/conventions-step.d.mts.map +1 -1
  185. package/esm/tooling/check/conventions-step.mjs +59 -8
  186. package/esm/tooling/check/conventions-step.mjs.map +1 -1
  187. package/esm/tooling/check/fixes.d.mts +8 -0
  188. package/esm/tooling/check/fixes.d.mts.map +1 -0
  189. package/esm/tooling/check/fixes.mjs +31 -0
  190. package/esm/tooling/check/fixes.mjs.map +1 -0
  191. package/esm/tooling/check/index.d.mts +12 -0
  192. package/esm/tooling/check/index.mjs +3 -2
  193. package/esm/tooling/check/run-check.d.mts +2 -0
  194. package/esm/tooling/check/run-check.d.mts.map +1 -1
  195. package/esm/tooling/check/run-check.mjs +23 -5
  196. package/esm/tooling/check/run-check.mjs.map +1 -1
  197. package/esm/tooling/check/run-tests.mjs +1 -1
  198. package/esm/tooling/check/source-ast.d.mts +1 -0
  199. package/esm/tooling/check/translation-conventions.mjs +32 -0
  200. package/esm/tooling/check/translation-conventions.mjs.map +1 -0
  201. package/esm/tooling/check/web-conventions.d.mts +10 -0
  202. package/esm/tooling/check/web-conventions.d.mts.map +1 -0
  203. package/esm/tooling/check/web-conventions.mjs +126 -0
  204. package/esm/tooling/check/web-conventions.mjs.map +1 -0
  205. package/esm/tooling/index.d.mts +7 -2
  206. package/esm/tooling/index.mjs +4 -2
  207. package/esm/tooling/inspect/collect-files.mjs +172 -0
  208. package/esm/tooling/inspect/collect-files.mjs.map +1 -0
  209. package/esm/tooling/inspect/collect-pages.mjs +3 -2
  210. package/esm/tooling/inspect/collect-pages.mjs.map +1 -1
  211. package/esm/tooling/inspect/collect-routes.mjs +1 -1
  212. package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
  213. package/esm/tooling/inspect/explain-file.d.mts +12 -0
  214. package/esm/tooling/inspect/explain-file.d.mts.map +1 -0
  215. package/esm/tooling/inspect/explain-file.mjs +45 -0
  216. package/esm/tooling/inspect/explain-file.mjs.map +1 -0
  217. package/esm/tooling/inspect/index.d.mts +3 -2
  218. package/esm/tooling/inspect/index.mjs +2 -1
  219. package/esm/tooling/inspect/inspect-app.d.mts.map +1 -1
  220. package/esm/tooling/inspect/inspect-app.mjs +9 -3
  221. package/esm/tooling/inspect/inspect-app.mjs.map +1 -1
  222. package/esm/tooling/inspect/inspect-report.schema.mjs +34 -1
  223. package/esm/tooling/inspect/inspect-report.schema.mjs.map +1 -1
  224. package/esm/tooling/inspect/inspect-report.types.d.mts +17 -1
  225. package/esm/tooling/inspect/inspect-report.types.d.mts.map +1 -1
  226. package/esm/tooling/inspect/inspect-report.types.mjs +2 -1
  227. package/esm/tooling/inspect/inspect-report.types.mjs.map +1 -1
  228. package/esm/tooling/mcp/mcp-tools.d.mts.map +1 -1
  229. package/esm/tooling/mcp/mcp-tools.mjs +8 -2
  230. package/esm/tooling/mcp/mcp-tools.mjs.map +1 -1
  231. package/esm/tooling/problem.d.mts +4 -0
  232. package/esm/tooling/problem.d.mts.map +1 -1
  233. package/esm/tooling/problem.mjs +1 -0
  234. package/esm/tooling/problem.mjs.map +1 -1
  235. package/esm/tooling/syntax/ast.d.mts +2 -0
  236. package/esm/tooling/typecheck/run-typecheck.mjs +5 -1
  237. package/esm/tooling/typecheck/run-typecheck.mjs.map +1 -1
  238. package/esm/upgrade/codemods/index.mjs +2 -0
  239. package/esm/upgrade/codemods/index.mjs.map +1 -1
  240. package/esm/upgrade/codemods/vite-vitest.mjs +170 -0
  241. package/esm/upgrade/codemods/vite-vitest.mjs.map +1 -0
  242. package/esm/upgrade/notes.mjs +4 -0
  243. package/esm/upgrade/notes.mjs.map +1 -1
  244. package/esm/vite/index.d.mts +3 -1
  245. package/esm/vite/index.mjs +3 -1
  246. package/esm/vite/test-setup-reporter.d.mts +16 -0
  247. package/esm/vite/test-setup-reporter.d.mts.map +1 -0
  248. package/esm/vite/test-setup-reporter.mjs +55 -0
  249. package/esm/vite/test-setup-reporter.mjs.map +1 -0
  250. package/esm/vite/test-workers.d.mts +5 -0
  251. package/esm/vite/test-workers.d.mts.map +1 -0
  252. package/esm/vite/test-workers.mjs +38 -0
  253. package/esm/vite/test-workers.mjs.map +1 -0
  254. package/esm/warlock-config/index.d.mts +2 -1
  255. package/esm/warlock-config/types.d.mts +3 -0
  256. package/esm/warlock-config/types.d.mts.map +1 -1
  257. package/llms-full.txt +874 -9
  258. package/llms.txt +2 -0
  259. package/package.json +15 -14
  260. package/schemas/inspect-report.schema.json +47 -1
  261. package/skills/build-budgets/SKILL.md +51 -0
  262. package/skills/build-restful/SKILL.md +56 -0
  263. package/skills/configure-app/SKILL.md +7 -0
  264. package/skills/create-module/SKILL.md +9 -0
  265. package/skills/generate-web-ui/SKILL.md +71 -0
  266. package/skills/index.md +2 -2
  267. package/skills/register-route/SKILL.md +13 -0
  268. package/skills/run-app/SKILL.md +1 -1
  269. package/skills/send-mail/SKILL.md +25 -0
  270. package/skills/send-response/SKILL.md +50 -0
  271. package/skills/test-browser/SKILL.md +53 -0
  272. package/skills/test-http/SKILL.md +91 -0
  273. package/skills/test-service/SKILL.md +161 -3
  274. package/skills/upgrade-from-5-29/SKILL.md +109 -0
  275. package/skills/use-localization/SKILL.md +25 -0
  276. package/skills/warlock-check/SKILL.md +75 -1
  277. package/skills/warlock-inspect/SKILL.md +45 -2
  278. package/skills/warlock-mcp/SKILL.md +4 -0
  279. package/skills/write-cli-command/SKILL.md +18 -0
  280. package/esm/production/build-app-production.mjs +0 -14
  281. package/esm/production/build-app-production.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"generate.command.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/generate/generate.command.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { command } from \"../../../commands/cli-command\";\nimport type { CommandActionData } from \"../../../commands/types\";\nimport { runGenerationCommand } from \"../../../generations/generation-run\";\nimport { generateController } from \"./generators/controller.generator\";\nimport { generateEndpoint } from \"./generators/endpoint.generator\";\nimport { generateMigration } from \"./generators/migration.generator\";\nimport { generateModel } from \"./generators/model.generator\";\nimport { generateModule } from \"./generators/module.generator\";\nimport { generateRepository } from \"./generators/repository.generator\";\nimport { generateResource } from \"./generators/resource.generator\";\nimport { generateService } from \"./generators/service.generator\";\nimport { generateUseCase } from \"./generators/use-case.generator\";\n\nconst generators: Record<string, (data: CommandActionData) => Promise<void>> = {\n module: generateModule,\n endpoint: generateEndpoint,\n controller: generateController,\n service: generateService,\n \"use-case\": generateUseCase,\n model: generateModel,\n migration: generateMigration,\n repository: generateRepository,\n resource: generateResource,\n};\n\n/**\n * Run one generator under the recorder that implements `--dry-run` and\n * `--json` (see `generations/generation-run.ts`).\n */\nfunction recorded(\n commandName: string,\n generator: (data: CommandActionData) => Promise<void>,\n): (data: CommandActionData) => Promise<void> {\n return (data) => runGenerationCommand(commandName, data, generator);\n}\n\n/** Options every generator accepts. */\nconst commonOptions = [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\" as const,\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be written or skipped without writing anything\",\n type: \"boolean\" as const,\n },\n {\n text: \"--json\",\n description:\n \"Print one JSON report on stdout: files written, files skipped because they exist, messages\",\n type: \"boolean\" as const,\n },\n];\n\n/** Options of `make endpoint` / `generate.endpoint`. */\nconst endpointOptions = [\n {\n text: \"--method <method>\",\n description: \"HTTP method: get, post, put, patch or delete (default: post)\",\n },\n {\n text: \"--path <path>\",\n description: \"Route path (default: /<module>, or /<module>/:id for put, patch and delete)\",\n },\n {\n text: \"--schema <file>\",\n description:\n \"A TypeScript file exporting the request's seal object schema; its fields shape the resource and the test\",\n },\n {\n text: \"--resource <Name>\",\n description: \"Resource the endpoint answers with (default: the module's singular name)\",\n },\n {\n text: \"--auth\",\n description: \"Guard the route with the auth middleware (the default)\",\n type: \"boolean\" as const,\n },\n {\n text: \"--public\",\n description: \"Register the route without a guard, marked public: true\",\n type: \"boolean\" as const,\n },\n];\n\n/** The `generate <generator>` / `make <generator>` dispatcher. */\nfunction dispatchGenerator(label: \"generate\" | \"make\") {\n return async (data: CommandActionData) => {\n const generatorName = data.args[0];\n const generator = generatorName ? generators[generatorName] : undefined;\n\n if (!generatorName || !generator) {\n console.log(\n colors.red(\n generatorName\n ? `Error: Unknown generator \"${generatorName}\"`\n : \"Error: Generator name is required\",\n ),\n );\n console.log(colors.yellow(\"\\nAvailable generators:\"));\n Object.keys(generators).forEach((name) => {\n console.log(colors.cyan(` - ${name}`));\n });\n console.log(colors.yellow(`\\nUsage: warlock ${label} <generator> <name> [options]`));\n process.exit(1);\n }\n\n // Remove the generator name from args for the generator\n await runGenerationCommand(\n `${label} ${generatorName}`,\n { ...data, args: data.args.slice(1) },\n generator,\n );\n };\n}\n\nconst generatorOptionsForDispatch = [\n ...commonOptions,\n {\n text: \"--overwrite\",\n description:\n \"With --force, also replace existing files that differ from the generated output (default: skip and list them)\",\n type: \"boolean\" as const,\n },\n ...endpointOptions,\n];\n\n// Master generate command\nexport const generateCommand = command({\n name: \"generate <generator> [args...]\",\n alias: \"g\", // Allows 'warlock g module ...'\n description: \"Scaffold a new component\",\n action: dispatchGenerator(\"generate\"),\n options: generatorOptionsForDispatch,\n});\n\n/**\n * `warlock make <generator>` — the same generators as `generate`, e.g.\n * `warlock make endpoint posts/create-post --schema ./schema.ts --public`.\n */\nexport const makeCommand = command({\n name: \"make <generator> [args...]\",\n description: \"Scaffold a new component (same generators as generate)\",\n action: dispatchGenerator(\"make\"),\n options: generatorOptionsForDispatch,\n});\n\n/** `warlock make.endpoint` / `generate.endpoint` — one spec-first endpoint. */\nexport const generateEndpointCommand = command({\n name: \"generate.endpoint <module>/<name>\",\n alias: \"make.endpoint\",\n description:\n \"Generate an endpoint: route, controller, validation, service, resource, locale key, OpenAPI response schema and an HTTP test\",\n action: recorded(\"make endpoint\", generateEndpoint),\n options: [...endpointOptions, ...commonOptions],\n});\n\n// Individual commands\nexport const generateModuleCommand = command({\n name: \"generate.module <name>\",\n alias: \"gen.m\",\n description: \"Generate a new module\",\n action: recorded(\"generate.module\", generateModule),\n options: [\n {\n text: \"--minimal, -m\",\n description:\n \"Generate a minimal skeleton (routes, main, locales, empty subfolders) instead of the full CRUD scaffold\",\n type: \"boolean\",\n },\n {\n text: \"--overwrite\",\n description:\n \"With --force, also replace existing files that differ from the generated output (default: skip and list them)\",\n type: \"boolean\",\n },\n ...commonOptions,\n ],\n});\n\nexport const generateControllerCommand = command({\n name: \"generate.controller <module>/<name>\",\n alias: \"gen.c\",\n description: \"Generate a new controller\",\n action: recorded(\"generate.controller\", generateController),\n options: [\n {\n text: \"--with-validation, -v\",\n description: \"Generate a validation schema and bind it to the controller\",\n type: \"boolean\",\n },\n ...commonOptions,\n ],\n});\n\nexport const generateServiceCommand = command({\n name: \"generate.service <module>/<name>\",\n alias: \"gen.s\",\n description: \"Generate a new service\",\n action: recorded(\"generate.service\", generateService),\n options: commonOptions,\n});\n\nexport const generateUseCaseCommand = command({\n name: \"generate.use-case <module>/<verb-noun>\",\n description: \"Generate a transport-agnostic use-case and its spec\",\n action: recorded(\"generate.use-case\", generateUseCase),\n options: commonOptions,\n});\n\nexport const generateModelCommand = command({\n name: \"generate.model <module>/<name>\",\n alias: \"gen.md\",\n description: \"Generate a new model with migration\",\n action: recorded(\"generate.model\", generateModel),\n options: [\n {\n text: \"--with-resource, -rs\",\n description: \"Generate resource\",\n type: \"boolean\",\n },\n {\n text: \"--table <name>\",\n description: \"Specify table name\",\n },\n {\n text: \"--overwrite\",\n description:\n \"With --force, also replace existing files that differ from the generated output (default: skip and list them)\",\n type: \"boolean\",\n },\n ...commonOptions,\n {\n text: \"--timestamps [bool]\",\n description: \"Include timestamps in migration (default: true)\",\n type: \"boolean\",\n },\n {\n text: \"--no-timestamps\",\n description: \"Disable timestamps (same as --timestamps false)\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateRepositoryCommand = command({\n name: \"generate.repository <module>/<name>\",\n alias: \"gen.r\",\n description: \"Generate a new repository\",\n action: recorded(\"generate.repository\", generateRepository),\n options: commonOptions,\n});\n\nexport const generateResourceCommand = command({\n name: \"generate.resource <module>/<name>\",\n alias: \"gen.rs\",\n description: \"Generate a new resource\",\n action: recorded(\"generate.resource\", generateResource),\n options: commonOptions,\n});\n\nexport const generateMigrationCommand = command({\n name: \"generate.migration <model-path>\",\n alias: \"gen.mig\",\n description: \"Generate a new migration for a model\",\n action: recorded(\"generate.migration\", generateMigration),\n options: [\n ...commonOptions,\n {\n text: \"--add <columns>\",\n description: \"Columns to add (DSL: name:type:modifier,...)\",\n },\n {\n text: \"--drop <columns>\",\n description: \"Columns to drop (comma-separated names)\",\n },\n {\n text: \"--rename <columns>\",\n description: \"Columns to rename (DSL: old:new,...)\",\n },\n {\n text: \"--timestamps [bool]\",\n description: \"Include timestamps (default: true)\",\n type: \"boolean\",\n },\n {\n text: \"--no-timestamps\",\n description: \"Disable timestamps (same as --timestamps false)\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"generate.command.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/generate/generate.command.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { command } from \"../../../commands/cli-command\";\nimport type { CommandActionData } from \"../../../commands/types\";\nimport { runGenerationCommand } from \"../../../generations/generation-run\";\nimport { generateController } from \"./generators/controller.generator\";\nimport { generateEndpoint } from \"./generators/endpoint.generator\";\nimport { generateMigration } from \"./generators/migration.generator\";\nimport { generateModel } from \"./generators/model.generator\";\nimport { generateModule } from \"./generators/module.generator\";\nimport { generateRepository } from \"./generators/repository.generator\";\nimport { generateResource } from \"./generators/resource.generator\";\nimport { generateService } from \"./generators/service.generator\";\nimport { generateUseCase } from \"./generators/use-case.generator\";\nimport { generatePage, generateForm, generateCrudUi } from \"./generators/web.generator\";\n\nconst generators: Record<string, (data: CommandActionData) => Promise<void>> = {\n page: generatePage,\n form: generateForm,\n \"crud-ui\": generateCrudUi,\n module: generateModule,\n endpoint: generateEndpoint,\n controller: generateController,\n service: generateService,\n \"use-case\": generateUseCase,\n model: generateModel,\n migration: generateMigration,\n repository: generateRepository,\n resource: generateResource,\n};\n\n/**\n * Run one generator under the recorder that implements `--dry-run` and\n * `--json` (see `generations/generation-run.ts`).\n */\nfunction recorded(\n commandName: string,\n generator: (data: CommandActionData) => Promise<void>,\n): (data: CommandActionData) => Promise<void> {\n return (data) => runGenerationCommand(commandName, data, generator);\n}\n\n/** Options every generator accepts. */\nconst commonOptions = [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\" as const,\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be written or skipped without writing anything\",\n type: \"boolean\" as const,\n },\n {\n text: \"--json\",\n description:\n \"Print one JSON report on stdout: files written, files skipped because they exist, messages\",\n type: \"boolean\" as const,\n },\n];\n\n/** Options of `make endpoint` / `generate.endpoint`. */\nconst endpointOptions = [\n {\n text: \"--method <method>\",\n description: \"HTTP method: get, post, put, patch or delete (default: post)\",\n },\n {\n text: \"--path <path>\",\n description: \"Route path (default: /<module>, or /<module>/:id for put, patch and delete)\",\n },\n {\n text: \"--schema <file>\",\n description:\n \"A TypeScript file exporting the request's seal object schema; its fields shape the resource and the test\",\n },\n {\n text: \"--resource <Name>\",\n description: \"Resource the endpoint answers with (default: the module's singular name)\",\n },\n {\n text: \"--auth\",\n description: \"Guard the route with the auth middleware (the default)\",\n type: \"boolean\" as const,\n },\n {\n text: \"--public\",\n description: \"Register the route without a guard, marked public: true\",\n type: \"boolean\" as const,\n },\n];\n\n/** The `generate <generator>` / `make <generator>` dispatcher. */\nfunction dispatchGenerator(label: \"generate\" | \"make\") {\n return async (data: CommandActionData) => {\n const generatorName = data.args[0];\n const generator = generatorName ? generators[generatorName] : undefined;\n\n if (!generatorName || !generator) {\n console.log(\n colors.red(\n generatorName\n ? `Error: Unknown generator \"${generatorName}\"`\n : \"Error: Generator name is required\",\n ),\n );\n console.log(colors.yellow(\"\\nAvailable generators:\"));\n Object.keys(generators).forEach((name) => {\n console.log(colors.cyan(` - ${name}`));\n });\n console.log(colors.yellow(`\\nUsage: warlock ${label} <generator> <name> [options]`));\n process.exit(1);\n }\n\n // Remove the generator name from args for the generator\n await runGenerationCommand(\n `${label} ${generatorName}`,\n { ...data, args: data.args.slice(1) },\n generator,\n );\n };\n}\n\nconst generatorOptionsForDispatch = [\n ...commonOptions,\n {\n text: \"--overwrite\",\n description:\n \"With --force, also replace existing files that differ from the generated output (default: skip and list them)\",\n type: \"boolean\" as const,\n },\n ...endpointOptions,\n { text: \"--module <name>\", description: \"Module owning the generated form\" },\n { text: \"--endpoint\", description: \"Generate a guarded form endpoint\", type: \"boolean\" as const },\n { text: \"--interactive\", description: \"Generate a page feature hook\", type: \"boolean\" as const },\n];\n\n// Master generate command\nexport const generateCommand = command({\n name: \"generate <generator> [args...]\",\n alias: \"g\", // Allows 'warlock g module ...'\n description: \"Scaffold a new component\",\n action: dispatchGenerator(\"generate\"),\n options: generatorOptionsForDispatch,\n});\n\n/**\n * `warlock make <generator>` — the same generators as `generate`, e.g.\n * `warlock make endpoint posts/create-post --schema ./schema.ts --public`.\n */\nexport const makeCommand = command({\n name: \"make <generator> [args...]\",\n description: \"Scaffold a new component (same generators as generate)\",\n action: dispatchGenerator(\"make\"),\n options: generatorOptionsForDispatch,\n});\n\n/** `warlock make.endpoint` / `generate.endpoint` — one spec-first endpoint. */\nexport const generateEndpointCommand = command({\n name: \"generate.endpoint <module>/<name>\",\n alias: \"make.endpoint\",\n description:\n \"Generate an endpoint: route, controller, validation, service, resource, locale key, OpenAPI response schema and an HTTP test\",\n action: recorded(\"make endpoint\", generateEndpoint),\n options: [...endpointOptions, ...commonOptions],\n});\n\n// Individual commands\nexport const generateModuleCommand = command({\n name: \"generate.module <name>\",\n alias: \"gen.m\",\n description: \"Generate a new module\",\n action: recorded(\"generate.module\", generateModule),\n options: [\n {\n text: \"--minimal, -m\",\n description:\n \"Generate a minimal skeleton (routes, main, locales, empty subfolders) instead of the full CRUD scaffold\",\n type: \"boolean\",\n },\n {\n text: \"--overwrite\",\n description:\n \"With --force, also replace existing files that differ from the generated output (default: skip and list them)\",\n type: \"boolean\",\n },\n ...commonOptions,\n ],\n});\n\nexport const generateControllerCommand = command({\n name: \"generate.controller <module>/<name>\",\n alias: \"gen.c\",\n description: \"Generate a new controller\",\n action: recorded(\"generate.controller\", generateController),\n options: [\n {\n text: \"--with-validation, -v\",\n description: \"Generate a validation schema and bind it to the controller\",\n type: \"boolean\",\n },\n ...commonOptions,\n ],\n});\n\nexport const generateServiceCommand = command({\n name: \"generate.service <module>/<name>\",\n alias: \"gen.s\",\n description: \"Generate a new service\",\n action: recorded(\"generate.service\", generateService),\n options: commonOptions,\n});\n\nexport const generateUseCaseCommand = command({\n name: \"generate.use-case <module>/<verb-noun>\",\n description: \"Generate a transport-agnostic use-case and its spec\",\n action: recorded(\"generate.use-case\", generateUseCase),\n options: commonOptions,\n});\n\nexport const generateModelCommand = command({\n name: \"generate.model <module>/<name>\",\n alias: \"gen.md\",\n description: \"Generate a new model with migration\",\n action: recorded(\"generate.model\", generateModel),\n options: [\n {\n text: \"--with-resource, -rs\",\n description: \"Generate resource\",\n type: \"boolean\",\n },\n {\n text: \"--table <name>\",\n description: \"Specify table name\",\n },\n {\n text: \"--overwrite\",\n description:\n \"With --force, also replace existing files that differ from the generated output (default: skip and list them)\",\n type: \"boolean\",\n },\n ...commonOptions,\n {\n text: \"--timestamps [bool]\",\n description: \"Include timestamps in migration (default: true)\",\n type: \"boolean\",\n },\n {\n text: \"--no-timestamps\",\n description: \"Disable timestamps (same as --timestamps false)\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateRepositoryCommand = command({\n name: \"generate.repository <module>/<name>\",\n alias: \"gen.r\",\n description: \"Generate a new repository\",\n action: recorded(\"generate.repository\", generateRepository),\n options: commonOptions,\n});\n\nexport const generateResourceCommand = command({\n name: \"generate.resource <module>/<name>\",\n alias: \"gen.rs\",\n description: \"Generate a new resource\",\n action: recorded(\"generate.resource\", generateResource),\n options: commonOptions,\n});\n\nexport const generateMigrationCommand = command({\n name: \"generate.migration <model-path>\",\n alias: \"gen.mig\",\n description: \"Generate a new migration for a model\",\n action: recorded(\"generate.migration\", generateMigration),\n options: [\n ...commonOptions,\n {\n text: \"--add <columns>\",\n description: \"Columns to add (DSL: name:type:modifier,...)\",\n },\n {\n text: \"--drop <columns>\",\n description: \"Columns to drop (comma-separated names)\",\n },\n {\n text: \"--rename <columns>\",\n description: \"Columns to rename (DSL: old:new,...)\",\n },\n {\n text: \"--timestamps [bool]\",\n description: \"Include timestamps (default: true)\",\n type: \"boolean\",\n },\n {\n text: \"--no-timestamps\",\n description: \"Disable timestamps (same as --timestamps false)\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":"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"}
@@ -309,5 +309,5 @@ function createEndpointGenerator(loadSchemaModule = loadSchemaModuleWithAppLoade
309
309
  const generateEndpoint = createEndpointGenerator();
310
310
 
311
311
  //#endregion
312
- export { createEndpointGenerator, generateEndpoint, pickSchemaExport, readEndpointInput, schemaModuleFromFile };
312
+ export { createEndpointGenerator, generateEndpoint, loadSchemaModuleWithAppLoader, pickSchemaExport, readEndpointInput, schemaModuleFromFile };
313
313
  //# sourceMappingURL=endpoint.generator.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"endpoint.generator.mjs","names":["putFileAsync","putEditedFileAsync"],"sources":["../../../../../../../../../../../packages/core/src/cli/commands/generate/generators/endpoint.generator.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { CommandActionData } from \"../../../../commands/types\";\nimport {\n fileExistsAsync,\n getFileAsync,\n putFileAsync as putEditedFileAsync,\n} from \"../../../../generations/generation-fs\";\nimport { recordSkippedFile, setGenerationDetail } from \"../../../../generations/generation-run\";\nimport { importAppModule } from \"../../../../loader/app-module-importer\";\nimport { appPath, srcPath } from \"../../../../utils\";\nimport {\n type EndpointAccess,\n type EndpointMethod,\n type EndpointSpec,\n endpointControllerStub,\n endpointLabel,\n endpointLocaleEntries,\n endpointResourceStub,\n endpointRouteImports,\n endpointRouteRegistration,\n endpointRoutesFileStub,\n endpointSchemaStub,\n endpointServiceStub,\n endpointTestStub,\n} from \"../templates/endpoint-stubs\";\nimport { readEndpointFields } from \"../utils/endpoint-schema\";\nimport { parseModulePath, parseName, singularName } from \"../utils/name-parser\";\nimport { isDryRun, putFileAsync, setDryRun } from \"../utils/writer\";\n\nconst METHODS: readonly EndpointMethod[] = [\"get\", \"post\", \"put\", \"patch\", \"delete\"];\n\nconst USAGE =\n \"Usage: warlock make endpoint <module>/<name> [--method post] [--path /posts] [--schema ./schema.ts] [--resource Post] [--auth | --public]\";\n\n/** Loads the `--schema` file and returns its exports. Replaceable in tests. */\nexport type SchemaModuleLoader = (absolutePath: string) => Promise<Record<string, unknown>>;\n\n/**\n * Import an app TypeScript file from the CLI: register the framework's\n * loader (path aliases, TypeScript) first, as `warlock dev` does.\n */\nconst loadSchemaModuleWithAppLoader: SchemaModuleLoader = async (absolutePath) => {\n const { filesOrchestrator } = await import(\"../../../../dev-server/files-orchestrator\");\n const { warlockConfigManager } =\n await import(\"../../../../warlock-config/warlock-config.manager\");\n\n await warlockConfigManager.load();\n await filesOrchestrator.init();\n\n return (await importAppModule(pathToFileURL(absolutePath).href)) as Record<string, unknown>;\n};\n\n/** A seal object validator, recognised structurally so a second seal copy still counts. */\ntype SchemaLike = { toJsonSchema(target?: string): unknown; schema: unknown };\n\nfunction isSchemaLike(value: unknown): value is SchemaLike {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"toJsonSchema\" in value &&\n typeof value.toJsonSchema === \"function\" &&\n \"schema\" in value &&\n typeof value.schema === \"object\"\n );\n}\n\nclass EndpointInputError extends Error {}\n\nfunction fail(message: string): never {\n throw new EndpointInputError(message);\n}\n\n/** Pick the request schema out of the `--schema` module's exports. */\nexport function pickSchemaExport(\n exports: Record<string, unknown>,\n preferredName: string,\n): { exportName: string; schema: SchemaLike } {\n const preferred = exports[preferredName];\n\n if (isSchemaLike(preferred)) return { exportName: preferredName, schema: preferred };\n\n const candidates = Object.entries(exports).filter(\n (entry): entry is [string, SchemaLike] => entry[0] !== \"default\" && isSchemaLike(entry[1]),\n );\n\n if (candidates.length === 1 && candidates[0]) {\n return { exportName: candidates[0][0], schema: candidates[0][1] };\n }\n\n const fallback = exports.default;\n\n if (candidates.length === 0 && isSchemaLike(fallback)) {\n return { exportName: \"default\", schema: fallback };\n }\n\n const names = candidates.map(([name]) => name);\n\n return fail(\n names.length === 0\n ? `The schema file exports no seal object schema (v.object(...)). Export one, e.g. \\`export const ${preferredName} = v.object({ ... })\\`.`\n : `The schema file exports several object schemas (${names.join(\", \")}). Name the one for this endpoint \\`${preferredName}\\`.`,\n );\n}\n\n/**\n * The `schema/<name>.schema.ts` content for a `--schema` file.\n *\n * A file already under `src/` is imported where it lives (one source of\n * truth). A file outside `src/` — a scratch spec — is copied in, with its\n * relative imports re-pointed at the new location. Either way the module\n * exports `<name>Schema` and its `<Name>Schema` type, the names the\n * controller and service import.\n */\nexport function schemaModuleFromFile(options: {\n sourcePath: string;\n source: string;\n exportName: string;\n targetPath: string;\n valueName: string;\n typeName: string;\n}): string {\n const { sourcePath, source, exportName, targetPath, valueName, typeName } = options;\n const targetDirectory = path.dirname(targetPath);\n\n if (isInside(srcPath(), sourcePath)) {\n const specifier = toImportSpecifier(targetDirectory, sourcePath);\n const header = `import type { Infer } from \"@warlock.js/seal\";`;\n const footer = `export type ${typeName} = Infer<typeof ${valueName}>;\\n`;\n\n if (exportName === valueName) {\n return `${header}\\nimport { ${valueName} } from \"${specifier}\";\\n\\nexport { ${valueName} };\\n\\n${footer}`;\n }\n\n const local = exportName === \"default\" ? \"source\" : exportName;\n const importLine =\n exportName === \"default\"\n ? `import source from \"${specifier}\";`\n : `import { ${exportName} } from \"${specifier}\";`;\n\n return `${header}\\n${importLine}\\n\\nexport const ${valueName} = ${local};\\n\\n${footer}`;\n }\n\n let content = source.replace(\n /(from\\s+|import\\s*\\(\\s*|import\\s+)([\"'])(\\.{1,2}\\/[^\"']+)\\2/g,\n (_match, prefix: string, quote: string, specifier: string) => {\n const resolved = path.resolve(path.dirname(sourcePath), specifier);\n\n return `${prefix}${quote}${toImportSpecifier(targetDirectory, resolved)}${quote}`;\n },\n );\n\n if (exportName === \"default\") {\n content = content.replace(/export\\s+default\\s+/, `export const ${valueName} = `);\n } else if (exportName !== valueName) {\n content = `${content.trimEnd()}\\n\\nexport const ${valueName} = ${exportName};\\n`;\n }\n\n const hasType = new RegExp(`export\\\\s+type\\\\s+${typeName}\\\\b`).test(content);\n\n if (!hasType) {\n const importsInfer = /\\bInfer\\b/.test(\n content\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"import\"))\n .join(\"\\n\"),\n );\n\n if (!importsInfer) content = `import type { Infer } from \"@warlock.js/seal\";\\n${content}`;\n\n content = `${content.trimEnd()}\\n\\nexport type ${typeName} = Infer<typeof ${valueName}>;\\n`;\n }\n\n return content;\n}\n\nfunction isInside(directory: string, file: string): boolean {\n const relative = path.relative(directory, file);\n\n return relative !== \"\" && !relative.startsWith(\"..\") && !path.isAbsolute(relative);\n}\n\n/** A relative, extensionless, `/`-separated import specifier from `fromDirectory` to `file`. */\nfunction toImportSpecifier(fromDirectory: string, file: string): string {\n const withoutExtension = file.replace(/\\.(?:ts|tsx|mts|js|mjs)$/, \"\");\n const relative = path.relative(fromDirectory, withoutExtension).split(path.sep).join(\"/\");\n\n return relative.startsWith(\".\") ? relative : `./${relative}`;\n}\n\n/** Parse and check the command input. Throws {@link EndpointInputError} with a fix. */\nexport function readEndpointInput(data: CommandActionData): {\n module: string;\n name: string;\n method: EndpointMethod;\n path: string;\n access: EndpointAccess;\n resource: string;\n schemaFile?: string;\n force: boolean;\n} {\n const input = data.args[0];\n\n if (!input) fail(`Endpoint name is required.\\n${USAGE}`);\n\n const { module, name } = parseModulePath(input);\n\n if (!module || !name)\n fail(`Write the endpoint as <module>/<name>, e.g. posts/create-post.\\n${USAGE}`);\n\n const moduleName = parseName(module).kebab;\n const rawMethod = String(data.options.method ?? \"post\").toLowerCase();\n const method = METHODS.find((candidate) => candidate === rawMethod);\n\n if (!method) fail(`Unknown --method \"${rawMethod}\". Use one of: ${METHODS.join(\", \")}.`);\n\n const wantsAuth = data.options.auth === true;\n const wantsPublic = data.options.public === true;\n\n if (wantsAuth && wantsPublic) fail(\"Pass either --auth or --public, not both.\");\n\n const rawPath = typeof data.options.path === \"string\" ? data.options.path : undefined;\n const defaultPath =\n method === \"get\" || method === \"post\" ? `/${moduleName}` : `/${moduleName}/:id`;\n const routePath = rawPath ? (rawPath.startsWith(\"/\") ? rawPath : `/${rawPath}`) : defaultPath;\n const resource =\n typeof data.options.resource === \"string\" && data.options.resource.trim() !== \"\"\n ? data.options.resource.trim()\n : singularName(moduleName).pascal;\n const schemaFile =\n typeof data.options.schema === \"string\" && data.options.schema.trim() !== \"\"\n ? data.options.schema.trim()\n : undefined;\n\n return {\n module: moduleName,\n name,\n method,\n path: routePath,\n // Guarded unless the caller says, in so many words, that it is public.\n access: wantsPublic ? \"public\" : \"guarded\",\n resource,\n schemaFile,\n force: data.options.force === true || data.options.f === true,\n };\n}\n\n/**\n * Write a whole generated file unless it exists. Without `--force` an\n * existing file is the app's own code: it is skipped and reported, never\n * overwritten.\n */\nasync function writeNewFile(target: string, content: string, force: boolean): Promise<void> {\n if (!force && (await fileExistsAsync(target))) {\n recordSkippedFile(target, \"exists (pass --force to overwrite)\");\n console.log(`${colors.yellow(\"-\")} skipped ${colors.gray(displayPath(target))} (exists)`);\n return;\n }\n\n await putFileAsync(target, content);\n}\n\nfunction displayPath(target: string): string {\n return path.relative(process.cwd(), target).split(path.sep).join(\"/\");\n}\n\n/** Append the endpoint to the module's `routes.ts`, creating the file if needed. */\nasync function registerRoute(spec: EndpointSpec, routesPath: string): Promise<void> {\n if (!existsSync(routesPath)) {\n await putEditedFileAsync(routesPath, endpointRoutesFileStub(spec));\n logEdit(routesPath, \"created\");\n return;\n }\n\n const current = await getFileAsync(routesPath);\n const controller = `${spec.name.camel}Controller`;\n\n if (new RegExp(`\\\\b${controller}\\\\b`).test(current)) {\n recordSkippedFile(routesPath, `${controller} is already registered`);\n console.log(\n `${colors.yellow(\"-\")} skipped ${colors.gray(displayPath(routesPath))} (${controller} is already registered)`,\n );\n return;\n }\n\n const imports = [...endpointRouteImports(spec)];\n\n if (!/import\\s*\\{[^}]*\\brouter\\b[^}]*\\}\\s*from\\s*[\"']@warlock\\.js\\/core[\"']/.test(current)) {\n imports.unshift(`import { router } from \"@warlock.js/core\";`);\n }\n\n if (spec.access === \"guarded\" && !/\\bguarded\\b/.test(current)) {\n imports.unshift(`import { guarded } from \"app/shared/utils/router\";`);\n }\n\n const lines = current.split(\"\\n\");\n let lastImport = -1;\n let inImport = false;\n\n // An import may span lines; new imports go after the last statement ends.\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index] ?? \"\";\n\n if (/^import\\s/.test(line)) inImport = true;\n\n if (inImport && (/;\\s*$/.test(line) || /from\\s+[\"'][^\"']+[\"']\\s*$/.test(line))) {\n lastImport = index;\n inImport = false;\n }\n }\n\n lines.splice(lastImport + 1, 0, ...imports);\n\n const updated = `${lines.join(\"\\n\").trimEnd()}\\n\\n${endpointRouteRegistration(spec)}`;\n\n await putEditedFileAsync(routesPath, updated);\n logEdit(routesPath, \"updated\");\n}\n\n/** Merge the endpoint's message into `utils/locales.json`; existing keys win. */\nasync function registerLocales(spec: EndpointSpec, localesPath: string): Promise<void> {\n const entries = endpointLocaleEntries(spec);\n\n if (!existsSync(localesPath)) {\n await putEditedFileAsync(localesPath, `${JSON.stringify(entries, null, 2)}\\n`);\n logEdit(localesPath, \"created\");\n return;\n }\n\n let dictionary: Record<string, unknown>;\n\n try {\n const parsed: unknown = JSON.parse(await getFileAsync(localesPath));\n\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) throw new Error();\n\n dictionary = parsed as Record<string, unknown>;\n } catch {\n recordSkippedFile(localesPath, \"not a JSON object; add the message key by hand\");\n console.log(\n colors.yellow(\n `! ${displayPath(localesPath)} is not a JSON object; add ${Object.keys(entries).join(\", \")} by hand.`,\n ),\n );\n return;\n }\n\n if (dictionary.$group !== undefined && dictionary.$group !== spec.module) {\n recordSkippedFile(localesPath, `declares $group \"${String(dictionary.$group)}\"`);\n console.log(\n colors.yellow(\n `! ${displayPath(localesPath)} declares another $group; add ${spec.module}.${Object.keys(entries).join(\", \")} by hand.`,\n ),\n );\n return;\n }\n\n const missing = Object.keys(entries).filter((key) => dictionary[key] === undefined);\n\n if (missing.length === 0) {\n recordSkippedFile(localesPath, \"message key already present\");\n return;\n }\n\n for (const key of missing) dictionary[key] = entries[key];\n\n await putEditedFileAsync(localesPath, `${JSON.stringify(dictionary, null, 2)}\\n`);\n logEdit(localesPath, \"updated\");\n}\n\nfunction logEdit(target: string, verb: \"created\" | \"updated\"): void {\n const prefix = isDryRun()\n ? `${colors.cyan(\"○\")} would ${verb === \"created\" ? \"create\" : \"update\"}`\n : `${colors.green(\"✓\")} ${verb}`;\n\n console.log(`${prefix} ${colors.gray(displayPath(target))}`);\n}\n\n/**\n * `warlock make endpoint <module>/<name>` — a spec-first endpoint.\n *\n * Writes, under `src/app/<module>/`: the request schema (from `--schema` or a\n * stub), a service, the resource (unless it exists), one controller with its\n * validation and OpenAPI `responseSchema`, the route (guarded, or\n * `public: true` with `--public`), the success message in `utils/locales.json`,\n * and `tests/<name>.test.ts`, which requests the endpoint over the real HTTP\n * stack. Existing files are skipped and reported unless `--force`.\n */\nexport function createEndpointGenerator(\n loadSchemaModule: SchemaModuleLoader = loadSchemaModuleWithAppLoader,\n): (data: CommandActionData) => Promise<void> {\n return async (data) => {\n let input: ReturnType<typeof readEndpointInput>;\n\n try {\n input = readEndpointInput(data);\n } catch (error) {\n if (!(error instanceof EndpointInputError)) throw error;\n\n console.log(colors.red(`Error: ${error.message}`));\n process.exit(1);\n }\n\n setDryRun(data.options.dryRun === true);\n\n const name = parseName(input.name);\n const modulePath = appPath(input.module);\n const schemaPath = path.join(modulePath, \"schema\", `${name.kebab}.schema.ts`);\n const valueName = `${name.camel}Schema`;\n const typeName = `${name.pascal}Schema`;\n\n let jsonSchema: unknown;\n let schemaContent: string | undefined;\n\n if (input.schemaFile) {\n const sourcePath = path.resolve(process.cwd(), input.schemaFile);\n\n if (!existsSync(sourcePath)) {\n console.log(colors.red(`Error: Schema file not found: ${input.schemaFile}`));\n process.exit(1);\n }\n\n let exports: Record<string, unknown>;\n\n try {\n exports = await loadSchemaModule(sourcePath);\n } catch (error) {\n console.log(\n colors.red(\n `Error: Could not load ${input.schemaFile} to read its fields: ${error instanceof Error ? error.message : String(error)}`,\n ),\n );\n process.exit(1);\n }\n\n let picked: ReturnType<typeof pickSchemaExport>;\n\n try {\n picked = pickSchemaExport(exports, valueName);\n } catch (error) {\n console.log(colors.red(`Error: ${error instanceof Error ? error.message : String(error)}`));\n process.exit(1);\n }\n\n jsonSchema = picked.schema.toJsonSchema(\"draft-2020-12\");\n\n if (path.resolve(sourcePath) !== path.resolve(schemaPath)) {\n schemaContent = schemaModuleFromFile({\n sourcePath,\n source: readFileSync(sourcePath, \"utf8\"),\n exportName: picked.exportName,\n targetPath: schemaPath,\n valueName,\n typeName,\n });\n }\n }\n\n const spec: EndpointSpec = {\n module: input.module,\n name,\n method: input.method,\n path: input.path,\n access: input.access,\n resource: parseName(input.resource),\n fields: readEndpointFields(jsonSchema),\n };\n\n const resourcePath = path.join(\n modulePath,\n \"resources\",\n `${spec.resource.singular.kebab}.resource.ts`,\n );\n const testPath = path.join(modulePath, \"tests\", `${name.kebab}.test.ts`);\n\n setGenerationDetail(\"endpoint\", {\n method: spec.method.toUpperCase(),\n path: spec.path,\n access: spec.access,\n controller: `${name.camel}Controller`,\n resource: `${spec.resource.pascal}Resource`,\n fields: spec.fields.map((field) => field.name),\n test: displayPath(testPath),\n });\n\n console.log(colors.cyan(`\\nGenerating ${endpointLabel(spec)} in src/app/${spec.module}\\n`));\n\n if (input.schemaFile === undefined) {\n await writeNewFile(schemaPath, endpointSchemaStub(spec), input.force);\n } else if (schemaContent !== undefined) {\n await writeNewFile(schemaPath, schemaContent, input.force);\n }\n\n await writeNewFile(\n path.join(modulePath, \"services\", `${name.kebab}.service.ts`),\n endpointServiceStub(spec),\n input.force,\n );\n await writeNewFile(resourcePath, endpointResourceStub(spec), input.force);\n await writeNewFile(\n path.join(modulePath, \"controllers\", `${name.kebab}.controller.ts`),\n endpointControllerStub(spec),\n input.force,\n );\n await registerRoute(spec, path.join(modulePath, \"routes.ts\"));\n await registerLocales(spec, path.join(modulePath, \"utils\", \"locales.json\"));\n await writeNewFile(testPath, endpointTestStub(spec), input.force);\n\n console.log(\n colors.cyan(\n `\\n${endpointLabel(spec)} ${isDryRun() ? \"previewed\" : \"generated\"} (${spec.access === \"public\" ? \"public: true\" : \"guarded\"}).`,\n ),\n );\n console.log(\n colors.gray(\n `Next: fill in services/${name.kebab}.service.ts, then run the test with \\`warlock test --related ${displayPath(testPath)}\\`.`,\n ),\n );\n };\n}\n\nexport const generateEndpoint = createEndpointGenerator();\n"],"mappings":"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"}
1
+ {"version":3,"file":"endpoint.generator.mjs","names":["putFileAsync","putEditedFileAsync"],"sources":["../../../../../../../../../../../packages/core/src/cli/commands/generate/generators/endpoint.generator.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { CommandActionData } from \"../../../../commands/types\";\nimport {\n fileExistsAsync,\n getFileAsync,\n putFileAsync as putEditedFileAsync,\n} from \"../../../../generations/generation-fs\";\nimport { recordSkippedFile, setGenerationDetail } from \"../../../../generations/generation-run\";\nimport { importAppModule } from \"../../../../loader/app-module-importer\";\nimport { appPath, srcPath } from \"../../../../utils\";\nimport {\n type EndpointAccess,\n type EndpointMethod,\n type EndpointSpec,\n endpointControllerStub,\n endpointLabel,\n endpointLocaleEntries,\n endpointResourceStub,\n endpointRouteImports,\n endpointRouteRegistration,\n endpointRoutesFileStub,\n endpointSchemaStub,\n endpointServiceStub,\n endpointTestStub,\n} from \"../templates/endpoint-stubs\";\nimport { readEndpointFields } from \"../utils/endpoint-schema\";\nimport { parseModulePath, parseName, singularName } from \"../utils/name-parser\";\nimport { isDryRun, putFileAsync, setDryRun } from \"../utils/writer\";\n\nconst METHODS: readonly EndpointMethod[] = [\"get\", \"post\", \"put\", \"patch\", \"delete\"];\n\nconst USAGE =\n \"Usage: warlock make endpoint <module>/<name> [--method post] [--path /posts] [--schema ./schema.ts] [--resource Post] [--auth | --public]\";\n\n/** Loads the `--schema` file and returns its exports. Replaceable in tests. */\nexport type SchemaModuleLoader = (absolutePath: string) => Promise<Record<string, unknown>>;\n\n/**\n * Import an app TypeScript file from the CLI: register the framework's\n * loader (path aliases, TypeScript) first, as `warlock dev` does.\n */\nexport const loadSchemaModuleWithAppLoader: SchemaModuleLoader = async (absolutePath) => {\n const { filesOrchestrator } = await import(\"../../../../dev-server/files-orchestrator\");\n const { warlockConfigManager } =\n await import(\"../../../../warlock-config/warlock-config.manager\");\n\n await warlockConfigManager.load();\n await filesOrchestrator.init();\n\n return (await importAppModule(pathToFileURL(absolutePath).href)) as Record<string, unknown>;\n};\n\n/** A seal object validator, recognised structurally so a second seal copy still counts. */\ntype SchemaLike = { toJsonSchema(target?: string): unknown; schema: unknown };\n\nfunction isSchemaLike(value: unknown): value is SchemaLike {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"toJsonSchema\" in value &&\n typeof value.toJsonSchema === \"function\" &&\n \"schema\" in value &&\n typeof value.schema === \"object\"\n );\n}\n\nclass EndpointInputError extends Error {}\n\nfunction fail(message: string): never {\n throw new EndpointInputError(message);\n}\n\n/** Pick the request schema out of the `--schema` module's exports. */\nexport function pickSchemaExport(\n exports: Record<string, unknown>,\n preferredName: string,\n): { exportName: string; schema: SchemaLike } {\n const preferred = exports[preferredName];\n\n if (isSchemaLike(preferred)) return { exportName: preferredName, schema: preferred };\n\n const candidates = Object.entries(exports).filter(\n (entry): entry is [string, SchemaLike] => entry[0] !== \"default\" && isSchemaLike(entry[1]),\n );\n\n if (candidates.length === 1 && candidates[0]) {\n return { exportName: candidates[0][0], schema: candidates[0][1] };\n }\n\n const fallback = exports.default;\n\n if (candidates.length === 0 && isSchemaLike(fallback)) {\n return { exportName: \"default\", schema: fallback };\n }\n\n const names = candidates.map(([name]) => name);\n\n return fail(\n names.length === 0\n ? `The schema file exports no seal object schema (v.object(...)). Export one, e.g. \\`export const ${preferredName} = v.object({ ... })\\`.`\n : `The schema file exports several object schemas (${names.join(\", \")}). Name the one for this endpoint \\`${preferredName}\\`.`,\n );\n}\n\n/**\n * The `schema/<name>.schema.ts` content for a `--schema` file.\n *\n * A file already under `src/` is imported where it lives (one source of\n * truth). A file outside `src/` — a scratch spec — is copied in, with its\n * relative imports re-pointed at the new location. Either way the module\n * exports `<name>Schema` and its `<Name>Schema` type, the names the\n * controller and service import.\n */\nexport function schemaModuleFromFile(options: {\n sourcePath: string;\n source: string;\n exportName: string;\n targetPath: string;\n valueName: string;\n typeName: string;\n}): string {\n const { sourcePath, source, exportName, targetPath, valueName, typeName } = options;\n const targetDirectory = path.dirname(targetPath);\n\n if (isInside(srcPath(), sourcePath)) {\n const specifier = toImportSpecifier(targetDirectory, sourcePath);\n const header = `import type { Infer } from \"@warlock.js/seal\";`;\n const footer = `export type ${typeName} = Infer<typeof ${valueName}>;\\n`;\n\n if (exportName === valueName) {\n return `${header}\\nimport { ${valueName} } from \"${specifier}\";\\n\\nexport { ${valueName} };\\n\\n${footer}`;\n }\n\n const local = exportName === \"default\" ? \"source\" : exportName;\n const importLine =\n exportName === \"default\"\n ? `import source from \"${specifier}\";`\n : `import { ${exportName} } from \"${specifier}\";`;\n\n return `${header}\\n${importLine}\\n\\nexport const ${valueName} = ${local};\\n\\n${footer}`;\n }\n\n let content = source.replace(\n /(from\\s+|import\\s*\\(\\s*|import\\s+)([\"'])(\\.{1,2}\\/[^\"']+)\\2/g,\n (_match, prefix: string, quote: string, specifier: string) => {\n const resolved = path.resolve(path.dirname(sourcePath), specifier);\n\n return `${prefix}${quote}${toImportSpecifier(targetDirectory, resolved)}${quote}`;\n },\n );\n\n if (exportName === \"default\") {\n content = content.replace(/export\\s+default\\s+/, `export const ${valueName} = `);\n } else if (exportName !== valueName) {\n content = `${content.trimEnd()}\\n\\nexport const ${valueName} = ${exportName};\\n`;\n }\n\n const hasType = new RegExp(`export\\\\s+type\\\\s+${typeName}\\\\b`).test(content);\n\n if (!hasType) {\n const importsInfer = /\\bInfer\\b/.test(\n content\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"import\"))\n .join(\"\\n\"),\n );\n\n if (!importsInfer) content = `import type { Infer } from \"@warlock.js/seal\";\\n${content}`;\n\n content = `${content.trimEnd()}\\n\\nexport type ${typeName} = Infer<typeof ${valueName}>;\\n`;\n }\n\n return content;\n}\n\nfunction isInside(directory: string, file: string): boolean {\n const relative = path.relative(directory, file);\n\n return relative !== \"\" && !relative.startsWith(\"..\") && !path.isAbsolute(relative);\n}\n\n/** A relative, extensionless, `/`-separated import specifier from `fromDirectory` to `file`. */\nfunction toImportSpecifier(fromDirectory: string, file: string): string {\n const withoutExtension = file.replace(/\\.(?:ts|tsx|mts|js|mjs)$/, \"\");\n const relative = path.relative(fromDirectory, withoutExtension).split(path.sep).join(\"/\");\n\n return relative.startsWith(\".\") ? relative : `./${relative}`;\n}\n\n/** Parse and check the command input. Throws {@link EndpointInputError} with a fix. */\nexport function readEndpointInput(data: CommandActionData): {\n module: string;\n name: string;\n method: EndpointMethod;\n path: string;\n access: EndpointAccess;\n resource: string;\n schemaFile?: string;\n force: boolean;\n} {\n const input = data.args[0];\n\n if (!input) fail(`Endpoint name is required.\\n${USAGE}`);\n\n const { module, name } = parseModulePath(input);\n\n if (!module || !name)\n fail(`Write the endpoint as <module>/<name>, e.g. posts/create-post.\\n${USAGE}`);\n\n const moduleName = parseName(module).kebab;\n const rawMethod = String(data.options.method ?? \"post\").toLowerCase();\n const method = METHODS.find((candidate) => candidate === rawMethod);\n\n if (!method) fail(`Unknown --method \"${rawMethod}\". Use one of: ${METHODS.join(\", \")}.`);\n\n const wantsAuth = data.options.auth === true;\n const wantsPublic = data.options.public === true;\n\n if (wantsAuth && wantsPublic) fail(\"Pass either --auth or --public, not both.\");\n\n const rawPath = typeof data.options.path === \"string\" ? data.options.path : undefined;\n const defaultPath =\n method === \"get\" || method === \"post\" ? `/${moduleName}` : `/${moduleName}/:id`;\n const routePath = rawPath ? (rawPath.startsWith(\"/\") ? rawPath : `/${rawPath}`) : defaultPath;\n const resource =\n typeof data.options.resource === \"string\" && data.options.resource.trim() !== \"\"\n ? data.options.resource.trim()\n : singularName(moduleName).pascal;\n const schemaFile =\n typeof data.options.schema === \"string\" && data.options.schema.trim() !== \"\"\n ? data.options.schema.trim()\n : undefined;\n\n return {\n module: moduleName,\n name,\n method,\n path: routePath,\n // Guarded unless the caller says, in so many words, that it is public.\n access: wantsPublic ? \"public\" : \"guarded\",\n resource,\n schemaFile,\n force: data.options.force === true || data.options.f === true,\n };\n}\n\n/**\n * Write a whole generated file unless it exists. Without `--force` an\n * existing file is the app's own code: it is skipped and reported, never\n * overwritten.\n */\nasync function writeNewFile(target: string, content: string, force: boolean): Promise<void> {\n if (!force && (await fileExistsAsync(target))) {\n recordSkippedFile(target, \"exists (pass --force to overwrite)\");\n console.log(`${colors.yellow(\"-\")} skipped ${colors.gray(displayPath(target))} (exists)`);\n return;\n }\n\n await putFileAsync(target, content);\n}\n\nfunction displayPath(target: string): string {\n return path.relative(process.cwd(), target).split(path.sep).join(\"/\");\n}\n\n/** Append the endpoint to the module's `routes.ts`, creating the file if needed. */\nasync function registerRoute(spec: EndpointSpec, routesPath: string): Promise<void> {\n if (!existsSync(routesPath)) {\n await putEditedFileAsync(routesPath, endpointRoutesFileStub(spec));\n logEdit(routesPath, \"created\");\n return;\n }\n\n const current = await getFileAsync(routesPath);\n const controller = `${spec.name.camel}Controller`;\n\n if (new RegExp(`\\\\b${controller}\\\\b`).test(current)) {\n recordSkippedFile(routesPath, `${controller} is already registered`);\n console.log(\n `${colors.yellow(\"-\")} skipped ${colors.gray(displayPath(routesPath))} (${controller} is already registered)`,\n );\n return;\n }\n\n const imports = [...endpointRouteImports(spec)];\n\n if (!/import\\s*\\{[^}]*\\brouter\\b[^}]*\\}\\s*from\\s*[\"']@warlock\\.js\\/core[\"']/.test(current)) {\n imports.unshift(`import { router } from \"@warlock.js/core\";`);\n }\n\n if (spec.access === \"guarded\" && !/\\bguarded\\b/.test(current)) {\n imports.unshift(`import { guarded } from \"app/shared/utils/router\";`);\n }\n\n const lines = current.split(\"\\n\");\n let lastImport = -1;\n let inImport = false;\n\n // An import may span lines; new imports go after the last statement ends.\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index] ?? \"\";\n\n if (/^import\\s/.test(line)) inImport = true;\n\n if (inImport && (/;\\s*$/.test(line) || /from\\s+[\"'][^\"']+[\"']\\s*$/.test(line))) {\n lastImport = index;\n inImport = false;\n }\n }\n\n lines.splice(lastImport + 1, 0, ...imports);\n\n const updated = `${lines.join(\"\\n\").trimEnd()}\\n\\n${endpointRouteRegistration(spec)}`;\n\n await putEditedFileAsync(routesPath, updated);\n logEdit(routesPath, \"updated\");\n}\n\n/** Merge the endpoint's message into `utils/locales.json`; existing keys win. */\nasync function registerLocales(spec: EndpointSpec, localesPath: string): Promise<void> {\n const entries = endpointLocaleEntries(spec);\n\n if (!existsSync(localesPath)) {\n await putEditedFileAsync(localesPath, `${JSON.stringify(entries, null, 2)}\\n`);\n logEdit(localesPath, \"created\");\n return;\n }\n\n let dictionary: Record<string, unknown>;\n\n try {\n const parsed: unknown = JSON.parse(await getFileAsync(localesPath));\n\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) throw new Error();\n\n dictionary = parsed as Record<string, unknown>;\n } catch {\n recordSkippedFile(localesPath, \"not a JSON object; add the message key by hand\");\n console.log(\n colors.yellow(\n `! ${displayPath(localesPath)} is not a JSON object; add ${Object.keys(entries).join(\", \")} by hand.`,\n ),\n );\n return;\n }\n\n if (dictionary.$group !== undefined && dictionary.$group !== spec.module) {\n recordSkippedFile(localesPath, `declares $group \"${String(dictionary.$group)}\"`);\n console.log(\n colors.yellow(\n `! ${displayPath(localesPath)} declares another $group; add ${spec.module}.${Object.keys(entries).join(\", \")} by hand.`,\n ),\n );\n return;\n }\n\n const missing = Object.keys(entries).filter((key) => dictionary[key] === undefined);\n\n if (missing.length === 0) {\n recordSkippedFile(localesPath, \"message key already present\");\n return;\n }\n\n for (const key of missing) dictionary[key] = entries[key];\n\n await putEditedFileAsync(localesPath, `${JSON.stringify(dictionary, null, 2)}\\n`);\n logEdit(localesPath, \"updated\");\n}\n\nfunction logEdit(target: string, verb: \"created\" | \"updated\"): void {\n const prefix = isDryRun()\n ? `${colors.cyan(\"○\")} would ${verb === \"created\" ? \"create\" : \"update\"}`\n : `${colors.green(\"✓\")} ${verb}`;\n\n console.log(`${prefix} ${colors.gray(displayPath(target))}`);\n}\n\n/**\n * `warlock make endpoint <module>/<name>` — a spec-first endpoint.\n *\n * Writes, under `src/app/<module>/`: the request schema (from `--schema` or a\n * stub), a service, the resource (unless it exists), one controller with its\n * validation and OpenAPI `responseSchema`, the route (guarded, or\n * `public: true` with `--public`), the success message in `utils/locales.json`,\n * and `tests/<name>.test.ts`, which requests the endpoint over the real HTTP\n * stack. Existing files are skipped and reported unless `--force`.\n */\nexport function createEndpointGenerator(\n loadSchemaModule: SchemaModuleLoader = loadSchemaModuleWithAppLoader,\n): (data: CommandActionData) => Promise<void> {\n return async (data) => {\n let input: ReturnType<typeof readEndpointInput>;\n\n try {\n input = readEndpointInput(data);\n } catch (error) {\n if (!(error instanceof EndpointInputError)) throw error;\n\n console.log(colors.red(`Error: ${error.message}`));\n process.exit(1);\n }\n\n setDryRun(data.options.dryRun === true);\n\n const name = parseName(input.name);\n const modulePath = appPath(input.module);\n const schemaPath = path.join(modulePath, \"schema\", `${name.kebab}.schema.ts`);\n const valueName = `${name.camel}Schema`;\n const typeName = `${name.pascal}Schema`;\n\n let jsonSchema: unknown;\n let schemaContent: string | undefined;\n\n if (input.schemaFile) {\n const sourcePath = path.resolve(process.cwd(), input.schemaFile);\n\n if (!existsSync(sourcePath)) {\n console.log(colors.red(`Error: Schema file not found: ${input.schemaFile}`));\n process.exit(1);\n }\n\n let exports: Record<string, unknown>;\n\n try {\n exports = await loadSchemaModule(sourcePath);\n } catch (error) {\n console.log(\n colors.red(\n `Error: Could not load ${input.schemaFile} to read its fields: ${error instanceof Error ? error.message : String(error)}`,\n ),\n );\n process.exit(1);\n }\n\n let picked: ReturnType<typeof pickSchemaExport>;\n\n try {\n picked = pickSchemaExport(exports, valueName);\n } catch (error) {\n console.log(colors.red(`Error: ${error instanceof Error ? error.message : String(error)}`));\n process.exit(1);\n }\n\n jsonSchema = picked.schema.toJsonSchema(\"draft-2020-12\");\n\n if (path.resolve(sourcePath) !== path.resolve(schemaPath)) {\n schemaContent = schemaModuleFromFile({\n sourcePath,\n source: readFileSync(sourcePath, \"utf8\"),\n exportName: picked.exportName,\n targetPath: schemaPath,\n valueName,\n typeName,\n });\n }\n }\n\n const spec: EndpointSpec = {\n module: input.module,\n name,\n method: input.method,\n path: input.path,\n access: input.access,\n resource: parseName(input.resource),\n fields: readEndpointFields(jsonSchema),\n };\n\n const resourcePath = path.join(\n modulePath,\n \"resources\",\n `${spec.resource.singular.kebab}.resource.ts`,\n );\n const testPath = path.join(modulePath, \"tests\", `${name.kebab}.test.ts`);\n\n setGenerationDetail(\"endpoint\", {\n method: spec.method.toUpperCase(),\n path: spec.path,\n access: spec.access,\n controller: `${name.camel}Controller`,\n resource: `${spec.resource.pascal}Resource`,\n fields: spec.fields.map((field) => field.name),\n test: displayPath(testPath),\n });\n\n console.log(colors.cyan(`\\nGenerating ${endpointLabel(spec)} in src/app/${spec.module}\\n`));\n\n if (input.schemaFile === undefined) {\n await writeNewFile(schemaPath, endpointSchemaStub(spec), input.force);\n } else if (schemaContent !== undefined) {\n await writeNewFile(schemaPath, schemaContent, input.force);\n }\n\n await writeNewFile(\n path.join(modulePath, \"services\", `${name.kebab}.service.ts`),\n endpointServiceStub(spec),\n input.force,\n );\n await writeNewFile(resourcePath, endpointResourceStub(spec), input.force);\n await writeNewFile(\n path.join(modulePath, \"controllers\", `${name.kebab}.controller.ts`),\n endpointControllerStub(spec),\n input.force,\n );\n await registerRoute(spec, path.join(modulePath, \"routes.ts\"));\n await registerLocales(spec, path.join(modulePath, \"utils\", \"locales.json\"));\n await writeNewFile(testPath, endpointTestStub(spec), input.force);\n\n console.log(\n colors.cyan(\n `\\n${endpointLabel(spec)} ${isDryRun() ? \"previewed\" : \"generated\"} (${spec.access === \"public\" ? \"public: true\" : \"guarded\"}).`,\n ),\n );\n console.log(\n colors.gray(\n `Next: fill in services/${name.kebab}.service.ts, then run the test with \\`warlock test --related ${displayPath(testPath)}\\`.`,\n ),\n );\n };\n}\n\nexport const generateEndpoint = createEndpointGenerator();\n"],"mappings":"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"}
@@ -0,0 +1,169 @@
1
+ import { srcPath } from "../../../../utils/paths.mjs";
2
+ import "../../../../utils/index.mjs";
3
+ import { forEachChild } from "../../../../tooling/syntax/ast.mjs";
4
+ import { getFileAsync, putFileAsync } from "../../../../generations/generation-fs.mjs";
5
+ import { crudRoutesStub } from "../templates/stubs.mjs";
6
+ import { parseName, pluralName } from "../utils/name-parser.mjs";
7
+ import { ensureDirectoryAsync, putFileAsync as putFileAsync$1, reportSkippedFiles, setDryRun, setSkipExisting } from "../utils/writer.mjs";
8
+ import { loadSchemaModuleWithAppLoader, pickSchemaExport } from "./endpoint.generator.mjs";
9
+ import { crudFiles, formFiles, pageFiles } from "../templates/web-stubs.mjs";
10
+ import path from "node:path";
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { parseSync } from "oxc-parser";
13
+
14
+ //#region ../../packages/core/src/cli/commands/generate/generators/web.generator.ts
15
+ /** Refuse traversal, absolute paths and source fragments before computing output paths. */
16
+ function webPath(input) {
17
+ if (!input || !/^[a-zA-Z][a-zA-Z0-9_-]*(\/[a-zA-Z][a-zA-Z0-9_-]*)*$/.test(input)) throw new Error("Use a relative name such as products or account/settings (letters, numbers, hyphens and underscores).");
18
+ return input.split("/").map((part) => parseName(part).kebab).join("/");
19
+ }
20
+ async function writeFiles(directory, files, data) {
21
+ setDryRun(data.options.dryRun === true);
22
+ setSkipExisting(!(data.options.force === true || data.options.f === true));
23
+ try {
24
+ for (const [file, content] of Object.entries(files)) {
25
+ const target = path.join(directory, file);
26
+ await ensureDirectoryAsync(path.dirname(target));
27
+ await putFileAsync$1(target, content);
28
+ }
29
+ reportSkippedFiles();
30
+ } finally {
31
+ setSkipExisting(false);
32
+ }
33
+ }
34
+ async function generatePage(data) {
35
+ const target = webPath(data.args[0]);
36
+ const name = parseName(path.basename(target));
37
+ await writeFiles(srcPath("web", path.dirname(target)), pageFiles(name, `/${target}`, data.options.interactive === true), data);
38
+ }
39
+ /** Turn a real Seal JSON Schema into controls; unsupported shapes fail before any file is written. */
40
+ function webFields(schema) {
41
+ if (!schema || typeof schema !== "object" || !("properties" in schema) || !schema.properties || typeof schema.properties !== "object") throw new Error("Expected a Seal object schema with properties.");
42
+ return Object.entries(schema.properties).map(([name, raw]) => {
43
+ if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(name) || !raw || typeof raw !== "object") throw new Error(`Unsupported form field: ${name}`);
44
+ const node = raw;
45
+ if (node.enum || node.anyOf || node.oneOf || Array.isArray(node.type) || node.format === "binary" || node.contentMediaType || node.contentEncoding) throw new Error(`Field ${name} needs a custom control; only plain string and boolean fields can be generated.`);
46
+ const component = node.type === "boolean" ? "CheckboxInput" : node.type === "string" ? (typeof node.format === "string" ? {
47
+ email: "EmailInput",
48
+ date: "DatePickerInput",
49
+ time: "TimePickerInput",
50
+ "date-time": "DateTimePickerInput"
51
+ }[node.format] : void 0) ?? (/password/i.test(name) ? "PasswordInput" : "TextInput") : void 0;
52
+ if (!component) throw new Error(`Field ${name} needs a custom control; only plain string and boolean fields can be generated.`);
53
+ return {
54
+ name,
55
+ component
56
+ };
57
+ });
58
+ }
59
+ async function generateForm(data) {
60
+ const target = webPath(data.args[0]);
61
+ if (target.includes("/")) throw new Error("Use a form name and --module <name> for a module-scoped form.");
62
+ const name = parseName(target);
63
+ const module = typeof data.options.module === "string" ? webPath(data.options.module) : void 0;
64
+ if (module?.includes("/")) throw new Error("The module must be a single folder name.");
65
+ if (data.options.endpoint && !module) throw new Error("--endpoint requires --module <name>.");
66
+ if (module && !existsSync(srcPath("app", module))) throw new Error(`Module ${module} does not exist. Generate the module first.`);
67
+ const schemaImport = `shared/schema/${module ? `${module}/` : ""}${name.kebab}.schema`;
68
+ const files = formFiles(name, schemaImport, `${name.camel}Schema`, [{
69
+ name: "name",
70
+ component: "TextInput"
71
+ }], `/${module ? `${module}/` : ""}${name.kebab}`);
72
+ const rootFiles = {};
73
+ const formDirectory = `web/${module ? `${module}/` : "forms/"}`;
74
+ for (const [file, content] of Object.entries(files)) rootFiles[`${formDirectory}${file}`] = content;
75
+ rootFiles[`${schemaImport}.ts`] = `import type { Infer } from "@warlock.js/seal";
76
+ import { v } from "@warlock.js/seal";
77
+
78
+ export const ${name.camel}Schema = v.object({ name: v.string().min(1) });
79
+ export type ${name.pascal}Schema = Infer<typeof ${name.camel}Schema>;
80
+ `;
81
+ let routesPath;
82
+ let routes;
83
+ if (data.options.endpoint && module) {
84
+ rootFiles[`app/${module}/controllers/${name.kebab}.controller.ts`] = `import type { Request, RequestHandler } from "@warlock.js/core";
85
+ import type { ${name.pascal}Schema } from "${schemaImport}";
86
+ import { ${name.camel}Schema } from "${schemaImport}";
87
+
88
+ export const ${name.camel}Controller: RequestHandler<Request<${name.pascal}Schema>> = async ({ request, response }) => {
89
+ return response.success({ data: request.validated() });
90
+ };
91
+
92
+ ${name.camel}Controller.validation = { schema: ${name.camel}Schema };
93
+ ${name.camel}Controller.responseSchema = { 200: { body: { data: { name: "string" } } } };
94
+ `;
95
+ routesPath = srcPath("app", module, "routes.ts");
96
+ const current = existsSync(routesPath) ? await getFileAsync(routesPath) : "";
97
+ const importLine = `import { ${name.camel}Controller } from "./controllers/${name.kebab}.controller";`;
98
+ if (!current.includes(importLine)) {
99
+ routes = `import { router as formRouter } from "@warlock.js/core";\nimport { guarded as guardForm } from "app/shared/utils/router";\n${importLine}\n${current.trimEnd()}\n\nguardForm(() => {\n formRouter.post("/${module}/${name.kebab}", ${name.camel}Controller);\n});\n`;
100
+ if (current.includes("import { router as formRouter }")) routes = routes.replace("import { router as formRouter } from \"@warlock.js/core\";\n", "");
101
+ if (current.includes("import { guarded as guardForm }")) routes = routes.replace("import { guarded as guardForm } from \"app/shared/utils/router\";\n", "");
102
+ }
103
+ }
104
+ await writeFiles(srcPath(), rootFiles, data);
105
+ if (routesPath && routes !== void 0) await putFileAsync(routesPath, routes);
106
+ }
107
+ /** Read inline query fields from the conventional generated list controller. */
108
+ function listFilters(source, schemaName) {
109
+ const parsed = parseSync("controller.ts", source);
110
+ if (parsed.errors.length) throw new Error("Cannot parse list controller.");
111
+ let fields;
112
+ const visit = (node) => {
113
+ if (node.type === "VariableDeclarator" && node.id.type === "Identifier" && node.id.name === schemaName && node.init?.type === "CallExpression") {
114
+ const object = node.init.arguments[0];
115
+ if (object?.type === "ObjectExpression") fields = object.properties.map((property) => {
116
+ if (property.type !== "Property" || property.computed || property.method || property.shorthand || !(property.key.type === "Identifier" || property.key.type === "Literal" && typeof property.key.value === "string")) throw new Error("List filters require literal field names.");
117
+ return property.key.type === "Identifier" ? property.key.name : String(property.key.value);
118
+ });
119
+ }
120
+ forEachChild(node, visit);
121
+ };
122
+ visit(parsed.program);
123
+ if (!fields) throw new Error(`Export or declare ${schemaName} as an inline Seal object schema in the list controller.`);
124
+ return fields.filter((field) => field !== "page" && field !== "limit");
125
+ }
126
+ function createCrudUiGenerator(loadSchema = loadSchemaModuleWithAppLoader) {
127
+ return async (data) => {
128
+ const input = webPath(data.args[0]);
129
+ if (input.includes("/")) throw new Error("Use a single module name.");
130
+ const module = pluralName(input);
131
+ const entity = module.singular;
132
+ const directory = srcPath("app", module.kebab);
133
+ const routesPath = path.join(directory, "routes.ts");
134
+ if (!existsSync(routesPath)) throw new Error("Generate a full CRUD module before generating its UI.");
135
+ const routes = readFileSync(routesPath, "utf8");
136
+ const normalized = (source) => {
137
+ const parsed = parseSync("routes.ts", source);
138
+ if (parsed.errors.length) throw new Error("CRUD UI requires the standard guarded module routes.");
139
+ return JSON.stringify(parsed.program, (key, value) => [
140
+ "start",
141
+ "end",
142
+ "loc",
143
+ "range",
144
+ "raw",
145
+ "comments"
146
+ ].includes(key) ? void 0 : value);
147
+ };
148
+ if (normalized(routes) !== normalized(crudRoutesStub(module))) throw new Error("CRUD UI requires the standard guarded module routes. Custom routes and guards need a hand-written UI.");
149
+ const filters = listFilters(readFileSync(path.join(directory, "controllers", `list-${module.kebab}.controller.ts`), "utf8"), `list${module.pascal}QuerySchema`);
150
+ const forms = {};
151
+ for (const action of ["create", "update"]) {
152
+ const name = parseName(`${action}-${entity.kebab}`);
153
+ const schemaName = `${name.camel}Schema`;
154
+ const picked = pickSchemaExport(await loadSchema(path.join(directory, "schema", `${name.kebab}.schema.ts`)), schemaName);
155
+ if (picked.exportName !== schemaName) throw new Error(`Export the module schema as ${schemaName} before generating its UI.`);
156
+ const fields = webFields(picked.schema.toJsonSchema("draft-2020-12"));
157
+ Object.assign(forms, formFiles(name, `app/${module.kebab}/schema/${name.kebab}.schema`, schemaName, fields, `/${module.kebab}`));
158
+ }
159
+ await writeFiles(srcPath("web", module.kebab), {
160
+ ...forms,
161
+ ...crudFiles(module, filters)
162
+ }, data);
163
+ };
164
+ }
165
+ const generateCrudUi = createCrudUiGenerator();
166
+
167
+ //#endregion
168
+ export { createCrudUiGenerator, generateCrudUi, generateForm, generatePage, listFilters, webFields, webPath };
169
+ //# sourceMappingURL=web.generator.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.generator.mjs","names":["putFileAsync","editFile"],"sources":["../../../../../../../../../../../packages/core/src/cli/commands/generate/generators/web.generator.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { parseSync } from \"oxc-parser\";\nimport * as ast from \"../../../../tooling/syntax/ast\";\nimport type { CommandActionData } from \"../../../../commands/types\";\nimport { getFileAsync, putFileAsync as editFile } from \"../../../../generations/generation-fs\";\nimport { srcPath } from \"../../../../utils\";\nimport { crudRoutesStub } from \"../templates/stubs\";\nimport {\n crudFiles,\n formFiles,\n pageFiles,\n type WebField,\n type WebFiles,\n} from \"../templates/web-stubs\";\nimport { parseName, pluralName } from \"../utils/name-parser\";\nimport {\n ensureDirectoryAsync,\n putFileAsync,\n reportSkippedFiles,\n setDryRun,\n setSkipExisting,\n} from \"../utils/writer\";\nimport {\n loadSchemaModuleWithAppLoader,\n pickSchemaExport,\n type SchemaModuleLoader,\n} from \"./endpoint.generator\";\n\n/** Refuse traversal, absolute paths and source fragments before computing output paths. */\nexport function webPath(input: string | undefined): string {\n if (!input || !/^[a-zA-Z][a-zA-Z0-9_-]*(\\/[a-zA-Z][a-zA-Z0-9_-]*)*$/.test(input)) {\n throw new Error(\n \"Use a relative name such as products or account/settings (letters, numbers, hyphens and underscores).\",\n );\n }\n return input\n .split(\"/\")\n .map((part) => parseName(part).kebab)\n .join(\"/\");\n}\n\nasync function writeFiles(\n directory: string,\n files: WebFiles,\n data: CommandActionData,\n): Promise<void> {\n setDryRun(data.options.dryRun === true);\n setSkipExisting(!(data.options.force === true || data.options.f === true));\n try {\n for (const [file, content] of Object.entries(files)) {\n const target = path.join(directory, file);\n await ensureDirectoryAsync(path.dirname(target));\n await putFileAsync(target, content);\n }\n reportSkippedFiles();\n } finally {\n setSkipExisting(false);\n }\n}\n\nexport async function generatePage(data: CommandActionData): Promise<void> {\n const target = webPath(data.args[0]);\n const name = parseName(path.basename(target));\n await writeFiles(\n srcPath(\"web\", path.dirname(target)),\n pageFiles(name, `/${target}`, data.options.interactive === true),\n data,\n );\n}\n\n/** Turn a real Seal JSON Schema into controls; unsupported shapes fail before any file is written. */\nexport function webFields(schema: unknown): WebField[] {\n if (\n !schema ||\n typeof schema !== \"object\" ||\n !(\"properties\" in schema) ||\n !schema.properties ||\n typeof schema.properties !== \"object\"\n ) {\n throw new Error(\"Expected a Seal object schema with properties.\");\n }\n return Object.entries(schema.properties).map(([name, raw]: [string, unknown]) => {\n if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(name) || !raw || typeof raw !== \"object\") {\n throw new Error(`Unsupported form field: ${name}`);\n }\n const node = raw as {\n type?: unknown;\n format?: unknown;\n enum?: unknown;\n anyOf?: unknown;\n oneOf?: unknown;\n contentMediaType?: unknown;\n contentEncoding?: unknown;\n };\n if (\n node.enum ||\n node.anyOf ||\n node.oneOf ||\n Array.isArray(node.type) ||\n node.format === \"binary\" ||\n node.contentMediaType ||\n node.contentEncoding\n ) {\n throw new Error(\n `Field ${name} needs a custom control; only plain string and boolean fields can be generated.`,\n );\n }\n const formats: Record<string, string> = {\n email: \"EmailInput\",\n date: \"DatePickerInput\",\n time: \"TimePickerInput\",\n \"date-time\": \"DateTimePickerInput\",\n };\n const component =\n node.type === \"boolean\"\n ? \"CheckboxInput\"\n : node.type === \"string\"\n ? ((typeof node.format === \"string\" ? formats[node.format] : undefined) ??\n (/password/i.test(name) ? \"PasswordInput\" : \"TextInput\"))\n : undefined;\n if (!component) {\n throw new Error(\n `Field ${name} needs a custom control; only plain string and boolean fields can be generated.`,\n );\n }\n return { name, component };\n });\n}\n\nexport async function generateForm(data: CommandActionData): Promise<void> {\n const target = webPath(data.args[0]);\n if (target.includes(\"/\"))\n throw new Error(\"Use a form name and --module <name> for a module-scoped form.\");\n const name = parseName(target);\n const module = typeof data.options.module === \"string\" ? webPath(data.options.module) : undefined;\n if (module?.includes(\"/\")) throw new Error(\"The module must be a single folder name.\");\n if (data.options.endpoint && !module) throw new Error(\"--endpoint requires --module <name>.\");\n if (module && !existsSync(srcPath(\"app\", module)))\n throw new Error(`Module ${module} does not exist. Generate the module first.`);\n const schemaImport = `shared/schema/${module ? `${module}/` : \"\"}${name.kebab}.schema`;\n const files = formFiles(\n name,\n schemaImport,\n `${name.camel}Schema`,\n [{ name: \"name\", component: \"TextInput\" }],\n `/${module ? `${module}/` : \"\"}${name.kebab}`,\n );\n const rootFiles: WebFiles = {};\n const formDirectory = `web/${module ? `${module}/` : \"forms/\"}`;\n for (const [file, content] of Object.entries(files))\n rootFiles[`${formDirectory}${file}`] = content;\n rootFiles[`${schemaImport}.ts`] = `import type { Infer } from \"@warlock.js/seal\";\nimport { v } from \"@warlock.js/seal\";\n\nexport const ${name.camel}Schema = v.object({ name: v.string().min(1) });\nexport type ${name.pascal}Schema = Infer<typeof ${name.camel}Schema>;\n`;\n let routesPath: string | undefined;\n let routes: string | undefined;\n if (data.options.endpoint && module) {\n rootFiles[`app/${module}/controllers/${name.kebab}.controller.ts`] =\n `import type { Request, RequestHandler } from \"@warlock.js/core\";\nimport type { ${name.pascal}Schema } from \"${schemaImport}\";\nimport { ${name.camel}Schema } from \"${schemaImport}\";\n\nexport const ${name.camel}Controller: RequestHandler<Request<${name.pascal}Schema>> = async ({ request, response }) => {\n return response.success({ data: request.validated() });\n};\n\n${name.camel}Controller.validation = { schema: ${name.camel}Schema };\n${name.camel}Controller.responseSchema = { 200: { body: { data: { name: \"string\" } } } };\n`;\n routesPath = srcPath(\"app\", module, \"routes.ts\");\n const current = existsSync(routesPath) ? await getFileAsync(routesPath) : \"\";\n const importLine = `import { ${name.camel}Controller } from \"./controllers/${name.kebab}.controller\";`;\n if (!current.includes(importLine)) {\n routes = `import { router as formRouter } from \"@warlock.js/core\";\\nimport { guarded as guardForm } from \"app/shared/utils/router\";\\n${importLine}\\n${current.trimEnd()}\\n\\nguardForm(() => {\\n formRouter.post(\"/${module}/${name.kebab}\", ${name.camel}Controller);\\n});\\n`;\n // Reuse aliases when several form endpoints are added to the same module.\n if (current.includes(\"import { router as formRouter }\"))\n routes = routes.replace('import { router as formRouter } from \"@warlock.js/core\";\\n', \"\");\n if (current.includes(\"import { guarded as guardForm }\"))\n routes = routes.replace(\n 'import { guarded as guardForm } from \"app/shared/utils/router\";\\n',\n \"\",\n );\n }\n }\n await writeFiles(srcPath(), rootFiles, data);\n if (routesPath && routes !== undefined) await editFile(routesPath, routes);\n}\n\n/** Read inline query fields from the conventional generated list controller. */\nexport function listFilters(source: string, schemaName: string): string[] {\n const parsed = parseSync(\"controller.ts\", source);\n if (parsed.errors.length) throw new Error(\"Cannot parse list controller.\");\n let fields: string[] | undefined;\n const visit = (node: ast.Node): void => {\n if (\n node.type === \"VariableDeclarator\" &&\n node.id.type === \"Identifier\" &&\n node.id.name === schemaName &&\n node.init?.type === \"CallExpression\"\n ) {\n const object = node.init.arguments[0];\n if (object?.type === \"ObjectExpression\") {\n fields = object.properties.map((property) => {\n if (\n property.type !== \"Property\" ||\n property.computed ||\n property.method ||\n property.shorthand ||\n !(\n property.key.type === \"Identifier\" ||\n (property.key.type === \"Literal\" && typeof property.key.value === \"string\")\n )\n )\n throw new Error(\"List filters require literal field names.\");\n return property.key.type === \"Identifier\"\n ? property.key.name\n : String(property.key.value);\n });\n }\n }\n ast.forEachChild(node, visit);\n };\n visit(parsed.program);\n if (!fields)\n throw new Error(\n `Export or declare ${schemaName} as an inline Seal object schema in the list controller.`,\n );\n return fields.filter((field) => field !== \"page\" && field !== \"limit\");\n}\n\nexport function createCrudUiGenerator(\n loadSchema: SchemaModuleLoader = loadSchemaModuleWithAppLoader,\n) {\n return async (data: CommandActionData): Promise<void> => {\n const input = webPath(data.args[0]);\n if (input.includes(\"/\")) throw new Error(\"Use a single module name.\");\n const module = pluralName(input);\n const entity = module.singular;\n const directory = srcPath(\"app\", module.kebab);\n const routesPath = path.join(directory, \"routes.ts\");\n if (!existsSync(routesPath))\n throw new Error(\"Generate a full CRUD module before generating its UI.\");\n const routes = readFileSync(routesPath, \"utf8\");\n // Custom authorization cannot safely be inferred. Refuse rather than silently weaken it.\n const normalized = (source: string) => {\n const parsed = parseSync(\"routes.ts\", source);\n if (parsed.errors.length)\n throw new Error(\"CRUD UI requires the standard guarded module routes.\");\n // Compare syntax, ignoring source positions, comments and literal spelling.\n return JSON.stringify(parsed.program, (key, value: unknown) =>\n [\"start\", \"end\", \"loc\", \"range\", \"raw\", \"comments\"].includes(key) ? undefined : value,\n );\n };\n if (normalized(routes) !== normalized(crudRoutesStub(module))) {\n throw new Error(\n \"CRUD UI requires the standard guarded module routes. Custom routes and guards need a hand-written UI.\",\n );\n }\n const controller = readFileSync(\n path.join(directory, \"controllers\", `list-${module.kebab}.controller.ts`),\n \"utf8\",\n );\n const filters = listFilters(controller, `list${module.pascal}QuerySchema`);\n const forms: WebFiles = {};\n for (const action of [\"create\", \"update\"]) {\n const name = parseName(`${action}-${entity.kebab}`);\n const schemaName = `${name.camel}Schema`;\n const picked = pickSchemaExport(\n await loadSchema(path.join(directory, \"schema\", `${name.kebab}.schema.ts`)),\n schemaName,\n );\n if (picked.exportName !== schemaName) {\n throw new Error(`Export the module schema as ${schemaName} before generating its UI.`);\n }\n const fields = webFields(picked.schema.toJsonSchema(\"draft-2020-12\"));\n Object.assign(\n forms,\n formFiles(\n name,\n `app/${module.kebab}/schema/${name.kebab}.schema`,\n schemaName,\n fields,\n `/${module.kebab}`,\n ),\n );\n }\n await writeFiles(\n srcPath(\"web\", module.kebab),\n { ...forms, ...crudFiles(module, filters) },\n data,\n );\n };\n}\n\nexport const generateCrudUi = createCrudUiGenerator();\n"],"mappings":"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"}