create-forgeon 0.3.5 → 0.3.7

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 (109) hide show
  1. package/package.json +1 -1
  2. package/src/cli/add-help.mjs +1 -0
  3. package/src/cli/add-options.mjs +6 -0
  4. package/src/cli/add-options.test.mjs +2 -0
  5. package/src/modules/dependencies.mjs +31 -0
  6. package/src/modules/dependencies.test.mjs +207 -5
  7. package/src/modules/executor.mjs +14 -0
  8. package/src/modules/executor.test.mjs +752 -14
  9. package/src/modules/files-access.mjs +446 -0
  10. package/src/modules/files-image.mjs +540 -0
  11. package/src/modules/files-local.mjs +221 -0
  12. package/src/modules/files-quotas.mjs +402 -0
  13. package/src/modules/files-s3.mjs +266 -0
  14. package/src/modules/files.mjs +527 -0
  15. package/src/modules/queue.mjs +410 -0
  16. package/src/modules/registry.mjs +93 -3
  17. package/src/modules/shared/patch-utils.mjs +25 -0
  18. package/src/run-add-module.mjs +89 -2
  19. package/templates/module-fragments/files/00_title.md +1 -0
  20. package/templates/module-fragments/files/10_overview.md +17 -0
  21. package/templates/module-fragments/files/20_scope.md +13 -0
  22. package/templates/module-fragments/files/90_status_implemented.md +3 -0
  23. package/templates/module-fragments/files-access/00_title.md +1 -0
  24. package/templates/module-fragments/files-access/10_overview.md +9 -0
  25. package/templates/module-fragments/files-access/20_scope.md +20 -0
  26. package/templates/module-fragments/files-access/90_status_implemented.md +3 -0
  27. package/templates/module-fragments/files-image/00_title.md +1 -0
  28. package/templates/module-fragments/files-image/10_overview.md +10 -0
  29. package/templates/module-fragments/files-image/20_scope.md +20 -0
  30. package/templates/module-fragments/files-image/90_status_implemented.md +3 -0
  31. package/templates/module-fragments/files-local/00_title.md +1 -0
  32. package/templates/module-fragments/files-local/10_overview.md +9 -0
  33. package/templates/module-fragments/files-local/20_scope.md +10 -0
  34. package/templates/module-fragments/files-local/90_status_implemented.md +3 -0
  35. package/templates/module-fragments/files-quotas/00_title.md +1 -0
  36. package/templates/module-fragments/files-quotas/10_overview.md +9 -0
  37. package/templates/module-fragments/files-quotas/20_scope.md +20 -0
  38. package/templates/module-fragments/files-quotas/90_status_implemented.md +3 -0
  39. package/templates/module-fragments/files-s3/00_title.md +1 -0
  40. package/templates/module-fragments/files-s3/10_overview.md +17 -0
  41. package/templates/module-fragments/files-s3/20_scope.md +11 -0
  42. package/templates/module-fragments/files-s3/90_status_implemented.md +5 -0
  43. package/templates/module-fragments/queue/20_scope.md +8 -7
  44. package/templates/module-fragments/queue/90_status_implemented.md +3 -0
  45. package/templates/module-presets/files/apps/api/prisma/migrations/20260306_files_file_record/migration.sql +30 -0
  46. package/templates/module-presets/files/apps/api/prisma/migrations/20260306_files_file_variant/migration.sql +55 -0
  47. package/templates/module-presets/files/packages/files/package.json +24 -0
  48. package/templates/module-presets/files/packages/files/src/dto/create-file.dto.ts +30 -0
  49. package/templates/module-presets/files/packages/files/src/files-config.loader.ts +21 -0
  50. package/templates/module-presets/files/packages/files/src/files-config.module.ts +12 -0
  51. package/templates/module-presets/files/packages/files/src/files-config.service.ts +32 -0
  52. package/templates/module-presets/files/packages/files/src/files-env.schema.ts +30 -0
  53. package/templates/module-presets/files/packages/files/src/files.controller.ts +90 -0
  54. package/templates/module-presets/files/packages/files/src/files.service.ts +762 -0
  55. package/templates/module-presets/files/packages/files/src/files.types.ts +35 -0
  56. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +12 -0
  57. package/templates/module-presets/files/packages/files/src/index.ts +9 -0
  58. package/templates/module-presets/files/packages/files/tsconfig.json +9 -0
  59. package/templates/module-presets/files-access/packages/files-access/package.json +17 -0
  60. package/templates/module-presets/files-access/packages/files-access/src/files-access.service.ts +59 -0
  61. package/templates/module-presets/files-access/packages/files-access/src/files-access.subject.ts +45 -0
  62. package/templates/module-presets/files-access/packages/files-access/src/files-access.types.ts +14 -0
  63. package/templates/module-presets/files-access/packages/files-access/src/forgeon-files-access.module.ts +8 -0
  64. package/templates/module-presets/files-access/packages/files-access/src/index.ts +4 -0
  65. package/templates/module-presets/files-access/packages/files-access/tsconfig.json +9 -0
  66. package/templates/module-presets/files-image/packages/files-image/package.json +21 -0
  67. package/templates/module-presets/files-image/packages/files-image/src/files-image-config.loader.ts +32 -0
  68. package/templates/module-presets/files-image/packages/files-image/src/files-image-config.module.ts +11 -0
  69. package/templates/module-presets/files-image/packages/files-image/src/files-image-config.service.ts +55 -0
  70. package/templates/module-presets/files-image/packages/files-image/src/files-image-env.schema.ts +28 -0
  71. package/templates/module-presets/files-image/packages/files-image/src/files-image.service.ts +420 -0
  72. package/templates/module-presets/files-image/packages/files-image/src/files-image.types.ts +18 -0
  73. package/templates/module-presets/files-image/packages/files-image/src/forgeon-files-image.module.ts +10 -0
  74. package/templates/module-presets/files-image/packages/files-image/src/index.ts +7 -0
  75. package/templates/module-presets/files-image/packages/files-image/tsconfig.json +9 -0
  76. package/templates/module-presets/files-local/packages/files-local/package.json +19 -0
  77. package/templates/module-presets/files-local/packages/files-local/src/files-local-config.loader.ts +13 -0
  78. package/templates/module-presets/files-local/packages/files-local/src/files-local-config.module.ts +12 -0
  79. package/templates/module-presets/files-local/packages/files-local/src/files-local-config.service.ts +11 -0
  80. package/templates/module-presets/files-local/packages/files-local/src/files-local-env.schema.ts +13 -0
  81. package/templates/module-presets/files-local/packages/files-local/src/index.ts +4 -0
  82. package/templates/module-presets/files-local/packages/files-local/tsconfig.json +9 -0
  83. package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -0
  84. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.loader.ts +22 -0
  85. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.module.ts +11 -0
  86. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.service.ts +27 -0
  87. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-env.schema.ts +15 -0
  88. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -0
  89. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.types.ts +22 -0
  90. package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +11 -0
  91. package/templates/module-presets/files-quotas/packages/files-quotas/src/index.ts +7 -0
  92. package/templates/module-presets/files-quotas/packages/files-quotas/tsconfig.json +9 -0
  93. package/templates/module-presets/files-s3/packages/files-s3/package.json +20 -0
  94. package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.loader.ts +57 -0
  95. package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.module.ts +12 -0
  96. package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.service.ts +44 -0
  97. package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-env.schema.ts +51 -0
  98. package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +4 -0
  99. package/templates/module-presets/files-s3/packages/files-s3/tsconfig.json +9 -0
  100. package/templates/module-presets/queue/packages/queue/package.json +21 -0
  101. package/templates/module-presets/queue/packages/queue/src/forgeon-queue.module.ts +10 -0
  102. package/templates/module-presets/queue/packages/queue/src/index.ts +6 -0
  103. package/templates/module-presets/queue/packages/queue/src/queue-config.loader.ts +24 -0
  104. package/templates/module-presets/queue/packages/queue/src/queue-config.module.ts +10 -0
  105. package/templates/module-presets/queue/packages/queue/src/queue-config.service.ts +69 -0
  106. package/templates/module-presets/queue/packages/queue/src/queue-env.schema.ts +17 -0
  107. package/templates/module-presets/queue/packages/queue/src/queue.service.ts +88 -0
  108. package/templates/module-presets/queue/packages/queue/tsconfig.json +9 -0
  109. package/templates/module-fragments/queue/90_status_planned.md +0 -3
@@ -86,6 +86,52 @@ function assertRateLimitWiring(projectRoot) {
86
86
  assert.match(readme, /no optional integration sync is required/i);
87
87
  }
88
88
 
89
+ function assertQueueWiring(projectRoot) {
90
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
91
+ assert.match(appModule, /queueConfig/);
92
+ assert.match(appModule, /queueEnvSchema/);
93
+ assert.match(appModule, /ForgeonQueueModule/);
94
+
95
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
96
+ assert.match(apiPackage, /@forgeon\/queue/);
97
+ assert.match(apiPackage, /pnpm --filter @forgeon\/queue build/);
98
+
99
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
100
+ assert.match(apiDockerfile, /COPY packages\/queue\/package\.json packages\/queue\/package\.json/);
101
+ assert.match(apiDockerfile, /COPY packages\/queue packages\/queue/);
102
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/queue build/);
103
+
104
+ const healthController = fs.readFileSync(
105
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
106
+ 'utf8',
107
+ );
108
+ assert.match(healthController, /QueueService/);
109
+ assert.match(healthController, /@Get\('queue'\)/);
110
+ assert.match(healthController, /queueService\.getProbeStatus/);
111
+
112
+ const webApp = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
113
+ assert.match(webApp, /Check queue health/);
114
+ assert.match(webApp, /Queue probe response/);
115
+
116
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
117
+ assert.match(apiEnv, /QUEUE_ENABLED=true/);
118
+ assert.match(apiEnv, /QUEUE_REDIS_URL=redis:\/\/localhost:6379/);
119
+ assert.match(apiEnv, /QUEUE_DEFAULT_ATTEMPTS=3/);
120
+ assert.match(apiEnv, /QUEUE_DEFAULT_BACKOFF_MS=1000/);
121
+
122
+ const dockerEnv = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', '.env.example'), 'utf8');
123
+ assert.match(dockerEnv, /QUEUE_REDIS_URL=redis:\/\/redis:6379/);
124
+
125
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
126
+ assert.match(compose, /^\s{2}redis:\s*$/m);
127
+ assert.match(compose, /QUEUE_ENABLED: \$\{QUEUE_ENABLED\}/);
128
+ assert.match(compose, /depends_on:\n\s+redis:\n\s+condition: service_healthy/);
129
+
130
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
131
+ assert.match(readme, /## Queue Module/);
132
+ assert.match(readme, /runtime baseline backed by Redis/i);
133
+ }
134
+
89
135
  function assertRbacWiring(projectRoot) {
90
136
  const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
91
137
  assert.match(appModule, /ForgeonRbacModule/);
@@ -119,6 +165,374 @@ function assertRbacWiring(projectRoot) {
119
165
  assert.match(readme, /jwt-auth.*optional/i);
120
166
  }
121
167
 
168
+ function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
169
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
170
+ assert.match(appModule, /filesConfig/);
171
+ assert.match(appModule, /filesEnvSchema/);
172
+ assert.match(appModule, /ForgeonFilesModule/);
173
+
174
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
175
+ assert.match(apiPackage, /@forgeon\/files/);
176
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files build/);
177
+
178
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
179
+ assert.match(apiDockerfile, /COPY packages\/files\/package\.json packages\/files\/package\.json/);
180
+ assert.match(apiDockerfile, /COPY packages\/files packages\/files/);
181
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files build/);
182
+
183
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
184
+ assert.match(apiEnv, /FILES_ENABLED=true/);
185
+ assert.match(apiEnv, new RegExp(`FILES_STORAGE_DRIVER=${expectedStorageDriver}`));
186
+ assert.match(apiEnv, /FILES_PUBLIC_BASE_PATH=\/files/);
187
+ assert.match(apiEnv, /FILES_MAX_FILE_SIZE_BYTES=10485760/);
188
+ assert.match(apiEnv, /FILES_ALLOWED_MIME_PREFIXES=image\/,application\/pdf,text\//);
189
+
190
+ const healthController = fs.readFileSync(
191
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
192
+ 'utf8',
193
+ );
194
+ assert.match(healthController, /@Post\('files'\)/);
195
+ assert.match(healthController, /@Get\('files-variants'\)/);
196
+ assert.match(healthController, /filesService\.createProbeRecord/);
197
+ assert.match(healthController, /filesService\.getVariantsProbeStatus/);
198
+ assert.match(healthController, /filesService\.deleteByPublicId/);
199
+
200
+ const filesController = fs.readFileSync(
201
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
202
+ 'utf8',
203
+ );
204
+ assert.match(filesController, /@Query\('variant'\) variantQuery\?: string/);
205
+ assert.match(filesController, /parseVariant\(variantQuery\)/);
206
+ assert.match(filesController, /@Delete\(':publicId'\)/);
207
+
208
+ const filesService = fs.readFileSync(
209
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
210
+ 'utf8',
211
+ );
212
+ assert.match(filesService, /getOrCreateBlob/);
213
+ assert.match(filesService, /cleanupReferencedBlobs/);
214
+ assert.match(filesService, /isUniqueConstraintError/);
215
+ assert.match(filesService, /fileBlob\.deleteMany/);
216
+ assert.match(filesService, /variants:\s*\{[\s\S]*?none:\s*\{[\s\S]*?\}/);
217
+ assert.match(filesService, /prisma\.fileBlob/);
218
+
219
+ const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
220
+ assert.match(appTsx, /Check files probe \(create metadata\)/);
221
+ assert.match(appTsx, /Check files variants capability/);
222
+ assert.match(appTsx, /Files probe response/);
223
+ assert.match(appTsx, /Files variants probe response/);
224
+
225
+ const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
226
+ assert.match(schema, /model FileRecord \{/);
227
+ assert.match(schema, /variants\s+FileVariant\[\]/);
228
+ assert.match(schema, /model FileVariant \{/);
229
+ assert.match(schema, /model FileBlob \{/);
230
+ assert.match(schema, /blobId\s+String/);
231
+ assert.match(schema, /@@unique\(\[hash,\s*size,\s*mimeType,\s*storageDriver\]\)/);
232
+ assert.match(schema, /@@unique\(\[fileId,\s*variantKey\]\)/);
233
+ assert.match(schema, /publicId\s+String\s+@unique/);
234
+ assert.match(schema, /@@index\(\[ownerType,\s*ownerId,\s*createdAt\]\)/);
235
+
236
+ const migration = path.join(
237
+ projectRoot,
238
+ 'apps',
239
+ 'api',
240
+ 'prisma',
241
+ 'migrations',
242
+ '20260306_files_file_record',
243
+ 'migration.sql',
244
+ );
245
+ assert.equal(fs.existsSync(migration), true);
246
+
247
+ const variantMigration = path.join(
248
+ projectRoot,
249
+ 'apps',
250
+ 'api',
251
+ 'prisma',
252
+ 'migrations',
253
+ '20260306_files_file_variant',
254
+ 'migration.sql',
255
+ );
256
+ assert.equal(fs.existsSync(variantMigration), true);
257
+ }
258
+
259
+ function assertFilesLocalWiring(projectRoot) {
260
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
261
+ assert.match(appModule, /filesLocalConfig/);
262
+ assert.match(appModule, /filesLocalEnvSchemaZod/);
263
+ assert.match(appModule, /FilesLocalConfigModule/);
264
+
265
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
266
+ assert.match(apiPackage, /@forgeon\/files-local/);
267
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files-local build/);
268
+
269
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
270
+ assert.match(apiDockerfile, /COPY packages\/files-local\/package\.json packages\/files-local\/package\.json/);
271
+ assert.match(apiDockerfile, /COPY packages\/files-local packages\/files-local/);
272
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-local build/);
273
+
274
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
275
+ assert.match(apiEnv, /FILES_LOCAL_ROOT=storage\/uploads/);
276
+
277
+ const gitignore = fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf8');
278
+ assert.match(gitignore, /storage\//);
279
+
280
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
281
+ assert.match(compose, /files_data:\/app\/storage/);
282
+ assert.match(compose, /^\s{2}files_data:\s*$/m);
283
+ }
284
+
285
+ function assertFilesS3Wiring(projectRoot) {
286
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
287
+ assert.match(appModule, /filesS3Config/);
288
+ assert.match(appModule, /filesS3EnvSchemaZod/);
289
+ assert.match(appModule, /FilesS3ConfigModule/);
290
+
291
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
292
+ assert.match(apiPackage, /@forgeon\/files-s3/);
293
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files-s3 build/);
294
+
295
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
296
+ assert.match(apiDockerfile, /COPY packages\/files-s3\/package\.json packages\/files-s3\/package\.json/);
297
+ assert.match(apiDockerfile, /COPY packages\/files-s3 packages\/files-s3/);
298
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-s3 build/);
299
+
300
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
301
+ assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
302
+ assert.match(apiEnv, /FILES_S3_PROVIDER_PRESET=minio/);
303
+ assert.match(apiEnv, /FILES_S3_BUCKET=forgeon-files/);
304
+ assert.match(apiEnv, /FILES_S3_REGION=/);
305
+ assert.match(apiEnv, /FILES_S3_ENDPOINT=/);
306
+ assert.match(apiEnv, /FILES_S3_FORCE_PATH_STYLE=/);
307
+ assert.match(apiEnv, /FILES_S3_MAX_ATTEMPTS=3/);
308
+
309
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
310
+ assert.match(compose, /FILES_S3_PROVIDER_PRESET: \$\{FILES_S3_PROVIDER_PRESET\}/);
311
+ assert.match(compose, /FILES_S3_MAX_ATTEMPTS: \$\{FILES_S3_MAX_ATTEMPTS\}/);
312
+
313
+ const filesS3Package = fs.readFileSync(
314
+ path.join(projectRoot, 'packages', 'files-s3', 'package.json'),
315
+ 'utf8',
316
+ );
317
+ assert.match(filesS3Package, /@aws-sdk\/client-s3/);
318
+ }
319
+
320
+ function assertFilesAccessWiring(projectRoot) {
321
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
322
+ assert.match(appModule, /ForgeonFilesAccessModule/);
323
+
324
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
325
+ assert.match(apiPackage, /@forgeon\/files-access/);
326
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files-access build/);
327
+ assert.equal(
328
+ apiPackage.indexOf('pnpm --filter @forgeon/files-access build') <
329
+ apiPackage.indexOf('pnpm --filter @forgeon/files build'),
330
+ true,
331
+ );
332
+
333
+ const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
334
+ assert.match(filesPackage, /@forgeon\/files-access/);
335
+
336
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
337
+ assert.match(
338
+ apiDockerfile,
339
+ /COPY packages\/files-access\/package\.json packages\/files-access\/package\.json/,
340
+ );
341
+ assert.match(apiDockerfile, /COPY packages\/files-access packages\/files-access/);
342
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-access build/);
343
+ assert.equal(
344
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-access build') <
345
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
346
+ true,
347
+ );
348
+
349
+ const filesController = fs.readFileSync(
350
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
351
+ 'utf8',
352
+ );
353
+ assert.match(filesController, /extractFilesAccessSubject/);
354
+ assert.match(filesController, /filesAccessService\.assertCanRead/);
355
+ assert.match(filesController, /filesAccessService\.assertCanDelete/);
356
+ assert.match(filesController, /@Req\(\) req: any/);
357
+ assert.match(filesController, /openDownload\(publicId,\s*variant\)/);
358
+
359
+ const healthController = fs.readFileSync(
360
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
361
+ 'utf8',
362
+ );
363
+ assert.match(healthController, /@Get\('files-access'\)/);
364
+ assert.match(healthController, /extractFilesAccessSubject/);
365
+ assert.match(healthController, /filesAccessService\.canRead/);
366
+
367
+ const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
368
+ assert.match(appTsx, /Check files access/);
369
+ assert.match(appTsx, /Files access probe response/);
370
+ assert.match(appTsx, /x-forgeon-user-id/);
371
+
372
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
373
+ assert.match(readme, /## Files Access Module/);
374
+ assert.match(readme, /resource-level authorization/i);
375
+ }
376
+
377
+ function assertFilesQuotasWiring(projectRoot) {
378
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
379
+ assert.match(appModule, /filesQuotasConfig/);
380
+ assert.match(appModule, /filesQuotasEnvSchema/);
381
+ assert.match(appModule, /ForgeonFilesQuotasModule/);
382
+
383
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
384
+ assert.match(apiPackage, /@forgeon\/files-quotas/);
385
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files-quotas build/);
386
+ assert.equal(
387
+ apiPackage.indexOf('pnpm --filter @forgeon/files-quotas build') <
388
+ apiPackage.indexOf('pnpm --filter @forgeon/files build'),
389
+ true,
390
+ );
391
+
392
+ const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
393
+ assert.match(filesPackage, /@forgeon\/files-quotas/);
394
+
395
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
396
+ assert.match(
397
+ apiDockerfile,
398
+ /COPY packages\/files-quotas\/package\.json packages\/files-quotas\/package\.json/,
399
+ );
400
+ assert.match(apiDockerfile, /COPY packages\/files-quotas packages\/files-quotas/);
401
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-quotas build/);
402
+ assert.equal(
403
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-quotas build') <
404
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
405
+ true,
406
+ );
407
+
408
+ const filesController = fs.readFileSync(
409
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
410
+ 'utf8',
411
+ );
412
+ assert.match(filesController, /FilesQuotasService/);
413
+ assert.match(filesController, /filesQuotasService\.assertUploadAllowed/);
414
+
415
+ const healthController = fs.readFileSync(
416
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
417
+ 'utf8',
418
+ );
419
+ assert.match(healthController, /@Get\('files-quotas'\)/);
420
+ assert.match(healthController, /filesQuotasService\.getProbeStatus/);
421
+
422
+ const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
423
+ assert.match(appTsx, /Check files quotas/);
424
+ assert.match(appTsx, /Files quotas probe response/);
425
+
426
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
427
+ assert.match(apiEnv, /FILES_QUOTAS_ENABLED=true/);
428
+ assert.match(apiEnv, /FILES_QUOTA_MAX_FILES_PER_OWNER=100/);
429
+ assert.match(apiEnv, /FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600/);
430
+
431
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
432
+ assert.match(compose, /FILES_QUOTAS_ENABLED: \$\{FILES_QUOTAS_ENABLED\}/);
433
+
434
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
435
+ assert.match(readme, /## Files Quotas Module/);
436
+ assert.match(readme, /owner-based limits/i);
437
+ }
438
+
439
+ function assertFilesImageWiring(projectRoot) {
440
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
441
+ assert.match(appModule, /filesImageConfig/);
442
+ assert.match(appModule, /filesImageEnvSchema/);
443
+
444
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
445
+ assert.match(apiPackage, /@forgeon\/files-image/);
446
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files-image build/);
447
+ assert.equal(
448
+ apiPackage.indexOf('pnpm --filter @forgeon/files-image build') <
449
+ apiPackage.indexOf('pnpm --filter @forgeon/files build'),
450
+ true,
451
+ );
452
+
453
+ const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
454
+ assert.match(filesPackage, /@forgeon\/files-image/);
455
+
456
+ const filesModule = fs.readFileSync(
457
+ path.join(projectRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts'),
458
+ 'utf8',
459
+ );
460
+ assert.match(filesModule, /ForgeonFilesImageModule/);
461
+
462
+ const filesService = fs.readFileSync(
463
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
464
+ 'utf8',
465
+ );
466
+ assert.match(filesService, /FilesImageService/);
467
+ assert.match(filesService, /filesImageService\.sanitizeForStorage/);
468
+ assert.match(filesService, /sanitizeForStorage\({/);
469
+ assert.match(filesService, /auditContext: input\.auditContext/);
470
+
471
+ const filesController = fs.readFileSync(
472
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
473
+ 'utf8',
474
+ );
475
+ assert.match(filesController, /@Req\(\) req: any/);
476
+ assert.match(filesController, /requestId:/);
477
+
478
+ const filesTypes = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'src', 'files.types.ts'), 'utf8');
479
+ assert.match(filesTypes, /auditContext\?: \{/);
480
+
481
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
482
+ assert.match(
483
+ apiDockerfile,
484
+ /COPY packages\/files-image\/package\.json packages\/files-image\/package\.json/,
485
+ );
486
+ assert.match(apiDockerfile, /COPY packages\/files-image packages\/files-image/);
487
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-image build/);
488
+ assert.equal(
489
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-image build') <
490
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
491
+ true,
492
+ );
493
+
494
+ const healthController = fs.readFileSync(
495
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
496
+ 'utf8',
497
+ );
498
+ assert.match(healthController, /@Get\('files-image'\)/);
499
+ assert.match(healthController, /filesImageService\.getProbeStatus/);
500
+
501
+ const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
502
+ assert.match(appTsx, /Check files image sanitize/);
503
+ assert.match(appTsx, /Files image probe response/);
504
+
505
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
506
+ assert.match(apiEnv, /FILES_IMAGE_ENABLED=true/);
507
+ assert.match(apiEnv, /FILES_IMAGE_STRIP_METADATA=true/);
508
+ assert.match(apiEnv, /FILES_IMAGE_MAX_WIDTH=4096/);
509
+ assert.match(apiEnv, /FILES_IMAGE_MAX_HEIGHT=4096/);
510
+ assert.match(apiEnv, /FILES_IMAGE_MAX_PIXELS=16777216/);
511
+ assert.match(apiEnv, /FILES_IMAGE_MAX_FRAMES=1/);
512
+ assert.match(apiEnv, /FILES_IMAGE_PROCESS_TIMEOUT_MS=5000/);
513
+ assert.match(apiEnv, /FILES_IMAGE_ALLOWED_MIME_TYPES=image\/jpeg,image\/png,image\/webp/);
514
+
515
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
516
+ assert.match(compose, /FILES_IMAGE_ENABLED: \$\{FILES_IMAGE_ENABLED\}/);
517
+ assert.match(compose, /FILES_IMAGE_STRIP_METADATA: \$\{FILES_IMAGE_STRIP_METADATA\}/);
518
+ assert.match(compose, /FILES_IMAGE_MAX_WIDTH: \$\{FILES_IMAGE_MAX_WIDTH\}/);
519
+ assert.match(compose, /FILES_IMAGE_MAX_HEIGHT: \$\{FILES_IMAGE_MAX_HEIGHT\}/);
520
+ assert.match(compose, /FILES_IMAGE_MAX_PIXELS: \$\{FILES_IMAGE_MAX_PIXELS\}/);
521
+ assert.match(compose, /FILES_IMAGE_MAX_FRAMES: \$\{FILES_IMAGE_MAX_FRAMES\}/);
522
+ assert.match(compose, /FILES_IMAGE_PROCESS_TIMEOUT_MS: \$\{FILES_IMAGE_PROCESS_TIMEOUT_MS\}/);
523
+
524
+ const filesImagePackage = fs.readFileSync(
525
+ path.join(projectRoot, 'packages', 'files-image', 'package.json'),
526
+ 'utf8',
527
+ );
528
+ assert.match(filesImagePackage, /"sharp":/);
529
+ assert.match(filesImagePackage, /"file-type":/);
530
+
531
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
532
+ assert.match(readme, /## Files Image Module/);
533
+ assert.match(readme, /metadata is stripped before storage/i);
534
+ }
535
+
122
536
  function assertJwtAuthWiring(projectRoot, withPrismaStore) {
123
537
  const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
124
538
  assert.match(apiPackage, /@forgeon\/auth-api/);
@@ -282,32 +696,48 @@ function stripDbPrismaArtifacts(projectRoot) {
282
696
  fs.writeFileSync(dockerEnvExamplePath, dockerEnv, 'utf8');
283
697
  }
284
698
 
285
- describe('addModule', () => {
699
+ describe('addModule', () => {
286
700
  const modulesDir = path.dirname(fileURLToPath(import.meta.url));
287
701
  const packageRoot = path.resolve(modulesDir, '..', '..');
288
702
 
289
- it('creates module docs note for planned module', () => {
290
- const targetRoot = mkTmp('forgeon-module-');
291
- try {
292
- createMinimalForgeonProject(targetRoot);
703
+ it('applies queue module on top of scaffold without db and i18n', () => {
704
+ const targetRoot = mkTmp('forgeon-module-queue-');
705
+ const projectRoot = path.join(targetRoot, 'demo-queue');
706
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
707
+
708
+ try {
709
+ scaffoldProject({
710
+ templateRoot,
711
+ packageRoot,
712
+ targetRoot: projectRoot,
713
+ projectName: 'demo-queue',
714
+ frontend: 'react',
715
+ db: 'prisma',
716
+ dbPrismaEnabled: false,
717
+ i18nEnabled: false,
718
+ proxy: 'caddy',
719
+ });
720
+
293
721
  const result = addModule({
294
722
  moduleId: 'queue',
295
- targetRoot,
723
+ targetRoot: projectRoot,
296
724
  packageRoot,
297
725
  });
298
-
299
- assert.equal(result.applied, false);
300
- assert.match(result.message, /planned/);
726
+
727
+ assert.equal(result.applied, true);
728
+ assert.match(result.message, /applied/);
301
729
  assert.equal(fs.existsSync(result.docsPath), true);
302
730
  assert.match(result.docsPath, /modules[\\/].+[\\/]README\.md$/);
303
- assert.equal(fs.existsSync(path.join(targetRoot, 'modules', 'README.md')), true);
731
+ assert.equal(fs.existsSync(path.join(projectRoot, 'modules', 'README.md')), true);
732
+
733
+ assertQueueWiring(projectRoot);
304
734
 
305
735
  const note = fs.readFileSync(result.docsPath, 'utf8');
306
736
  assert.match(note, /Queue Worker/);
307
- assert.match(note, /Status: planned/);
308
- } finally {
309
- fs.rmSync(targetRoot, { recursive: true, force: true });
310
- }
737
+ assert.match(note, /Status: implemented/);
738
+ } finally {
739
+ fs.rmSync(targetRoot, { recursive: true, force: true });
740
+ }
311
741
  });
312
742
 
313
743
  it('throws for unknown module id', () => {
@@ -713,6 +1143,314 @@ describe('addModule', () => {
713
1143
  }
714
1144
  });
715
1145
 
1146
+ it('applies files-local then files foundation modules without breaking api wiring', () => {
1147
+ const targetRoot = mkTmp('forgeon-module-files-local-');
1148
+ const projectRoot = path.join(targetRoot, 'demo-files-local');
1149
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
1150
+
1151
+ try {
1152
+ scaffoldProject({
1153
+ templateRoot,
1154
+ packageRoot,
1155
+ targetRoot: projectRoot,
1156
+ projectName: 'demo-files-local',
1157
+ frontend: 'react',
1158
+ db: 'prisma',
1159
+ dbPrismaEnabled: true,
1160
+ i18nEnabled: false,
1161
+ proxy: 'caddy',
1162
+ });
1163
+
1164
+ const localResult = addModule({
1165
+ moduleId: 'files-local',
1166
+ targetRoot: projectRoot,
1167
+ packageRoot,
1168
+ });
1169
+ assert.equal(localResult.applied, true);
1170
+ assertFilesLocalWiring(projectRoot);
1171
+
1172
+ const filesResult = addModule({
1173
+ moduleId: 'files',
1174
+ targetRoot: projectRoot,
1175
+ packageRoot,
1176
+ });
1177
+ assert.equal(filesResult.applied, true);
1178
+ assertFilesWiring(projectRoot);
1179
+
1180
+ const moduleDoc = fs.readFileSync(filesResult.docsPath, 'utf8');
1181
+ assert.match(moduleDoc, /requires `db-adapter`/i);
1182
+ assert.match(moduleDoc, /requires `files-storage-adapter`/i);
1183
+ } finally {
1184
+ fs.rmSync(targetRoot, { recursive: true, force: true });
1185
+ }
1186
+ });
1187
+
1188
+ it('applies files-s3 foundation module with env and docker wiring', () => {
1189
+ const targetRoot = mkTmp('forgeon-module-files-s3-');
1190
+ const projectRoot = path.join(targetRoot, 'demo-files-s3');
1191
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
1192
+
1193
+ try {
1194
+ scaffoldProject({
1195
+ templateRoot,
1196
+ packageRoot,
1197
+ targetRoot: projectRoot,
1198
+ projectName: 'demo-files-s3',
1199
+ frontend: 'react',
1200
+ db: 'prisma',
1201
+ dbPrismaEnabled: true,
1202
+ i18nEnabled: false,
1203
+ proxy: 'caddy',
1204
+ });
1205
+
1206
+ const result = addModule({
1207
+ moduleId: 'files-s3',
1208
+ targetRoot: projectRoot,
1209
+ packageRoot,
1210
+ });
1211
+ assert.equal(result.applied, true);
1212
+ assertFilesS3Wiring(projectRoot);
1213
+ } finally {
1214
+ fs.rmSync(targetRoot, { recursive: true, force: true });
1215
+ }
1216
+ });
1217
+
1218
+ it('applies files-s3 then files and keeps s3 driver default without requiring files-local', () => {
1219
+ const targetRoot = mkTmp('forgeon-module-files-s3-runtime-');
1220
+ const projectRoot = path.join(targetRoot, 'demo-files-s3-runtime');
1221
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
1222
+
1223
+ try {
1224
+ scaffoldProject({
1225
+ templateRoot,
1226
+ packageRoot,
1227
+ targetRoot: projectRoot,
1228
+ projectName: 'demo-files-s3-runtime',
1229
+ frontend: 'react',
1230
+ db: 'prisma',
1231
+ dbPrismaEnabled: true,
1232
+ i18nEnabled: false,
1233
+ proxy: 'caddy',
1234
+ });
1235
+
1236
+ addModule({
1237
+ moduleId: 'files-s3',
1238
+ targetRoot: projectRoot,
1239
+ packageRoot,
1240
+ });
1241
+ addModule({
1242
+ moduleId: 'files',
1243
+ targetRoot: projectRoot,
1244
+ packageRoot,
1245
+ });
1246
+
1247
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
1248
+ assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
1249
+
1250
+ const filesService = fs.readFileSync(
1251
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
1252
+ 'utf8',
1253
+ );
1254
+ assert.match(filesService, /storeS3/);
1255
+ assert.match(filesService, /openS3/);
1256
+ assert.match(filesService, /deleteS3/);
1257
+ assert.match(filesService, /@aws-sdk\/client-s3/);
1258
+ } finally {
1259
+ fs.rmSync(targetRoot, { recursive: true, force: true });
1260
+ }
1261
+ });
1262
+
1263
+ it('applies files-access after files and wires file route checks and probe UI', () => {
1264
+ const targetRoot = mkTmp('forgeon-module-files-access-');
1265
+ const projectRoot = path.join(targetRoot, 'demo-files-access');
1266
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
1267
+
1268
+ try {
1269
+ scaffoldProject({
1270
+ templateRoot,
1271
+ packageRoot,
1272
+ targetRoot: projectRoot,
1273
+ projectName: 'demo-files-access',
1274
+ frontend: 'react',
1275
+ db: 'prisma',
1276
+ dbPrismaEnabled: true,
1277
+ i18nEnabled: false,
1278
+ proxy: 'caddy',
1279
+ });
1280
+
1281
+ addModule({
1282
+ moduleId: 'files-local',
1283
+ targetRoot: projectRoot,
1284
+ packageRoot,
1285
+ });
1286
+ addModule({
1287
+ moduleId: 'files',
1288
+ targetRoot: projectRoot,
1289
+ packageRoot,
1290
+ });
1291
+ const result = addModule({
1292
+ moduleId: 'files-access',
1293
+ targetRoot: projectRoot,
1294
+ packageRoot,
1295
+ });
1296
+
1297
+ assert.equal(result.applied, true);
1298
+ assertFilesAccessWiring(projectRoot);
1299
+ } finally {
1300
+ fs.rmSync(targetRoot, { recursive: true, force: true });
1301
+ }
1302
+ });
1303
+
1304
+ it('applies files-quotas after files and wires upload quota checks and probe UI', () => {
1305
+ const targetRoot = mkTmp('forgeon-module-files-quotas-');
1306
+ const projectRoot = path.join(targetRoot, 'demo-files-quotas');
1307
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
1308
+
1309
+ try {
1310
+ scaffoldProject({
1311
+ templateRoot,
1312
+ packageRoot,
1313
+ targetRoot: projectRoot,
1314
+ projectName: 'demo-files-quotas',
1315
+ frontend: 'react',
1316
+ db: 'prisma',
1317
+ dbPrismaEnabled: true,
1318
+ i18nEnabled: false,
1319
+ proxy: 'caddy',
1320
+ });
1321
+
1322
+ addModule({
1323
+ moduleId: 'files-local',
1324
+ targetRoot: projectRoot,
1325
+ packageRoot,
1326
+ });
1327
+ addModule({
1328
+ moduleId: 'files',
1329
+ targetRoot: projectRoot,
1330
+ packageRoot,
1331
+ });
1332
+ const result = addModule({
1333
+ moduleId: 'files-quotas',
1334
+ targetRoot: projectRoot,
1335
+ packageRoot,
1336
+ });
1337
+
1338
+ assert.equal(result.applied, true);
1339
+ assertFilesQuotasWiring(projectRoot);
1340
+ } finally {
1341
+ fs.rmSync(targetRoot, { recursive: true, force: true });
1342
+ }
1343
+ });
1344
+
1345
+ it('applies files-image after files and wires sanitize pipeline with default metadata stripping', () => {
1346
+ const targetRoot = mkTmp('forgeon-module-files-image-');
1347
+ const projectRoot = path.join(targetRoot, 'demo-files-image');
1348
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
1349
+
1350
+ try {
1351
+ scaffoldProject({
1352
+ templateRoot,
1353
+ packageRoot,
1354
+ targetRoot: projectRoot,
1355
+ projectName: 'demo-files-image',
1356
+ frontend: 'react',
1357
+ db: 'prisma',
1358
+ dbPrismaEnabled: true,
1359
+ i18nEnabled: false,
1360
+ proxy: 'caddy',
1361
+ });
1362
+
1363
+ addModule({
1364
+ moduleId: 'files-local',
1365
+ targetRoot: projectRoot,
1366
+ packageRoot,
1367
+ });
1368
+ addModule({
1369
+ moduleId: 'files',
1370
+ targetRoot: projectRoot,
1371
+ packageRoot,
1372
+ });
1373
+ const result = addModule({
1374
+ moduleId: 'files-image',
1375
+ targetRoot: projectRoot,
1376
+ packageRoot,
1377
+ });
1378
+
1379
+ assert.equal(result.applied, true);
1380
+ assertFilesImageWiring(projectRoot);
1381
+ } finally {
1382
+ fs.rmSync(targetRoot, { recursive: true, force: true });
1383
+ }
1384
+ });
1385
+
1386
+ it('applies full files stack in mixed order and keeps runtime probes consistent', () => {
1387
+ const targetRoot = mkTmp('forgeon-module-files-stack-smoke-');
1388
+ const projectRoot = path.join(targetRoot, 'demo-files-stack-smoke');
1389
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
1390
+
1391
+ try {
1392
+ scaffoldProject({
1393
+ templateRoot,
1394
+ packageRoot,
1395
+ targetRoot: projectRoot,
1396
+ projectName: 'demo-files-stack-smoke',
1397
+ frontend: 'react',
1398
+ db: 'prisma',
1399
+ dbPrismaEnabled: true,
1400
+ i18nEnabled: false,
1401
+ proxy: 'caddy',
1402
+ });
1403
+
1404
+ addModule({
1405
+ moduleId: 'files-s3',
1406
+ targetRoot: projectRoot,
1407
+ packageRoot,
1408
+ });
1409
+ addModule({
1410
+ moduleId: 'files',
1411
+ targetRoot: projectRoot,
1412
+ packageRoot,
1413
+ });
1414
+ addModule({
1415
+ moduleId: 'files-image',
1416
+ targetRoot: projectRoot,
1417
+ packageRoot,
1418
+ });
1419
+ addModule({
1420
+ moduleId: 'files-access',
1421
+ targetRoot: projectRoot,
1422
+ packageRoot,
1423
+ });
1424
+ addModule({
1425
+ moduleId: 'files-quotas',
1426
+ targetRoot: projectRoot,
1427
+ packageRoot,
1428
+ });
1429
+
1430
+ assertFilesS3Wiring(projectRoot);
1431
+ assertFilesWiring(projectRoot, 's3');
1432
+ assertFilesImageWiring(projectRoot);
1433
+ assertFilesAccessWiring(projectRoot);
1434
+ assertFilesQuotasWiring(projectRoot);
1435
+
1436
+ const healthController = fs.readFileSync(
1437
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
1438
+ 'utf8',
1439
+ );
1440
+ assert.match(healthController, /@Post\('files'\)/);
1441
+ assert.match(healthController, /@Get\('files-variants'\)/);
1442
+ assert.match(healthController, /@Get\('files-image'\)/);
1443
+ assert.match(healthController, /@Get\('files-access'\)/);
1444
+ assert.match(healthController, /@Get\('files-quotas'\)/);
1445
+
1446
+ const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
1447
+ const filesChecks = appTsx.match(/Check files /g) ?? [];
1448
+ assert.equal(filesChecks.length, 5);
1449
+ } finally {
1450
+ fs.rmSync(targetRoot, { recursive: true, force: true });
1451
+ }
1452
+ });
1453
+
716
1454
  it('applies swagger module on top of scaffold without i18n', () => {
717
1455
  const targetRoot = mkTmp('forgeon-module-swagger-');
718
1456
  const projectRoot = path.join(targetRoot, 'demo-swagger');