create-forgeon 0.1.38 → 0.2.1

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 (55) hide show
  1. package/README.md +12 -4
  2. package/package.json +1 -1
  3. package/src/cli/help.mjs +3 -2
  4. package/src/cli/options.mjs +142 -121
  5. package/src/cli/options.test.mjs +13 -10
  6. package/src/constants.mjs +11 -9
  7. package/src/core/docs.mjs +44 -23
  8. package/src/core/docs.test.mjs +21 -15
  9. package/src/core/scaffold.mjs +27 -15
  10. package/src/modules/db-prisma.mjs +134 -32
  11. package/src/modules/executor.test.mjs +44 -13
  12. package/src/modules/i18n.mjs +11 -28
  13. package/src/modules/logger.mjs +4 -1
  14. package/src/modules/swagger.mjs +7 -2
  15. package/src/presets/i18n.mjs +63 -40
  16. package/src/run-add-module.mjs +87 -17
  17. package/src/run-create-forgeon.mjs +33 -24
  18. package/templates/base/README.md +16 -3
  19. package/templates/base/apps/api/Dockerfile +6 -11
  20. package/templates/base/apps/api/package.json +13 -24
  21. package/templates/base/apps/api/src/app.module.ts +3 -5
  22. package/templates/base/apps/api/src/health/health.controller.ts +1 -19
  23. package/templates/base/apps/web/src/App.tsx +0 -5
  24. package/templates/base/docs/AI/MODULE_CHECKS.md +1 -1
  25. package/templates/base/docs/AI/ROADMAP.md +1 -1
  26. package/templates/base/infra/docker/.env.example +1 -6
  27. package/templates/base/infra/docker/compose.caddy.yml +13 -37
  28. package/templates/base/infra/docker/compose.nginx.yml +13 -37
  29. package/templates/base/infra/docker/compose.none.yml +8 -32
  30. package/templates/base/infra/docker/compose.yml +16 -40
  31. package/templates/base/package.json +12 -9
  32. package/templates/base/scripts/forgeon-sync-integrations.mjs +399 -0
  33. package/templates/docs-fragments/AI_ARCHITECTURE/20_env_base.md +0 -1
  34. package/templates/docs-fragments/AI_ARCHITECTURE/20b_env_db_prisma.md +1 -0
  35. package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db.md +2 -2
  36. package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db_none.md +7 -0
  37. package/templates/docs-fragments/AI_PROJECT/20_structure_base.md +0 -1
  38. package/templates/docs-fragments/AI_PROJECT/20b_structure_db_none.md +2 -0
  39. package/templates/docs-fragments/AI_PROJECT/20b_structure_db_prisma.md +1 -0
  40. package/templates/docs-fragments/README/10_stack.md +4 -4
  41. package/templates/docs-fragments/README/21_quick_start_dev_no_db.md +6 -0
  42. package/templates/module-presets/i18n/apps/web/src/App.tsx +0 -5
  43. /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/migrations/0001_init/migration.sql +0 -0
  44. /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/migrations/migration_lock.toml +0 -0
  45. /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/schema.prisma +0 -0
  46. /package/templates/{base → module-presets/db-prisma}/apps/api/prisma/seed.ts +0 -0
  47. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/README.md +0 -0
  48. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/package.json +0 -0
  49. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma-config.loader.ts +0 -0
  50. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma-config.service.ts +0 -0
  51. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma-env.schema.ts +0 -0
  52. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/db-prisma.module.ts +0 -0
  53. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/index.ts +0 -0
  54. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/src/prisma.service.ts +0 -0
  55. /package/templates/{base → module-presets/db-prisma}/packages/db-prisma/tsconfig.json +0 -0
@@ -2,10 +2,10 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
4
 
5
- function copyFromBase(packageRoot, targetRoot, relativePath) {
6
- const source = path.join(packageRoot, 'templates', 'base', relativePath);
5
+ function copyFromPreset(packageRoot, targetRoot, relativePath) {
6
+ const source = path.join(packageRoot, 'templates', 'module-presets', 'db-prisma', relativePath);
7
7
  if (!fs.existsSync(source)) {
8
- throw new Error(`Missing db-prisma source template: ${source}`);
8
+ throw new Error(`Missing db-prisma preset template: ${source}`);
9
9
  }
10
10
  const destination = path.join(targetRoot, relativePath);
11
11
  copyRecursive(source, destination);
@@ -84,6 +84,26 @@ function ensureLineBefore(content, anchorLine, lineToInsert) {
84
84
  return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
85
85
  }
86
86
 
87
+ function ensureNestCommonImport(content, importName) {
88
+ const pattern = /import\s*\{([^}]*)\}\s*from '@nestjs\/common';/m;
89
+ const match = content.match(pattern);
90
+ if (!match) {
91
+ return `import { ${importName} } from '@nestjs/common';\n${content}`;
92
+ }
93
+
94
+ const names = match[1]
95
+ .split(',')
96
+ .map((item) => item.trim())
97
+ .filter(Boolean);
98
+
99
+ if (!names.includes(importName)) {
100
+ names.push(importName);
101
+ }
102
+
103
+ const replacement = `import { ${names.join(', ')} } from '@nestjs/common';`;
104
+ return content.replace(pattern, replacement);
105
+ }
106
+
87
107
  function upsertEnvLines(filePath, lines) {
88
108
  let content = '';
89
109
  if (fs.existsSync(filePath)) {
@@ -244,10 +264,13 @@ function patchHealthController(targetRoot) {
244
264
  }
245
265
 
246
266
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
267
+ content = ensureNestCommonImport(content, 'Post');
268
+
247
269
  if (!content.includes("from '@forgeon/db-prisma';")) {
270
+ const nestCommonImport = content.match(/import\s*\{[^}]*\}\s*from '@nestjs\/common';/m)?.[0];
248
271
  const anchor = content.includes("import { I18nService } from 'nestjs-i18n';")
249
272
  ? "import { I18nService } from 'nestjs-i18n';"
250
- : "import { BadRequestException, ConflictException, Controller, Get, Post, Query } from '@nestjs/common';";
273
+ : nestCommonImport;
251
274
  content = ensureLineAfter(content, anchor, "import { PrismaService } from '@forgeon/db-prisma';");
252
275
  }
253
276
 
@@ -256,8 +279,9 @@ function patchHealthController(targetRoot) {
256
279
  if (constructorMatch) {
257
280
  const original = constructorMatch[0];
258
281
  const inner = constructorMatch[1].trimEnd();
259
- const separator = inner.length > 0 ? ',' : '';
260
- const next = `constructor(${inner}${separator}
282
+ const normalizedInner = inner.replace(/,\s*$/, '');
283
+ const separator = normalizedInner.length > 0 ? ',' : '';
284
+ const next = `constructor(${normalizedInner}${separator}
261
285
  private readonly prisma: PrismaService,
262
286
  ) {`;
263
287
  content = content.replace(original, next);
@@ -293,6 +317,49 @@ function patchHealthController(targetRoot) {
293
317
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
294
318
  }
295
319
 
320
+ function patchWebApp(targetRoot) {
321
+ const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
322
+ if (!fs.existsSync(filePath)) {
323
+ return;
324
+ }
325
+
326
+ let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
327
+
328
+ if (!content.includes('dbProbeResult')) {
329
+ const stateAnchor = ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);';
330
+ if (content.includes(stateAnchor)) {
331
+ content = ensureLineAfter(
332
+ content,
333
+ stateAnchor,
334
+ ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
335
+ );
336
+ }
337
+ }
338
+
339
+ if (!content.includes('Check database (create user)')) {
340
+ const buttonAnchor = " <button onClick={() => runProbe(setValidationProbeResult, '/api/health/validation')>";
341
+ const buttonAnchorI18n = " <button onClick={() => runProbe(setValidationProbeResult, '/health/validation')>";
342
+ const dbButton = content.includes(buttonAnchorI18n)
343
+ ? " <button onClick={() => runProbe(setDbProbeResult, '/health/db', { method: 'POST' })}>\n Check database (create user)\n </button>"
344
+ : " <button onClick={() => runProbe(setDbProbeResult, '/api/health/db', { method: 'POST' })}>\n Check database (create user)\n </button>";
345
+
346
+ if (content.includes(buttonAnchor)) {
347
+ content = ensureLineAfter(content, buttonAnchor, dbButton);
348
+ } else if (content.includes(buttonAnchorI18n)) {
349
+ content = ensureLineAfter(content, buttonAnchorI18n, dbButton);
350
+ }
351
+ }
352
+
353
+ if (!content.includes("{renderResult('DB probe response', dbProbeResult)}")) {
354
+ const resultAnchor = "{renderResult('Validation probe response', validationProbeResult)}";
355
+ if (content.includes(resultAnchor)) {
356
+ content = ensureLineAfter(content, resultAnchor, " {renderResult('DB probe response', dbProbeResult)}");
357
+ }
358
+ }
359
+
360
+ fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
361
+ }
362
+
296
363
  function patchApiDockerfile(targetRoot) {
297
364
  const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
298
365
  if (!fs.existsSync(dockerfilePath)) {
@@ -300,34 +367,26 @@ function patchApiDockerfile(targetRoot) {
300
367
  }
301
368
 
302
369
  let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
303
- content = ensureLineAfter(
304
- content,
305
- 'COPY apps/api/package.json apps/api/package.json',
306
- 'COPY apps/api/prisma apps/api/prisma',
307
- );
308
- content = ensureLineAfter(
309
- content,
310
- 'COPY packages/core/package.json packages/core/package.json',
311
- 'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
312
- );
370
+ content = ensureLineAfter(content, 'COPY apps/api/package.json apps/api/package.json', 'COPY apps/api/prisma apps/api/prisma');
371
+ content = ensureLineAfter(content, 'COPY packages/core/package.json packages/core/package.json', 'COPY packages/db-prisma/package.json packages/db-prisma/package.json');
313
372
  content = ensureLineAfter(content, 'COPY packages/core packages/core', 'COPY packages/db-prisma packages/db-prisma');
314
373
 
315
- content = content.replace(/^RUN pnpm --filter @forgeon\/db-prisma build\r?\n?/gm, '');
316
- content = ensureLineBefore(
317
- content,
318
- 'RUN pnpm --filter @forgeon/api prisma:generate',
319
- 'RUN pnpm --filter @forgeon/db-prisma build',
320
- );
321
-
322
- if (!content.includes('RUN pnpm --filter @forgeon/api prisma:generate')) {
323
- content = ensureLineBefore(
324
- content,
325
- 'RUN pnpm --filter @forgeon/api build',
326
- 'RUN pnpm --filter @forgeon/api prisma:generate',
374
+ content = content
375
+ .replace(/^RUN pnpm --filter @forgeon\/db-prisma build\r?\n?/gm, '')
376
+ .replace(/^RUN pnpm --filter @forgeon\/core build\r?\n?/gm, '')
377
+ .replace(/^RUN pnpm --filter @forgeon\/api prisma:generate\r?\n?/gm, '')
378
+ .replace(/^CMD \["node", "apps\/api\/dist\/main\.js"\]\r?\n?/gm, '')
379
+ .replace(
380
+ /^CMD \["sh", "-c", "pnpm --filter @forgeon\/api prisma:migrate:deploy && node apps\/api\/dist\/main\.js"\]\r?\n?/gm,
381
+ '',
327
382
  );
328
- }
329
383
 
330
- fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
384
+ content = ensureLineBefore(content, 'RUN pnpm --filter @forgeon/api build', 'RUN pnpm --filter @forgeon/core build');
385
+ content = ensureLineBefore(content, 'RUN pnpm --filter @forgeon/api build', 'RUN pnpm --filter @forgeon/db-prisma build');
386
+ content = ensureLineBefore(content, 'RUN pnpm --filter @forgeon/api build', 'RUN pnpm --filter @forgeon/api prisma:generate');
387
+ content = `${content.trimEnd()}\nCMD ["sh", "-c", "pnpm --filter @forgeon/api prisma:migrate:deploy && node apps/api/dist/main.js"]\n`;
388
+
389
+ fs.writeFileSync(dockerfilePath, content, 'utf8');
331
390
  }
332
391
 
333
392
  function patchCompose(targetRoot) {
@@ -337,6 +396,29 @@ function patchCompose(targetRoot) {
337
396
  }
338
397
 
339
398
  let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
399
+
400
+ const dbServiceBlock = ` db:
401
+ image: postgres:16-alpine
402
+ restart: unless-stopped
403
+ environment:
404
+ POSTGRES_USER: \${POSTGRES_USER}
405
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
406
+ POSTGRES_DB: \${POSTGRES_DB}
407
+ ports:
408
+ - "5432:5432"
409
+ volumes:
410
+ - db_data:/var/lib/postgresql/data
411
+ healthcheck:
412
+ test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER}"]
413
+ interval: 10s
414
+ timeout: 5s
415
+ retries: 10
416
+ `;
417
+
418
+ if (!/\n\s{2}db:\n/.test(content)) {
419
+ content = content.replace(/^services:\n/m, `services:\n${dbServiceBlock}\n`);
420
+ }
421
+
340
422
  if (!content.includes('DATABASE_URL: ${DATABASE_URL}')) {
341
423
  content = content.replace(
342
424
  /^(\s+API_PREFIX:.*)$/m,
@@ -345,6 +427,22 @@ function patchCompose(targetRoot) {
345
427
  );
346
428
  }
347
429
 
430
+ if (!content.includes('depends_on:') || !content.includes('condition: service_healthy')) {
431
+ content = content.replace(
432
+ /^(\s{2}api:\n[\s\S]*?^\s{4}environment:\n(?:\s{6}.+\n)+)/m,
433
+ `$1 depends_on:
434
+ db:
435
+ condition: service_healthy
436
+ `,
437
+ );
438
+ }
439
+
440
+ if (!/^volumes:\n/m.test(content)) {
441
+ content = `${content.trimEnd()}\n\nvolumes:\n db_data:\n`;
442
+ } else if (!/^\s{2}db_data:\s*$/m.test(content)) {
443
+ content = `${content.trimEnd()}\n db_data:\n`;
444
+ }
445
+
348
446
  fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
349
447
  }
350
448
 
@@ -381,13 +479,14 @@ Configuration (env):
381
479
  }
382
480
 
383
481
  export function applyDbPrismaModule({ packageRoot, targetRoot }) {
384
- copyFromBase(packageRoot, targetRoot, path.join('packages', 'db-prisma'));
385
- copyFromBase(packageRoot, targetRoot, path.join('apps', 'api', 'prisma'));
482
+ copyFromPreset(packageRoot, targetRoot, path.join('packages', 'db-prisma'));
483
+ copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'prisma'));
386
484
 
387
485
  patchApiPackage(targetRoot);
388
486
  patchRootPackage(targetRoot);
389
487
  patchAppModule(targetRoot);
390
488
  patchHealthController(targetRoot);
489
+ patchWebApp(targetRoot);
391
490
  patchApiDockerfile(targetRoot);
392
491
  patchCompose(targetRoot);
393
492
  patchReadme(targetRoot);
@@ -396,6 +495,9 @@ export function applyDbPrismaModule({ packageRoot, targetRoot }) {
396
495
  'DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app?schema=public',
397
496
  ]);
398
497
  upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
498
+ 'POSTGRES_USER=postgres',
499
+ 'POSTGRES_PASSWORD=postgres',
500
+ 'POSTGRES_DB=app',
399
501
  'DATABASE_URL=postgresql://postgres:postgres@db:5432/app?schema=public',
400
502
  ]);
401
503
  }
@@ -261,6 +261,7 @@ describe('addModule', () => {
261
261
  projectName: 'demo-i18n',
262
262
  frontend: 'react',
263
263
  db: 'prisma',
264
+ dbPrismaEnabled: true,
264
265
  i18nEnabled: false,
265
266
  proxy: 'caddy',
266
267
  });
@@ -394,13 +395,17 @@ describe('addModule', () => {
394
395
  assert.doesNotMatch(i18nTs, /I18N_DEFAULT_LANG/);
395
396
 
396
397
  const rootPackage = fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8');
398
+ assert.match(rootPackage, /"forgeon:sync-integrations"/);
397
399
  assert.match(rootPackage, /"i18n:sync"/);
398
400
  assert.match(rootPackage, /"i18n:check"/);
399
401
  assert.match(rootPackage, /"i18n:types"/);
400
402
  assert.match(rootPackage, /"i18n:add"/);
403
+ assert.match(rootPackage, /"ts-morph":/);
401
404
 
402
405
  const i18nAddScriptPath = path.join(projectRoot, 'scripts', 'i18n-add.mjs');
403
406
  assert.equal(fs.existsSync(i18nAddScriptPath), true);
407
+ const syncScriptPath = path.join(projectRoot, 'scripts', 'forgeon-sync-integrations.mjs');
408
+ assert.equal(fs.existsSync(syncScriptPath), true);
404
409
 
405
410
  const caddyDockerfile = fs.readFileSync(
406
411
  path.join(projectRoot, 'infra', 'docker', 'caddy.Dockerfile'),
@@ -444,6 +449,7 @@ describe('addModule', () => {
444
449
  projectName: 'demo-logger',
445
450
  frontend: 'react',
446
451
  db: 'prisma',
452
+ dbPrismaEnabled: true,
447
453
  i18nEnabled: false,
448
454
  proxy: 'caddy',
449
455
  });
@@ -536,6 +542,7 @@ describe('addModule', () => {
536
542
  projectName: 'demo-swagger',
537
543
  frontend: 'react',
538
544
  db: 'prisma',
545
+ dbPrismaEnabled: true,
539
546
  i18nEnabled: false,
540
547
  proxy: 'caddy',
541
548
  });
@@ -629,6 +636,7 @@ describe('addModule', () => {
629
636
  projectName: 'demo-swagger-i18n',
630
637
  frontend: 'react',
631
638
  db: 'prisma',
639
+ dbPrismaEnabled: true,
632
640
  i18nEnabled: true,
633
641
  proxy: 'caddy',
634
642
  });
@@ -642,11 +650,19 @@ describe('addModule', () => {
642
650
  assert.equal(result.applied, true);
643
651
 
644
652
  const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
645
- assert.match(appModule, /load: \[coreConfig,\s*dbPrismaConfig,\s*i18nConfig,\s*swaggerConfig\]/);
646
- assert.match(
647
- appModule,
648
- /validate: createEnvValidator\(\[coreEnvSchema,\s*dbPrismaEnvSchema,\s*i18nEnvSchema,\s*swaggerEnvSchema\]\)/,
649
- );
653
+ const loadMatch = appModule.match(/load: \[([^\]]+)\]/);
654
+ assert.ok(loadMatch);
655
+ assert.match(loadMatch[1], /coreConfig/);
656
+ assert.match(loadMatch[1], /dbPrismaConfig/);
657
+ assert.match(loadMatch[1], /i18nConfig/);
658
+ assert.match(loadMatch[1], /swaggerConfig/);
659
+
660
+ const validateMatch = appModule.match(/validate: createEnvValidator\(\[([^\]]+)\]\)/);
661
+ assert.ok(validateMatch);
662
+ assert.match(validateMatch[1], /coreEnvSchema/);
663
+ assert.match(validateMatch[1], /dbPrismaEnvSchema/);
664
+ assert.match(validateMatch[1], /i18nEnvSchema/);
665
+ assert.match(validateMatch[1], /swaggerEnvSchema/);
650
666
  assert.match(appModule, /ForgeonSwaggerModule/);
651
667
  assert.match(appModule, /ForgeonI18nModule/);
652
668
  } finally {
@@ -667,6 +683,7 @@ describe('addModule', () => {
667
683
  projectName: 'demo-swagger-logger',
668
684
  frontend: 'react',
669
685
  db: 'prisma',
686
+ dbPrismaEnabled: true,
670
687
  i18nEnabled: true,
671
688
  proxy: 'caddy',
672
689
  });
@@ -686,14 +703,21 @@ describe('addModule', () => {
686
703
  assert.equal(loggerResult.applied, true);
687
704
 
688
705
  const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
689
- assert.match(
690
- appModule,
691
- /load: \[coreConfig,\s*dbPrismaConfig,\s*i18nConfig,\s*swaggerConfig,\s*loggerConfig\]/,
692
- );
693
- assert.match(
694
- appModule,
695
- /validate: createEnvValidator\(\[coreEnvSchema,\s*dbPrismaEnvSchema,\s*i18nEnvSchema,\s*swaggerEnvSchema,\s*loggerEnvSchema\]\)/,
696
- );
706
+ const loadMatch = appModule.match(/load: \[([^\]]+)\]/);
707
+ assert.ok(loadMatch);
708
+ assert.match(loadMatch[1], /coreConfig/);
709
+ assert.match(loadMatch[1], /dbPrismaConfig/);
710
+ assert.match(loadMatch[1], /i18nConfig/);
711
+ assert.match(loadMatch[1], /swaggerConfig/);
712
+ assert.match(loadMatch[1], /loggerConfig/);
713
+
714
+ const validateMatch = appModule.match(/validate: createEnvValidator\(\[([^\]]+)\]\)/);
715
+ assert.ok(validateMatch);
716
+ assert.match(validateMatch[1], /coreEnvSchema/);
717
+ assert.match(validateMatch[1], /dbPrismaEnvSchema/);
718
+ assert.match(validateMatch[1], /i18nEnvSchema/);
719
+ assert.match(validateMatch[1], /swaggerEnvSchema/);
720
+ assert.match(validateMatch[1], /loggerEnvSchema/);
697
721
  assert.match(appModule, /ForgeonSwaggerModule/);
698
722
  assert.match(appModule, /ForgeonLoggerModule/);
699
723
  } finally {
@@ -714,6 +738,7 @@ describe('addModule', () => {
714
738
  projectName: 'demo-logger-i18n',
715
739
  frontend: 'react',
716
740
  db: 'prisma',
741
+ dbPrismaEnabled: true,
717
742
  i18nEnabled: false,
718
743
  proxy: 'caddy',
719
744
  });
@@ -770,6 +795,7 @@ describe('addModule', () => {
770
795
  projectName: 'demo-swagger-i18n-order',
771
796
  frontend: 'react',
772
797
  db: 'prisma',
798
+ dbPrismaEnabled: true,
773
799
  i18nEnabled: false,
774
800
  proxy: 'caddy',
775
801
  });
@@ -826,6 +852,7 @@ describe('addModule', () => {
826
852
  projectName: 'demo-mixed-order',
827
853
  frontend: 'react',
828
854
  db: 'prisma',
855
+ dbPrismaEnabled: true,
829
856
  i18nEnabled: false,
830
857
  proxy: 'caddy',
831
858
  });
@@ -879,6 +906,7 @@ describe('addModule', () => {
879
906
  projectName: 'demo-jwt-db',
880
907
  frontend: 'react',
881
908
  db: 'prisma',
909
+ dbPrismaEnabled: true,
882
910
  i18nEnabled: true,
883
911
  proxy: 'caddy',
884
912
  });
@@ -944,6 +972,7 @@ describe('addModule', () => {
944
972
  projectName: 'demo-jwt-nodb',
945
973
  frontend: 'react',
946
974
  db: 'prisma',
975
+ dbPrismaEnabled: true,
947
976
  i18nEnabled: false,
948
977
  proxy: 'caddy',
949
978
  });
@@ -995,6 +1024,7 @@ describe('addModule', () => {
995
1024
  projectName: `demo-db-${sequence.join('-')}`,
996
1025
  frontend: 'react',
997
1026
  db: 'prisma',
1027
+ dbPrismaEnabled: true,
998
1028
  i18nEnabled: false,
999
1029
  proxy: 'caddy',
1000
1030
  });
@@ -1023,6 +1053,7 @@ describe('addModule', () => {
1023
1053
  projectName: 'demo-db-last',
1024
1054
  frontend: 'react',
1025
1055
  db: 'prisma',
1056
+ dbPrismaEnabled: true,
1026
1057
  i18nEnabled: false,
1027
1058
  proxy: 'caddy',
1028
1059
  });
@@ -169,11 +169,6 @@ function patchApiDockerfile(targetRoot) {
169
169
  content = ensureLineAfter(
170
170
  content,
171
171
  'COPY packages/core/package.json packages/core/package.json',
172
- 'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
173
- );
174
- content = ensureLineAfter(
175
- content,
176
- 'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
177
172
  'COPY packages/i18n-contracts/package.json packages/i18n-contracts/package.json',
178
173
  );
179
174
  content = ensureLineAfter(
@@ -181,16 +176,7 @@ function patchApiDockerfile(targetRoot) {
181
176
  'COPY packages/i18n-contracts/package.json packages/i18n-contracts/package.json',
182
177
  'COPY packages/i18n/package.json packages/i18n/package.json',
183
178
  );
184
- content = ensureLineAfter(
185
- content,
186
- 'COPY packages/core packages/core',
187
- 'COPY packages/db-prisma packages/db-prisma',
188
- );
189
- content = ensureLineAfter(
190
- content,
191
- 'COPY packages/db-prisma packages/db-prisma',
192
- 'COPY packages/i18n-contracts packages/i18n-contracts',
193
- );
179
+ content = ensureLineAfter(content, 'COPY packages/core packages/core', 'COPY packages/i18n-contracts packages/i18n-contracts');
194
180
  content = ensureLineAfter(
195
181
  content,
196
182
  'COPY packages/i18n-contracts packages/i18n-contracts',
@@ -199,28 +185,26 @@ function patchApiDockerfile(targetRoot) {
199
185
 
200
186
  content = content
201
187
  .replace(/^RUN pnpm --filter @forgeon\/core build\r?\n?/gm, '')
202
- .replace(/^RUN pnpm --filter @forgeon\/db-prisma build\r?\n?/gm, '')
203
188
  .replace(/^RUN pnpm --filter @forgeon\/i18n-contracts build\r?\n?/gm, '')
204
189
  .replace(/^RUN pnpm --filter @forgeon\/i18n build\r?\n?/gm, '');
205
190
 
191
+ const apiBuildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
192
+ ? 'RUN pnpm --filter @forgeon/api prisma:generate'
193
+ : 'RUN pnpm --filter @forgeon/api build';
194
+
206
195
  content = ensureLineBefore(
207
196
  content,
208
- 'RUN pnpm --filter @forgeon/api prisma:generate',
197
+ apiBuildAnchor,
209
198
  'RUN pnpm --filter @forgeon/core build',
210
199
  );
211
200
  content = ensureLineBefore(
212
201
  content,
213
- 'RUN pnpm --filter @forgeon/api prisma:generate',
214
- 'RUN pnpm --filter @forgeon/db-prisma build',
215
- );
216
- content = ensureLineBefore(
217
- content,
218
- 'RUN pnpm --filter @forgeon/api prisma:generate',
202
+ apiBuildAnchor,
219
203
  'RUN pnpm --filter @forgeon/i18n-contracts build',
220
204
  );
221
205
  content = ensureLineBefore(
222
206
  content,
223
- 'RUN pnpm --filter @forgeon/api prisma:generate',
207
+ apiBuildAnchor,
224
208
  'RUN pnpm --filter @forgeon/i18n build',
225
209
  );
226
210
 
@@ -291,8 +275,10 @@ function patchCompose(targetRoot) {
291
275
 
292
276
  let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
293
277
  if (!content.includes('I18N_DEFAULT_LANG: ${I18N_DEFAULT_LANG}')) {
278
+ const hasDatabaseUrl = /^(\s+DATABASE_URL:.*)$/m.test(content);
279
+ const anchorPattern = hasDatabaseUrl ? /^(\s+DATABASE_URL:.*)$/m : /^(\s+API_PREFIX:.*)$/m;
294
280
  content = content.replace(
295
- /^(\s+DATABASE_URL:.*)$/m,
281
+ anchorPattern,
296
282
  `$1
297
283
  I18N_DEFAULT_LANG: \${I18N_DEFAULT_LANG}
298
284
  I18N_FALLBACK_LANG: \${I18N_FALLBACK_LANG}`,
@@ -311,13 +297,11 @@ function patchApiPackage(targetRoot) {
311
297
  const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
312
298
  ensureBuildSteps(packageJson, 'predev', [
313
299
  'pnpm --filter @forgeon/core build',
314
- 'pnpm --filter @forgeon/db-prisma build',
315
300
  'pnpm --filter @forgeon/i18n-contracts build',
316
301
  'pnpm --filter @forgeon/i18n build',
317
302
  ]);
318
303
  ensureDependency(packageJson, '@forgeon/i18n', 'workspace:*');
319
304
  ensureDependency(packageJson, '@forgeon/i18n-contracts', 'workspace:*');
320
- ensureDependency(packageJson, '@forgeon/db-prisma', 'workspace:*');
321
305
  ensureDependency(packageJson, 'nestjs-i18n', '^10.5.1');
322
306
  writeJson(packagePath, packageJson);
323
307
  }
@@ -544,7 +528,6 @@ function patchRootPackage(targetRoot) {
544
528
 
545
529
  export function applyI18nModule({ packageRoot, targetRoot }) {
546
530
  copyFromBase(packageRoot, targetRoot, path.join('scripts', 'i18n-add.mjs'));
547
- copyFromBase(packageRoot, targetRoot, path.join('packages', 'db-prisma'));
548
531
  copyFromBase(packageRoot, targetRoot, path.join('packages', 'i18n'));
549
532
  copyFromBase(packageRoot, targetRoot, path.join('resources', 'i18n'));
550
533
 
@@ -238,9 +238,12 @@ function patchApiDockerfile(targetRoot) {
238
238
  content = ensureLineAfter(content, sourceAnchor, 'COPY packages/logger packages/logger');
239
239
 
240
240
  content = content.replace(/^RUN pnpm --filter @forgeon\/logger build\r?\n?/gm, '');
241
+ const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
242
+ ? 'RUN pnpm --filter @forgeon/api prisma:generate'
243
+ : 'RUN pnpm --filter @forgeon/api build';
241
244
  content = ensureLineBefore(
242
245
  content,
243
- 'RUN pnpm --filter @forgeon/api prisma:generate',
246
+ buildAnchor,
244
247
  'RUN pnpm --filter @forgeon/logger build',
245
248
  );
246
249
 
@@ -241,9 +241,12 @@ function patchApiDockerfile(targetRoot) {
241
241
  content = ensureLineAfter(content, sourceAnchor, 'COPY packages/swagger packages/swagger');
242
242
 
243
243
  content = content.replace(/^RUN pnpm --filter @forgeon\/swagger build\r?\n?/gm, '');
244
+ const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
245
+ ? 'RUN pnpm --filter @forgeon/api prisma:generate'
246
+ : 'RUN pnpm --filter @forgeon/api build';
244
247
  content = ensureLineBefore(
245
248
  content,
246
- 'RUN pnpm --filter @forgeon/api prisma:generate',
249
+ buildAnchor,
247
250
  'RUN pnpm --filter @forgeon/swagger build',
248
251
  );
249
252
 
@@ -258,8 +261,10 @@ function patchCompose(targetRoot) {
258
261
 
259
262
  let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
260
263
  if (!content.includes('SWAGGER_ENABLED: ${SWAGGER_ENABLED}')) {
264
+ const hasDatabaseUrl = /^(\s+DATABASE_URL:.*)$/m.test(content);
265
+ const anchorPattern = hasDatabaseUrl ? /^(\s+DATABASE_URL:.*)$/m : /^(\s+API_PREFIX:.*)$/m;
261
266
  content = content.replace(
262
- /^(\s+DATABASE_URL:.*)$/m,
267
+ anchorPattern,
263
268
  `$1
264
269
  SWAGGER_ENABLED: \${SWAGGER_ENABLED}
265
270
  SWAGGER_PATH: \${SWAGGER_PATH}