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.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/modules/db-prisma.mjs +401 -0
- package/src/modules/executor.mjs +4 -0
- package/src/modules/executor.test.mjs +585 -13
- package/src/modules/i18n.mjs +244 -22
- package/src/modules/jwt-auth.mjs +612 -0
- package/src/modules/logger.mjs +76 -27
- package/src/modules/registry.mjs +15 -7
- package/src/modules/swagger.mjs +12 -2
- package/templates/module-fragments/db-prisma/00_title.md +6 -0
- package/templates/module-fragments/db-prisma/10_overview.md +10 -0
- package/templates/module-fragments/db-prisma/20_scope.md +14 -0
- package/templates/module-fragments/db-prisma/90_status_implemented.md +4 -0
- package/templates/module-fragments/jwt-auth/20_scope.md +17 -7
- package/templates/module-fragments/jwt-auth/90_status_implemented.md +7 -0
- package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +3 -0
- package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +36 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/package.json +28 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.loader.ts +27 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.module.ts +8 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.service.ts +36 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-env.schema.ts +19 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +23 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +71 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +155 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +6 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +2 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/login.dto.ts +11 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/refresh.dto.ts +8 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +47 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +12 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt-auth.guard.ts +5 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt.strategy.ts +20 -0
- package/templates/module-presets/jwt-auth/packages/auth-api/tsconfig.json +9 -0
- package/templates/module-presets/jwt-auth/packages/auth-contracts/package.json +21 -0
- package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +47 -0
- 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: '
|
|
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, /
|
|
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
|
});
|