@warlock.js/core 5.4.0 → 5.6.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 (144) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/esm/application/app.d.mts.map +1 -1
  3. package/esm/application/app.mjs +7 -1
  4. package/esm/application/app.mjs.map +1 -1
  5. package/esm/cli/cli-commands.utils.mjs.map +1 -1
  6. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
  7. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
  8. package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
  9. package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
  10. package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -1
  11. package/esm/cli/commands/routes/route-row.mjs.map +1 -1
  12. package/esm/cli/commands/routes.command.mjs.map +1 -1
  13. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  14. package/esm/config/locale-configuration.mjs +11 -1
  15. package/esm/config/locale-configuration.mjs.map +1 -1
  16. package/esm/container/container-instance-registry.mjs +40 -0
  17. package/esm/container/container-instance-registry.mjs.map +1 -0
  18. package/esm/container/index.d.mts +11 -0
  19. package/esm/container/index.d.mts.map +1 -1
  20. package/esm/container/index.mjs +11 -0
  21. package/esm/container/index.mjs.map +1 -1
  22. package/esm/database/drop-tables-action.mjs.map +1 -1
  23. package/esm/database/migrate-action.mjs.map +1 -1
  24. package/esm/database/seed-command-action.mjs.map +1 -1
  25. package/esm/dev-server/dependency-graph.d.mts.map +1 -1
  26. package/esm/dev-server/dependency-graph.mjs.map +1 -1
  27. package/esm/dev-server/dev-logger.mjs.map +1 -1
  28. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  29. package/esm/dev-server/layer-executor.mjs.map +1 -1
  30. package/esm/dev-server/loader/build-transpile-init.mjs.map +1 -1
  31. package/esm/dev-server/loader/load-hook.mjs.map +1 -1
  32. package/esm/dev-server/loader/register-loader.mjs.map +1 -1
  33. package/esm/dev-server/loader/resolve-hook.mjs.map +1 -1
  34. package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
  35. package/esm/dev-server/module-loader.mjs.map +1 -1
  36. package/esm/dev-server/parse-imports.mjs.map +1 -1
  37. package/esm/dev-server/type-generator.mjs.map +1 -1
  38. package/esm/dev-server/types.d.mts.map +1 -1
  39. package/esm/dev-server/utils.mjs.map +1 -1
  40. package/esm/errors/container-key-missing-error.mjs +28 -0
  41. package/esm/errors/container-key-missing-error.mjs.map +1 -0
  42. package/esm/errors/unknown-locale-error.mjs +28 -0
  43. package/esm/errors/unknown-locale-error.mjs.map +1 -0
  44. package/esm/generations/add-command.action.mjs.map +1 -1
  45. package/esm/generations/features/access.feature.mjs.map +1 -1
  46. package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
  47. package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
  48. package/esm/generations/features/ai-google.feature.mjs.map +1 -1
  49. package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
  50. package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
  51. package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
  52. package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
  53. package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
  54. package/esm/generations/features/ai.feature.mjs.map +1 -1
  55. package/esm/generations/features/herald.feature.mjs.map +1 -1
  56. package/esm/generations/features/notifications.feature.mjs.map +1 -1
  57. package/esm/generations/features/scheduler.feature.mjs.map +1 -1
  58. package/esm/generations/features/shared/patch-tsconfig-include.mjs.map +1 -1
  59. package/esm/generations/features/shared/resolve-contact-scaffold.mjs +45 -0
  60. package/esm/generations/features/shared/resolve-contact-scaffold.mjs.map +1 -0
  61. package/esm/generations/features/tailwind.feature.mjs.map +1 -1
  62. package/esm/generations/features/types.mjs.map +1 -1
  63. package/esm/generations/features/web.feature.mjs +14 -4
  64. package/esm/generations/features/web.feature.mjs.map +1 -1
  65. package/esm/http/context/request-memo.mjs.map +1 -1
  66. package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
  67. package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
  68. package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
  69. package/esm/http/middleware/idempotency.middleware.d.mts.map +1 -1
  70. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  71. package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
  72. package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
  73. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  74. package/esm/http/port-preflight.d.mts.map +1 -1
  75. package/esm/http/port-preflight.mjs.map +1 -1
  76. package/esm/http/request.d.mts.map +1 -1
  77. package/esm/http/request.mjs +2 -2
  78. package/esm/http/request.mjs.map +1 -1
  79. package/esm/http/response.d.mts +38 -0
  80. package/esm/http/response.d.mts.map +1 -1
  81. package/esm/http/response.mjs +47 -0
  82. package/esm/http/response.mjs.map +1 -1
  83. package/esm/http/server.d.mts.map +1 -1
  84. package/esm/http/server.mjs.map +1 -1
  85. package/esm/http/types.d.mts.map +1 -1
  86. package/esm/image/image.d.mts.map +1 -1
  87. package/esm/image/image.mjs.map +1 -1
  88. package/esm/mail/send-mail.mjs.map +1 -1
  89. package/esm/production/build-contributions.mjs.map +1 -1
  90. package/esm/production/bundle-framework-dependencies.mjs +152 -0
  91. package/esm/production/bundle-framework-dependencies.mjs.map +1 -0
  92. package/esm/production/production-builder.mjs +30 -3
  93. package/esm/production/production-builder.mjs.map +1 -1
  94. package/esm/production/production-supervisor.mjs.map +1 -1
  95. package/esm/production/tsconfig-path-aliases.mjs +99 -0
  96. package/esm/production/tsconfig-path-aliases.mjs.map +1 -0
  97. package/esm/react/index.d.mts.map +1 -1
  98. package/esm/react/index.mjs.map +1 -1
  99. package/esm/repositories/contracts/query-builder.contract.d.mts.map +1 -1
  100. package/esm/repositories/contracts/types.d.mts.map +1 -1
  101. package/esm/resource/resource-field-builder.d.mts.map +1 -1
  102. package/esm/resource/resource-field-builder.mjs.map +1 -1
  103. package/esm/resource/types.d.mts.map +1 -1
  104. package/esm/router/router.d.mts.map +1 -1
  105. package/esm/router/router.mjs +1 -2
  106. package/esm/router/router.mjs.map +1 -1
  107. package/esm/router/types.d.mts.map +1 -1
  108. package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
  109. package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
  110. package/esm/storage/scoped-storage.d.mts.map +1 -1
  111. package/esm/storage/scoped-storage.mjs.map +1 -1
  112. package/esm/storage/storage.d.mts.map +1 -1
  113. package/esm/storage/storage.mjs.map +1 -1
  114. package/esm/storage/types.d.mts.map +1 -1
  115. package/esm/storage/utils/contain-path.mjs.map +1 -1
  116. package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
  117. package/esm/storage/utils/safe-fetch.mjs.map +1 -1
  118. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  119. package/esm/tests/start-http-development-server.mjs.map +1 -1
  120. package/esm/tests/test-lifecycle-state.mjs.map +1 -1
  121. package/esm/tests/test-setup-timeout.mjs.map +1 -1
  122. package/esm/tests/vitest-setup.d.mts.map +1 -1
  123. package/esm/tests/vitest-setup.mjs.map +1 -1
  124. package/esm/use-cases/use-cases-registry.d.mts.map +1 -1
  125. package/esm/use-cases/use-cases-registry.mjs.map +1 -1
  126. package/esm/utils/app-log.mjs.map +1 -1
  127. package/esm/utils/queue.mjs.map +1 -1
  128. package/esm/utils/sleep.mjs.map +1 -1
  129. package/esm/validation/plugins/database-plugin.mjs.map +1 -1
  130. package/llms-full.txt +60 -60
  131. package/llms.txt +3 -3
  132. package/package.json +12 -12
  133. package/skills/create-controller/SKILL.md +1 -1
  134. package/skills/create-module/SKILL.md +11 -11
  135. package/skills/hash-password/SKILL.md +4 -4
  136. package/skills/run-app/SKILL.md +16 -16
  137. package/skills/send-mail/SKILL.md +2 -2
  138. package/skills/use-model-transformers/SKILL.md +2 -2
  139. package/skills/use-repository/SKILL.md +1 -1
  140. package/skills/validate-input/SKILL.md +1 -1
  141. package/skills/warlock-doctor/SKILL.md +3 -3
  142. package/skills/warlock-routes/SKILL.md +10 -10
  143. package/skills/write-cli-command/SKILL.md +1 -1
  144. package/skills/write-seeder/SKILL.md +8 -8
@@ -1 +1 @@
1
- {"version":3,"file":"ai-panoptic.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/ai-panoptic.feature.ts"],"sourcesContent":["import { linkAiPackageImport } from \"./shared/link-ai-package-import\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\r\nexport const aiPanopticFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/ai-panoptic — observability for @warlock.js/ai (collector, exporters, zero-setup local dashboard) via ai.config({ panoptic }) (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-panoptic\": INSTALLED_WARLOCK_VERSION,\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-panoptic\"),\r\n};\r\n"],"mappings":";;;;AAGA,MAAa,oBAAuC;CAClD,aACE;CACF,UAAU,CAAC,IAAI;CACf,cAAc,EACZ,2BAA2B,0BAC7B;CACA,mBAAmB,oBAAoB,yBAAyB;AAClE"}
1
+ {"version":3,"file":"ai-panoptic.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/ai-panoptic.feature.ts"],"sourcesContent":["import { linkAiPackageImport } from \"./shared/link-ai-package-import\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\nexport const aiPanopticFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/ai-panoptic — observability for @warlock.js/ai (collector, exporters, zero-setup local dashboard) via ai.config({ panoptic }) (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-panoptic\": INSTALLED_WARLOCK_VERSION,\r\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-panoptic\"),\r\n};\r\n"],"mappings":";;;;AAGA,MAAa,oBAAuC;CAClD,aACE;CACF,UAAU,CAAC,IAAI;CACf,cAAc,EACZ,2BAA2B,0BAC7B;CACA,mBAAmB,oBAAoB,yBAAyB;AAClE"}
@@ -1 +1 @@
1
- {"version":3,"file":"ai-tools.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/ai-tools.feature.ts"],"sourcesContent":["import { linkAiPackageImport } from \"./shared/link-ai-package-import\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\r\nexport const aiToolsFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/ai-tools — ready-made agent tools (web search, fetch, HTTP, calculator, date-time) + an MCP client/server, under ai.tools.* / ai.mcp (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-tools\": INSTALLED_WARLOCK_VERSION,\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-tools\"),\r\n};\r\n"],"mappings":";;;;AAGA,MAAa,iBAAoC;CAC/C,aACE;CACF,UAAU,CAAC,IAAI;CACf,cAAc,EACZ,wBAAwB,0BAC1B;CACA,mBAAmB,oBAAoB,sBAAsB;AAC/D"}
1
+ {"version":3,"file":"ai-tools.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/ai-tools.feature.ts"],"sourcesContent":["import { linkAiPackageImport } from \"./shared/link-ai-package-import\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\nexport const aiToolsFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/ai-tools — ready-made agent tools (web search, fetch, HTTP, calculator, date-time) + an MCP client/server, under ai.tools.* / ai.mcp (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-tools\": INSTALLED_WARLOCK_VERSION,\r\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-tools\"),\r\n};\r\n"],"mappings":";;;;AAGA,MAAa,iBAAoC;CAC/C,aACE;CACF,UAAU,CAAC,IAAI;CACf,cAAc,EACZ,wBAAwB,0BAC1B;CACA,mBAAmB,oBAAoB,sBAAsB;AAC/D"}
@@ -1 +1 @@
1
- {"version":3,"file":"ai-workspace.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/ai-workspace.feature.ts"],"sourcesContent":["import { linkAiPackageImport } from \"./shared/link-ai-package-import\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\r\nexport const aiWorkspaceFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/ai-workspace — a policy-jailed filesystem + shell workspace for coding agents, as ai.workspace (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-workspace\": INSTALLED_WARLOCK_VERSION,\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-workspace\"),\r\n};\r\n"],"mappings":";;;;AAGA,MAAa,qBAAwC;CACnD,aACE;CACF,UAAU,CAAC,IAAI;CACf,cAAc,EACZ,4BAA4B,0BAC9B;CACA,mBAAmB,oBAAoB,0BAA0B;AACnE"}
1
+ {"version":3,"file":"ai-workspace.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/ai-workspace.feature.ts"],"sourcesContent":["import { linkAiPackageImport } from \"./shared/link-ai-package-import\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\nexport const aiWorkspaceFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/ai-workspace — a policy-jailed filesystem + shell workspace for coding agents, as ai.workspace (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-workspace\": INSTALLED_WARLOCK_VERSION,\r\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-workspace\"),\r\n};\r\n"],"mappings":";;;;AAGA,MAAa,qBAAwC;CACnD,aACE;CACF,UAAU,CAAC,IAAI;CACf,cAAc,EACZ,4BAA4B,0BAC9B;CACA,mBAAmB,oBAAoB,0BAA0B;AACnE"}
@@ -1 +1 @@
1
- {"version":3,"file":"ai.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/ai.feature.ts"],"sourcesContent":["import { aiConfigStub } from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\r\nexport const aiFeature: FeatureDefinition = {\r\n description: \"Installs @warlock.js/ai — the core AI toolkit (agents, tools, workflows)\",\r\n dependencies: {\r\n \"@warlock.js/ai\": INSTALLED_WARLOCK_VERSION,\n },\r\n ejectConfig: {\r\n content: aiConfigStub,\r\n name: \"ai\",\r\n },\r\n};\r\n"],"mappings":";;;;AAGA,MAAa,YAA+B;CAC1C,aAAa;CACb,cAAc,EACZ,kBAAkB,0BACpB;CACA,aAAa;EACX,SAAS;EACT,MAAM;CACR;AACF"}
1
+ {"version":3,"file":"ai.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/ai.feature.ts"],"sourcesContent":["import { aiConfigStub } from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\nexport const aiFeature: FeatureDefinition = {\r\n description: \"Installs @warlock.js/ai — the core AI toolkit (agents, tools, workflows)\",\r\n dependencies: {\r\n \"@warlock.js/ai\": INSTALLED_WARLOCK_VERSION,\r\n },\r\n ejectConfig: {\r\n content: aiConfigStub,\r\n name: \"ai\",\r\n },\r\n};\r\n"],"mappings":";;;;AAGA,MAAa,YAA+B;CAC1C,aAAa;CACb,cAAc,EACZ,kBAAkB,0BACpB;CACA,aAAa;EACX,SAAS;EACT,MAAM;CACR;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"herald.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/herald.feature.ts"],"sourcesContent":["import { communicatorsConfigStub } from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\r\nexport const heraldFeature: FeatureDefinition = {\r\n description: \"Installs herald for message broker (Herald Package)\",\r\n dependencies: {\r\n \"@warlock.js/herald\": INSTALLED_WARLOCK_VERSION,\n amqplib: \"^0.10.0\",\r\n },\r\n devDependencies: {\r\n \"@types/amqplib\": \"^0.10.0\",\r\n },\r\n ejectConfig: {\r\n content: communicatorsConfigStub,\r\n name: \"herald\",\r\n },\r\n};\r\n"],"mappings":";;;;AAGA,MAAa,gBAAmC;CAC9C,aAAa;CACb,cAAc;EACZ,sBAAsB;EACtB,SAAS;CACX;CACA,iBAAiB,EACf,kBAAkB,UACpB;CACA,aAAa;EACX,SAAS;EACT,MAAM;CACR;AACF"}
1
+ {"version":3,"file":"herald.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/herald.feature.ts"],"sourcesContent":["import { communicatorsConfigStub } from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\nexport const heraldFeature: FeatureDefinition = {\r\n description: \"Installs herald for message broker (Herald Package)\",\r\n dependencies: {\r\n \"@warlock.js/herald\": INSTALLED_WARLOCK_VERSION,\r\n amqplib: \"^0.10.0\",\r\n },\r\n devDependencies: {\r\n \"@types/amqplib\": \"^0.10.0\",\r\n },\r\n ejectConfig: {\r\n content: communicatorsConfigStub,\r\n name: \"herald\",\r\n },\r\n};\r\n"],"mappings":";;;;AAGA,MAAa,gBAAmC;CAC9C,aAAa;CACb,cAAc;EACZ,sBAAsB;EACtB,SAAS;CACX;CACA,iBAAiB,EACf,kBAAkB,UACpB;CACA,aAAa;EACX,SAAS;EACT,MAAM;CACR;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/notifications.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { ensureDirectoryAsync, fileExistsAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport type { CommandActionData } from \"../../commands/types\";\r\nimport { srcPath } from \"../../utils\";\r\nimport {\r\n notificationControllersStub,\r\n notificationMigrationStub,\r\n notificationModelStub,\r\n notificationRoutesStub,\r\n notificationsConfigStub,\r\n} from \"../stubs\";\r\nimport { migrationTimestamp } from \"./shared/migration-timestamp\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\r\nasync function completeNotificationsInstallation(_options: CommandActionData) {\r\n const modelPath = srcPath(\"app/notifications/notification.model.ts\");\r\n\r\n // The model file is the sentinel for \"notifications already scaffolded\" —\r\n // its presence means the migration was created too (timestamped, so we must\r\n // not re-emit a duplicate on a second run).\r\n if (await fileExistsAsync(modelPath)) {\r\n console.log(\r\n `${colors.yellowBright(\"src/app/notifications\")} already scaffolded, skipping model + migration...`,\r\n );\r\n return;\r\n }\r\n\r\n // 1. Notification model — extends the package's DatabaseNotification base.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications\"));\r\n await putFileAsync(modelPath, notificationModelStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/notification.model.ts`);\r\n\r\n // 2. Migration — timestamped MM-DD-YYYY_HH-MM-SS prefix so cascade infers its\r\n // createdAt and orders it deterministically (migrate-action discovers\r\n // src/app/*/migrations/*).\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/migrations\"));\r\n\r\n const migrationFile = `${migrationTimestamp()}-notification.migration.ts`;\r\n\r\n await putFileAsync(\r\n srcPath(\"app/notifications/migrations\", migrationFile),\r\n notificationMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/notifications/migrations/${migrationFile}`,\r\n );\r\n\r\n // 3. HTTP surface — the in-app read/dismiss endpoints (routes + controllers),\r\n // gated by authMiddleware. Delete if the app exposes notifications another way.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/notifications/controllers/notifications.controller.ts\"),\r\n notificationControllersStub,\r\n );\r\n await putFileAsync(srcPath(\"app/notifications/routes.ts\"), notificationRoutesStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/routes.ts + controllers`);\r\n}\r\n\r\nexport const notificationsFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds the Notification model + migration plus the recipient-scoped read/dismiss routes + controllers into src/app/notifications\",\r\n // The ejected config wires a `mail` channel by default (needs nodemailer,\r\n // via the `mail` feature); the scaffolded routes are gated by\r\n // `authMiddleware`, so `@warlock.js/auth` is pulled in too.\r\n requires: [\"mail\"],\r\n dependencies: {\r\n \"@warlock.js/notifications\": INSTALLED_WARLOCK_VERSION,\n \"@warlock.js/auth\": INSTALLED_WARLOCK_VERSION,\n },\r\n ejectConfig: {\r\n content: notificationsConfigStub,\r\n name: \"notifications\",\r\n },\r\n onExecuting: completeNotificationsInstallation,\r\n};\r\n"],"mappings":";;;;;;;;;AAcA,eAAe,kCAAkC,UAA6B;CAC5E,MAAM,YAAY,QAAQ,yCAAyC;CAKnE,IAAI,MAAM,gBAAgB,SAAS,GAAG;EACpC,QAAQ,IACN,GAAG,OAAO,aAAa,uBAAuB,EAAE,mDAClD;EACA;CACF;CAGA,MAAM,qBAAqB,QAAQ,mBAAmB,CAAC;CACvD,MAAM,aAAa,WAAW,qBAAqB;CACnD,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,qDAAqD;CAKtF,MAAM,qBAAqB,QAAQ,8BAA8B,CAAC;CAElE,MAAM,gBAAgB,GAAG,mBAAmB,EAAE;CAE9C,MAAM,aACJ,QAAQ,gCAAgC,aAAa,GACrD,yBACF;CACA,QAAQ,IACN,GAAG,OAAO,MAAM,GAAG,EAAE,4CAA4C,eACnE;CAIA,MAAM,qBAAqB,QAAQ,+BAA+B,CAAC;CACnE,MAAM,aACJ,QAAQ,2DAA2D,GACnE,2BACF;CACA,MAAM,aAAa,QAAQ,6BAA6B,GAAG,sBAAsB;CACjF,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,uDAAuD;AAC1F;AAEA,MAAa,uBAA0C;CACrD,aACE;CAIF,UAAU,CAAC,MAAM;CACjB,cAAc;EACZ,6BAA6B;EAC7B,oBAAoB;CACtB;CACA,aAAa;EACX,SAAS;EACT,MAAM;CACR;CACA,aAAa;AACf"}
1
+ {"version":3,"file":"notifications.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/notifications.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { ensureDirectoryAsync, fileExistsAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport type { CommandActionData } from \"../../commands/types\";\r\nimport { srcPath } from \"../../utils\";\r\nimport {\r\n notificationControllersStub,\r\n notificationMigrationStub,\r\n notificationModelStub,\r\n notificationRoutesStub,\r\n notificationsConfigStub,\r\n} from \"../stubs\";\r\nimport { migrationTimestamp } from \"./shared/migration-timestamp\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\nasync function completeNotificationsInstallation(_options: CommandActionData) {\r\n const modelPath = srcPath(\"app/notifications/notification.model.ts\");\r\n\r\n // The model file is the sentinel for \"notifications already scaffolded\" —\r\n // its presence means the migration was created too (timestamped, so we must\r\n // not re-emit a duplicate on a second run).\r\n if (await fileExistsAsync(modelPath)) {\r\n console.log(\r\n `${colors.yellowBright(\"src/app/notifications\")} already scaffolded, skipping model + migration...`,\r\n );\r\n return;\r\n }\r\n\r\n // 1. Notification model — extends the package's DatabaseNotification base.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications\"));\r\n await putFileAsync(modelPath, notificationModelStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/notification.model.ts`);\r\n\r\n // 2. Migration — timestamped MM-DD-YYYY_HH-MM-SS prefix so cascade infers its\r\n // createdAt and orders it deterministically (migrate-action discovers\r\n // src/app/*/migrations/*).\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/migrations\"));\r\n\r\n const migrationFile = `${migrationTimestamp()}-notification.migration.ts`;\r\n\r\n await putFileAsync(\r\n srcPath(\"app/notifications/migrations\", migrationFile),\r\n notificationMigrationStub,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/migrations/${migrationFile}`);\r\n\r\n // 3. HTTP surface — the in-app read/dismiss endpoints (routes + controllers),\r\n // gated by authMiddleware. Delete if the app exposes notifications another way.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/notifications/controllers/notifications.controller.ts\"),\r\n notificationControllersStub,\r\n );\r\n await putFileAsync(srcPath(\"app/notifications/routes.ts\"), notificationRoutesStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/routes.ts + controllers`);\r\n}\r\n\r\nexport const notificationsFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds the Notification model + migration plus the recipient-scoped read/dismiss routes + controllers into src/app/notifications\",\r\n // The ejected config wires a `mail` channel by default (needs nodemailer,\r\n // via the `mail` feature); the scaffolded routes are gated by\r\n // `authMiddleware`, so `@warlock.js/auth` is pulled in too.\r\n requires: [\"mail\"],\r\n dependencies: {\r\n \"@warlock.js/notifications\": INSTALLED_WARLOCK_VERSION,\r\n \"@warlock.js/auth\": INSTALLED_WARLOCK_VERSION,\r\n },\r\n ejectConfig: {\r\n content: notificationsConfigStub,\r\n name: \"notifications\",\r\n },\r\n onExecuting: completeNotificationsInstallation,\r\n};\r\n"],"mappings":";;;;;;;;;AAcA,eAAe,kCAAkC,UAA6B;CAC5E,MAAM,YAAY,QAAQ,yCAAyC;CAKnE,IAAI,MAAM,gBAAgB,SAAS,GAAG;EACpC,QAAQ,IACN,GAAG,OAAO,aAAa,uBAAuB,EAAE,mDAClD;EACA;CACF;CAGA,MAAM,qBAAqB,QAAQ,mBAAmB,CAAC;CACvD,MAAM,aAAa,WAAW,qBAAqB;CACnD,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,qDAAqD;CAKtF,MAAM,qBAAqB,QAAQ,8BAA8B,CAAC;CAElE,MAAM,gBAAgB,GAAG,mBAAmB,EAAE;CAE9C,MAAM,aACJ,QAAQ,gCAAgC,aAAa,GACrD,yBACF;CACA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,4CAA4C,eAAe;CAI5F,MAAM,qBAAqB,QAAQ,+BAA+B,CAAC;CACnE,MAAM,aACJ,QAAQ,2DAA2D,GACnE,2BACF;CACA,MAAM,aAAa,QAAQ,6BAA6B,GAAG,sBAAsB;CACjF,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,uDAAuD;AAC1F;AAEA,MAAa,uBAA0C;CACrD,aACE;CAIF,UAAU,CAAC,MAAM;CACjB,cAAc;EACZ,6BAA6B;EAC7B,oBAAoB;CACtB;CACA,aAAa;EACX,SAAS;EACT,MAAM;CACR;CACA,aAAa;AACf"}
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/scheduler.feature.ts"],"sourcesContent":["import { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\r\nexport const schedulerFeature: FeatureDefinition = {\r\n description: \"Installs warlock scheduler for scheduling tasks\",\r\n dependencies: {\r\n \"@warlock.js/scheduler\": INSTALLED_WARLOCK_VERSION,\n },\r\n};\r\n"],"mappings":";;;AAEA,MAAa,mBAAsC;CACjD,aAAa;CACb,cAAc,EACZ,yBAAyB,0BAC3B;AACF"}
1
+ {"version":3,"file":"scheduler.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/scheduler.feature.ts"],"sourcesContent":["import { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\nexport const schedulerFeature: FeatureDefinition = {\r\n description: \"Installs warlock scheduler for scheduling tasks\",\r\n dependencies: {\r\n \"@warlock.js/scheduler\": INSTALLED_WARLOCK_VERSION,\r\n },\r\n};\r\n"],"mappings":";;;AAEA,MAAa,mBAAsC;CACjD,aAAa;CACb,cAAc,EACZ,yBAAyB,0BAC3B;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"patch-tsconfig-include.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/patch-tsconfig-include.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport { rootPath } from \"../../../utils\";\n\n/** What {@link insertIncludeEntry} did, or could not do, to the source it was given. */\nexport type IncludeInsertion =\n | { status: \"added\"; next: string }\n | { status: \"already-present\" }\n | { status: \"unrecognised\" };\n\n/**\n * Insert one entry into the `include` array of a tsconfig's SOURCE TEXT.\n *\n * **String surgery, never parse-and-write.** The project template's\n * `tsconfig.json` carries `//` comments — it is JSONC, which TypeScript accepts\n * and `JSON.parse` rejects. A parse-first patch therefore throws on exactly the\n * projects a feature is generated into: `warlock add react-email` failed on\n * every freshly scaffolded app with\n *\n * ```\n * ✖ add <features...> failed\n * Expected double-quoted property name in JSON at position 454 (line 17 column 5)\n * ```\n *\n * where line 17 is the comment explaining the project's `moduleResolution`. And\n * even where a parse succeeded, writing the object back would silently delete\n * every comment in the file — comments that are load-bearing documentation\n * there.\n *\n * `shadcn.feature.ts`'s `addWebPathAlias` already reached this conclusion for\n * `compilerOptions.paths`; this is the same rule for `include`, kept in one\n * place so the next feature that needs it cannot get it wrong again.\n *\n * @param source The tsconfig file's current text.\n * @param entry The include entry to add, e.g. `\"emails\"`.\n * @returns What happened, and the new text when there is any.\n */\nexport function insertIncludeEntry(source: string, entry: string): IncludeInsertion {\n // Matches the entry whichever quote style the file uses, so a re-run against a\n // hand-edited tsconfig does not stack a second copy.\n if (new RegExp(`[\"']${escapeForRegExp(entry)}[\"']`).test(source)) {\n return { status: \"already-present\" };\n }\n\n const includeArray = /\"include\"\\s*:\\s*\\[/.exec(source);\n\n if (!includeArray) {\n return { status: \"unrecognised\" };\n }\n\n const insertAt = includeArray.index + includeArray[0].length;\n\n return {\n status: \"added\",\n next: `${source.slice(0, insertAt)}${JSON.stringify(entry)}, ${source.slice(insertAt)}`,\n };\n}\n\n/**\n * Add one entry to the project's `tsconfig.json` `include` array.\n *\n * Never throws. A tsconfig this cannot patch produces a printed instruction the\n * developer can follow by hand — a feature must not abort a whole `add` because\n * one optional convenience could not be applied.\n *\n * @param entry The include entry to add, e.g. `\"emails\"`.\n * @param reason One line explaining what breaks without it, printed when the\n * patch has to be handed back to the developer.\n */\nexport async function patchTsconfigInclude(entry: string, reason: string): Promise<void> {\n const tsconfigPath = rootPath(\"tsconfig.json\");\n\n const printManualInstruction = (problem: string) => {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"tsconfig.json\")} ${problem} — ` +\n `add ${JSON.stringify(entry)} to its \\`include\\` array yourself.\\n ${reason}`,\n );\n };\n\n if (!(await fileExistsAsync(tsconfigPath))) {\n printManualInstruction(\"not found\");\n\n return;\n }\n\n const insertion = insertIncludeEntry(await getFileAsync(tsconfigPath), entry);\n\n if (insertion.status === \"already-present\") {\n return;\n }\n\n if (insertion.status === \"unrecognised\") {\n printManualInstruction(\"has no recognisable `include` array\");\n\n return;\n }\n\n await putFileAsync(tsconfigPath, insertion.next);\n console.log(`${colors.green(\"✓\")} Added ${JSON.stringify(entry)} to tsconfig.json include`);\n}\n\n/**\n * Escape a literal string for embedding in a regular expression.\n *\n * @param value The literal to escape.\n * @returns The escaped literal.\n */\nfunction escapeForRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAgB,mBAAmB,QAAgB,OAAiC;CAGlF,IAAI,IAAI,OAAO,OAAO,gBAAgB,KAAK,EAAE,KAAK,CAAC,CAAC,KAAK,MAAM,GAC7D,OAAO,EAAE,QAAQ,kBAAkB;CAGrC,MAAM,eAAe,qBAAqB,KAAK,MAAM;CAErD,IAAI,CAAC,cACH,OAAO,EAAE,QAAQ,eAAe;CAGlC,MAAM,WAAW,aAAa,QAAQ,aAAa,EAAE,CAAC;CAEtD,OAAO;EACL,QAAQ;EACR,MAAM,GAAG,OAAO,MAAM,GAAG,QAAQ,IAAI,KAAK,UAAU,KAAK,EAAE,IAAI,OAAO,MAAM,QAAQ;CACtF;AACF;;;;;;;;;;;;AAaA,eAAsB,qBAAqB,OAAe,QAA+B;CACvF,MAAM,eAAe,SAAS,eAAe;CAE7C,MAAM,0BAA0B,YAAoB;EAClD,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,eAAe,EAAE,GAAG,QAAQ,SACtE,KAAK,UAAU,KAAK,EAAE,yCAAyC,QAC1E;CACF;CAEA,IAAI,CAAE,MAAM,gBAAgB,YAAY,GAAI;EAC1C,uBAAuB,WAAW;EAElC;CACF;CAEA,MAAM,YAAY,mBAAmB,MAAM,aAAa,YAAY,GAAG,KAAK;CAE5E,IAAI,UAAU,WAAW,mBACvB;CAGF,IAAI,UAAU,WAAW,gBAAgB;EACvC,uBAAuB,qCAAqC;EAE5D;CACF;CAEA,MAAM,aAAa,cAAc,UAAU,IAAI;CAC/C,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,SAAS,KAAK,UAAU,KAAK,EAAE,0BAA0B;AAC5F;;;;;;;AAQA,SAAS,gBAAgB,OAAuB;CAC9C,OAAO,MAAM,QAAQ,uBAAuB,MAAM;AACpD"}
1
+ {"version":3,"file":"patch-tsconfig-include.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/patch-tsconfig-include.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport { rootPath } from \"../../../utils\";\n\n/** What {@link insertIncludeEntry} did, or could not do, to the source it was given. */\nexport type IncludeInsertion =\n { status: \"added\"; next: string } | { status: \"already-present\" } | { status: \"unrecognised\" };\n\n/**\n * Insert one entry into the `include` array of a tsconfig's SOURCE TEXT.\n *\n * **String surgery, never parse-and-write.** The project template's\n * `tsconfig.json` carries `//` comments — it is JSONC, which TypeScript accepts\n * and `JSON.parse` rejects. A parse-first patch therefore throws on exactly the\n * projects a feature is generated into: `warlock add react-email` failed on\n * every freshly scaffolded app with\n *\n * ```\n * ✖ add <features...> failed\n * Expected double-quoted property name in JSON at position 454 (line 17 column 5)\n * ```\n *\n * where line 17 is the comment explaining the project's `moduleResolution`. And\n * even where a parse succeeded, writing the object back would silently delete\n * every comment in the file — comments that are load-bearing documentation\n * there.\n *\n * `shadcn.feature.ts`'s `addWebPathAlias` already reached this conclusion for\n * `compilerOptions.paths`; this is the same rule for `include`, kept in one\n * place so the next feature that needs it cannot get it wrong again.\n *\n * @param source The tsconfig file's current text.\n * @param entry The include entry to add, e.g. `\"emails\"`.\n * @returns What happened, and the new text when there is any.\n */\nexport function insertIncludeEntry(source: string, entry: string): IncludeInsertion {\n // Matches the entry whichever quote style the file uses, so a re-run against a\n // hand-edited tsconfig does not stack a second copy.\n if (new RegExp(`[\"']${escapeForRegExp(entry)}[\"']`).test(source)) {\n return { status: \"already-present\" };\n }\n\n const includeArray = /\"include\"\\s*:\\s*\\[/.exec(source);\n\n if (!includeArray) {\n return { status: \"unrecognised\" };\n }\n\n const insertAt = includeArray.index + includeArray[0].length;\n\n return {\n status: \"added\",\n next: `${source.slice(0, insertAt)}${JSON.stringify(entry)}, ${source.slice(insertAt)}`,\n };\n}\n\n/**\n * Add one entry to the project's `tsconfig.json` `include` array.\n *\n * Never throws. A tsconfig this cannot patch produces a printed instruction the\n * developer can follow by hand — a feature must not abort a whole `add` because\n * one optional convenience could not be applied.\n *\n * @param entry The include entry to add, e.g. `\"emails\"`.\n * @param reason One line explaining what breaks without it, printed when the\n * patch has to be handed back to the developer.\n */\nexport async function patchTsconfigInclude(entry: string, reason: string): Promise<void> {\n const tsconfigPath = rootPath(\"tsconfig.json\");\n\n const printManualInstruction = (problem: string) => {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"tsconfig.json\")} ${problem} — ` +\n `add ${JSON.stringify(entry)} to its \\`include\\` array yourself.\\n ${reason}`,\n );\n };\n\n if (!(await fileExistsAsync(tsconfigPath))) {\n printManualInstruction(\"not found\");\n\n return;\n }\n\n const insertion = insertIncludeEntry(await getFileAsync(tsconfigPath), entry);\n\n if (insertion.status === \"already-present\") {\n return;\n }\n\n if (insertion.status === \"unrecognised\") {\n printManualInstruction(\"has no recognisable `include` array\");\n\n return;\n }\n\n await putFileAsync(tsconfigPath, insertion.next);\n console.log(`${colors.green(\"✓\")} Added ${JSON.stringify(entry)} to tsconfig.json include`);\n}\n\n/**\n * Escape a literal string for embedding in a regular expression.\n *\n * @param value The literal to escape.\n * @returns The escaped literal.\n */\nfunction escapeForRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,mBAAmB,QAAgB,OAAiC;CAGlF,IAAI,IAAI,OAAO,OAAO,gBAAgB,KAAK,EAAE,KAAK,CAAC,CAAC,KAAK,MAAM,GAC7D,OAAO,EAAE,QAAQ,kBAAkB;CAGrC,MAAM,eAAe,qBAAqB,KAAK,MAAM;CAErD,IAAI,CAAC,cACH,OAAO,EAAE,QAAQ,eAAe;CAGlC,MAAM,WAAW,aAAa,QAAQ,aAAa,EAAE,CAAC;CAEtD,OAAO;EACL,QAAQ;EACR,MAAM,GAAG,OAAO,MAAM,GAAG,QAAQ,IAAI,KAAK,UAAU,KAAK,EAAE,IAAI,OAAO,MAAM,QAAQ;CACtF;AACF;;;;;;;;;;;;AAaA,eAAsB,qBAAqB,OAAe,QAA+B;CACvF,MAAM,eAAe,SAAS,eAAe;CAE7C,MAAM,0BAA0B,YAAoB;EAClD,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,eAAe,EAAE,GAAG,QAAQ,SACtE,KAAK,UAAU,KAAK,EAAE,yCAAyC,QAC1E;CACF;CAEA,IAAI,CAAE,MAAM,gBAAgB,YAAY,GAAI;EAC1C,uBAAuB,WAAW;EAElC;CACF;CAEA,MAAM,YAAY,mBAAmB,MAAM,aAAa,YAAY,GAAG,KAAK;CAE5E,IAAI,UAAU,WAAW,mBACvB;CAGF,IAAI,UAAU,WAAW,gBAAgB;EACvC,uBAAuB,qCAAqC;EAE5D;CACF;CAEA,MAAM,aAAa,cAAc,UAAU,IAAI;CAC/C,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,SAAS,KAAK,UAAU,KAAK,EAAE,0BAA0B;AAC5F;;;;;;;AAQA,SAAS,gBAAgB,OAAuB;CAC9C,OAAO,MAAM,QAAQ,uBAAuB,MAAM;AACpD"}
@@ -0,0 +1,45 @@
1
+ import { colors } from "@mongez/copper";
2
+
3
+ //#region ../core/src/generations/features/shared/resolve-contact-scaffold.ts
4
+ const CONTROLLER_PATH = "src/app/contact/controllers/contact.controller.ts";
5
+ const ROUTES_PATH = "src/app/contact/routes.ts";
6
+ /**
7
+ * Decide which of the two contact-module files are safe to (re)write, and
8
+ * report loudly about the ones that are not.
9
+ *
10
+ * Pure by design: no filesystem access here, so a test can exercise every
11
+ * combination directly. {@link completeWebInstallation} in `web.feature.ts`
12
+ * is the thin I/O wrapper — it stats both files, calls this, writes back
13
+ * whatever this says to write, and prints whatever this says to print.
14
+ *
15
+ * `src/web/root.tsx` is the "a human has been here" sentinel for the web
16
+ * layer as a whole, but `src/app/contact/**` is an ordinary application
17
+ * module the web feature happens to also scaffold — nothing stops a project
18
+ * from having its own `contact` module (with its own controller and routes)
19
+ * independently of ever having run `warlock add web`. Trusting root.tsx's
20
+ * absence as license to write these two files meant `warlock add web` could
21
+ * silently overwrite a human's existing `contact` module. Exactly like
22
+ * {@link relocateConflictingHomeRoute} already insists for the home route:
23
+ * "`warlock add web` runs against a project a human has been living in, and
24
+ * silently unlinking their code is not a thing an `add` command gets to do."
25
+ * So each file gets its own independent existence guard, and an existing
26
+ * file is skipped — never overwritten, never a reason to abort the rest of
27
+ * the install.
28
+ */
29
+ function resolveContactScaffold(state) {
30
+ const writeController = !state.controllerExists;
31
+ const writeRoutes = !state.routesExists;
32
+ const messages = [];
33
+ if (state.controllerExists) messages.push(`${colors.yellowBright("!")} ${colors.yellowBright(CONTROLLER_PATH)} already exists, skipping — the contact form's endpoint is missing this controller's logic unless it already handles ${colors.yellowBright("POST /api/contact")}, so the form will 404 until you wire it yourself.`);
34
+ if (state.routesExists) messages.push(`${colors.yellowBright("!")} ${colors.yellowBright(ROUTES_PATH)} already exists, skipping — the contact form's endpoint (${colors.yellowBright("POST /api/contact")}) was not registered here, so the form will 404 until you wire it yourself.`);
35
+ if (writeController && writeRoutes) messages.push(`${colors.green("✓")} Created POST /api/contact starter route`);
36
+ return {
37
+ writeController,
38
+ writeRoutes,
39
+ messages
40
+ };
41
+ }
42
+
43
+ //#endregion
44
+ export { resolveContactScaffold };
45
+ //# sourceMappingURL=resolve-contact-scaffold.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-contact-scaffold.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/resolve-contact-scaffold.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\n\n/** Whether each contact-module file `completeWebInstallation` writes already exists on disk. */\nexport type ContactScaffoldState = {\n controllerExists: boolean;\n routesExists: boolean;\n};\n\n/** Which contact-module files may be written, and what to tell the user. */\nexport type ContactScaffoldPlan = {\n writeController: boolean;\n writeRoutes: boolean;\n messages: string[];\n};\n\nconst CONTROLLER_PATH = \"src/app/contact/controllers/contact.controller.ts\";\nconst ROUTES_PATH = \"src/app/contact/routes.ts\";\n\n/**\n * Decide which of the two contact-module files are safe to (re)write, and\n * report loudly about the ones that are not.\n *\n * Pure by design: no filesystem access here, so a test can exercise every\n * combination directly. {@link completeWebInstallation} in `web.feature.ts`\n * is the thin I/O wrapper — it stats both files, calls this, writes back\n * whatever this says to write, and prints whatever this says to print.\n *\n * `src/web/root.tsx` is the \"a human has been here\" sentinel for the web\n * layer as a whole, but `src/app/contact/**` is an ordinary application\n * module the web feature happens to also scaffold — nothing stops a project\n * from having its own `contact` module (with its own controller and routes)\n * independently of ever having run `warlock add web`. Trusting root.tsx's\n * absence as license to write these two files meant `warlock add web` could\n * silently overwrite a human's existing `contact` module. Exactly like\n * {@link relocateConflictingHomeRoute} already insists for the home route:\n * \"`warlock add web` runs against a project a human has been living in, and\n * silently unlinking their code is not a thing an `add` command gets to do.\"\n * So each file gets its own independent existence guard, and an existing\n * file is skipped — never overwritten, never a reason to abort the rest of\n * the install.\n */\nexport function resolveContactScaffold(state: ContactScaffoldState): ContactScaffoldPlan {\n const writeController = !state.controllerExists;\n const writeRoutes = !state.routesExists;\n const messages: string[] = [];\n\n if (state.controllerExists) {\n messages.push(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(CONTROLLER_PATH)} already exists, skipping — ` +\n \"the contact form's endpoint is missing this controller's logic unless it already handles \" +\n `${colors.yellowBright(\"POST /api/contact\")}, so the form will 404 until you wire it yourself.`,\n );\n }\n\n if (state.routesExists) {\n messages.push(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(ROUTES_PATH)} already exists, skipping — ` +\n `the contact form's endpoint (${colors.yellowBright(\"POST /api/contact\")}) was not registered here, ` +\n \"so the form will 404 until you wire it yourself.\",\n );\n }\n\n if (writeController && writeRoutes) {\n messages.push(`${colors.green(\"✓\")} Created POST /api/contact starter route`);\n }\n\n return { writeController, writeRoutes, messages };\n}\n"],"mappings":";;;AAeA,MAAM,kBAAkB;AACxB,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;AAyBpB,SAAgB,uBAAuB,OAAkD;CACvF,MAAM,kBAAkB,CAAC,MAAM;CAC/B,MAAM,cAAc,CAAC,MAAM;CAC3B,MAAM,WAAqB,CAAC;CAE5B,IAAI,MAAM,kBACR,SAAS,KACP,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,eAAe,EAAE,uHAE/D,OAAO,aAAa,mBAAmB,EAAE,mDAChD;CAGF,IAAI,MAAM,cACR,SAAS,KACP,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,WAAW,EAAE,2DAC9B,OAAO,aAAa,mBAAmB,EAAE,4EAE7E;CAGF,IAAI,mBAAmB,aACrB,SAAS,KAAK,GAAG,OAAO,MAAM,GAAG,EAAE,yCAAyC;CAG9E,OAAO;EAAE;EAAiB;EAAa;CAAS;AAClD"}
@@ -1 +1 @@
1
- {"version":3,"file":"tailwind.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/tailwind.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport {\n ensureDirectoryAsync,\n fileExistsAsync,\n getFileAsync,\n putFileAsync,\n} from \"@warlock.js/fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport type { FeatureDefinition } from \"./types\";\n\n/**\n * The stylesheet entry, and the whole of Tailwind's configuration.\n *\n * Tailwind v4 is CSS-first: there is no `tailwind.config.js` and no `content`\n * globs to keep in sync with the project layout — the engine discovers the\n * templates that reach it through the bundler graph, and everything a v3 config\n * held (`theme`, `plugins`, `darkMode`) is expressed in CSS beside the import.\n * So this one file is the config, which is why the feature writes it rather\n * than an `ejectConfig` entry: `ejectConfig` lands in `src/config/*.ts`, and a\n * stylesheet that must sit in the bundler's path does not belong there.\n */\nconst appCssStub = `@import \"tailwindcss\";\n\n/*\n Tailwind v4 configures itself from CSS — there is no tailwind.config.js.\n\n Design tokens go in an @theme block, and each one becomes both a CSS variable\n and a utility class:\n\n @theme {\n --color-brand: oklch(0.62 0.19 259);\n --font-display: \"Inter\", sans-serif;\n }\n\n gives you \\`bg-brand\\`, \\`text-brand\\`, \\`font-display\\` and \\`var(--color-brand)\\`.\n\n Plugins are imported here too — \\`@plugin \"@tailwindcss/typography\";\\` — and\n your own non-utility CSS can simply follow this comment.\n*/\n`;\n\n/**\n * PostCSS rather than a Vite config file, and deliberately at the project root.\n *\n * Vite discovers `postcss.config.mjs` from the project root on its own, in the\n * dev server and in `vite build` alike, without the application owning a Vite\n * config at all. That matters here: the only app-facing Vite plugin array in\n * this framework is `webConnector({ plugins })`, which is wired into the DEV\n * server exclusively — the client build composes its own plugin list from its\n * caller. Registering `@tailwindcss/vite` there would produce styles in `dev`\n * and silently drop them from a production build, which is a worse failure than\n * any config file, because it only shows up after deploy.\n *\n * `.mjs` because a Warlock project's `package.json` is not guaranteed to set\n * `\"type\": \"module\"`, and `export default` in a `.js` file would throw in a\n * CommonJS project.\n */\nconst postcssConfigStub = `/**\n * Tailwind v4 runs as a PostCSS plugin. Vite loads this file automatically —\n * in dev and in build — so no Vite configuration is required.\n *\n * The plugin lives in its own package in v4: \\`tailwindcss\\` is the engine,\n * \\`@tailwindcss/postcss\\` is the adapter. Naming \\`tailwindcss\\` here directly is\n * the v3 spelling and will not work.\n */\nexport default {\n plugins: {\n \"@tailwindcss/postcss\": {},\n },\n};\n`;\n\n/**\n * Every filename Vite/PostCSS will pick up from the project root.\n *\n * Checked as a set rather than just writing our own: two PostCSS configs in one\n * directory is not a merge, it is a coin toss over which one loads, so a project\n * that already has one gets instructions instead of a second file.\n */\nconst POSTCSS_CONFIG_FILES = [\n \"postcss.config.mjs\",\n \"postcss.config.js\",\n \"postcss.config.cjs\",\n \"postcss.config.ts\",\n \"postcss.config.json\",\n \".postcssrc\",\n \".postcssrc.js\",\n \".postcssrc.json\",\n];\n\n/** The side-effect import that pulls the stylesheet into the bundler graph. */\nconst APP_CSS_IMPORT = 'import \"./app.css\";';\n\n/**\n * Create `src/web/app.css` — the sentinel for \"this feature already ran\".\n *\n * Nothing in the project template creates this file (the scaffold ships a\n * static `public/home.css` instead), so its presence means `add tailwind` has\n * been here, and its contents may since have been edited into a real design\n * system. It is never rewritten.\n */\nasync function createStylesheet(): Promise<boolean> {\n const cssFile = srcPath(\"web/app.css\");\n\n if (await fileExistsAsync(cssFile)) {\n console.log(`${colors.yellowBright(\"src/web/app.css\")} already exists, skipping...`);\n\n return false;\n }\n\n await ensureDirectoryAsync(srcPath(\"web\"));\n await putFileAsync(cssFile, appCssStub);\n console.log(`${colors.green(\"✓\")} Created src/web/app.css`);\n\n return true;\n}\n\n/**\n * Write `postcss.config.mjs`, unless the project already has a PostCSS config.\n *\n * An existing config is left completely alone. It is an app-owned build file\n * that may already register autoprefixer, nesting, or a CSS-modules pass, and\n * appending Tailwind to it by string surgery would be guessing at both its\n * module format and its plugin order.\n */\nasync function writePostcssConfig(): Promise<void> {\n for (const name of POSTCSS_CONFIG_FILES) {\n if (!(await fileExistsAsync(rootPath(name)))) continue;\n\n const current = await getFileAsync(rootPath(name)).catch(() => \"\");\n\n if (current.includes(\"@tailwindcss/postcss\")) {\n console.log(`${colors.yellowBright(name)} already registers Tailwind, skipping...`);\n } else {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(name)} already exists — add Tailwind to it yourself:\\n` +\n ` plugins: { \"@tailwindcss/postcss\": {} }\\n` +\n \" Two PostCSS configs in one project is undefined behaviour, so this feature did not write a second one.\",\n );\n }\n\n return;\n }\n\n await putFileAsync(rootPath(\"postcss.config.mjs\"), postcssConfigStub);\n console.log(`${colors.green(\"✓\")} Created postcss.config.mjs`);\n}\n\n/**\n * Import the stylesheet from `src/web/root.tsx`.\n *\n * The root is the one module every SSR page renders through, so importing the\n * stylesheet there is what puts it in the client bundle for every route.\n *\n * `root.tsx` is guaranteed to exist by the time this runs: `requires: [\"web\"]`\n * makes the add command resolve `web` ahead of `tailwind` and run its\n * `onExecuting` first, and that is what creates the file. The check below is\n * therefore for a root a human has since moved or deleted — a case worth a\n * printed instruction, not a failure.\n */\nasync function importStylesheetFromRoot(): Promise<void> {\n const rootFile = srcPath(\"web/root.tsx\");\n\n if (!(await fileExistsAsync(rootFile))) {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"src/web/root.tsx\")} not found — ` +\n `add ${colors.yellowBright(APP_CSS_IMPORT)} to your application root yourself.\\n` +\n \" Until the stylesheet is imported from a module the bundler reaches, no Tailwind CSS is emitted.\",\n );\n\n return;\n }\n\n const current = await getFileAsync(rootFile);\n\n // Matches the import whichever quote style and specifier the file uses, so a\n // re-run against a hand-edited root does not stack a second copy.\n if (/import\\s+[\"'].*app\\.css[\"']/.test(current)) {\n console.log(`${colors.yellowBright(\"src/web/root.tsx\")} already imports app.css, skipping...`);\n\n return;\n }\n\n // FIRST line, above the framework imports. A side-effect CSS import has no\n // binding to order against, and putting it at the top keeps it from being\n // swept away by an import sorter that only ranks module specifiers.\n await putFileAsync(rootFile, `${APP_CSS_IMPORT}\\n${current}`);\n console.log(`${colors.green(\"✓\")} Imported app.css in src/web/root.tsx`);\n}\n\n/**\n * Wire Tailwind v4 into the page layer.\n *\n * Three files, none of which needs `node_modules` to be populated: on the\n * `create-warlock` path this runs under `--no-install`, so the dependencies\n * declared below are only recorded in `package.json` and nothing here may\n * import, resolve, or execute Tailwind itself.\n */\nasync function completeTailwindInstallation(_options: CommandActionData) {\n await createStylesheet();\n await writePostcssConfig();\n await importStylesheetFromRoot();\n}\n\nexport const tailwindFeature: FeatureDefinition = {\n description:\n \"Installs Tailwind CSS v4 for the SSR page layer. Creates src/web/app.css (the CSS-first config — v4 has no tailwind.config.js), imports it from src/web/root.tsx, and registers the engine through postcss.config.mjs, which Vite loads in dev and build alike.\",\n // `web` owns root.tsx and the CSS pipeline that serves this stylesheet.\n // Requiring it also fixes the order: the add command resolves requirements\n // depth-first, so `web` scaffolds src/web before this feature writes into it.\n requires: [\"web\"],\n devDependencies: {\n // Build-time only, both of them — Tailwind compiles to a plain stylesheet\n // and nothing it ships is imported at runtime.\n tailwindcss: \"^4.1.16\",\n // The engine and its PostCSS adapter are separate packages in v4 and are\n // released in lockstep; keep these two ranges identical.\n \"@tailwindcss/postcss\": \"^4.1.16\",\n },\n onExecuting: completeTailwindInstallation,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnB,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;AAsB1B,MAAM,uBAAuB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAM,iBAAiB;;;;;;;;;AAUvB,eAAe,mBAAqC;CAClD,MAAM,UAAU,QAAQ,aAAa;CAErC,IAAI,MAAM,gBAAgB,OAAO,GAAG;EAClC,QAAQ,IAAI,GAAG,OAAO,aAAa,iBAAiB,EAAE,6BAA6B;EAEnF,OAAO;CACT;CAEA,MAAM,qBAAqB,QAAQ,KAAK,CAAC;CACzC,MAAM,aAAa,SAAS,UAAU;CACtC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,yBAAyB;CAE1D,OAAO;AACT;;;;;;;;;AAUA,eAAe,qBAAoC;CACjD,KAAK,MAAM,QAAQ,sBAAsB;EACvC,IAAI,CAAE,MAAM,gBAAgB,SAAS,IAAI,CAAC,GAAI;EAI9C,KAAI,MAFkB,aAAa,SAAS,IAAI,CAAC,CAAC,CAAC,YAAY,EAAE,EAEtD,CAAC,SAAS,sBAAsB,GACzC,QAAQ,IAAI,GAAG,OAAO,aAAa,IAAI,EAAE,yCAAyC;OAElF,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,IAAI,EAAE,oMAG3D;EAGF;CACF;CAEA,MAAM,aAAa,SAAS,oBAAoB,GAAG,iBAAiB;CACpE,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,4BAA4B;AAC/D;;;;;;;;;;;;;AAcA,eAAe,2BAA0C;CACvD,MAAM,WAAW,QAAQ,cAAc;CAEvC,IAAI,CAAE,MAAM,gBAAgB,QAAQ,GAAI;EACtC,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,kBAAkB,EAAE,mBAC9D,OAAO,aAAa,cAAc,EAAE,uIAE/C;EAEA;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,QAAQ;CAI3C,IAAI,8BAA8B,KAAK,OAAO,GAAG;EAC/C,QAAQ,IAAI,GAAG,OAAO,aAAa,kBAAkB,EAAE,sCAAsC;EAE7F;CACF;CAKA,MAAM,aAAa,UAAU,GAAG,eAAe,IAAI,SAAS;CAC5D,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,sCAAsC;AACzE;;;;;;;;;AAUA,eAAe,6BAA6B,UAA6B;CACvE,MAAM,iBAAiB;CACvB,MAAM,mBAAmB;CACzB,MAAM,yBAAyB;AACjC;AAEA,MAAa,kBAAqC;CAChD,aACE;CAIF,UAAU,CAAC,KAAK;CAChB,iBAAiB;EAGf,aAAa;EAGb,wBAAwB;CAC1B;CACA,aAAa;AACf"}
1
+ {"version":3,"file":"tailwind.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/tailwind.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport type { FeatureDefinition } from \"./types\";\n\n/**\n * The stylesheet entry, and the whole of Tailwind's configuration.\n *\n * Tailwind v4 is CSS-first: there is no `tailwind.config.js` and no `content`\n * globs to keep in sync with the project layout — the engine discovers the\n * templates that reach it through the bundler graph, and everything a v3 config\n * held (`theme`, `plugins`, `darkMode`) is expressed in CSS beside the import.\n * So this one file is the config, which is why the feature writes it rather\n * than an `ejectConfig` entry: `ejectConfig` lands in `src/config/*.ts`, and a\n * stylesheet that must sit in the bundler's path does not belong there.\n */\nconst appCssStub = `@import \"tailwindcss\";\n\n/*\n Tailwind v4 configures itself from CSS — there is no tailwind.config.js.\n\n Design tokens go in an @theme block, and each one becomes both a CSS variable\n and a utility class:\n\n @theme {\n --color-brand: oklch(0.62 0.19 259);\n --font-display: \"Inter\", sans-serif;\n }\n\n gives you \\`bg-brand\\`, \\`text-brand\\`, \\`font-display\\` and \\`var(--color-brand)\\`.\n\n Plugins are imported here too — \\`@plugin \"@tailwindcss/typography\";\\` — and\n your own non-utility CSS can simply follow this comment.\n*/\n`;\n\n/**\n * PostCSS rather than a Vite config file, and deliberately at the project root.\n *\n * Vite discovers `postcss.config.mjs` from the project root on its own, in the\n * dev server and in `vite build` alike, without the application owning a Vite\n * config at all. That matters here: the only app-facing Vite plugin array in\n * this framework is `webConnector({ plugins })`, which is wired into the DEV\n * server exclusively — the client build composes its own plugin list from its\n * caller. Registering `@tailwindcss/vite` there would produce styles in `dev`\n * and silently drop them from a production build, which is a worse failure than\n * any config file, because it only shows up after deploy.\n *\n * `.mjs` because a Warlock project's `package.json` is not guaranteed to set\n * `\"type\": \"module\"`, and `export default` in a `.js` file would throw in a\n * CommonJS project.\n */\nconst postcssConfigStub = `/**\n * Tailwind v4 runs as a PostCSS plugin. Vite loads this file automatically —\n * in dev and in build — so no Vite configuration is required.\n *\n * The plugin lives in its own package in v4: \\`tailwindcss\\` is the engine,\n * \\`@tailwindcss/postcss\\` is the adapter. Naming \\`tailwindcss\\` here directly is\n * the v3 spelling and will not work.\n */\nexport default {\n plugins: {\n \"@tailwindcss/postcss\": {},\n },\n};\n`;\n\n/**\n * Every filename Vite/PostCSS will pick up from the project root.\n *\n * Checked as a set rather than just writing our own: two PostCSS configs in one\n * directory is not a merge, it is a coin toss over which one loads, so a project\n * that already has one gets instructions instead of a second file.\n */\nconst POSTCSS_CONFIG_FILES = [\n \"postcss.config.mjs\",\n \"postcss.config.js\",\n \"postcss.config.cjs\",\n \"postcss.config.ts\",\n \"postcss.config.json\",\n \".postcssrc\",\n \".postcssrc.js\",\n \".postcssrc.json\",\n];\n\n/** The side-effect import that pulls the stylesheet into the bundler graph. */\nconst APP_CSS_IMPORT = 'import \"./app.css\";';\n\n/**\n * Create `src/web/app.css` — the sentinel for \"this feature already ran\".\n *\n * Nothing in the project template creates this file (the scaffold ships a\n * static `public/home.css` instead), so its presence means `add tailwind` has\n * been here, and its contents may since have been edited into a real design\n * system. It is never rewritten.\n */\nasync function createStylesheet(): Promise<boolean> {\n const cssFile = srcPath(\"web/app.css\");\n\n if (await fileExistsAsync(cssFile)) {\n console.log(`${colors.yellowBright(\"src/web/app.css\")} already exists, skipping...`);\n\n return false;\n }\n\n await ensureDirectoryAsync(srcPath(\"web\"));\n await putFileAsync(cssFile, appCssStub);\n console.log(`${colors.green(\"✓\")} Created src/web/app.css`);\n\n return true;\n}\n\n/**\n * Write `postcss.config.mjs`, unless the project already has a PostCSS config.\n *\n * An existing config is left completely alone. It is an app-owned build file\n * that may already register autoprefixer, nesting, or a CSS-modules pass, and\n * appending Tailwind to it by string surgery would be guessing at both its\n * module format and its plugin order.\n */\nasync function writePostcssConfig(): Promise<void> {\n for (const name of POSTCSS_CONFIG_FILES) {\n if (!(await fileExistsAsync(rootPath(name)))) continue;\n\n const current = await getFileAsync(rootPath(name)).catch(() => \"\");\n\n if (current.includes(\"@tailwindcss/postcss\")) {\n console.log(`${colors.yellowBright(name)} already registers Tailwind, skipping...`);\n } else {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(name)} already exists — add Tailwind to it yourself:\\n` +\n ` plugins: { \"@tailwindcss/postcss\": {} }\\n` +\n \" Two PostCSS configs in one project is undefined behaviour, so this feature did not write a second one.\",\n );\n }\n\n return;\n }\n\n await putFileAsync(rootPath(\"postcss.config.mjs\"), postcssConfigStub);\n console.log(`${colors.green(\"✓\")} Created postcss.config.mjs`);\n}\n\n/**\n * Import the stylesheet from `src/web/root.tsx`.\n *\n * The root is the one module every SSR page renders through, so importing the\n * stylesheet there is what puts it in the client bundle for every route.\n *\n * `root.tsx` is guaranteed to exist by the time this runs: `requires: [\"web\"]`\n * makes the add command resolve `web` ahead of `tailwind` and run its\n * `onExecuting` first, and that is what creates the file. The check below is\n * therefore for a root a human has since moved or deleted — a case worth a\n * printed instruction, not a failure.\n */\nasync function importStylesheetFromRoot(): Promise<void> {\n const rootFile = srcPath(\"web/root.tsx\");\n\n if (!(await fileExistsAsync(rootFile))) {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"src/web/root.tsx\")} not found — ` +\n `add ${colors.yellowBright(APP_CSS_IMPORT)} to your application root yourself.\\n` +\n \" Until the stylesheet is imported from a module the bundler reaches, no Tailwind CSS is emitted.\",\n );\n\n return;\n }\n\n const current = await getFileAsync(rootFile);\n\n // Matches the import whichever quote style and specifier the file uses, so a\n // re-run against a hand-edited root does not stack a second copy.\n if (/import\\s+[\"'].*app\\.css[\"']/.test(current)) {\n console.log(`${colors.yellowBright(\"src/web/root.tsx\")} already imports app.css, skipping...`);\n\n return;\n }\n\n // FIRST line, above the framework imports. A side-effect CSS import has no\n // binding to order against, and putting it at the top keeps it from being\n // swept away by an import sorter that only ranks module specifiers.\n await putFileAsync(rootFile, `${APP_CSS_IMPORT}\\n${current}`);\n console.log(`${colors.green(\"✓\")} Imported app.css in src/web/root.tsx`);\n}\n\n/**\n * Wire Tailwind v4 into the page layer.\n *\n * Three files, none of which needs `node_modules` to be populated: on the\n * `create-warlock` path this runs under `--no-install`, so the dependencies\n * declared below are only recorded in `package.json` and nothing here may\n * import, resolve, or execute Tailwind itself.\n */\nasync function completeTailwindInstallation(_options: CommandActionData) {\n await createStylesheet();\n await writePostcssConfig();\n await importStylesheetFromRoot();\n}\n\nexport const tailwindFeature: FeatureDefinition = {\n description:\n \"Installs Tailwind CSS v4 for the SSR page layer. Creates src/web/app.css (the CSS-first config — v4 has no tailwind.config.js), imports it from src/web/root.tsx, and registers the engine through postcss.config.mjs, which Vite loads in dev and build alike.\",\n // `web` owns root.tsx and the CSS pipeline that serves this stylesheet.\n // Requiring it also fixes the order: the add command resolves requirements\n // depth-first, so `web` scaffolds src/web before this feature writes into it.\n requires: [\"web\"],\n devDependencies: {\n // Build-time only, both of them — Tailwind compiles to a plain stylesheet\n // and nothing it ships is imported at runtime.\n tailwindcss: \"^4.1.16\",\n // The engine and its PostCSS adapter are separate packages in v4 and are\n // released in lockstep; keep these two ranges identical.\n \"@tailwindcss/postcss\": \"^4.1.16\",\n },\n onExecuting: completeTailwindInstallation,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnB,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;AAsB1B,MAAM,uBAAuB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAM,iBAAiB;;;;;;;;;AAUvB,eAAe,mBAAqC;CAClD,MAAM,UAAU,QAAQ,aAAa;CAErC,IAAI,MAAM,gBAAgB,OAAO,GAAG;EAClC,QAAQ,IAAI,GAAG,OAAO,aAAa,iBAAiB,EAAE,6BAA6B;EAEnF,OAAO;CACT;CAEA,MAAM,qBAAqB,QAAQ,KAAK,CAAC;CACzC,MAAM,aAAa,SAAS,UAAU;CACtC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,yBAAyB;CAE1D,OAAO;AACT;;;;;;;;;AAUA,eAAe,qBAAoC;CACjD,KAAK,MAAM,QAAQ,sBAAsB;EACvC,IAAI,CAAE,MAAM,gBAAgB,SAAS,IAAI,CAAC,GAAI;EAI9C,KAAI,MAFkB,aAAa,SAAS,IAAI,CAAC,CAAC,CAAC,YAAY,EAAE,EAEtD,CAAC,SAAS,sBAAsB,GACzC,QAAQ,IAAI,GAAG,OAAO,aAAa,IAAI,EAAE,yCAAyC;OAElF,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,IAAI,EAAE,oMAG3D;EAGF;CACF;CAEA,MAAM,aAAa,SAAS,oBAAoB,GAAG,iBAAiB;CACpE,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,4BAA4B;AAC/D;;;;;;;;;;;;;AAcA,eAAe,2BAA0C;CACvD,MAAM,WAAW,QAAQ,cAAc;CAEvC,IAAI,CAAE,MAAM,gBAAgB,QAAQ,GAAI;EACtC,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,kBAAkB,EAAE,mBAC9D,OAAO,aAAa,cAAc,EAAE,uIAE/C;EAEA;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,QAAQ;CAI3C,IAAI,8BAA8B,KAAK,OAAO,GAAG;EAC/C,QAAQ,IAAI,GAAG,OAAO,aAAa,kBAAkB,EAAE,sCAAsC;EAE7F;CACF;CAKA,MAAM,aAAa,UAAU,GAAG,eAAe,IAAI,SAAS;CAC5D,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,sCAAsC;AACzE;;;;;;;;;AAUA,eAAe,6BAA6B,UAA6B;CACvE,MAAM,iBAAiB;CACvB,MAAM,mBAAmB;CACzB,MAAM,yBAAyB;AACjC;AAEA,MAAa,kBAAqC;CAChD,aACE;CAIF,UAAU,CAAC,KAAK;CAChB,iBAAiB;EAGf,aAAa;EAGb,wBAAwB;CAC1B;CACA,aAAa;AACf"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/types.ts"],"sourcesContent":["import type { CommandActionData } from \"../../commands/types\";\n\n/**\n * Internal placeholder for an @warlock.js dependency whose concrete version\n * comes from the installed Core package at command execution time.\n *\n * This is deliberately not a valid package-manager range. If a future code\n * path forgets to resolve it, installation fails loudly instead of silently\n * falling back to a stale framework major copied into a feature definition.\n */\nexport const INSTALLED_WARLOCK_VERSION = \"__INSTALLED_WARLOCK_VERSION__\";\n\r\n/**\r\n * One entry in the feature registry.\r\n *\r\n * Every feature owns a module of its own that exports a single value of this\r\n * shape, with its `onExecuting` body beside it; `features/index.ts` is only the\r\n * ordered index that collects them.\r\n */\r\nexport type FeatureDefinition = {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n description: string;\r\n requires?: string[];\r\n script?: Record<string, string>;\r\n onExecuting?: (options: CommandActionData) => Promise<any>;\r\n ejectConfig?: {\r\n content: string;\r\n name: string;\r\n };\r\n};\r\n"],"mappings":";;;;;;;;;AAUA,MAAa,4BAA4B"}
1
+ {"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/types.ts"],"sourcesContent":["import type { CommandActionData } from \"../../commands/types\";\r\n\r\n/**\r\n * Internal placeholder for an @warlock.js dependency whose concrete version\r\n * comes from the installed Core package at command execution time.\r\n *\r\n * This is deliberately not a valid package-manager range. If a future code\r\n * path forgets to resolve it, installation fails loudly instead of silently\r\n * falling back to a stale framework major copied into a feature definition.\r\n */\r\nexport const INSTALLED_WARLOCK_VERSION = \"__INSTALLED_WARLOCK_VERSION__\";\r\n\r\n/**\r\n * One entry in the feature registry.\r\n *\r\n * Every feature owns a module of its own that exports a single value of this\r\n * shape, with its `onExecuting` body beside it; `features/index.ts` is only the\r\n * ordered index that collects them.\r\n */\r\nexport type FeatureDefinition = {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n description: string;\r\n requires?: string[];\r\n script?: Record<string, string>;\r\n onExecuting?: (options: CommandActionData) => Promise<any>;\r\n ejectConfig?: {\r\n content: string;\r\n name: string;\r\n };\r\n};\r\n"],"mappings":";;;;;;;;;AAUA,MAAa,4BAA4B"}
@@ -3,6 +3,7 @@ import "../../utils/index.mjs";
3
3
  import { webContactControllerStub, webContactRoutesStub, webHomePageStub, webHomeRegisterStub, webRootStub } from "../stubs.mjs";
4
4
  import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
5
5
  import { relocateConflictingHomeRoute } from "./shared/relocate-conflicting-home-route.mjs";
6
+ import { resolveContactScaffold } from "./shared/resolve-contact-scaffold.mjs";
6
7
  import { colors } from "@mongez/copper";
7
8
  import { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
8
9
 
@@ -70,11 +71,20 @@ async function completeWebInstallation(_options) {
70
71
  } else {
71
72
  await putFileAsync(srcPath("web/index.page.tsx"), webHomePageStub);
72
73
  await putFileAsync(srcPath("web/index.register.ts"), webHomeRegisterStub);
73
- await ensureDirectoryAsync(srcPath("app/contact/controllers"));
74
- await putFileAsync(srcPath("app/contact/controllers/contact.controller.ts"), webContactControllerStub);
75
- await putFileAsync(srcPath("app/contact/routes.ts"), webContactRoutesStub);
74
+ const contactPlan = resolveContactScaffold({
75
+ controllerExists: await fileExistsAsync(srcPath("app/contact/controllers/contact.controller.ts")),
76
+ routesExists: await fileExistsAsync(srcPath("app/contact/routes.ts"))
77
+ });
78
+ if (contactPlan.writeController) {
79
+ await ensureDirectoryAsync(srcPath("app/contact/controllers"));
80
+ await putFileAsync(srcPath("app/contact/controllers/contact.controller.ts"), webContactControllerStub);
81
+ }
82
+ if (contactPlan.writeRoutes) {
83
+ await ensureDirectoryAsync(srcPath("app/contact"));
84
+ await putFileAsync(srcPath("app/contact/routes.ts"), webContactRoutesStub);
85
+ }
76
86
  console.log(`${colors.green("✓")} Created src/web/index.page.tsx`);
77
- console.log(`${colors.green("✓")} Created POST /api/contact starter route`);
87
+ for (const message of contactPlan.messages) console.log(message);
78
88
  }
79
89
  }
80
90
  await registerWebConnector();
@@ -1 +1 @@
1
- {"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport type { CommandActionData } from \"../../commands/types\";\r\nimport { rootPath, srcPath } from \"../../utils\";\r\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\r\nimport {\r\n webContactControllerStub,\r\n webContactRoutesStub,\r\n webHomePageStub,\r\n webHomeRegisterStub,\r\n webRootStub,\r\n} from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\n/**\r\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\r\n *\r\n * It belongs to the config array or to app code, never both. Both halves are\r\n * registered before app code loads — the CLI preloader in dev, the generated\r\n * entry in production — so also calling `connectorsManager.register(...)` in\r\n * `src/app/main.ts` boots the connector twice and installs every page route\r\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\r\n * taken`, because pages and API routes share one route-name namespace.\r\n *\r\n * The config array is the half to prefer: `warlock build` reads the same array\r\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\r\n * cannot drift.\r\n *\r\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\r\n * app-owned file that may carry any formatting, and a parse-and-print would\r\n * reformat the parts we did not come to change.\r\n */\r\nasync function registerWebConnector(): Promise<void> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\r\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\r\n ` export default defineConfig({ connectors: [webConnector()] });`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n const current = await getFileAsync(configPath);\r\n\r\n if (current.includes(\"webConnector\")) {\r\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\r\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\r\n\r\n // An existing `connectors: [` gains one entry; otherwise the key is added to\r\n // the object `defineConfig` receives.\r\n if (/connectors:\\s*\\[/.test(next)) {\r\n next = next.replace(/connectors:\\s*\\[/, \"connectors: [webConnector(),\");\r\n } else if (next.includes(\"defineConfig({\")) {\r\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\r\n } else {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\r\n \"add `connectors: [webConnector()]` yourself.\",\r\n );\r\n\r\n return;\r\n }\r\n\r\n await putFileAsync(configPath, next);\r\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\r\n}\r\n\r\n/**\r\n * Scaffold the smallest page layer that renders, and register the connector.\r\n *\r\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\r\n * ships a default root, so its presence means a human has been here.\r\n */\r\nasync function completeWebInstallation(_options: CommandActionData) {\r\n const rootFile = srcPath(\"web/root.tsx\");\r\n\r\n if (await fileExistsAsync(rootFile)) {\r\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\r\n } else {\r\n await ensureDirectoryAsync(srcPath(\"web\"));\r\n await putFileAsync(rootFile, webRootStub);\r\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\r\n\r\n const collision = await relocateConflictingHomeRoute();\r\n\r\n if (collision.outcome === \"relocated\") {\r\n console.log(\r\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\r\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\r\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\r\n );\r\n }\r\n\r\n // The page is written ONLY when `/` is provably free. Writing it while\r\n // another handler holds `/` produces a homepage that 500s on first request,\r\n // which is precisely the outcome a scaffolder must never hand back.\r\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\r\n const verb = collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\r\n\r\n console.log(\r\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\r\n ` The page stub declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\r\n \"path is a 500 at request time, not a startup error.\\n\" +\r\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\r\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the page a `route` other \" +\r\n \"than `/` works too.\",\r\n );\r\n\r\n // Non-zero on BOTH branches. The page layer this command exists to\r\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\r\n // worked\" signal that put `/` in this state to begin with — a conflict we\r\n // declined to guess at is still an incomplete install, not a success.\r\n //\r\n // `exitCode` rather than `exit(1)`: the connector below still has to be\r\n // registered, and any other feature in the same `warlock add` invocation\r\n // still has to install, or the project is left half-wired on top of this.\r\n process.exitCode = 1;\r\n } else {\r\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\r\n await putFileAsync(srcPath(\"web/index.register.ts\"), webHomeRegisterStub);\r\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n webContactControllerStub,\r\n );\r\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\r\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\r\n console.log(`${colors.green(\"✓\")} Created POST /api/contact starter route`);\r\n }\r\n }\r\n\r\n await registerWebConnector();\r\n}\r\n\r\nexport const webFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\r\n dependencies: {\r\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\r\n \"@mongez/http\": \"^3.5.0\",\r\n \"@mongez/react-form\": \"^4.0.0\",\r\n \"@mongez/react-localization\": \"^3.4.7\",\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n // Loaded through `await import()` by the dev server only, so both are\r\n // optional peers of `web` rather than hard dependencies.\r\n vite: \"^7.3.5\",\r\n \"@vitejs/plugin-react\": \"^5.2.0\",\r\n },\r\n onExecuting: completeWebInstallation,\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,eAAe,uBAAsC;CACnD,MAAM,aAAa,SAAS,mBAAmB;CAE/C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;EACxC,QAAQ,IACN,GAAG,OAAO,aAAa,mBAAmB,EAAE,+JAG9C;EAEA;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,UAAU;CAE7C,IAAI,QAAQ,SAAS,cAAc,GAAG;EACpC,QAAQ,IAAI,GAAG,OAAO,aAAa,cAAc,EAAE,iCAAiC;EAEpF;CACF;CAEA,MAAM,aAAa;CACnB,IAAI,OAAO,QAAQ,SAAS,UAAU,IAAI,UAAU,GAAG,WAAW,IAAI;CAItE,IAAI,mBAAmB,KAAK,IAAI,GAC9B,OAAO,KAAK,QAAQ,oBAAoB,8BAA8B;MACjE,IAAI,KAAK,SAAS,gBAAgB,GACvC,OAAO,KAAK,QAAQ,kBAAkB,iDAAiD;MAClF;EACL,QAAQ,IACN,GAAG,OAAO,aAAa,mBAAmB,EAAE,0FAE9C;EAEA;CACF;CAEA,MAAM,aAAa,YAAY,IAAI;CACnC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,8CAA8C;AACjF;;;;;;;AAQA,eAAe,wBAAwB,UAA6B;CAClE,MAAM,WAAW,QAAQ,cAAc;CAEvC,IAAI,MAAM,gBAAgB,QAAQ,GAChC,QAAQ,IAAI,GAAG,OAAO,aAAa,SAAS,EAAE,iCAAiC;MAC1E;EACL,MAAM,qBAAqB,QAAQ,KAAK,CAAC;EACzC,MAAM,aAAa,UAAU,WAAW;EACxC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,0BAA0B;EAE3D,MAAM,YAAY,MAAM,6BAA6B;EAErD,IAAI,UAAU,YAAY,aACxB,QAAQ,IACN,GAAG,OAAO,MAAM,GAAG,EAAE,sBAAsB,OAAO,aAAa,WAAS,EAAE,YACrE,OAAO,aAAa,cAAY,EAAE,MAAM,OAAO,aAAa,OAAO,UAAU,cAAc,EAAE,sFAEpG;EAMF,IAAI,UAAU,YAAY,cAAc,UAAU,YAAY,UAAU;GACtE,MAAM,OAAO,UAAU,YAAY,WAAW,OAAO,UAAU,GAAG,IAAI,OAAO,aAAa,GAAG;GAE7F,QAAQ,IACN,GAAG,KAAK,0CAA0C,UAAU,OAAO,8BACrC,OAAO,aAAa,oBAAkB,EAAE;YAEvD,OAAO,aAAa,WAAS,EAAE,mKAGhD;GAUA,QAAQ,WAAW;EACrB,OAAO;GACL,MAAM,aAAa,QAAQ,oBAAoB,GAAG,eAAe;GACjE,MAAM,aAAa,QAAQ,uBAAuB,GAAG,mBAAmB;GACxE,MAAM,qBAAqB,QAAQ,yBAAyB,CAAC;GAC7D,MAAM,aACJ,QAAQ,+CAA+C,GACvD,wBACF;GACA,MAAM,aAAa,QAAQ,uBAAuB,GAAG,oBAAoB;GACzE,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,gCAAgC;GACjE,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,yCAAyC;EAC5E;CACF;CAEA,MAAM,qBAAqB;AAC7B;AAEA,MAAa,aAAgC;CAC3C,aACE;CACF,cAAc;EACZ,mBAAmB;EACnB,gBAAgB;EAChB,sBAAsB;EACtB,8BAA8B;EAC9B,OAAO;EACP,aAAa;CACf;CACA,iBAAiB;EACf,gBAAgB;EAChB,oBAAoB;EAGpB,MAAM;EACN,wBAAwB;CAC1B;CACA,aAAa;AACf"}
1
+ {"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport type { CommandActionData } from \"../../commands/types\";\r\nimport { rootPath, srcPath } from \"../../utils\";\r\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\r\nimport { resolveContactScaffold } from \"./shared/resolve-contact-scaffold\";\r\nimport {\r\n webContactControllerStub,\r\n webContactRoutesStub,\r\n webHomePageStub,\r\n webHomeRegisterStub,\r\n webRootStub,\r\n} from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\n/**\r\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\r\n *\r\n * It belongs to the config array or to app code, never both. Both halves are\r\n * registered before app code loads — the CLI preloader in dev, the generated\r\n * entry in production — so also calling `connectorsManager.register(...)` in\r\n * `src/app/main.ts` boots the connector twice and installs every page route\r\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\r\n * taken`, because pages and API routes share one route-name namespace.\r\n *\r\n * The config array is the half to prefer: `warlock build` reads the same array\r\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\r\n * cannot drift.\r\n *\r\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\r\n * app-owned file that may carry any formatting, and a parse-and-print would\r\n * reformat the parts we did not come to change.\r\n */\r\nasync function registerWebConnector(): Promise<void> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\r\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\r\n ` export default defineConfig({ connectors: [webConnector()] });`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n const current = await getFileAsync(configPath);\r\n\r\n if (current.includes(\"webConnector\")) {\r\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\r\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\r\n\r\n // An existing `connectors: [` gains one entry; otherwise the key is added to\r\n // the object `defineConfig` receives.\r\n if (/connectors:\\s*\\[/.test(next)) {\r\n next = next.replace(/connectors:\\s*\\[/, \"connectors: [webConnector(),\");\r\n } else if (next.includes(\"defineConfig({\")) {\r\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\r\n } else {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\r\n \"add `connectors: [webConnector()]` yourself.\",\r\n );\r\n\r\n return;\r\n }\r\n\r\n await putFileAsync(configPath, next);\r\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\r\n}\r\n\r\n/**\r\n * Scaffold the smallest page layer that renders, and register the connector.\r\n *\r\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\r\n * ships a default root, so its presence means a human has been here.\r\n */\r\nasync function completeWebInstallation(_options: CommandActionData) {\r\n const rootFile = srcPath(\"web/root.tsx\");\r\n\r\n if (await fileExistsAsync(rootFile)) {\r\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\r\n } else {\r\n await ensureDirectoryAsync(srcPath(\"web\"));\r\n await putFileAsync(rootFile, webRootStub);\r\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\r\n\r\n const collision = await relocateConflictingHomeRoute();\r\n\r\n if (collision.outcome === \"relocated\") {\r\n console.log(\r\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\r\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\r\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\r\n );\r\n }\r\n\r\n // The page is written ONLY when `/` is provably free. Writing it while\r\n // another handler holds `/` produces a homepage that 500s on first request,\r\n // which is precisely the outcome a scaffolder must never hand back.\r\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\r\n const verb =\r\n collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\r\n\r\n console.log(\r\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\r\n ` The page stub declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\r\n \"path is a 500 at request time, not a startup error.\\n\" +\r\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\r\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the page a `route` other \" +\r\n \"than `/` works too.\",\r\n );\r\n\r\n // Non-zero on BOTH branches. The page layer this command exists to\r\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\r\n // worked\" signal that put `/` in this state to begin with — a conflict we\r\n // declined to guess at is still an incomplete install, not a success.\r\n //\r\n // `exitCode` rather than `exit(1)`: the connector below still has to be\r\n // registered, and any other feature in the same `warlock add` invocation\r\n // still has to install, or the project is left half-wired on top of this.\r\n process.exitCode = 1;\r\n } else {\r\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\r\n await putFileAsync(srcPath(\"web/index.register.ts\"), webHomeRegisterStub);\r\n\r\n // Unlike root.tsx above, the contact controller and its routes file are\r\n // ordinary application files a project can already have — independently\r\n // of ever having run `warlock add web`. Each is guarded on its OWN\r\n // existence, not on the (already-consumed) root.tsx sentinel, so an\r\n // existing `contact` module is skipped rather than clobbered. See\r\n // resolveContactScaffold's doc comment for why.\r\n const contactPlan = resolveContactScaffold({\r\n controllerExists: await fileExistsAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n ),\r\n routesExists: await fileExistsAsync(srcPath(\"app/contact/routes.ts\")),\r\n });\r\n\r\n if (contactPlan.writeController) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n webContactControllerStub,\r\n );\r\n }\r\n\r\n if (contactPlan.writeRoutes) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact\"));\r\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\r\n }\r\n\r\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\r\n\r\n for (const message of contactPlan.messages) {\r\n console.log(message);\r\n }\r\n }\r\n }\r\n\r\n await registerWebConnector();\r\n}\r\n\r\nexport const webFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\r\n dependencies: {\r\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\r\n \"@mongez/http\": \"^3.5.0\",\r\n \"@mongez/react-form\": \"^4.0.0\",\r\n \"@mongez/react-localization\": \"^3.4.7\",\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n // Loaded through `await import()` by the dev server only, so both are\r\n // optional peers of `web` rather than hard dependencies.\r\n vite: \"^7.3.5\",\r\n \"@vitejs/plugin-react\": \"^5.2.0\",\r\n },\r\n onExecuting: completeWebInstallation,\r\n};\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"request-memo.mjs","names":[],"sources":["../../../../../../../../core/src/http/context/request-memo.ts"],"sourcesContent":["import { requestContext, type RequestContextStore } from \"./request-context\";\n\n/**\n * In-flight/settled promises for the current request, keyed by the caller's\n * key. Never a process-global map: the outer key is the request's own store\n * object (fresh per request, see `RequestContext.buildStore`), so two\n * concurrent requests can never share — or even see — each other's entries,\n * and everything is GC-eligible once the request's ALS frame ends.\n */\nconst requestMemoStores = new WeakMap<RequestContextStore, Map<string, Promise<any>>>();\n\n/**\n * Single-flight, request-scoped memoization.\n *\n * The first call for a given `key` within the current request runs `fn` once\n * and caches the resulting promise; any call for the same `key` issued while\n * that promise is still pending — or after it has resolved — gets that exact\n * promise back, so parallel loaders that all need the same value never issue\n * duplicate work. A rejection is evicted immediately, so the next call for\n * that `key` retries `fn` from scratch instead of caching the failure.\n *\n * Must run inside a request context (established by\n * `createRequestStore`/`contextManager.runAll` for every HTTP request). There\n * is deliberately no process-global fallback — that is the exact cross-request\n * leak this helper exists to prevent — so calling it outside a request\n * context throws instead of silently memoizing across requests.\n */\nexport function requestMemo<T>(key: string, fn: () => Promise<T>): Promise<T> {\n const store = requestContext.getStore();\n\n if (!store) {\n throw new Error(\n \"requestMemo() was called outside a request context. \" +\n \"It caches work for the lifetime of a single request and must run inside \" +\n \"code executed by the HTTP request pipeline (middleware, a loader, or a \" +\n \"controller) — call it from there. If you need to share a value across \" +\n \"requests, use a process-level cache instead; requestMemo will not fall \" +\n \"back to one.\",\n );\n }\n\n let entries = requestMemoStores.get(store);\n\n if (!entries) {\n entries = new Map();\n requestMemoStores.set(store, entries);\n }\n\n const cached = entries.get(key);\n\n if (cached) return cached;\n\n const promise = (async () => fn())().catch(error => {\n entries!.delete(key);\n throw error;\n });\n\n entries.set(key, promise);\n\n return promise;\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,oCAAoB,IAAI,QAAwD;;;;;;;;;;;;;;;;;AAkBtF,SAAgB,YAAe,KAAa,IAAkC;CAC5E,MAAM,QAAQ,eAAe,SAAS;CAEtC,IAAI,CAAC,OACH,MAAM,IAAI,MACR,8VAMF;CAGF,IAAI,UAAU,kBAAkB,IAAI,KAAK;CAEzC,IAAI,CAAC,SAAS;EACZ,0BAAU,IAAI,IAAI;EAClB,kBAAkB,IAAI,OAAO,OAAO;CACtC;CAEA,MAAM,SAAS,QAAQ,IAAI,GAAG;CAE9B,IAAI,QAAQ,OAAO;CAEnB,MAAM,WAAW,YAAY,GAAG,EAAC,CAAE,CAAC,CAAC,OAAM,UAAS;EAClD,QAAS,OAAO,GAAG;EACnB,MAAM;CACR,CAAC;CAED,QAAQ,IAAI,KAAK,OAAO;CAExB,OAAO;AACT"}
1
+ {"version":3,"file":"request-memo.mjs","names":[],"sources":["../../../../../../../../core/src/http/context/request-memo.ts"],"sourcesContent":["import { requestContext, type RequestContextStore } from \"./request-context\";\n\n/**\n * In-flight/settled promises for the current request, keyed by the caller's\n * key. Never a process-global map: the outer key is the request's own store\n * object (fresh per request, see `RequestContext.buildStore`), so two\n * concurrent requests can never share — or even see — each other's entries,\n * and everything is GC-eligible once the request's ALS frame ends.\n */\nconst requestMemoStores = new WeakMap<RequestContextStore, Map<string, Promise<any>>>();\n\n/**\n * Single-flight, request-scoped memoization.\n *\n * The first call for a given `key` within the current request runs `fn` once\n * and caches the resulting promise; any call for the same `key` issued while\n * that promise is still pending — or after it has resolved — gets that exact\n * promise back, so parallel loaders that all need the same value never issue\n * duplicate work. A rejection is evicted immediately, so the next call for\n * that `key` retries `fn` from scratch instead of caching the failure.\n *\n * Must run inside a request context (established by\n * `createRequestStore`/`contextManager.runAll` for every HTTP request). There\n * is deliberately no process-global fallback — that is the exact cross-request\n * leak this helper exists to prevent — so calling it outside a request\n * context throws instead of silently memoizing across requests.\n */\nexport function requestMemo<T>(key: string, fn: () => Promise<T>): Promise<T> {\n const store = requestContext.getStore();\n\n if (!store) {\n throw new Error(\n \"requestMemo() was called outside a request context. \" +\n \"It caches work for the lifetime of a single request and must run inside \" +\n \"code executed by the HTTP request pipeline (middleware, a loader, or a \" +\n \"controller) — call it from there. If you need to share a value across \" +\n \"requests, use a process-level cache instead; requestMemo will not fall \" +\n \"back to one.\",\n );\n }\n\n let entries = requestMemoStores.get(store);\n\n if (!entries) {\n entries = new Map();\n requestMemoStores.set(store, entries);\n }\n\n const cached = entries.get(key);\n\n if (cached) return cached;\n\n const promise = (async () => fn())().catch((error) => {\n entries!.delete(key);\n throw error;\n });\n\n entries.set(key, promise);\n\n return promise;\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,oCAAoB,IAAI,QAAwD;;;;;;;;;;;;;;;;;AAkBtF,SAAgB,YAAe,KAAa,IAAkC;CAC5E,MAAM,QAAQ,eAAe,SAAS;CAEtC,IAAI,CAAC,OACH,MAAM,IAAI,MACR,8VAMF;CAGF,IAAI,UAAU,kBAAkB,IAAI,KAAK;CAEzC,IAAI,CAAC,SAAS;EACZ,0BAAU,IAAI,IAAI;EAClB,kBAAkB,IAAI,OAAO,OAAO;CACtC;CAEA,MAAM,SAAS,QAAQ,IAAI,GAAG;CAE9B,IAAI,QAAQ,OAAO;CAEnB,MAAM,WAAW,YAAY,GAAG,EAAC,CAAE,CAAC,CAAC,OAAO,UAAU;EACpD,QAAS,OAAO,GAAG;EACnB,MAAM;CACR,CAAC;CAED,QAAQ,IAAI,KAAK,OAAO;CAExB,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"cache-response-middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"mappings":";;;;KAsBY,sBAAA;;AAAZ;;EAIE,QAAA,aAEM,OAAA,EAAS,OAAA,iBACT,OAAA,EAAS,OAAA,KAAY,OAAA;EADZ;;;;;;EAQf,UAAA;EARM;;;;;EAcN,IAAA;EAIA;;;EAAA,GAAA;EAkDc;;;;;;EA3Cd,MAAA;AAAA;AAAA,iBA2Cc,eAAA,CACd,oBAAA,EAAsB,sBAAA,YACrB,UAAU"}
1
+ {"version":3,"file":"cache-response-middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"mappings":";;;;KAsBY,sBAAA;;AAAZ;;EAIE,QAAA,aAAqB,OAAA,EAAS,OAAA,iBAAwB,OAAA,EAAS,OAAA,KAAY,OAAA;EAA7C;;;;;;EAO9B,UAAA;EAPqB;;;;;EAarB,IAAA;EAIA;;;EAAA,GAAA;EA+Cc;;;;;;EAxCd,MAAA;AAAA;AAAA,iBAwCc,eAAA,CAAgB,oBAAA,EAAsB,sBAAA,YAAkC,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"cache-response-middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"sourcesContent":["import { except } from \"@mongez/reinforcements\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Middleware } from \"../../router/types\";\nimport type { Request } from \"./../request\";\nimport type { Response } from \"./../response\";\n\n/**\n * Shape persisted to the cache for a cached response. Stores the status and\n * content-type alongside the body so the HIT path can replay the response\n * faithfully via {@link Response.replay} instead of re-entering `send()`.\n */\ntype CachedResponsePayload = {\n status: number;\n data: unknown;\n contentType?: string;\n};\n\n// TODO: Add option to determine whether to cache the response or not\n// TODO: add option to determine what to be cached from the response\n// TODO: add cache middleware config options for example to set the default driver, ttl, etc\n\nexport type CacheMiddlewareOptions = {\n /**\n * Cache key\n */\n cacheKey:\n | string\n | ((request: Request) => string)\n | ((request: Request) => Promise<string>);\n /**\n * If true, then the response will be cached based on the current locale code\n * This is useful when you have a multi-language website, and you want to cache the response based on the current locale\n *\n * @default true\n */\n withLocale?: boolean;\n /**\n * List of keys from the response object to omit from the cached response\n *\n * @default ['user']\n */\n omit?: string[];\n /**\n * Expires after number of seconds\n */\n ttl?: number;\n /**\n * Cache driver\n *\n * @see config/cache.ts: drivers object\n * @default cache manager\n */\n driver?: string;\n};\n\nconst defaultCacheOptions: Partial<CacheMiddlewareOptions> = {\n withLocale: true,\n};\n\ntype ParsedCacheOptions = Required<CacheMiddlewareOptions> & {\n cacheKey: string;\n};\n\nasync function parseCacheOptions(\n cacheOptions: CacheMiddlewareOptions | string,\n request: Request,\n) {\n if (typeof cacheOptions === \"string\") {\n cacheOptions = {\n cacheKey: cacheOptions,\n };\n }\n\n if (typeof cacheOptions.cacheKey === \"function\") {\n cacheOptions.cacheKey = await cacheOptions.cacheKey(request);\n }\n\n const finalCacheOptions = {\n ...defaultCacheOptions,\n ...cacheOptions,\n } as ParsedCacheOptions;\n\n if (finalCacheOptions.withLocale) {\n const locale = request.getLocaleCode();\n\n finalCacheOptions.cacheKey = `${finalCacheOptions.cacheKey}:${locale}`;\n }\n\n if (!finalCacheOptions.omit) {\n finalCacheOptions.omit = [\"user\", \"settings\"];\n }\n\n return finalCacheOptions;\n}\n\nexport function cacheMiddleware(\n responseCacheOptions: CacheMiddlewareOptions | string,\n): Middleware {\n // The `Middleware` return annotation is load-bearing: without it, tsc never\n // checks this factory's calling convention, which is how the positional v4\n // shape survived an earlier refactor unnoticed.\n return async function ({ request, response }) {\n const { ttl, omit, cacheKey, driver } = await parseCacheOptions(\n responseCacheOptions,\n request,\n );\n const cacheDriver = driver ? await cache.use(driver) : cache;\n\n const content = (await cacheDriver.get(cacheKey)) as CachedResponsePayload | null;\n\n if (content) {\n // Replay through the standard pipeline (status + content-type preserved)\n // instead of `baseResponse.send()`, which would re-enter Response.send()\n // on an already-sent reply, trip the double-send guard, and drop the\n // status / content-type.\n return response.replay({\n status: content.status ?? 200,\n body: content.data,\n contentType: content.contentType,\n });\n }\n\n response.onSent((response: Response) => {\n if (!response.isOk || response.request.path !== request.path) {\n return;\n }\n\n const sentContentType = response.contentType;\n\n const content: CachedResponsePayload = {\n status: response.statusCode,\n data: except(response.parsedBody, omit),\n contentType: typeof sentContentType === \"string\" ? sentContentType : undefined,\n };\n\n // `set` is fire-and-forget inside `onSent`; without a `.catch` a rejected\n // write (e.g. Redis down) would surface as an unhandledRejection.\n cacheDriver.set(cacheKey, content, ttl).catch((error: unknown) => {\n log.error(\"cache-middleware\", \"set\", error);\n });\n });\n };\n}\n"],"mappings":";;;;;AAwDA,MAAM,sBAAuD,EAC3D,YAAY,KACd;AAMA,eAAe,kBACb,cACA,SACA;CACA,IAAI,OAAO,iBAAiB,UAC1B,eAAe,EACb,UAAU,aACZ;CAGF,IAAI,OAAO,aAAa,aAAa,YACnC,aAAa,WAAW,MAAM,aAAa,SAAS,OAAO;CAG7D,MAAM,oBAAoB;EACxB,GAAG;EACH,GAAG;CACL;CAEA,IAAI,kBAAkB,YAAY;EAChC,MAAM,SAAS,QAAQ,cAAc;EAErC,kBAAkB,WAAW,GAAG,kBAAkB,SAAS,GAAG;CAChE;CAEA,IAAI,CAAC,kBAAkB,MACrB,kBAAkB,OAAO,CAAC,QAAQ,UAAU;CAG9C,OAAO;AACT;AAEA,SAAgB,gBACd,sBACY;CAIZ,OAAO,eAAgB,EAAE,SAAS,YAAY;EAC5C,MAAM,EAAE,KAAK,MAAM,UAAU,WAAW,MAAM,kBAC5C,sBACA,OACF;EACA,MAAM,cAAc,SAAS,MAAM,MAAM,IAAI,MAAM,IAAI;EAEvD,MAAM,UAAW,MAAM,YAAY,IAAI,QAAQ;EAE/C,IAAI,SAKF,OAAO,SAAS,OAAO;GACrB,QAAQ,QAAQ,UAAU;GAC1B,MAAM,QAAQ;GACd,aAAa,QAAQ;EACvB,CAAC;EAGH,SAAS,QAAQ,aAAuB;GACtC,IAAI,CAAC,SAAS,QAAQ,SAAS,QAAQ,SAAS,QAAQ,MACtD;GAGF,MAAM,kBAAkB,SAAS;GAEjC,MAAM,UAAiC;IACrC,QAAQ,SAAS;IACjB,MAAM,OAAO,SAAS,YAAY,IAAI;IACtC,aAAa,OAAO,oBAAoB,WAAW,kBAAkB;GACvE;GAIA,YAAY,IAAI,UAAU,SAAS,GAAG,CAAC,CAAC,OAAO,UAAmB;IAChE,IAAI,MAAM,oBAAoB,OAAO,KAAK;GAC5C,CAAC;EACH,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"cache-response-middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"sourcesContent":["import { except } from \"@mongez/reinforcements\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Middleware } from \"../../router/types\";\nimport type { Request } from \"./../request\";\nimport type { Response } from \"./../response\";\n\n/**\n * Shape persisted to the cache for a cached response. Stores the status and\n * content-type alongside the body so the HIT path can replay the response\n * faithfully via {@link Response.replay} instead of re-entering `send()`.\n */\ntype CachedResponsePayload = {\n status: number;\n data: unknown;\n contentType?: string;\n};\n\n// TODO: Add option to determine whether to cache the response or not\n// TODO: add option to determine what to be cached from the response\n// TODO: add cache middleware config options for example to set the default driver, ttl, etc\n\nexport type CacheMiddlewareOptions = {\n /**\n * Cache key\n */\n cacheKey: string | ((request: Request) => string) | ((request: Request) => Promise<string>);\n /**\n * If true, then the response will be cached based on the current locale code\n * This is useful when you have a multi-language website, and you want to cache the response based on the current locale\n *\n * @default true\n */\n withLocale?: boolean;\n /**\n * List of keys from the response object to omit from the cached response\n *\n * @default ['user']\n */\n omit?: string[];\n /**\n * Expires after number of seconds\n */\n ttl?: number;\n /**\n * Cache driver\n *\n * @see config/cache.ts: drivers object\n * @default cache manager\n */\n driver?: string;\n};\n\nconst defaultCacheOptions: Partial<CacheMiddlewareOptions> = {\n withLocale: true,\n};\n\ntype ParsedCacheOptions = Required<CacheMiddlewareOptions> & {\n cacheKey: string;\n};\n\nasync function parseCacheOptions(cacheOptions: CacheMiddlewareOptions | string, request: Request) {\n if (typeof cacheOptions === \"string\") {\n cacheOptions = {\n cacheKey: cacheOptions,\n };\n }\n\n if (typeof cacheOptions.cacheKey === \"function\") {\n cacheOptions.cacheKey = await cacheOptions.cacheKey(request);\n }\n\n const finalCacheOptions = {\n ...defaultCacheOptions,\n ...cacheOptions,\n } as ParsedCacheOptions;\n\n if (finalCacheOptions.withLocale) {\n const locale = request.getLocaleCode();\n\n finalCacheOptions.cacheKey = `${finalCacheOptions.cacheKey}:${locale}`;\n }\n\n if (!finalCacheOptions.omit) {\n finalCacheOptions.omit = [\"user\", \"settings\"];\n }\n\n return finalCacheOptions;\n}\n\nexport function cacheMiddleware(responseCacheOptions: CacheMiddlewareOptions | string): Middleware {\n // The `Middleware` return annotation is load-bearing: without it, tsc never\n // checks this factory's calling convention, which is how the positional v4\n // shape survived an earlier refactor unnoticed.\n return async function ({ request, response }) {\n const { ttl, omit, cacheKey, driver } = await parseCacheOptions(responseCacheOptions, request);\n const cacheDriver = driver ? await cache.use(driver) : cache;\n\n const content = (await cacheDriver.get(cacheKey)) as CachedResponsePayload | null;\n\n if (content) {\n // Replay through the standard pipeline (status + content-type preserved)\n // instead of `baseResponse.send()`, which would re-enter Response.send()\n // on an already-sent reply, trip the double-send guard, and drop the\n // status / content-type.\n return response.replay({\n status: content.status ?? 200,\n body: content.data,\n contentType: content.contentType,\n });\n }\n\n response.onSent((response: Response) => {\n if (!response.isOk || response.request.path !== request.path) {\n return;\n }\n\n const sentContentType = response.contentType;\n\n const content: CachedResponsePayload = {\n status: response.statusCode,\n data: except(response.parsedBody, omit),\n contentType: typeof sentContentType === \"string\" ? sentContentType : undefined,\n };\n\n // `set` is fire-and-forget inside `onSent`; without a `.catch` a rejected\n // write (e.g. Redis down) would surface as an unhandledRejection.\n cacheDriver.set(cacheKey, content, ttl).catch((error: unknown) => {\n log.error(\"cache-middleware\", \"set\", error);\n });\n });\n };\n}\n"],"mappings":";;;;;AAqDA,MAAM,sBAAuD,EAC3D,YAAY,KACd;AAMA,eAAe,kBAAkB,cAA+C,SAAkB;CAChG,IAAI,OAAO,iBAAiB,UAC1B,eAAe,EACb,UAAU,aACZ;CAGF,IAAI,OAAO,aAAa,aAAa,YACnC,aAAa,WAAW,MAAM,aAAa,SAAS,OAAO;CAG7D,MAAM,oBAAoB;EACxB,GAAG;EACH,GAAG;CACL;CAEA,IAAI,kBAAkB,YAAY;EAChC,MAAM,SAAS,QAAQ,cAAc;EAErC,kBAAkB,WAAW,GAAG,kBAAkB,SAAS,GAAG;CAChE;CAEA,IAAI,CAAC,kBAAkB,MACrB,kBAAkB,OAAO,CAAC,QAAQ,UAAU;CAG9C,OAAO;AACT;AAEA,SAAgB,gBAAgB,sBAAmE;CAIjG,OAAO,eAAgB,EAAE,SAAS,YAAY;EAC5C,MAAM,EAAE,KAAK,MAAM,UAAU,WAAW,MAAM,kBAAkB,sBAAsB,OAAO;EAC7F,MAAM,cAAc,SAAS,MAAM,MAAM,IAAI,MAAM,IAAI;EAEvD,MAAM,UAAW,MAAM,YAAY,IAAI,QAAQ;EAE/C,IAAI,SAKF,OAAO,SAAS,OAAO;GACrB,QAAQ,QAAQ,UAAU;GAC1B,MAAM,QAAQ;GACd,aAAa,QAAQ;EACvB,CAAC;EAGH,SAAS,QAAQ,aAAuB;GACtC,IAAI,CAAC,SAAS,QAAQ,SAAS,QAAQ,SAAS,QAAQ,MACtD;GAGF,MAAM,kBAAkB,SAAS;GAEjC,MAAM,UAAiC;IACrC,QAAQ,SAAS;IACjB,MAAM,OAAO,SAAS,YAAY,IAAI;IACtC,aAAa,OAAO,oBAAoB,WAAW,kBAAkB;GACvE;GAIA,YAAY,IAAI,UAAU,SAAS,GAAG,CAAC,CAAC,OAAO,UAAmB;IAChE,IAAI,MAAM,oBAAoB,OAAO,KAAK;GAC5C,CAAC;EACH,CAAC;CACH;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"concurrency-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/concurrency-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport type { Response } from \"../response\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the concurrency limit middleware.\n */\nexport type ConcurrencyLimitOptions = {\n /**\n * Group key generator. Defaults to `request.route.path` — i.e. the cap\n * applies across all callers of that route. Override to scope per-user\n * or per-tenant.\n *\n * @example\n * keyGenerator: (request) => `${request.route.path}:${request.user?.id ?? request.ip}`,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\nconst counters = new Map<string, number>();\n\nfunction release(key: string) {\n const after = (counters.get(key) || 1) - 1;\n\n if (after <= 0) {\n counters.delete(key);\n\n return;\n }\n\n counters.set(key, after);\n}\n\n/**\n * Cap the number of in-flight requests against a route. Above the cap, new\n * requests get a fast 429 + `Retry-After: 1` — no queue, no timeout.\n *\n * Use for endpoints whose cost is unbounded per-request: report generation,\n * AI completions, image processing, expensive aggregations. Different from\n * `rateLimit()` — rate-limit caps requests-per-time, concurrency caps\n * in-flight requests at any instant.\n *\n * The counter is process-local. With `N` replicas the effective cap is\n * `N × max`. Document this in the route's behavior; if you need shared\n * concurrency across replicas, reach for a `@warlock.js/cache` lock instead.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/reports/generate\", reportController, {\n * middleware: [middleware.concurrencyLimit(3)],\n * });\n *\n * router.post(\"/ai/summarize\", summarizeController, {\n * middleware: [\n * middleware.concurrencyLimit(10, {\n * keyGenerator: (request) => `ai:${request.user?.id ?? request.ip}`,\n * }),\n * ],\n * });\n */\nexport function concurrencyLimitMiddleware(\n max: number,\n options: ConcurrencyLimitOptions = {},\n): Middleware {\n return ({ request, response }) => {\n const key = options.keyGenerator?.(request) || request.route.path;\n const current = counters.get(key) || 0;\n\n if (current >= max) {\n response.header(\"Retry-After\", 1);\n\n return response.tooManyRequests({\n error: options.errorMessage || t(\"http.concurrencyLimitReached\"),\n errorCode: HttpErrorCodes.ConcurrencyLimitReached,\n limit: max,\n },\n );\n }\n\n counters.set(key, current + 1);\n\n let released = false;\n\n const releaseOnce = () => {\n if (released) {\n return;\n }\n\n released = true;\n release(key);\n };\n\n // `onSent` only fires from Response.send()/stream().end()/sse().end().\n // Bare-reply paths (noContent, redirect, sendFile, download, sendBuffer,\n // raw send) never trigger it, so the slot would leak and the route would\n // permanently 429 after `max` such requests. Bind to the raw socket\n // lifecycle as well so the slot frees regardless of response path —\n // `releaseOnce` is idempotent, so double-firing is harmless.\n response.onSent((_sentResponse: Response) => releaseOnce());\n response.baseResponse.raw.once(\"finish\", releaseOnce);\n response.baseResponse.raw.once(\"close\", releaseOnce);\n };\n}\n"],"mappings":";;;;AAyBA,MAAM,2BAAW,IAAI,IAAoB;AAEzC,SAAS,QAAQ,KAAa;CAC5B,MAAM,SAAS,SAAS,IAAI,GAAG,KAAK,KAAK;CAEzC,IAAI,SAAS,GAAG;EACd,SAAS,OAAO,GAAG;EAEnB;CACF;CAEA,SAAS,IAAI,KAAK,KAAK;AACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,2BACd,KACA,UAAmC,CAAC,GACxB;CACZ,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,MAAM,QAAQ,eAAe,OAAO,KAAK,QAAQ,MAAM;EAC7D,MAAM,UAAU,SAAS,IAAI,GAAG,KAAK;EAErC,IAAI,WAAW,KAAK;GAClB,SAAS,OAAO,eAAe,CAAC;GAEhC,OAAO,SAAS,gBAAgB;IAC5B,OAAO,QAAQ,gBAAgB,EAAE,8BAA8B;IAC/D;IACA,OAAO;GACT,CACF;EACF;EAEA,SAAS,IAAI,KAAK,UAAU,CAAC;EAE7B,IAAI,WAAW;EAEf,MAAM,oBAAoB;GACxB,IAAI,UACF;GAGF,WAAW;GACX,QAAQ,GAAG;EACb;EAQA,SAAS,QAAQ,kBAA4B,YAAY,CAAC;EAC1D,SAAS,aAAa,IAAI,KAAK,UAAU,WAAW;EACpD,SAAS,aAAa,IAAI,KAAK,SAAS,WAAW;CACrD;AACF"}
1
+ {"version":3,"file":"concurrency-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/concurrency-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport type { Response } from \"../response\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the concurrency limit middleware.\n */\nexport type ConcurrencyLimitOptions = {\n /**\n * Group key generator. Defaults to `request.route.path` — i.e. the cap\n * applies across all callers of that route. Override to scope per-user\n * or per-tenant.\n *\n * @example\n * keyGenerator: (request) => `${request.route.path}:${request.user?.id ?? request.ip}`,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\nconst counters = new Map<string, number>();\n\nfunction release(key: string) {\n const after = (counters.get(key) || 1) - 1;\n\n if (after <= 0) {\n counters.delete(key);\n\n return;\n }\n\n counters.set(key, after);\n}\n\n/**\n * Cap the number of in-flight requests against a route. Above the cap, new\n * requests get a fast 429 + `Retry-After: 1` — no queue, no timeout.\n *\n * Use for endpoints whose cost is unbounded per-request: report generation,\n * AI completions, image processing, expensive aggregations. Different from\n * `rateLimit()` — rate-limit caps requests-per-time, concurrency caps\n * in-flight requests at any instant.\n *\n * The counter is process-local. With `N` replicas the effective cap is\n * `N × max`. Document this in the route's behavior; if you need shared\n * concurrency across replicas, reach for a `@warlock.js/cache` lock instead.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/reports/generate\", reportController, {\n * middleware: [middleware.concurrencyLimit(3)],\n * });\n *\n * router.post(\"/ai/summarize\", summarizeController, {\n * middleware: [\n * middleware.concurrencyLimit(10, {\n * keyGenerator: (request) => `ai:${request.user?.id ?? request.ip}`,\n * }),\n * ],\n * });\n */\nexport function concurrencyLimitMiddleware(\n max: number,\n options: ConcurrencyLimitOptions = {},\n): Middleware {\n return ({ request, response }) => {\n const key = options.keyGenerator?.(request) || request.route.path;\n const current = counters.get(key) || 0;\n\n if (current >= max) {\n response.header(\"Retry-After\", 1);\n\n return response.tooManyRequests({\n error: options.errorMessage || t(\"http.concurrencyLimitReached\"),\n errorCode: HttpErrorCodes.ConcurrencyLimitReached,\n limit: max,\n });\n }\n\n counters.set(key, current + 1);\n\n let released = false;\n\n const releaseOnce = () => {\n if (released) {\n return;\n }\n\n released = true;\n release(key);\n };\n\n // `onSent` only fires from Response.send()/stream().end()/sse().end().\n // Bare-reply paths (noContent, redirect, sendFile, download, sendBuffer,\n // raw send) never trigger it, so the slot would leak and the route would\n // permanently 429 after `max` such requests. Bind to the raw socket\n // lifecycle as well so the slot frees regardless of response path —\n // `releaseOnce` is idempotent, so double-firing is harmless.\n response.onSent((_sentResponse: Response) => releaseOnce());\n response.baseResponse.raw.once(\"finish\", releaseOnce);\n response.baseResponse.raw.once(\"close\", releaseOnce);\n };\n}\n"],"mappings":";;;;AAyBA,MAAM,2BAAW,IAAI,IAAoB;AAEzC,SAAS,QAAQ,KAAa;CAC5B,MAAM,SAAS,SAAS,IAAI,GAAG,KAAK,KAAK;CAEzC,IAAI,SAAS,GAAG;EACd,SAAS,OAAO,GAAG;EAEnB;CACF;CAEA,SAAS,IAAI,KAAK,KAAK;AACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,2BACd,KACA,UAAmC,CAAC,GACxB;CACZ,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,MAAM,QAAQ,eAAe,OAAO,KAAK,QAAQ,MAAM;EAC7D,MAAM,UAAU,SAAS,IAAI,GAAG,KAAK;EAErC,IAAI,WAAW,KAAK;GAClB,SAAS,OAAO,eAAe,CAAC;GAEhC,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,8BAA8B;IAC/D;IACA,OAAO;GACT,CAAC;EACH;EAEA,SAAS,IAAI,KAAK,UAAU,CAAC;EAE7B,IAAI,WAAW;EAEf,MAAM,oBAAoB;GACxB,IAAI,UACF;GAGF,WAAW;GACX,QAAQ,GAAG;EACb;EAQA,SAAS,QAAQ,kBAA4B,YAAY,CAAC;EAC1D,SAAS,aAAa,IAAI,KAAK,UAAU,WAAW;EACpD,SAAS,aAAa,IAAI,KAAK,SAAS,WAAW;CACrD;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/idempotency.middleware.ts"],"mappings":";;;;;KAgBY,kBAAA;EAAkB;;;EAI5B,GAAA;EAKA;;;;EAAA,UAAA;EAqDc;;;;;EA/Cd,OAAA;EA+CuE;;AAAU;;EA1CjF,MAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0Cc,qBAAA,CAAsB,OAAA,GAAS,kBAAA,GAA0B,UAAU"}
1
+ {"version":3,"file":"idempotency.middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/idempotency.middleware.ts"],"mappings":";;;;;KAYY,kBAAA;EAAkB;;;EAI5B,GAAA;EAKA;;;;EAAA,UAAA;EAqDc;;;;;EA/Cd,OAAA;EA+CuE;;AAAU;;EA1CjF,MAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0Cc,qBAAA,CAAsB,OAAA,GAAS,kBAAA,GAA0B,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/idempotency.middleware.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Response } from \"../response\";\nimport { t } from \"./inject-request-context\";\nimport {\n buildIdempotencyCacheKey,\n hashBody,\n isValidIdempotencyKey,\n} from \"./utils/idempotency-key\";\n\n/**\n * Options for the idempotency middleware.\n */\nexport type IdempotencyOptions = {\n /**\n * Cache TTL in seconds. Falls back to `http.idempotency.ttl`, then `86400` (24h).\n */\n ttl?: number;\n /**\n * Header name carrying the client's key. Falls back to\n * `http.idempotency.headerName`, then `\"Idempotency-Key\"`.\n */\n headerName?: string;\n /**\n * HTTP methods eligible for idempotency. Falls back to\n * `http.idempotency.methods`, then `[\"POST\",\"PUT\",\"PATCH\",\"DELETE\"]`.\n * Safe methods (GET/HEAD) are skipped regardless.\n */\n methods?: string[];\n /**\n * Cache driver name. Falls back to `http.idempotency.driver`, then the\n * default driver of the cache manager.\n */\n driver?: string;\n};\n\ntype CachedResponse = {\n status: number;\n body: unknown;\n bodyHash: string;\n contentType?: string;\n};\n\nconst DEFAULT_METHODS = [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\n/**\n * Dedupe non-idempotent writes by an `Idempotency-Key` header — same key,\n * same body, within TTL → cached replay; same key, different body → 422\n * `IdempotencyKeyConflict`.\n *\n * **Must run after `authMiddleware`** — the cache key is scoped per-user\n * (`idem:{userType}:{userId}:{key}`) so user A can't replay user B's key.\n * Anonymous requests fall back to IP scope.\n *\n * The replay sets `Idempotent-Replay: true` on the response for easy\n * client-side / observability detection.\n *\n * Eligible methods default to POST/PUT/PATCH/DELETE. GET/HEAD pass through\n * even with the header set (RFC: safe methods are already idempotent).\n *\n * @example\n * import { authMiddleware } from \"@warlock.js/auth\";\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/orders\", createOrderController, {\n * middleware: [authMiddleware(\"client\"), middleware.idempotency()],\n * });\n *\n * router.post(\"/ai/summarize\", summarizeController, {\n * middleware: [\n * authMiddleware(\"client\"),\n * middleware.idempotency({ ttl: 60 * 60 }), // 1h is enough for client retries\n * ],\n * });\n */\nexport function idempotencyMiddleware(options: IdempotencyOptions = {}): Middleware {\n return async ({ request, response }) => {\n const headerName =\n options.headerName ||\n config.get(\"http.idempotency.headerName\", \"Idempotency-Key\");\n const methods =\n options.methods || config.get(\"http.idempotency.methods\", DEFAULT_METHODS);\n const ttl = options.ttl || config.get(\"http.idempotency.ttl\", 86400);\n const driverName = options.driver || config.get(\"http.idempotency.driver\");\n\n if (!methods.includes(request.method.toUpperCase())) return;\n\n const idempotencyKey = request.header(headerName.toLowerCase());\n\n if (!idempotencyKey) return;\n\n if (!isValidIdempotencyKey(idempotencyKey)) {\n return response.badRequest({\n error: t(\"http.idempotencyKeyInvalid\"),\n errorCode: HttpErrorCodes.IdempotencyKeyInvalid,\n });\n }\n\n const cacheDriver = driverName ? await cache.use(driverName) : cache;\n const cacheKey = buildIdempotencyCacheKey(request, idempotencyKey);\n const bodyHash = hashBody(request.body);\n\n const cached = (await cacheDriver.get(cacheKey)) as CachedResponse | null;\n\n if (cached) {\n if (cached.bodyHash !== bodyHash) {\n return response.unprocessableEntity({\n error: t(\"http.idempotencyKeyConflict\"),\n errorCode: HttpErrorCodes.IdempotencyKeyConflict,\n });\n }\n\n response.header(\"Idempotent-Replay\", \"true\");\n\n return response.replay({\n status: cached.status,\n body: cached.body,\n contentType: cached.contentType,\n });\n }\n\n response.onSent((sentResponse: Response) => {\n // Don't cache server errors — clients should be able to retry past a 5xx.\n // 4xx are deterministic outcomes of the request, so caching is fine.\n if (sentResponse.statusCode >= 500) return;\n\n const sentContentType = sentResponse.contentType;\n\n // `set` is fire-and-forget inside `onSent`; without a `.catch` a rejected\n // write (e.g. Redis down) would surface as an unhandledRejection.\n cacheDriver\n .set(\n cacheKey,\n {\n status: sentResponse.statusCode,\n body: sentResponse.parsedBody,\n bodyHash,\n contentType: typeof sentContentType === \"string\" ? sentContentType : undefined,\n },\n ttl,\n )\n .catch((error: unknown) => {\n log.error(\"idempotency-middleware\", \"set\", error);\n });\n });\n };\n}\n"],"mappings":";;;;;;;;AA8CA,MAAM,kBAAkB;CAAC;CAAQ;CAAO;CAAS;AAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCzD,SAAgB,sBAAsB,UAA8B,CAAC,GAAe;CAClF,OAAO,OAAO,EAAE,SAAS,eAAe;EACtC,MAAM,aACJ,QAAQ,cACR,OAAO,IAAI,+BAA+B,iBAAiB;EAC7D,MAAM,UACJ,QAAQ,WAAW,OAAO,IAAI,4BAA4B,eAAe;EAC3E,MAAM,MAAM,QAAQ,OAAO,OAAO,IAAI,wBAAwB,KAAK;EACnE,MAAM,aAAa,QAAQ,UAAU,OAAO,IAAI,yBAAyB;EAEzE,IAAI,CAAC,QAAQ,SAAS,QAAQ,OAAO,YAAY,CAAC,GAAG;EAErD,MAAM,iBAAiB,QAAQ,OAAO,WAAW,YAAY,CAAC;EAE9D,IAAI,CAAC,gBAAgB;EAErB,IAAI,CAAC,sBAAsB,cAAc,GACvC,OAAO,SAAS,WAAW;GACzB,OAAO,EAAE,4BAA4B;GACrC;EACF,CAAC;EAGH,MAAM,cAAc,aAAa,MAAM,MAAM,IAAI,UAAU,IAAI;EAC/D,MAAM,WAAW,yBAAyB,SAAS,cAAc;EACjE,MAAM,WAAW,SAAS,QAAQ,IAAI;EAEtC,MAAM,SAAU,MAAM,YAAY,IAAI,QAAQ;EAE9C,IAAI,QAAQ;GACV,IAAI,OAAO,aAAa,UACtB,OAAO,SAAS,oBAAoB;IAClC,OAAO,EAAE,6BAA6B;IACtC;GACF,CAAC;GAGH,SAAS,OAAO,qBAAqB,MAAM;GAE3C,OAAO,SAAS,OAAO;IACrB,QAAQ,OAAO;IACf,MAAM,OAAO;IACb,aAAa,OAAO;GACtB,CAAC;EACH;EAEA,SAAS,QAAQ,iBAA2B;GAG1C,IAAI,aAAa,cAAc,KAAK;GAEpC,MAAM,kBAAkB,aAAa;GAIrC,YACG,IACC,UACA;IACE,QAAQ,aAAa;IACrB,MAAM,aAAa;IACnB;IACA,aAAa,OAAO,oBAAoB,WAAW,kBAAkB;GACvE,GACA,GACF,CAAC,CACA,OAAO,UAAmB;IACzB,IAAI,MAAM,0BAA0B,OAAO,KAAK;GAClD,CAAC;EACL,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"idempotency.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/idempotency.middleware.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Response } from \"../response\";\nimport { t } from \"./inject-request-context\";\nimport { buildIdempotencyCacheKey, hashBody, isValidIdempotencyKey } from \"./utils/idempotency-key\";\n\n/**\n * Options for the idempotency middleware.\n */\nexport type IdempotencyOptions = {\n /**\n * Cache TTL in seconds. Falls back to `http.idempotency.ttl`, then `86400` (24h).\n */\n ttl?: number;\n /**\n * Header name carrying the client's key. Falls back to\n * `http.idempotency.headerName`, then `\"Idempotency-Key\"`.\n */\n headerName?: string;\n /**\n * HTTP methods eligible for idempotency. Falls back to\n * `http.idempotency.methods`, then `[\"POST\",\"PUT\",\"PATCH\",\"DELETE\"]`.\n * Safe methods (GET/HEAD) are skipped regardless.\n */\n methods?: string[];\n /**\n * Cache driver name. Falls back to `http.idempotency.driver`, then the\n * default driver of the cache manager.\n */\n driver?: string;\n};\n\ntype CachedResponse = {\n status: number;\n body: unknown;\n bodyHash: string;\n contentType?: string;\n};\n\nconst DEFAULT_METHODS = [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\n/**\n * Dedupe non-idempotent writes by an `Idempotency-Key` header — same key,\n * same body, within TTL → cached replay; same key, different body → 422\n * `IdempotencyKeyConflict`.\n *\n * **Must run after `authMiddleware`** — the cache key is scoped per-user\n * (`idem:{userType}:{userId}:{key}`) so user A can't replay user B's key.\n * Anonymous requests fall back to IP scope.\n *\n * The replay sets `Idempotent-Replay: true` on the response for easy\n * client-side / observability detection.\n *\n * Eligible methods default to POST/PUT/PATCH/DELETE. GET/HEAD pass through\n * even with the header set (RFC: safe methods are already idempotent).\n *\n * @example\n * import { authMiddleware } from \"@warlock.js/auth\";\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/orders\", createOrderController, {\n * middleware: [authMiddleware(\"client\"), middleware.idempotency()],\n * });\n *\n * router.post(\"/ai/summarize\", summarizeController, {\n * middleware: [\n * authMiddleware(\"client\"),\n * middleware.idempotency({ ttl: 60 * 60 }), // 1h is enough for client retries\n * ],\n * });\n */\nexport function idempotencyMiddleware(options: IdempotencyOptions = {}): Middleware {\n return async ({ request, response }) => {\n const headerName =\n options.headerName || config.get(\"http.idempotency.headerName\", \"Idempotency-Key\");\n const methods = options.methods || config.get(\"http.idempotency.methods\", DEFAULT_METHODS);\n const ttl = options.ttl || config.get(\"http.idempotency.ttl\", 86400);\n const driverName = options.driver || config.get(\"http.idempotency.driver\");\n\n if (!methods.includes(request.method.toUpperCase())) return;\n\n const idempotencyKey = request.header(headerName.toLowerCase());\n\n if (!idempotencyKey) return;\n\n if (!isValidIdempotencyKey(idempotencyKey)) {\n return response.badRequest({\n error: t(\"http.idempotencyKeyInvalid\"),\n errorCode: HttpErrorCodes.IdempotencyKeyInvalid,\n });\n }\n\n const cacheDriver = driverName ? await cache.use(driverName) : cache;\n const cacheKey = buildIdempotencyCacheKey(request, idempotencyKey);\n const bodyHash = hashBody(request.body);\n\n const cached = (await cacheDriver.get(cacheKey)) as CachedResponse | null;\n\n if (cached) {\n if (cached.bodyHash !== bodyHash) {\n return response.unprocessableEntity({\n error: t(\"http.idempotencyKeyConflict\"),\n errorCode: HttpErrorCodes.IdempotencyKeyConflict,\n });\n }\n\n response.header(\"Idempotent-Replay\", \"true\");\n\n return response.replay({\n status: cached.status,\n body: cached.body,\n contentType: cached.contentType,\n });\n }\n\n response.onSent((sentResponse: Response) => {\n // Don't cache server errors — clients should be able to retry past a 5xx.\n // 4xx are deterministic outcomes of the request, so caching is fine.\n if (sentResponse.statusCode >= 500) return;\n\n const sentContentType = sentResponse.contentType;\n\n // `set` is fire-and-forget inside `onSent`; without a `.catch` a rejected\n // write (e.g. Redis down) would surface as an unhandledRejection.\n cacheDriver\n .set(\n cacheKey,\n {\n status: sentResponse.statusCode,\n body: sentResponse.parsedBody,\n bodyHash,\n contentType: typeof sentContentType === \"string\" ? sentContentType : undefined,\n },\n ttl,\n )\n .catch((error: unknown) => {\n log.error(\"idempotency-middleware\", \"set\", error);\n });\n });\n };\n}\n"],"mappings":";;;;;;;;AA0CA,MAAM,kBAAkB;CAAC;CAAQ;CAAO;CAAS;AAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCzD,SAAgB,sBAAsB,UAA8B,CAAC,GAAe;CAClF,OAAO,OAAO,EAAE,SAAS,eAAe;EACtC,MAAM,aACJ,QAAQ,cAAc,OAAO,IAAI,+BAA+B,iBAAiB;EACnF,MAAM,UAAU,QAAQ,WAAW,OAAO,IAAI,4BAA4B,eAAe;EACzF,MAAM,MAAM,QAAQ,OAAO,OAAO,IAAI,wBAAwB,KAAK;EACnE,MAAM,aAAa,QAAQ,UAAU,OAAO,IAAI,yBAAyB;EAEzE,IAAI,CAAC,QAAQ,SAAS,QAAQ,OAAO,YAAY,CAAC,GAAG;EAErD,MAAM,iBAAiB,QAAQ,OAAO,WAAW,YAAY,CAAC;EAE9D,IAAI,CAAC,gBAAgB;EAErB,IAAI,CAAC,sBAAsB,cAAc,GACvC,OAAO,SAAS,WAAW;GACzB,OAAO,EAAE,4BAA4B;GACrC;EACF,CAAC;EAGH,MAAM,cAAc,aAAa,MAAM,MAAM,IAAI,UAAU,IAAI;EAC/D,MAAM,WAAW,yBAAyB,SAAS,cAAc;EACjE,MAAM,WAAW,SAAS,QAAQ,IAAI;EAEtC,MAAM,SAAU,MAAM,YAAY,IAAI,QAAQ;EAE9C,IAAI,QAAQ;GACV,IAAI,OAAO,aAAa,UACtB,OAAO,SAAS,oBAAoB;IAClC,OAAO,EAAE,6BAA6B;IACtC;GACF,CAAC;GAGH,SAAS,OAAO,qBAAqB,MAAM;GAE3C,OAAO,SAAS,OAAO;IACrB,QAAQ,OAAO;IACf,MAAM,OAAO;IACb,aAAa,OAAO;GACtB,CAAC;EACH;EAEA,SAAS,QAAQ,iBAA2B;GAG1C,IAAI,aAAa,cAAc,KAAK;GAEpC,MAAM,kBAAkB,aAAa;GAIrC,YACG,IACC,UACA;IACE,QAAQ,aAAa;IACrB,MAAM,aAAa;IACnB;IACA,aAAa,OAAO,oBAAoB,WAAW,kBAAkB;GACvE,GACA,GACF,CAAC,CACA,OAAO,UAAmB;IACzB,IAAI,MAAM,0BAA0B,OAAO,KAAK;GAClD,CAAC;EACL,CAAC;CACH;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"maintenance.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/maintenance.middleware.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the maintenance middleware.\n */\nexport type MaintenanceOptions = {\n /**\n * Path prefixes (ending in `*`) or exact paths to bypass even when\n * maintenance is on. Falls back to `http.maintenance.allowlist`, then\n * `[\"/health\"]`.\n *\n * @example\n * allowlist: [\"/health\", \"/admin/*\", \"/webhooks/stripe\"]\n */\n allowlist?: string[];\n /**\n * Seconds advertised in the `Retry-After` header. Falls back to\n * `http.maintenance.retryAfter`, then `60`.\n */\n retryAfter?: number;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\nfunction isAllowlisted(path: string, patterns: string[]) {\n return patterns.some((pattern) => {\n if (pattern.endsWith(\"*\")) {\n return path.startsWith(pattern.slice(0, -1));\n }\n\n return path === pattern;\n });\n}\n\n/**\n * Return 503 + `Retry-After` for every request when `http.maintenance.enabled`\n * is true, except for paths matching the allowlist.\n *\n * Designed for app-wide registration via `http.middleware.all`. Toggled via\n * config — flipping the flag requires a restart (no runtime hot-flip yet).\n * Allowlist defaults to `[\"/health\"]` so health checks still pass during\n * planned downtime.\n *\n * @example\n * // src/config/http.ts\n * import { middleware } from \"@warlock.js/core\";\n *\n * export default {\n * maintenance: { enabled: env(\"MAINTENANCE_MODE\") === \"true\" },\n * middleware: {\n * all: [middleware.maintenance({ allowlist: [\"/health\", \"/admin/*\"] })],\n * },\n * };\n */\nexport function maintenanceMiddleware(options: MaintenanceOptions = {}): Middleware {\n return ({ request, response }) => {\n const enabled = config.get(\"http.maintenance.enabled\", false);\n\n if (!enabled) return;\n\n const allowlist =\n options.allowlist || config.get(\"http.maintenance.allowlist\", [\"/health\"]);\n\n // `request.path` includes the query string, but allowlist entries are\n // path-only (\"/webhooks/stripe\"), so strip the query before matching —\n // otherwise \"/webhooks/stripe?sig=...\" never matches its exact entry.\n const pathname = request.path.split(\"?\")[0];\n\n if (isAllowlisted(pathname, allowlist)) return;\n\n const retryAfter =\n options.retryAfter || config.get(\"http.maintenance.retryAfter\", 60);\n\n response.header(\"Retry-After\", retryAfter);\n\n return response.serviceUnavailable({\n error: options.errorMessage || t(\"http.maintenance\"),\n errorCode: HttpErrorCodes.Maintenance,\n });\n };\n}\n"],"mappings":";;;;;AA6BA,SAAS,cAAc,MAAc,UAAoB;CACvD,OAAO,SAAS,MAAM,YAAY;EAChC,IAAI,QAAQ,SAAS,GAAG,GACtB,OAAO,KAAK,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC;EAG7C,OAAO,SAAS;CAClB,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,sBAAsB,UAA8B,CAAC,GAAe;CAClF,QAAQ,EAAE,SAAS,eAAe;EAGhC,IAAI,CAFY,OAAO,IAAI,4BAA4B,KAE5C,GAAG;EAEd,MAAM,YACJ,QAAQ,aAAa,OAAO,IAAI,8BAA8B,CAAC,SAAS,CAAC;EAK3E,MAAM,WAAW,QAAQ,KAAK,MAAM,GAAG,CAAC,CAAC;EAEzC,IAAI,cAAc,UAAU,SAAS,GAAG;EAExC,MAAM,aACJ,QAAQ,cAAc,OAAO,IAAI,+BAA+B,EAAE;EAEpE,SAAS,OAAO,eAAe,UAAU;EAEzC,OAAO,SAAS,mBAAmB;GACjC,OAAO,QAAQ,gBAAgB,EAAE,kBAAkB;GACnD;EACF,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"maintenance.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/maintenance.middleware.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the maintenance middleware.\n */\nexport type MaintenanceOptions = {\n /**\n * Path prefixes (ending in `*`) or exact paths to bypass even when\n * maintenance is on. Falls back to `http.maintenance.allowlist`, then\n * `[\"/health\"]`.\n *\n * @example\n * allowlist: [\"/health\", \"/admin/*\", \"/webhooks/stripe\"]\n */\n allowlist?: string[];\n /**\n * Seconds advertised in the `Retry-After` header. Falls back to\n * `http.maintenance.retryAfter`, then `60`.\n */\n retryAfter?: number;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\nfunction isAllowlisted(path: string, patterns: string[]) {\n return patterns.some((pattern) => {\n if (pattern.endsWith(\"*\")) {\n return path.startsWith(pattern.slice(0, -1));\n }\n\n return path === pattern;\n });\n}\n\n/**\n * Return 503 + `Retry-After` for every request when `http.maintenance.enabled`\n * is true, except for paths matching the allowlist.\n *\n * Designed for app-wide registration via `http.middleware.all`. Toggled via\n * config — flipping the flag requires a restart (no runtime hot-flip yet).\n * Allowlist defaults to `[\"/health\"]` so health checks still pass during\n * planned downtime.\n *\n * @example\n * // src/config/http.ts\n * import { middleware } from \"@warlock.js/core\";\n *\n * export default {\n * maintenance: { enabled: env(\"MAINTENANCE_MODE\") === \"true\" },\n * middleware: {\n * all: [middleware.maintenance({ allowlist: [\"/health\", \"/admin/*\"] })],\n * },\n * };\n */\nexport function maintenanceMiddleware(options: MaintenanceOptions = {}): Middleware {\n return ({ request, response }) => {\n const enabled = config.get(\"http.maintenance.enabled\", false);\n\n if (!enabled) return;\n\n const allowlist = options.allowlist || config.get(\"http.maintenance.allowlist\", [\"/health\"]);\n\n // `request.path` includes the query string, but allowlist entries are\n // path-only (\"/webhooks/stripe\"), so strip the query before matching —\n // otherwise \"/webhooks/stripe?sig=...\" never matches its exact entry.\n const pathname = request.path.split(\"?\")[0];\n\n if (isAllowlisted(pathname, allowlist)) return;\n\n const retryAfter = options.retryAfter || config.get(\"http.maintenance.retryAfter\", 60);\n\n response.header(\"Retry-After\", retryAfter);\n\n return response.serviceUnavailable({\n error: options.errorMessage || t(\"http.maintenance\"),\n errorCode: HttpErrorCodes.Maintenance,\n });\n };\n}\n"],"mappings":";;;;;AA6BA,SAAS,cAAc,MAAc,UAAoB;CACvD,OAAO,SAAS,MAAM,YAAY;EAChC,IAAI,QAAQ,SAAS,GAAG,GACtB,OAAO,KAAK,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC;EAG7C,OAAO,SAAS;CAClB,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,sBAAsB,UAA8B,CAAC,GAAe;CAClF,QAAQ,EAAE,SAAS,eAAe;EAGhC,IAAI,CAFY,OAAO,IAAI,4BAA4B,KAE5C,GAAG;EAEd,MAAM,YAAY,QAAQ,aAAa,OAAO,IAAI,8BAA8B,CAAC,SAAS,CAAC;EAK3F,MAAM,WAAW,QAAQ,KAAK,MAAM,GAAG,CAAC,CAAC;EAEzC,IAAI,cAAc,UAAU,SAAS,GAAG;EAExC,MAAM,aAAa,QAAQ,cAAc,OAAO,IAAI,+BAA+B,EAAE;EAErF,SAAS,OAAO,eAAe,UAAU;EAEzC,OAAO,SAAS,mBAAmB;GACjC,OAAO,QAAQ,gBAAgB,EAAE,kBAAkB;GACnD;EACF,CAAC;CACH;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"max-body-size.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/max-body-size.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport { t } from \"./inject-request-context\";\nimport { parseSize } from \"./utils/parse-size\";\n\n/**\n * Reject requests whose `Content-Length` exceeds the configured cap.\n *\n * ⚠️ **This runs AFTER the body has been parsed, not before it.** Framework\n * middleware executes inside the Fastify route handler, and `@fastify/multipart`\n * is registered with `attachFieldsToBody: true` — so by the time this sees the\n * request, the bytes are already resident. It inspects `Content-Length` and\n * answers 413; it does not and cannot prevent the read.\n *\n * **So this is not a resource-exhaustion control.** For that, lower\n * `http.bodyLimit`, or set `bodyLimit` in the route's own `serverOptions` —\n * Fastify enforces those while reading. This middleware is a per-route\n * convenience for returning a consistent 413, not a replacement for either.\n *\n * The same caveat applies to `concurrencyLimit`: it bounds how many requests\n * run at once, not how many bytes are resident.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/comments\", createCommentController, {\n * middleware: [middleware.maxBodySize(\"8kb\")],\n * });\n *\n * router.post(\"/uploads\", uploadController, {\n * middleware: [middleware.maxBodySize(\"10mb\")],\n * });\n */\nexport function maxBodySizeMiddleware(limit: string | number): Middleware {\n const limitBytes = parseSize(limit);\n\n return ({ request, response }) => {\n const contentLengthHeader = request.header(\"content-length\");\n\n if (!contentLengthHeader) return;\n\n const contentLength = Number(contentLengthHeader);\n\n if (!Number.isFinite(contentLength)) return;\n\n if (contentLength <= limitBytes) return;\n\n response.header(\"Connection\", \"close\");\n\n return response.contentTooLarge({\n error: t(\"http.bodyTooLarge\"),\n errorCode: HttpErrorCodes.BodyTooLarge,\n limit: limitBytes,\n received: contentLength,\n },\n );\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,sBAAsB,OAAoC;CACxE,MAAM,aAAa,UAAU,KAAK;CAElC,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,sBAAsB,QAAQ,OAAO,gBAAgB;EAE3D,IAAI,CAAC,qBAAqB;EAE1B,MAAM,gBAAgB,OAAO,mBAAmB;EAEhD,IAAI,CAAC,OAAO,SAAS,aAAa,GAAG;EAErC,IAAI,iBAAiB,YAAY;EAEjC,SAAS,OAAO,cAAc,OAAO;EAErC,OAAO,SAAS,gBAAgB;GAC5B,OAAO,EAAE,mBAAmB;GAC5B;GACA,OAAO;GACP,UAAU;EACZ,CACF;CACF;AACF"}
1
+ {"version":3,"file":"max-body-size.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/max-body-size.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport { t } from \"./inject-request-context\";\nimport { parseSize } from \"./utils/parse-size\";\n\n/**\n * Reject requests whose `Content-Length` exceeds the configured cap.\n *\n * ⚠️ **This runs AFTER the body has been parsed, not before it.** Framework\n * middleware executes inside the Fastify route handler, and `@fastify/multipart`\n * is registered with `attachFieldsToBody: true` — so by the time this sees the\n * request, the bytes are already resident. It inspects `Content-Length` and\n * answers 413; it does not and cannot prevent the read.\n *\n * **So this is not a resource-exhaustion control.** For that, lower\n * `http.bodyLimit`, or set `bodyLimit` in the route's own `serverOptions` —\n * Fastify enforces those while reading. This middleware is a per-route\n * convenience for returning a consistent 413, not a replacement for either.\n *\n * The same caveat applies to `concurrencyLimit`: it bounds how many requests\n * run at once, not how many bytes are resident.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/comments\", createCommentController, {\n * middleware: [middleware.maxBodySize(\"8kb\")],\n * });\n *\n * router.post(\"/uploads\", uploadController, {\n * middleware: [middleware.maxBodySize(\"10mb\")],\n * });\n */\nexport function maxBodySizeMiddleware(limit: string | number): Middleware {\n const limitBytes = parseSize(limit);\n\n return ({ request, response }) => {\n const contentLengthHeader = request.header(\"content-length\");\n\n if (!contentLengthHeader) return;\n\n const contentLength = Number(contentLengthHeader);\n\n if (!Number.isFinite(contentLength)) return;\n\n if (contentLength <= limitBytes) return;\n\n response.header(\"Connection\", \"close\");\n\n return response.contentTooLarge({\n error: t(\"http.bodyTooLarge\"),\n errorCode: HttpErrorCodes.BodyTooLarge,\n limit: limitBytes,\n received: contentLength,\n });\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,sBAAsB,OAAoC;CACxE,MAAM,aAAa,UAAU,KAAK;CAElC,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,sBAAsB,QAAQ,OAAO,gBAAgB;EAE3D,IAAI,CAAC,qBAAqB;EAE1B,MAAM,gBAAgB,OAAO,mBAAmB;EAEhD,IAAI,CAAC,OAAO,SAAS,aAAa,GAAG;EAErC,IAAI,iBAAiB,YAAY;EAEjC,SAAS,OAAO,cAAc,OAAO;EAErC,OAAO,SAAS,gBAAgB;GAC9B,OAAO,EAAE,mBAAmB;GAC5B;GACA,OAAO;GACP,UAAU;EACZ,CAAC;CACH;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/rate-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the per-route rate limit middleware.\n */\nexport type RateLimitOptions = {\n /**\n * Max requests allowed within the time window.\n */\n max: number;\n /**\n * Time window in milliseconds.\n */\n duration: number;\n /**\n * Group key generator. Defaults to the client IP. Override to scope per-user,\n * per-organization, per-tenant, etc.\n *\n * @example\n * keyGenerator: (request) => request.user?.id ?? request.ip,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\ntype Bucket = {\n count: number;\n resetAt: number;\n};\n\nconst buckets = new Map<string, Bucket>();\n\n/**\n * Sweep expired buckets so the Map doesn't grow unbounded with one-shot keys\n * (rare per-user keys, dynamic path parameters in `keyGenerator`, etc.).\n * Called opportunistically, at most once per minute, not on a timer — keeps\n * the module side-effect-free for testing.\n */\nlet lastPruneAt = 0;\n\nfunction pruneExpired(now: number) {\n for (const [key, bucket] of buckets) {\n if (bucket.resetAt <= now) {\n buckets.delete(key);\n }\n }\n}\n\n/**\n * Per-route / per-group rate limit. Layers on top of `@fastify/rate-limit`'s\n * global cap (configured via `http.rateLimit`) — both run, and either can\n * 429. Use this for endpoints that need a tighter cap than the global default:\n * login, OTP request, password reset, expensive search, AI completions.\n *\n * The counter lives in-process. With `N` replicas the effective cap is\n * `N × max`. For genuinely shared limits, configure `@fastify/rate-limit`'s\n * Redis store via `http.rateLimit` instead.\n *\n * Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` on\n * every response; adds `Retry-After` (seconds) on a 429.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n * router.post(\"/auth/login\", loginController, {\n * middleware: [middleware.rateLimit({ max: 5, duration: 60_000 })],\n * });\n */\nexport function rateLimitMiddleware(options: RateLimitOptions): Middleware {\n return ({ request, response }) => {\n const now = Date.now();\n\n if (now - lastPruneAt > 60_000) {\n pruneExpired(now);\n lastPruneAt = now;\n }\n\n const groupKey = options.keyGenerator?.(request) || request.detectIp() || \"unknown\";\n const cacheKey = `${request.route.path}:${groupKey}`;\n\n let bucket = buckets.get(cacheKey);\n\n if (!bucket || bucket.resetAt <= now) {\n bucket = { count: 0, resetAt: now + options.duration };\n buckets.set(cacheKey, bucket);\n }\n\n bucket.count++;\n\n const remaining = Math.max(0, options.max - bucket.count);\n const retryAfter = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));\n\n response.header(\"X-RateLimit-Limit\", options.max);\n response.header(\"X-RateLimit-Remaining\", remaining);\n response.header(\"X-RateLimit-Reset\", Math.ceil(bucket.resetAt / 1000));\n\n if (bucket.count > options.max) {\n response.header(\"Retry-After\", retryAfter);\n\n return response.tooManyRequests({\n error: options.errorMessage || t(\"http.rateLimitExceeded\"),\n errorCode: HttpErrorCodes.RateLimitExceeded,\n },\n );\n }\n };\n}\n"],"mappings":";;;;AAoCA,MAAM,0BAAU,IAAI,IAAoB;;;;;;;AAQxC,IAAI,cAAc;AAElB,SAAS,aAAa,KAAa;CACjC,KAAK,MAAM,CAAC,KAAK,WAAW,SAC1B,IAAI,OAAO,WAAW,KACpB,QAAQ,OAAO,GAAG;AAGxB;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,oBAAoB,SAAuC;CACzE,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,MAAM,KAAK,IAAI;EAErB,IAAI,MAAM,cAAc,KAAQ;GAC9B,aAAa,GAAG;GAChB,cAAc;EAChB;EAEA,MAAM,WAAW,QAAQ,eAAe,OAAO,KAAK,QAAQ,SAAS,KAAK;EAC1E,MAAM,WAAW,GAAG,QAAQ,MAAM,KAAK,GAAG;EAE1C,IAAI,SAAS,QAAQ,IAAI,QAAQ;EAEjC,IAAI,CAAC,UAAU,OAAO,WAAW,KAAK;GACpC,SAAS;IAAE,OAAO;IAAG,SAAS,MAAM,QAAQ;GAAS;GACrD,QAAQ,IAAI,UAAU,MAAM;EAC9B;EAEA,OAAO;EAEP,MAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,MAAM,OAAO,KAAK;EACxD,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,UAAU,OAAO,GAAI,CAAC;EAEvE,SAAS,OAAO,qBAAqB,QAAQ,GAAG;EAChD,SAAS,OAAO,yBAAyB,SAAS;EAClD,SAAS,OAAO,qBAAqB,KAAK,KAAK,OAAO,UAAU,GAAI,CAAC;EAErE,IAAI,OAAO,QAAQ,QAAQ,KAAK;GAC9B,SAAS,OAAO,eAAe,UAAU;GAEzC,OAAO,SAAS,gBAAgB;IAC5B,OAAO,QAAQ,gBAAgB,EAAE,wBAAwB;IACzD;GACF,CACF;EACF;CACF;AACF"}
1
+ {"version":3,"file":"rate-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/rate-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the per-route rate limit middleware.\n */\nexport type RateLimitOptions = {\n /**\n * Max requests allowed within the time window.\n */\n max: number;\n /**\n * Time window in milliseconds.\n */\n duration: number;\n /**\n * Group key generator. Defaults to the client IP. Override to scope per-user,\n * per-organization, per-tenant, etc.\n *\n * @example\n * keyGenerator: (request) => request.user?.id ?? request.ip,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\ntype Bucket = {\n count: number;\n resetAt: number;\n};\n\nconst buckets = new Map<string, Bucket>();\n\n/**\n * Sweep expired buckets so the Map doesn't grow unbounded with one-shot keys\n * (rare per-user keys, dynamic path parameters in `keyGenerator`, etc.).\n * Called opportunistically, at most once per minute, not on a timer — keeps\n * the module side-effect-free for testing.\n */\nlet lastPruneAt = 0;\n\nfunction pruneExpired(now: number) {\n for (const [key, bucket] of buckets) {\n if (bucket.resetAt <= now) {\n buckets.delete(key);\n }\n }\n}\n\n/**\n * Per-route / per-group rate limit. Layers on top of `@fastify/rate-limit`'s\n * global cap (configured via `http.rateLimit`) — both run, and either can\n * 429. Use this for endpoints that need a tighter cap than the global default:\n * login, OTP request, password reset, expensive search, AI completions.\n *\n * The counter lives in-process. With `N` replicas the effective cap is\n * `N × max`. For genuinely shared limits, configure `@fastify/rate-limit`'s\n * Redis store via `http.rateLimit` instead.\n *\n * Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` on\n * every response; adds `Retry-After` (seconds) on a 429.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n * router.post(\"/auth/login\", loginController, {\n * middleware: [middleware.rateLimit({ max: 5, duration: 60_000 })],\n * });\n */\nexport function rateLimitMiddleware(options: RateLimitOptions): Middleware {\n return ({ request, response }) => {\n const now = Date.now();\n\n if (now - lastPruneAt > 60_000) {\n pruneExpired(now);\n lastPruneAt = now;\n }\n\n const groupKey = options.keyGenerator?.(request) || request.detectIp() || \"unknown\";\n const cacheKey = `${request.route.path}:${groupKey}`;\n\n let bucket = buckets.get(cacheKey);\n\n if (!bucket || bucket.resetAt <= now) {\n bucket = { count: 0, resetAt: now + options.duration };\n buckets.set(cacheKey, bucket);\n }\n\n bucket.count++;\n\n const remaining = Math.max(0, options.max - bucket.count);\n const retryAfter = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));\n\n response.header(\"X-RateLimit-Limit\", options.max);\n response.header(\"X-RateLimit-Remaining\", remaining);\n response.header(\"X-RateLimit-Reset\", Math.ceil(bucket.resetAt / 1000));\n\n if (bucket.count > options.max) {\n response.header(\"Retry-After\", retryAfter);\n\n return response.tooManyRequests({\n error: options.errorMessage || t(\"http.rateLimitExceeded\"),\n errorCode: HttpErrorCodes.RateLimitExceeded,\n });\n }\n };\n}\n"],"mappings":";;;;AAoCA,MAAM,0BAAU,IAAI,IAAoB;;;;;;;AAQxC,IAAI,cAAc;AAElB,SAAS,aAAa,KAAa;CACjC,KAAK,MAAM,CAAC,KAAK,WAAW,SAC1B,IAAI,OAAO,WAAW,KACpB,QAAQ,OAAO,GAAG;AAGxB;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,oBAAoB,SAAuC;CACzE,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,MAAM,KAAK,IAAI;EAErB,IAAI,MAAM,cAAc,KAAQ;GAC9B,aAAa,GAAG;GAChB,cAAc;EAChB;EAEA,MAAM,WAAW,QAAQ,eAAe,OAAO,KAAK,QAAQ,SAAS,KAAK;EAC1E,MAAM,WAAW,GAAG,QAAQ,MAAM,KAAK,GAAG;EAE1C,IAAI,SAAS,QAAQ,IAAI,QAAQ;EAEjC,IAAI,CAAC,UAAU,OAAO,WAAW,KAAK;GACpC,SAAS;IAAE,OAAO;IAAG,SAAS,MAAM,QAAQ;GAAS;GACrD,QAAQ,IAAI,UAAU,MAAM;EAC9B;EAEA,OAAO;EAEP,MAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,MAAM,OAAO,KAAK;EACxD,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,UAAU,OAAO,GAAI,CAAC;EAEvE,SAAS,OAAO,qBAAqB,QAAQ,GAAG;EAChD,SAAS,OAAO,yBAAyB,SAAS;EAClD,SAAS,OAAO,qBAAqB,KAAK,KAAK,OAAO,UAAU,GAAI,CAAC;EAErE,IAAI,OAAO,QAAQ,QAAQ,KAAK;GAC9B,SAAS,OAAO,eAAe,UAAU;GAEzC,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,wBAAwB;IACzD;GACF,CAAC;EACH;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"port-preflight.d.mts","names":[],"sources":["../../../../../../../core/src/http/port-preflight.ts"],"mappings":";;AAoBA;;;;;;;;;;;;cAAa,cAAA,SAAuB,KAAK;EAAA,SAKrB,IAAA;EAAA,SACA,IAAA;;WAJF,IAAA;cAGE,IAAA,UACA,IAAA;AAAA;;;AAoBV;AAmCV;;;;iBAtCgB,eAAA,CACd,IAAA,UACA,IAAA,YACC,OAAO;;;;AAsCA;;;;;;iBAHY,qBAAA,CACpB,IAAA,UACA,IAAA,YACC,OAAO"}
1
+ {"version":3,"file":"port-preflight.d.mts","names":[],"sources":["../../../../../../../core/src/http/port-preflight.ts"],"mappings":";;AAoBA;;;;;;;;;;;;cAAa,cAAA,SAAuB,KAAK;EAAA,SAKrB,IAAA;EAAA,SACA,IAAA;;WAJF,IAAA;cAGE,IAAA,UACA,IAAA;AAAA;;;AAiBqE;AAmCzF;;;;iBAnCgB,eAAA,CAAgB,IAAA,UAAc,IAAA,YAAoC,OAAO;;;;AAsC/E;;;;;;iBAHY,qBAAA,CACpB,IAAA,UACA,IAAA,YACC,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"port-preflight.mjs","names":[],"sources":["../../../../../../../core/src/http/port-preflight.ts"],"sourcesContent":["import { createServer } from \"node:net\";\n\n/**\n * Host used when the caller has no configured bind host.\n */\nconst DEFAULT_PROBE_HOST = \"localhost\";\n\n/**\n * Raised when a port the framework is about to bind is already taken.\n *\n * Carries the port and host so a caller can report them without parsing the\n * message, and its message is the developer-facing instruction — the whole\n * point of preflighting is that nobody has to decode a raw `EADDRINUSE`.\n *\n * The message leads with `EADDRINUSE` on purpose: it is the string a\n * developer greps their terminal for, and the one every other Node tool\n * already trained them to recognize as \"something else is on this port\".\n * `code` mirrors it as a property so a caller can branch on it without\n * parsing text, the same way `NodeJS.ErrnoException.code` works.\n */\nexport class PortInUseError extends Error {\n /** Mirrors Node's own `ErrnoException.code` for this failure. */\n public readonly code = \"EADDRINUSE\";\n\n public constructor(\n public readonly port: number,\n public readonly host: string,\n ) {\n super(\n `EADDRINUSE: Port ${port} is already in use on ${host}. Stop the dev server (or whatever else is listening on port ${port}) and run again, or start on a free port — e.g. startHttpTestServer({ port: ${port + 1} }).`,\n );\n\n this.name = \"PortInUseError\";\n }\n}\n\n/**\n * Probe a TCP port by attempting a bind and immediately releasing it.\n *\n * @returns `true` when the port is free, `false` when it is taken or the\n * process is not allowed to bind it. Any other bind failure is re-thrown —\n * an unreadable network error must not be reported as \"port in use\".\n */\nexport function isPortAvailable(\n port: number,\n host: string = DEFAULT_PROBE_HOST,\n): Promise<boolean> {\n return new Promise((resolve, reject) => {\n const probe = createServer();\n\n probe.unref();\n\n probe.once(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\" || error.code === \"EACCES\") {\n resolve(false);\n\n return;\n }\n\n reject(error);\n });\n\n probe.once(\"listening\", () => {\n probe.close(() => resolve(true));\n });\n\n // `exclusive` refuses a shared bind, which is what makes the probe honest:\n // with load balancing enabled a second bind on a served port can succeed.\n probe.listen({ port, host, exclusive: true });\n });\n}\n\n/**\n * Assert a port is free before something binds it.\n *\n * Run this ahead of the actual `listen()` so a collision surfaces as an\n * instruction the developer can act on, instead of a raw `EADDRINUSE` thrown\n * from deep inside the HTTP stack.\n *\n * @throws {PortInUseError} when the port is taken.\n */\nexport async function assertPortIsAvailable(\n port: number,\n host: string = DEFAULT_PROBE_HOST,\n): Promise<void> {\n const available = await isPortAvailable(port, host);\n\n if (!available) {\n throw new PortInUseError(port, host);\n }\n}\n"],"mappings":";;;;;;AAKA,MAAM,qBAAqB;;;;;;;;;;;;;;AAe3B,IAAa,iBAAb,cAAoC,MAAM;CAIxC,AAAO,YACL,AAAgB,MAChB,AAAgB,MAChB;EACA,MACE,oBAAoB,KAAK,wBAAwB,KAAK,+DAA+D,KAAK,8EAA8E,OAAO,EAAE,KACnN;EALgB;EACA;cAJK;EAUrB,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,SAAgB,gBACd,MACA,OAAe,oBACG;CAClB,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,QAAQ,aAAa;EAE3B,MAAM,MAAM;EAEZ,MAAM,KAAK,UAAU,UAAiC;GACpD,IAAI,MAAM,SAAS,gBAAgB,MAAM,SAAS,UAAU;IAC1D,QAAQ,KAAK;IAEb;GACF;GAEA,OAAO,KAAK;EACd,CAAC;EAED,MAAM,KAAK,mBAAmB;GAC5B,MAAM,YAAY,QAAQ,IAAI,CAAC;EACjC,CAAC;EAID,MAAM,OAAO;GAAE;GAAM;GAAM,WAAW;EAAK,CAAC;CAC9C,CAAC;AACH;;;;;;;;;;AAWA,eAAsB,sBACpB,MACA,OAAe,oBACA;CAGf,IAAI,CAAC,MAFmB,gBAAgB,MAAM,IAAI,GAGhD,MAAM,IAAI,eAAe,MAAM,IAAI;AAEvC"}
1
+ {"version":3,"file":"port-preflight.mjs","names":[],"sources":["../../../../../../../core/src/http/port-preflight.ts"],"sourcesContent":["import { createServer } from \"node:net\";\n\n/**\n * Host used when the caller has no configured bind host.\n */\nconst DEFAULT_PROBE_HOST = \"localhost\";\n\n/**\n * Raised when a port the framework is about to bind is already taken.\n *\n * Carries the port and host so a caller can report them without parsing the\n * message, and its message is the developer-facing instruction — the whole\n * point of preflighting is that nobody has to decode a raw `EADDRINUSE`.\n *\n * The message leads with `EADDRINUSE` on purpose: it is the string a\n * developer greps their terminal for, and the one every other Node tool\n * already trained them to recognize as \"something else is on this port\".\n * `code` mirrors it as a property so a caller can branch on it without\n * parsing text, the same way `NodeJS.ErrnoException.code` works.\n */\nexport class PortInUseError extends Error {\n /** Mirrors Node's own `ErrnoException.code` for this failure. */\n public readonly code = \"EADDRINUSE\";\n\n public constructor(\n public readonly port: number,\n public readonly host: string,\n ) {\n super(\n `EADDRINUSE: Port ${port} is already in use on ${host}. Stop the dev server (or whatever else is listening on port ${port}) and run again, or start on a free port — e.g. startHttpTestServer({ port: ${port + 1} }).`,\n );\n\n this.name = \"PortInUseError\";\n }\n}\n\n/**\n * Probe a TCP port by attempting a bind and immediately releasing it.\n *\n * @returns `true` when the port is free, `false` when it is taken or the\n * process is not allowed to bind it. Any other bind failure is re-thrown —\n * an unreadable network error must not be reported as \"port in use\".\n */\nexport function isPortAvailable(port: number, host: string = DEFAULT_PROBE_HOST): Promise<boolean> {\n return new Promise((resolve, reject) => {\n const probe = createServer();\n\n probe.unref();\n\n probe.once(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\" || error.code === \"EACCES\") {\n resolve(false);\n\n return;\n }\n\n reject(error);\n });\n\n probe.once(\"listening\", () => {\n probe.close(() => resolve(true));\n });\n\n // `exclusive` refuses a shared bind, which is what makes the probe honest:\n // with load balancing enabled a second bind on a served port can succeed.\n probe.listen({ port, host, exclusive: true });\n });\n}\n\n/**\n * Assert a port is free before something binds it.\n *\n * Run this ahead of the actual `listen()` so a collision surfaces as an\n * instruction the developer can act on, instead of a raw `EADDRINUSE` thrown\n * from deep inside the HTTP stack.\n *\n * @throws {PortInUseError} when the port is taken.\n */\nexport async function assertPortIsAvailable(\n port: number,\n host: string = DEFAULT_PROBE_HOST,\n): Promise<void> {\n const available = await isPortAvailable(port, host);\n\n if (!available) {\n throw new PortInUseError(port, host);\n }\n}\n"],"mappings":";;;;;;AAKA,MAAM,qBAAqB;;;;;;;;;;;;;;AAe3B,IAAa,iBAAb,cAAoC,MAAM;CAIxC,AAAO,YACL,AAAgB,MAChB,AAAgB,MAChB;EACA,MACE,oBAAoB,KAAK,wBAAwB,KAAK,+DAA+D,KAAK,8EAA8E,OAAO,EAAE,KACnN;EALgB;EACA;cAJK;EAUrB,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,SAAgB,gBAAgB,MAAc,OAAe,oBAAsC;CACjG,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,QAAQ,aAAa;EAE3B,MAAM,MAAM;EAEZ,MAAM,KAAK,UAAU,UAAiC;GACpD,IAAI,MAAM,SAAS,gBAAgB,MAAM,SAAS,UAAU;IAC1D,QAAQ,KAAK;IAEb;GACF;GAEA,OAAO,KAAK;EACd,CAAC;EAED,MAAM,KAAK,mBAAmB;GAC5B,MAAM,YAAY,QAAQ,IAAI,CAAC;EACjC,CAAC;EAID,MAAM,OAAO;GAAE;GAAM;GAAM,WAAW;EAAK,CAAC;CAC9C,CAAC;AACH;;;;;;;;;;AAWA,eAAsB,sBACpB,MACA,OAAe,oBACA;CAGf,IAAI,CAAC,MAFmB,gBAAgB,MAAM,IAAI,GAGhD,MAAM,IAAI,eAAe,MAAM,IAAI;AAEvC"}
@@ -1 +1 @@
1
- {"version":3,"file":"request.d.mts","names":[],"sources":["../../../../../../../core/src/http/request.ts"],"mappings":"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"}
1
+ {"version":3,"file":"request.d.mts","names":[],"sources":["../../../../../../../core/src/http/request.ts"],"mappings":"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"}