create-forgeon 0.1.33 → 0.1.35

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.
@@ -11,11 +11,136 @@ 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 stripDbPrismaArtifacts(projectRoot) {
45
+ const dbPackageDir = path.join(projectRoot, 'packages', 'db-prisma');
46
+ if (fs.existsSync(dbPackageDir)) {
47
+ fs.rmSync(dbPackageDir, { recursive: true, force: true });
48
+ }
49
+
50
+ const prismaDir = path.join(projectRoot, 'apps', 'api', 'prisma');
51
+ if (fs.existsSync(prismaDir)) {
52
+ fs.rmSync(prismaDir, { recursive: true, force: true });
53
+ }
54
+
55
+ const apiPackagePath = path.join(projectRoot, 'apps', 'api', 'package.json');
56
+ const apiPackage = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
57
+ if (apiPackage.dependencies) {
58
+ delete apiPackage.dependencies['@forgeon/db-prisma'];
59
+ delete apiPackage.dependencies['@prisma/client'];
60
+ }
61
+ if (apiPackage.devDependencies) {
62
+ delete apiPackage.devDependencies.prisma;
63
+ }
64
+ if (apiPackage.scripts) {
65
+ for (const key of Object.keys(apiPackage.scripts)) {
66
+ if (key.startsWith('prisma:')) {
67
+ delete apiPackage.scripts[key];
68
+ }
69
+ }
70
+ if (typeof apiPackage.scripts.predev === 'string') {
71
+ apiPackage.scripts.predev = apiPackage.scripts.predev
72
+ .replace('pnpm --filter @forgeon/db-prisma build && ', '')
73
+ .replace(' && pnpm --filter @forgeon/db-prisma build', '')
74
+ .replace('pnpm --filter @forgeon/db-prisma build', '')
75
+ .trim();
76
+ if (apiPackage.scripts.predev.length === 0) {
77
+ delete apiPackage.scripts.predev;
78
+ }
79
+ }
80
+ }
81
+ delete apiPackage.prisma;
82
+ fs.writeFileSync(apiPackagePath, `${JSON.stringify(apiPackage, null, 2)}\n`, 'utf8');
83
+
84
+ const rootPackagePath = path.join(projectRoot, 'package.json');
85
+ const rootPackage = JSON.parse(fs.readFileSync(rootPackagePath, 'utf8'));
86
+ if (rootPackage.scripts && typeof rootPackage.scripts.postinstall === 'string') {
87
+ rootPackage.scripts.postinstall = rootPackage.scripts.postinstall
88
+ .replace(/\s*&&\s*pnpm --filter @forgeon\/api prisma:generate/g, '')
89
+ .replace(/pnpm --filter @forgeon\/api prisma:generate\s*&&\s*/g, '')
90
+ .trim();
91
+ if (rootPackage.scripts.postinstall.length === 0) {
92
+ delete rootPackage.scripts.postinstall;
93
+ }
94
+ }
95
+ fs.writeFileSync(rootPackagePath, `${JSON.stringify(rootPackage, null, 2)}\n`, 'utf8');
96
+
97
+ const appModulePath = path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts');
98
+ let appModule = fs.readFileSync(appModulePath, 'utf8');
99
+ appModule = appModule
100
+ .replace(/^import \{ dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule \} from '@forgeon\/db-prisma';\r?\n/m, '')
101
+ .replace(/,\s*dbPrismaConfig/g, '')
102
+ .replace(/dbPrismaConfig,\s*/g, '')
103
+ .replace(/,\s*dbPrismaEnvSchema/g, '')
104
+ .replace(/dbPrismaEnvSchema,\s*/g, '')
105
+ .replace(/^\s*DbPrismaModule,\r?\n/gm, '');
106
+ fs.writeFileSync(appModulePath, appModule, 'utf8');
107
+
108
+ const healthControllerPath = path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
109
+ let healthController = fs.readFileSync(healthControllerPath, 'utf8');
110
+ healthController = healthController
111
+ .replace(/^import \{ PrismaService \} from '@forgeon\/db-prisma';\r?\n/m, '')
112
+ .replace(/\s*private readonly prisma: PrismaService,\r?\n/g, '\n')
113
+ .replace(
114
+ /\s*@Post\('db'\)\s*async getDbProbe\(\)\s*\{[\s\S]*?\n\s*\}\r?\n/g,
115
+ '\n',
116
+ );
117
+ fs.writeFileSync(healthControllerPath, healthController, 'utf8');
118
+
119
+ const apiDockerfilePath = path.join(projectRoot, 'apps', 'api', 'Dockerfile');
120
+ let apiDockerfile = fs.readFileSync(apiDockerfilePath, 'utf8');
121
+ apiDockerfile = apiDockerfile
122
+ .replace(/^COPY apps\/api\/prisma apps\/api\/prisma\r?\n/gm, '')
123
+ .replace(/^COPY packages\/db-prisma\/package\.json packages\/db-prisma\/package\.json\r?\n/gm, '')
124
+ .replace(/^COPY packages\/db-prisma packages\/db-prisma\r?\n/gm, '')
125
+ .replace(/^RUN pnpm --filter @forgeon\/db-prisma build\r?\n/gm, '')
126
+ .replace(/^RUN pnpm --filter @forgeon\/api prisma:generate\r?\n/gm, '');
127
+ fs.writeFileSync(apiDockerfilePath, apiDockerfile, 'utf8');
128
+
129
+ const composePath = path.join(projectRoot, 'infra', 'docker', 'compose.yml');
130
+ let compose = fs.readFileSync(composePath, 'utf8');
131
+ compose = compose.replace(/^\s+DATABASE_URL:.*\r?\n/gm, '');
132
+ fs.writeFileSync(composePath, compose, 'utf8');
133
+
134
+ const apiEnvExamplePath = path.join(projectRoot, 'apps', 'api', '.env.example');
135
+ let apiEnv = fs.readFileSync(apiEnvExamplePath, 'utf8');
136
+ apiEnv = apiEnv.replace(/^DATABASE_URL=.*\r?\n/gm, '');
137
+ fs.writeFileSync(apiEnvExamplePath, apiEnv, 'utf8');
138
+
139
+ const dockerEnvExamplePath = path.join(projectRoot, 'infra', 'docker', '.env.example');
140
+ let dockerEnv = fs.readFileSync(dockerEnvExamplePath, 'utf8');
141
+ dockerEnv = dockerEnv.replace(/^DATABASE_URL=.*\r?\n/gm, '');
142
+ fs.writeFileSync(dockerEnvExamplePath, dockerEnv, 'utf8');
143
+ }
19
144
 
20
145
  describe('addModule', () => {
21
146
  const modulesDir = path.dirname(fileURLToPath(import.meta.url));
@@ -419,7 +544,7 @@ describe('addModule', () => {
419
544
  const rootReadme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
420
545
  assert.match(rootReadme, /## Swagger \/ OpenAPI Module/);
421
546
  assert.match(rootReadme, /SWAGGER_ENABLED=false/);
422
- assert.match(rootReadme, /localhost:3000\/docs/);
547
+ assert.match(rootReadme, /localhost:3000\/api\/docs/);
423
548
 
424
549
  const moduleDoc = fs.readFileSync(result.docsPath, 'utf8');
425
550
  assert.match(moduleDoc, /Swagger \/ OpenAPI/);
@@ -466,4 +591,287 @@ describe('addModule', () => {
466
591
  fs.rmSync(targetRoot, { recursive: true, force: true });
467
592
  }
468
593
  });
594
+
595
+ it('applies logger after swagger without losing logger config keys', () => {
596
+ const targetRoot = mkTmp('forgeon-module-swagger-logger-');
597
+ const projectRoot = path.join(targetRoot, 'demo-swagger-logger');
598
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
599
+
600
+ try {
601
+ scaffoldProject({
602
+ templateRoot,
603
+ packageRoot,
604
+ targetRoot: projectRoot,
605
+ projectName: 'demo-swagger-logger',
606
+ frontend: 'react',
607
+ db: 'prisma',
608
+ i18nEnabled: true,
609
+ proxy: 'caddy',
610
+ });
611
+
612
+ const swaggerResult = addModule({
613
+ moduleId: 'swagger',
614
+ targetRoot: projectRoot,
615
+ packageRoot,
616
+ });
617
+ assert.equal(swaggerResult.applied, true);
618
+
619
+ const loggerResult = addModule({
620
+ moduleId: 'logger',
621
+ targetRoot: projectRoot,
622
+ packageRoot,
623
+ });
624
+ assert.equal(loggerResult.applied, true);
625
+
626
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
627
+ assert.match(
628
+ appModule,
629
+ /load: \[coreConfig,\s*dbPrismaConfig,\s*i18nConfig,\s*swaggerConfig,\s*loggerConfig\]/,
630
+ );
631
+ assert.match(
632
+ appModule,
633
+ /validate: createEnvValidator\(\[coreEnvSchema,\s*dbPrismaEnvSchema,\s*i18nEnvSchema,\s*swaggerEnvSchema,\s*loggerEnvSchema\]\)/,
634
+ );
635
+ assert.match(appModule, /ForgeonSwaggerModule/);
636
+ assert.match(appModule, /ForgeonLoggerModule/);
637
+ } finally {
638
+ fs.rmSync(targetRoot, { recursive: true, force: true });
639
+ }
640
+ });
641
+
642
+ it('applies i18n after logger without losing logger config keys', () => {
643
+ const targetRoot = mkTmp('forgeon-module-logger-i18n-');
644
+ const projectRoot = path.join(targetRoot, 'demo-logger-i18n');
645
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
646
+
647
+ try {
648
+ scaffoldProject({
649
+ templateRoot,
650
+ packageRoot,
651
+ targetRoot: projectRoot,
652
+ projectName: 'demo-logger-i18n',
653
+ frontend: 'react',
654
+ db: 'prisma',
655
+ i18nEnabled: false,
656
+ proxy: 'caddy',
657
+ });
658
+
659
+ addModule({
660
+ moduleId: 'logger',
661
+ targetRoot: projectRoot,
662
+ packageRoot,
663
+ });
664
+
665
+ const i18nResult = addModule({
666
+ moduleId: 'i18n',
667
+ targetRoot: projectRoot,
668
+ packageRoot,
669
+ });
670
+ assert.equal(i18nResult.applied, true);
671
+
672
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
673
+ assert.match(
674
+ appModule,
675
+ /load: \[coreConfig,\s*dbPrismaConfig,\s*loggerConfig,\s*i18nConfig\]/,
676
+ );
677
+ assert.match(
678
+ appModule,
679
+ /validate: createEnvValidator\(\[coreEnvSchema,\s*dbPrismaEnvSchema,\s*loggerEnvSchema,\s*i18nEnvSchema\]\)/,
680
+ );
681
+ assert.match(appModule, /ForgeonLoggerModule/);
682
+ assert.match(appModule, /ForgeonI18nModule/);
683
+
684
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
685
+ assert.match(apiPackage, /@forgeon\/logger/);
686
+ assert.match(apiPackage, /@forgeon\/i18n/);
687
+ assert.match(apiPackage, /pnpm --filter @forgeon\/logger build/);
688
+ assert.match(apiPackage, /pnpm --filter @forgeon\/i18n build/);
689
+
690
+ const mainTs = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'main.ts'), 'utf8');
691
+ assert.match(mainTs, /ForgeonLoggerService/);
692
+ assert.match(mainTs, /ForgeonHttpLoggingInterceptor/);
693
+ } finally {
694
+ fs.rmSync(targetRoot, { recursive: true, force: true });
695
+ }
696
+ });
697
+
698
+ it('applies i18n after swagger without losing swagger config keys', () => {
699
+ const targetRoot = mkTmp('forgeon-module-swagger-i18n-order-');
700
+ const projectRoot = path.join(targetRoot, 'demo-swagger-i18n-order');
701
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
702
+
703
+ try {
704
+ scaffoldProject({
705
+ templateRoot,
706
+ packageRoot,
707
+ targetRoot: projectRoot,
708
+ projectName: 'demo-swagger-i18n-order',
709
+ frontend: 'react',
710
+ db: 'prisma',
711
+ i18nEnabled: false,
712
+ proxy: 'caddy',
713
+ });
714
+
715
+ addModule({
716
+ moduleId: 'swagger',
717
+ targetRoot: projectRoot,
718
+ packageRoot,
719
+ });
720
+
721
+ const i18nResult = addModule({
722
+ moduleId: 'i18n',
723
+ targetRoot: projectRoot,
724
+ packageRoot,
725
+ });
726
+ assert.equal(i18nResult.applied, true);
727
+
728
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
729
+ assert.match(
730
+ appModule,
731
+ /load: \[coreConfig,\s*dbPrismaConfig,\s*swaggerConfig,\s*i18nConfig\]/,
732
+ );
733
+ assert.match(
734
+ appModule,
735
+ /validate: createEnvValidator\(\[coreEnvSchema,\s*dbPrismaEnvSchema,\s*swaggerEnvSchema,\s*i18nEnvSchema\]\)/,
736
+ );
737
+ assert.match(appModule, /ForgeonSwaggerModule/);
738
+ assert.match(appModule, /ForgeonI18nModule/);
739
+
740
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
741
+ assert.match(apiPackage, /@forgeon\/swagger/);
742
+ assert.match(apiPackage, /@forgeon\/i18n/);
743
+ assert.match(apiPackage, /pnpm --filter @forgeon\/swagger build/);
744
+ assert.match(apiPackage, /pnpm --filter @forgeon\/i18n build/);
745
+
746
+ const mainTs = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'main.ts'), 'utf8');
747
+ assert.match(mainTs, /setupSwagger/);
748
+ assert.match(mainTs, /SwaggerConfigService/);
749
+ } finally {
750
+ fs.rmSync(targetRoot, { recursive: true, force: true });
751
+ }
752
+ });
753
+
754
+ it('applies swagger -> logger -> i18n and keeps all module wiring', () => {
755
+ const targetRoot = mkTmp('forgeon-module-mixed-order-');
756
+ const projectRoot = path.join(targetRoot, 'demo-mixed-order');
757
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
758
+
759
+ try {
760
+ scaffoldProject({
761
+ templateRoot,
762
+ packageRoot,
763
+ targetRoot: projectRoot,
764
+ projectName: 'demo-mixed-order',
765
+ frontend: 'react',
766
+ db: 'prisma',
767
+ i18nEnabled: false,
768
+ proxy: 'caddy',
769
+ });
770
+
771
+ addModule({ moduleId: 'swagger', targetRoot: projectRoot, packageRoot });
772
+ addModule({ moduleId: 'logger', targetRoot: projectRoot, packageRoot });
773
+ addModule({ moduleId: 'i18n', targetRoot: projectRoot, packageRoot });
774
+
775
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
776
+ assert.match(
777
+ appModule,
778
+ /load: \[coreConfig,\s*dbPrismaConfig,\s*swaggerConfig,\s*loggerConfig,\s*i18nConfig\]/,
779
+ );
780
+ assert.match(
781
+ appModule,
782
+ /validate: createEnvValidator\(\[coreEnvSchema,\s*dbPrismaEnvSchema,\s*swaggerEnvSchema,\s*loggerEnvSchema,\s*i18nEnvSchema\]\)/,
783
+ );
784
+ assert.match(appModule, /ForgeonSwaggerModule/);
785
+ assert.match(appModule, /ForgeonLoggerModule/);
786
+ assert.match(appModule, /ForgeonI18nModule/);
787
+
788
+ const mainTs = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'main.ts'), 'utf8');
789
+ assert.match(mainTs, /setupSwagger\(app,\s*swaggerConfigService\)/);
790
+ assert.match(mainTs, /app\.useLogger\(app\.get\(ForgeonLoggerService\)\);/);
791
+ assert.match(mainTs, /app\.useGlobalInterceptors\(app\.get\(ForgeonHttpLoggingInterceptor\)\);/);
792
+
793
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
794
+ assert.match(apiPackage, /@forgeon\/swagger/);
795
+ assert.match(apiPackage, /@forgeon\/logger/);
796
+ assert.match(apiPackage, /@forgeon\/i18n/);
797
+ assert.match(apiPackage, /pnpm --filter @forgeon\/swagger build/);
798
+ assert.match(apiPackage, /pnpm --filter @forgeon\/logger build/);
799
+ assert.match(apiPackage, /pnpm --filter @forgeon\/i18n build/);
800
+
801
+ assertDbPrismaWiring(projectRoot);
802
+ } finally {
803
+ fs.rmSync(targetRoot, { recursive: true, force: true });
804
+ }
805
+ });
806
+
807
+ it('keeps db-prisma wiring across module installation orders', () => {
808
+ const sequences = [
809
+ ['logger', 'swagger', 'i18n'],
810
+ ['swagger', 'i18n', 'logger'],
811
+ ['i18n', 'logger', 'swagger'],
812
+ ];
813
+
814
+ for (const sequence of sequences) {
815
+ const targetRoot = mkTmp(`forgeon-module-db-order-${sequence.join('-')}-`);
816
+ const projectRoot = path.join(targetRoot, `demo-db-${sequence.join('-')}`);
817
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
818
+
819
+ try {
820
+ scaffoldProject({
821
+ templateRoot,
822
+ packageRoot,
823
+ targetRoot: projectRoot,
824
+ projectName: `demo-db-${sequence.join('-')}`,
825
+ frontend: 'react',
826
+ db: 'prisma',
827
+ i18nEnabled: false,
828
+ proxy: 'caddy',
829
+ });
830
+
831
+ for (const moduleId of sequence) {
832
+ addModule({ moduleId, targetRoot: projectRoot, packageRoot });
833
+ }
834
+
835
+ assertDbPrismaWiring(projectRoot);
836
+ } finally {
837
+ fs.rmSync(targetRoot, { recursive: true, force: true });
838
+ }
839
+ }
840
+ });
841
+
842
+ it('applies db-prisma as final module after other modules', () => {
843
+ const targetRoot = mkTmp('forgeon-module-db-last-');
844
+ const projectRoot = path.join(targetRoot, 'demo-db-last');
845
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
846
+
847
+ try {
848
+ scaffoldProject({
849
+ templateRoot,
850
+ packageRoot,
851
+ targetRoot: projectRoot,
852
+ projectName: 'demo-db-last',
853
+ frontend: 'react',
854
+ db: 'prisma',
855
+ i18nEnabled: false,
856
+ proxy: 'caddy',
857
+ });
858
+
859
+ stripDbPrismaArtifacts(projectRoot);
860
+
861
+ addModule({ moduleId: 'logger', targetRoot: projectRoot, packageRoot });
862
+ addModule({ moduleId: 'swagger', targetRoot: projectRoot, packageRoot });
863
+ addModule({ moduleId: 'i18n', targetRoot: projectRoot, packageRoot });
864
+ const dbResult = addModule({ moduleId: 'db-prisma', targetRoot: projectRoot, packageRoot });
865
+ assert.equal(dbResult.applied, true);
866
+
867
+ assertDbPrismaWiring(projectRoot);
868
+
869
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
870
+ assert.match(appModule, /ForgeonLoggerModule/);
871
+ assert.match(appModule, /ForgeonSwaggerModule/);
872
+ assert.match(appModule, /ForgeonI18nModule/);
873
+ } finally {
874
+ fs.rmSync(targetRoot, { recursive: true, force: true });
875
+ }
876
+ });
469
877
  });