create-forgeon 0.1.34 → 0.1.36

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 (38) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/modules/db-prisma.mjs +401 -0
  4. package/src/modules/executor.mjs +4 -0
  5. package/src/modules/executor.test.mjs +585 -13
  6. package/src/modules/i18n.mjs +244 -22
  7. package/src/modules/jwt-auth.mjs +612 -0
  8. package/src/modules/logger.mjs +76 -27
  9. package/src/modules/registry.mjs +15 -7
  10. package/src/modules/swagger.mjs +12 -2
  11. package/templates/module-fragments/db-prisma/00_title.md +6 -0
  12. package/templates/module-fragments/db-prisma/10_overview.md +10 -0
  13. package/templates/module-fragments/db-prisma/20_scope.md +14 -0
  14. package/templates/module-fragments/db-prisma/90_status_implemented.md +4 -0
  15. package/templates/module-fragments/jwt-auth/20_scope.md +17 -7
  16. package/templates/module-fragments/jwt-auth/90_status_implemented.md +7 -0
  17. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +3 -0
  18. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +36 -0
  19. package/templates/module-presets/jwt-auth/packages/auth-api/package.json +28 -0
  20. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.loader.ts +27 -0
  21. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.module.ts +8 -0
  22. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.service.ts +36 -0
  23. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-env.schema.ts +19 -0
  24. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +23 -0
  25. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +71 -0
  26. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +155 -0
  27. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +6 -0
  28. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +2 -0
  29. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/login.dto.ts +11 -0
  30. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/refresh.dto.ts +8 -0
  31. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +47 -0
  32. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +12 -0
  33. package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt-auth.guard.ts +5 -0
  34. package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt.strategy.ts +20 -0
  35. package/templates/module-presets/jwt-auth/packages/auth-api/tsconfig.json +9 -0
  36. package/templates/module-presets/jwt-auth/packages/auth-contracts/package.json +21 -0
  37. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +47 -0
  38. package/templates/module-presets/jwt-auth/packages/auth-contracts/tsconfig.json +9 -0
@@ -11,33 +11,213 @@ function mkTmp(prefix) {
11
11
  return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
12
12
  }
13
13
 
14
- function createMinimalForgeonProject(targetRoot) {
15
- fs.mkdirSync(path.join(targetRoot, 'apps', 'api'), { recursive: true });
16
- fs.writeFileSync(path.join(targetRoot, 'package.json'), '{"name":"demo"}\n', 'utf8');
17
- fs.writeFileSync(path.join(targetRoot, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n', 'utf8');
18
- }
14
+ function createMinimalForgeonProject(targetRoot) {
15
+ fs.mkdirSync(path.join(targetRoot, 'apps', 'api'), { recursive: true });
16
+ fs.writeFileSync(path.join(targetRoot, 'package.json'), '{"name":"demo"}\n', 'utf8');
17
+ fs.writeFileSync(path.join(targetRoot, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n', 'utf8');
18
+ }
19
+
20
+ function assertDbPrismaWiring(projectRoot) {
21
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
22
+ assert.match(appModule, /dbPrismaConfig/);
23
+ assert.match(appModule, /dbPrismaEnvSchema/);
24
+ assert.match(appModule, /DbPrismaModule/);
25
+
26
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
27
+ assert.match(apiPackage, /@forgeon\/db-prisma/);
28
+
29
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
30
+ assert.match(apiDockerfile, /COPY packages\/db-prisma\/package\.json packages\/db-prisma\/package\.json/);
31
+ assert.match(apiDockerfile, /COPY packages\/db-prisma packages\/db-prisma/);
32
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/db-prisma build/);
33
+
34
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
35
+ assert.match(compose, /DATABASE_URL: \$\{DATABASE_URL\}/);
36
+
37
+ const healthController = fs.readFileSync(
38
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
39
+ 'utf8',
40
+ );
41
+ assert.match(healthController, /PrismaService/);
42
+ }
43
+
44
+ function assertJwtAuthWiring(projectRoot, withPrismaStore) {
45
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
46
+ assert.match(apiPackage, /@forgeon\/auth-api/);
47
+ assert.match(apiPackage, /@forgeon\/auth-contracts/);
48
+ assert.match(apiPackage, /pnpm --filter @forgeon\/auth-contracts build/);
49
+ assert.match(apiPackage, /pnpm --filter @forgeon\/auth-api build/);
50
+
51
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
52
+ assert.match(appModule, /authConfig/);
53
+ assert.match(appModule, /authEnvSchema/);
54
+ assert.match(appModule, /ForgeonAuthModule\.register\(/);
55
+ if (withPrismaStore) {
56
+ assert.match(appModule, /AUTH_REFRESH_TOKEN_STORE/);
57
+ assert.match(appModule, /PrismaAuthRefreshTokenStore/);
58
+ } else {
59
+ assert.doesNotMatch(appModule, /PrismaAuthRefreshTokenStore/);
60
+ }
61
+
62
+ const healthController = fs.readFileSync(
63
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
64
+ 'utf8',
65
+ );
66
+ assert.match(healthController, /@Get\('auth'\)/);
67
+ assert.match(healthController, /authService\.getProbeStatus/);
68
+ assert.doesNotMatch(healthController, /,\s*,/);
69
+
70
+ const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
71
+ assert.match(appTsx, /Check JWT auth probe/);
72
+ assert.match(appTsx, /Auth probe response/);
73
+
74
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
75
+ assert.match(
76
+ apiDockerfile,
77
+ /COPY packages\/auth-contracts\/package\.json packages\/auth-contracts\/package\.json/,
78
+ );
79
+ assert.match(apiDockerfile, /COPY packages\/auth-api\/package\.json packages\/auth-api\/package\.json/);
80
+ assert.match(apiDockerfile, /COPY packages\/auth-contracts packages\/auth-contracts/);
81
+ assert.match(apiDockerfile, /COPY packages\/auth-api packages\/auth-api/);
82
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/auth-contracts build/);
83
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/auth-api build/);
84
+
85
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
86
+ assert.match(apiEnv, /JWT_ACCESS_SECRET=/);
87
+ assert.match(apiEnv, /JWT_REFRESH_SECRET=/);
88
+ assert.match(apiEnv, /AUTH_DEMO_EMAIL=/);
89
+ assert.match(apiEnv, /AUTH_DEMO_PASSWORD=/);
90
+
91
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
92
+ assert.match(compose, /JWT_ACCESS_SECRET: \$\{JWT_ACCESS_SECRET\}/);
93
+ assert.match(compose, /JWT_REFRESH_SECRET: \$\{JWT_REFRESH_SECRET\}/);
94
+
95
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
96
+ assert.match(readme, /## JWT Auth Module/);
97
+ }
98
+
99
+ function stripDbPrismaArtifacts(projectRoot) {
100
+ const dbPackageDir = path.join(projectRoot, 'packages', 'db-prisma');
101
+ if (fs.existsSync(dbPackageDir)) {
102
+ fs.rmSync(dbPackageDir, { recursive: true, force: true });
103
+ }
104
+
105
+ const prismaDir = path.join(projectRoot, 'apps', 'api', 'prisma');
106
+ if (fs.existsSync(prismaDir)) {
107
+ fs.rmSync(prismaDir, { recursive: true, force: true });
108
+ }
109
+
110
+ const apiPackagePath = path.join(projectRoot, 'apps', 'api', 'package.json');
111
+ const apiPackage = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
112
+ if (apiPackage.dependencies) {
113
+ delete apiPackage.dependencies['@forgeon/db-prisma'];
114
+ delete apiPackage.dependencies['@prisma/client'];
115
+ }
116
+ if (apiPackage.devDependencies) {
117
+ delete apiPackage.devDependencies.prisma;
118
+ }
119
+ if (apiPackage.scripts) {
120
+ for (const key of Object.keys(apiPackage.scripts)) {
121
+ if (key.startsWith('prisma:')) {
122
+ delete apiPackage.scripts[key];
123
+ }
124
+ }
125
+ if (typeof apiPackage.scripts.predev === 'string') {
126
+ apiPackage.scripts.predev = apiPackage.scripts.predev
127
+ .replace('pnpm --filter @forgeon/db-prisma build && ', '')
128
+ .replace(' && pnpm --filter @forgeon/db-prisma build', '')
129
+ .replace('pnpm --filter @forgeon/db-prisma build', '')
130
+ .trim();
131
+ if (apiPackage.scripts.predev.length === 0) {
132
+ delete apiPackage.scripts.predev;
133
+ }
134
+ }
135
+ }
136
+ delete apiPackage.prisma;
137
+ fs.writeFileSync(apiPackagePath, `${JSON.stringify(apiPackage, null, 2)}\n`, 'utf8');
138
+
139
+ const rootPackagePath = path.join(projectRoot, 'package.json');
140
+ const rootPackage = JSON.parse(fs.readFileSync(rootPackagePath, 'utf8'));
141
+ if (rootPackage.scripts && typeof rootPackage.scripts.postinstall === 'string') {
142
+ rootPackage.scripts.postinstall = rootPackage.scripts.postinstall
143
+ .replace(/\s*&&\s*pnpm --filter @forgeon\/api prisma:generate/g, '')
144
+ .replace(/pnpm --filter @forgeon\/api prisma:generate\s*&&\s*/g, '')
145
+ .trim();
146
+ if (rootPackage.scripts.postinstall.length === 0) {
147
+ delete rootPackage.scripts.postinstall;
148
+ }
149
+ }
150
+ fs.writeFileSync(rootPackagePath, `${JSON.stringify(rootPackage, null, 2)}\n`, 'utf8');
151
+
152
+ const appModulePath = path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts');
153
+ let appModule = fs.readFileSync(appModulePath, 'utf8');
154
+ appModule = appModule
155
+ .replace(/^import \{ dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule \} from '@forgeon\/db-prisma';\r?\n/m, '')
156
+ .replace(/,\s*dbPrismaConfig/g, '')
157
+ .replace(/dbPrismaConfig,\s*/g, '')
158
+ .replace(/,\s*dbPrismaEnvSchema/g, '')
159
+ .replace(/dbPrismaEnvSchema,\s*/g, '')
160
+ .replace(/^\s*DbPrismaModule,\r?\n/gm, '');
161
+ fs.writeFileSync(appModulePath, appModule, 'utf8');
162
+
163
+ const healthControllerPath = path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
164
+ let healthController = fs.readFileSync(healthControllerPath, 'utf8');
165
+ healthController = healthController
166
+ .replace(/^import \{ PrismaService \} from '@forgeon\/db-prisma';\r?\n/m, '')
167
+ .replace(/\s*private readonly prisma: PrismaService,\r?\n/g, '\n')
168
+ .replace(
169
+ /\s*@Post\('db'\)\s*async getDbProbe\(\)\s*\{[\s\S]*?\n\s*\}\r?\n/g,
170
+ '\n',
171
+ );
172
+ fs.writeFileSync(healthControllerPath, healthController, 'utf8');
173
+
174
+ const apiDockerfilePath = path.join(projectRoot, 'apps', 'api', 'Dockerfile');
175
+ let apiDockerfile = fs.readFileSync(apiDockerfilePath, 'utf8');
176
+ apiDockerfile = apiDockerfile
177
+ .replace(/^COPY apps\/api\/prisma apps\/api\/prisma\r?\n/gm, '')
178
+ .replace(/^COPY packages\/db-prisma\/package\.json packages\/db-prisma\/package\.json\r?\n/gm, '')
179
+ .replace(/^COPY packages\/db-prisma packages\/db-prisma\r?\n/gm, '')
180
+ .replace(/^RUN pnpm --filter @forgeon\/db-prisma build\r?\n/gm, '')
181
+ .replace(/^RUN pnpm --filter @forgeon\/api prisma:generate\r?\n/gm, '');
182
+ fs.writeFileSync(apiDockerfilePath, apiDockerfile, 'utf8');
183
+
184
+ const composePath = path.join(projectRoot, 'infra', 'docker', 'compose.yml');
185
+ let compose = fs.readFileSync(composePath, 'utf8');
186
+ compose = compose.replace(/^\s+DATABASE_URL:.*\r?\n/gm, '');
187
+ fs.writeFileSync(composePath, compose, 'utf8');
188
+
189
+ const apiEnvExamplePath = path.join(projectRoot, 'apps', 'api', '.env.example');
190
+ let apiEnv = fs.readFileSync(apiEnvExamplePath, 'utf8');
191
+ apiEnv = apiEnv.replace(/^DATABASE_URL=.*\r?\n/gm, '');
192
+ fs.writeFileSync(apiEnvExamplePath, apiEnv, 'utf8');
193
+
194
+ const dockerEnvExamplePath = path.join(projectRoot, 'infra', 'docker', '.env.example');
195
+ let dockerEnv = fs.readFileSync(dockerEnvExamplePath, 'utf8');
196
+ dockerEnv = dockerEnv.replace(/^DATABASE_URL=.*\r?\n/gm, '');
197
+ fs.writeFileSync(dockerEnvExamplePath, dockerEnv, 'utf8');
198
+ }
19
199
 
20
200
  describe('addModule', () => {
21
201
  const modulesDir = path.dirname(fileURLToPath(import.meta.url));
22
202
  const packageRoot = path.resolve(modulesDir, '..', '..');
23
203
 
24
- it('creates module docs note for planned module', () => {
204
+ it('creates module docs note for planned module', () => {
25
205
  const targetRoot = mkTmp('forgeon-module-');
26
206
  try {
27
207
  createMinimalForgeonProject(targetRoot);
28
- const result = addModule({
29
- moduleId: 'jwt-auth',
30
- targetRoot,
31
- packageRoot,
32
- });
208
+ const result = addModule({
209
+ moduleId: 'queue',
210
+ targetRoot,
211
+ packageRoot,
212
+ });
33
213
 
34
214
  assert.equal(result.applied, false);
35
215
  assert.match(result.message, /planned/);
36
216
  assert.equal(fs.existsSync(result.docsPath), true);
37
217
 
38
218
  const note = fs.readFileSync(result.docsPath, 'utf8');
39
- assert.match(note, /JWT Auth/);
40
- assert.match(note, /Status: planned/);
219
+ assert.match(note, /Queue Worker/);
220
+ assert.match(note, /Status: planned/);
41
221
  } finally {
42
222
  fs.rmSync(targetRoot, { recursive: true, force: true });
43
223
  }
@@ -466,4 +646,396 @@ describe('addModule', () => {
466
646
  fs.rmSync(targetRoot, { recursive: true, force: true });
467
647
  }
468
648
  });
649
+
650
+ it('applies logger after swagger without losing logger config keys', () => {
651
+ const targetRoot = mkTmp('forgeon-module-swagger-logger-');
652
+ const projectRoot = path.join(targetRoot, 'demo-swagger-logger');
653
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
654
+
655
+ try {
656
+ scaffoldProject({
657
+ templateRoot,
658
+ packageRoot,
659
+ targetRoot: projectRoot,
660
+ projectName: 'demo-swagger-logger',
661
+ frontend: 'react',
662
+ db: 'prisma',
663
+ i18nEnabled: true,
664
+ proxy: 'caddy',
665
+ });
666
+
667
+ const swaggerResult = addModule({
668
+ moduleId: 'swagger',
669
+ targetRoot: projectRoot,
670
+ packageRoot,
671
+ });
672
+ assert.equal(swaggerResult.applied, true);
673
+
674
+ const loggerResult = addModule({
675
+ moduleId: 'logger',
676
+ targetRoot: projectRoot,
677
+ packageRoot,
678
+ });
679
+ assert.equal(loggerResult.applied, true);
680
+
681
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
682
+ assert.match(
683
+ appModule,
684
+ /load: \[coreConfig,\s*dbPrismaConfig,\s*i18nConfig,\s*swaggerConfig,\s*loggerConfig\]/,
685
+ );
686
+ assert.match(
687
+ appModule,
688
+ /validate: createEnvValidator\(\[coreEnvSchema,\s*dbPrismaEnvSchema,\s*i18nEnvSchema,\s*swaggerEnvSchema,\s*loggerEnvSchema\]\)/,
689
+ );
690
+ assert.match(appModule, /ForgeonSwaggerModule/);
691
+ assert.match(appModule, /ForgeonLoggerModule/);
692
+ } finally {
693
+ fs.rmSync(targetRoot, { recursive: true, force: true });
694
+ }
695
+ });
696
+
697
+ it('applies i18n after logger without losing logger config keys', () => {
698
+ const targetRoot = mkTmp('forgeon-module-logger-i18n-');
699
+ const projectRoot = path.join(targetRoot, 'demo-logger-i18n');
700
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
701
+
702
+ try {
703
+ scaffoldProject({
704
+ templateRoot,
705
+ packageRoot,
706
+ targetRoot: projectRoot,
707
+ projectName: 'demo-logger-i18n',
708
+ frontend: 'react',
709
+ db: 'prisma',
710
+ i18nEnabled: false,
711
+ proxy: 'caddy',
712
+ });
713
+
714
+ addModule({
715
+ moduleId: 'logger',
716
+ targetRoot: projectRoot,
717
+ packageRoot,
718
+ });
719
+
720
+ const i18nResult = addModule({
721
+ moduleId: 'i18n',
722
+ targetRoot: projectRoot,
723
+ packageRoot,
724
+ });
725
+ assert.equal(i18nResult.applied, true);
726
+
727
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
728
+ assert.match(
729
+ appModule,
730
+ /load: \[coreConfig,\s*dbPrismaConfig,\s*loggerConfig,\s*i18nConfig\]/,
731
+ );
732
+ assert.match(
733
+ appModule,
734
+ /validate: createEnvValidator\(\[coreEnvSchema,\s*dbPrismaEnvSchema,\s*loggerEnvSchema,\s*i18nEnvSchema\]\)/,
735
+ );
736
+ assert.match(appModule, /ForgeonLoggerModule/);
737
+ assert.match(appModule, /ForgeonI18nModule/);
738
+
739
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
740
+ assert.match(apiPackage, /@forgeon\/logger/);
741
+ assert.match(apiPackage, /@forgeon\/i18n/);
742
+ assert.match(apiPackage, /pnpm --filter @forgeon\/logger build/);
743
+ assert.match(apiPackage, /pnpm --filter @forgeon\/i18n build/);
744
+
745
+ const mainTs = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'main.ts'), 'utf8');
746
+ assert.match(mainTs, /ForgeonLoggerService/);
747
+ assert.match(mainTs, /ForgeonHttpLoggingInterceptor/);
748
+ } finally {
749
+ fs.rmSync(targetRoot, { recursive: true, force: true });
750
+ }
751
+ });
752
+
753
+ it('applies i18n after swagger without losing swagger config keys', () => {
754
+ const targetRoot = mkTmp('forgeon-module-swagger-i18n-order-');
755
+ const projectRoot = path.join(targetRoot, 'demo-swagger-i18n-order');
756
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
757
+
758
+ try {
759
+ scaffoldProject({
760
+ templateRoot,
761
+ packageRoot,
762
+ targetRoot: projectRoot,
763
+ projectName: 'demo-swagger-i18n-order',
764
+ frontend: 'react',
765
+ db: 'prisma',
766
+ i18nEnabled: false,
767
+ proxy: 'caddy',
768
+ });
769
+
770
+ addModule({
771
+ moduleId: 'swagger',
772
+ targetRoot: projectRoot,
773
+ packageRoot,
774
+ });
775
+
776
+ const i18nResult = addModule({
777
+ moduleId: 'i18n',
778
+ targetRoot: projectRoot,
779
+ packageRoot,
780
+ });
781
+ assert.equal(i18nResult.applied, true);
782
+
783
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
784
+ assert.match(
785
+ appModule,
786
+ /load: \[coreConfig,\s*dbPrismaConfig,\s*swaggerConfig,\s*i18nConfig\]/,
787
+ );
788
+ assert.match(
789
+ appModule,
790
+ /validate: createEnvValidator\(\[coreEnvSchema,\s*dbPrismaEnvSchema,\s*swaggerEnvSchema,\s*i18nEnvSchema\]\)/,
791
+ );
792
+ assert.match(appModule, /ForgeonSwaggerModule/);
793
+ assert.match(appModule, /ForgeonI18nModule/);
794
+
795
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
796
+ assert.match(apiPackage, /@forgeon\/swagger/);
797
+ assert.match(apiPackage, /@forgeon\/i18n/);
798
+ assert.match(apiPackage, /pnpm --filter @forgeon\/swagger build/);
799
+ assert.match(apiPackage, /pnpm --filter @forgeon\/i18n build/);
800
+
801
+ const mainTs = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'main.ts'), 'utf8');
802
+ assert.match(mainTs, /setupSwagger/);
803
+ assert.match(mainTs, /SwaggerConfigService/);
804
+ } finally {
805
+ fs.rmSync(targetRoot, { recursive: true, force: true });
806
+ }
807
+ });
808
+
809
+ it('applies swagger -> logger -> i18n and keeps all module wiring', () => {
810
+ const targetRoot = mkTmp('forgeon-module-mixed-order-');
811
+ const projectRoot = path.join(targetRoot, 'demo-mixed-order');
812
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
813
+
814
+ try {
815
+ scaffoldProject({
816
+ templateRoot,
817
+ packageRoot,
818
+ targetRoot: projectRoot,
819
+ projectName: 'demo-mixed-order',
820
+ frontend: 'react',
821
+ db: 'prisma',
822
+ i18nEnabled: false,
823
+ proxy: 'caddy',
824
+ });
825
+
826
+ addModule({ moduleId: 'swagger', targetRoot: projectRoot, packageRoot });
827
+ addModule({ moduleId: 'logger', targetRoot: projectRoot, packageRoot });
828
+ addModule({ moduleId: 'i18n', targetRoot: projectRoot, packageRoot });
829
+
830
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
831
+ assert.match(
832
+ appModule,
833
+ /load: \[coreConfig,\s*dbPrismaConfig,\s*swaggerConfig,\s*loggerConfig,\s*i18nConfig\]/,
834
+ );
835
+ assert.match(
836
+ appModule,
837
+ /validate: createEnvValidator\(\[coreEnvSchema,\s*dbPrismaEnvSchema,\s*swaggerEnvSchema,\s*loggerEnvSchema,\s*i18nEnvSchema\]\)/,
838
+ );
839
+ assert.match(appModule, /ForgeonSwaggerModule/);
840
+ assert.match(appModule, /ForgeonLoggerModule/);
841
+ assert.match(appModule, /ForgeonI18nModule/);
842
+
843
+ const mainTs = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'main.ts'), 'utf8');
844
+ assert.match(mainTs, /setupSwagger\(app,\s*swaggerConfigService\)/);
845
+ assert.match(mainTs, /app\.useLogger\(app\.get\(ForgeonLoggerService\)\);/);
846
+ assert.match(mainTs, /app\.useGlobalInterceptors\(app\.get\(ForgeonHttpLoggingInterceptor\)\);/);
847
+
848
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
849
+ assert.match(apiPackage, /@forgeon\/swagger/);
850
+ assert.match(apiPackage, /@forgeon\/logger/);
851
+ assert.match(apiPackage, /@forgeon\/i18n/);
852
+ assert.match(apiPackage, /pnpm --filter @forgeon\/swagger build/);
853
+ assert.match(apiPackage, /pnpm --filter @forgeon\/logger build/);
854
+ assert.match(apiPackage, /pnpm --filter @forgeon\/i18n build/);
855
+
856
+ assertDbPrismaWiring(projectRoot);
857
+ } finally {
858
+ fs.rmSync(targetRoot, { recursive: true, force: true });
859
+ }
860
+ });
861
+
862
+ it('applies jwt-auth with db-prisma adapter and wires persistent token store', () => {
863
+ const targetRoot = mkTmp('forgeon-module-jwt-db-');
864
+ const projectRoot = path.join(targetRoot, 'demo-jwt-db');
865
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
866
+
867
+ try {
868
+ scaffoldProject({
869
+ templateRoot,
870
+ packageRoot,
871
+ targetRoot: projectRoot,
872
+ projectName: 'demo-jwt-db',
873
+ frontend: 'react',
874
+ db: 'prisma',
875
+ i18nEnabled: true,
876
+ proxy: 'caddy',
877
+ });
878
+
879
+ const result = addModule({
880
+ moduleId: 'jwt-auth',
881
+ targetRoot: projectRoot,
882
+ packageRoot,
883
+ });
884
+
885
+ assert.equal(result.applied, true);
886
+ assertJwtAuthWiring(projectRoot, true);
887
+
888
+ const storeFile = path.join(
889
+ projectRoot,
890
+ 'apps',
891
+ 'api',
892
+ 'src',
893
+ 'auth',
894
+ 'prisma-auth-refresh-token.store.ts',
895
+ );
896
+ assert.equal(fs.existsSync(storeFile), true);
897
+
898
+ const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
899
+ assert.match(schema, /refreshTokenHash/);
900
+
901
+ const migrationPath = path.join(
902
+ projectRoot,
903
+ 'apps',
904
+ 'api',
905
+ 'prisma',
906
+ 'migrations',
907
+ '0002_auth_refresh_token_hash',
908
+ 'migration.sql',
909
+ );
910
+ assert.equal(fs.existsSync(migrationPath), true);
911
+
912
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
913
+ assert.match(readme, /refresh token persistence: enabled/);
914
+ assert.match(readme, /0002_auth_refresh_token_hash/);
915
+
916
+ const moduleDoc = fs.readFileSync(result.docsPath, 'utf8');
917
+ assert.match(moduleDoc, /Status: implemented/);
918
+ } finally {
919
+ fs.rmSync(targetRoot, { recursive: true, force: true });
920
+ }
921
+ });
922
+
923
+ it('applies jwt-auth without db and prints warning with stateless fallback', () => {
924
+ const targetRoot = mkTmp('forgeon-module-jwt-nodb-');
925
+ const projectRoot = path.join(targetRoot, 'demo-jwt-nodb');
926
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
927
+
928
+ const originalError = console.error;
929
+ const warnings = [];
930
+ console.error = (...args) => warnings.push(args.join(' '));
931
+
932
+ try {
933
+ scaffoldProject({
934
+ templateRoot,
935
+ packageRoot,
936
+ targetRoot: projectRoot,
937
+ projectName: 'demo-jwt-nodb',
938
+ frontend: 'react',
939
+ db: 'prisma',
940
+ i18nEnabled: false,
941
+ proxy: 'caddy',
942
+ });
943
+
944
+ stripDbPrismaArtifacts(projectRoot);
945
+
946
+ const result = addModule({
947
+ moduleId: 'jwt-auth',
948
+ targetRoot: projectRoot,
949
+ packageRoot,
950
+ });
951
+
952
+ assert.equal(result.applied, true);
953
+ assertJwtAuthWiring(projectRoot, false);
954
+ assert.equal(
955
+ fs.existsSync(path.join(projectRoot, 'apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts')),
956
+ false,
957
+ );
958
+
959
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
960
+ assert.match(readme, /refresh token persistence: disabled/);
961
+ assert.match(readme, /create-forgeon add db-prisma/);
962
+
963
+ assert.equal(warnings.length > 0, true);
964
+ assert.match(warnings.join('\n'), /jwt-auth installed without persistent refresh token store/);
965
+ } finally {
966
+ console.error = originalError;
967
+ fs.rmSync(targetRoot, { recursive: true, force: true });
968
+ }
969
+ });
970
+
971
+ it('keeps db-prisma wiring across module installation orders', () => {
972
+ const sequences = [
973
+ ['logger', 'swagger', 'i18n'],
974
+ ['swagger', 'i18n', 'logger'],
975
+ ['i18n', 'logger', 'swagger'],
976
+ ];
977
+
978
+ for (const sequence of sequences) {
979
+ const targetRoot = mkTmp(`forgeon-module-db-order-${sequence.join('-')}-`);
980
+ const projectRoot = path.join(targetRoot, `demo-db-${sequence.join('-')}`);
981
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
982
+
983
+ try {
984
+ scaffoldProject({
985
+ templateRoot,
986
+ packageRoot,
987
+ targetRoot: projectRoot,
988
+ projectName: `demo-db-${sequence.join('-')}`,
989
+ frontend: 'react',
990
+ db: 'prisma',
991
+ i18nEnabled: false,
992
+ proxy: 'caddy',
993
+ });
994
+
995
+ for (const moduleId of sequence) {
996
+ addModule({ moduleId, targetRoot: projectRoot, packageRoot });
997
+ }
998
+
999
+ assertDbPrismaWiring(projectRoot);
1000
+ } finally {
1001
+ fs.rmSync(targetRoot, { recursive: true, force: true });
1002
+ }
1003
+ }
1004
+ });
1005
+
1006
+ it('applies db-prisma as final module after other modules', () => {
1007
+ const targetRoot = mkTmp('forgeon-module-db-last-');
1008
+ const projectRoot = path.join(targetRoot, 'demo-db-last');
1009
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
1010
+
1011
+ try {
1012
+ scaffoldProject({
1013
+ templateRoot,
1014
+ packageRoot,
1015
+ targetRoot: projectRoot,
1016
+ projectName: 'demo-db-last',
1017
+ frontend: 'react',
1018
+ db: 'prisma',
1019
+ i18nEnabled: false,
1020
+ proxy: 'caddy',
1021
+ });
1022
+
1023
+ stripDbPrismaArtifacts(projectRoot);
1024
+
1025
+ addModule({ moduleId: 'logger', targetRoot: projectRoot, packageRoot });
1026
+ addModule({ moduleId: 'swagger', targetRoot: projectRoot, packageRoot });
1027
+ addModule({ moduleId: 'i18n', targetRoot: projectRoot, packageRoot });
1028
+ const dbResult = addModule({ moduleId: 'db-prisma', targetRoot: projectRoot, packageRoot });
1029
+ assert.equal(dbResult.applied, true);
1030
+
1031
+ assertDbPrismaWiring(projectRoot);
1032
+
1033
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
1034
+ assert.match(appModule, /ForgeonLoggerModule/);
1035
+ assert.match(appModule, /ForgeonSwaggerModule/);
1036
+ assert.match(appModule, /ForgeonI18nModule/);
1037
+ } finally {
1038
+ fs.rmSync(targetRoot, { recursive: true, force: true });
1039
+ }
1040
+ });
469
1041
  });