create-forgeon 0.3.2 → 0.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-forgeon",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Forgeon project generator CLI",
5
5
  "license": "MIT",
6
6
  "author": "Forgeon",
@@ -70,10 +70,13 @@ function assertRateLimitWiring(projectRoot) {
70
70
  path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
71
71
  'utf8',
72
72
  );
73
+ assert.match(healthController, /import \{ Header \} from '@nestjs\/common';/);
74
+ assert.match(healthController, /@Header\('Cache-Control', 'no-store, no-cache, must-revalidate'\)/);
73
75
  assert.match(healthController, /@Get\('rate-limit'\)/);
74
76
  assert.match(healthController, /TOO_MANY_REQUESTS/);
75
77
 
76
78
  const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
79
+ assert.match(appTsx, /cache: 'no-store'/);
77
80
  assert.match(appTsx, /Check rate limit \(click repeatedly\)/);
78
81
  assert.match(appTsx, /Rate limit probe response/);
79
82
 
@@ -1261,6 +1264,48 @@ describe('addModule', () => {
1261
1264
  }
1262
1265
  });
1263
1266
 
1267
+ it('scans auth persistence as db-adapter participant while remaining triggerable from db-prisma install order', () => {
1268
+ const targetRoot = mkTmp('forgeon-module-jwt-db-scan-');
1269
+ const projectRoot = path.join(targetRoot, 'demo-jwt-db-scan');
1270
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
1271
+
1272
+ try {
1273
+ scaffoldProject({
1274
+ templateRoot,
1275
+ packageRoot,
1276
+ targetRoot: projectRoot,
1277
+ projectName: 'demo-jwt-db-scan',
1278
+ frontend: 'react',
1279
+ db: 'prisma',
1280
+ dbPrismaEnabled: false,
1281
+ i18nEnabled: false,
1282
+ proxy: 'caddy',
1283
+ });
1284
+
1285
+ addModule({
1286
+ moduleId: 'jwt-auth',
1287
+ targetRoot: projectRoot,
1288
+ packageRoot,
1289
+ });
1290
+ addModule({
1291
+ moduleId: 'db-prisma',
1292
+ targetRoot: projectRoot,
1293
+ packageRoot,
1294
+ });
1295
+
1296
+ const scan = scanIntegrations({
1297
+ targetRoot: projectRoot,
1298
+ relatedModuleId: 'db-prisma',
1299
+ });
1300
+ const persistenceGroup = scan.groups.find((group) => group.id === 'auth-persistence');
1301
+
1302
+ assert.ok(persistenceGroup);
1303
+ assert.deepEqual(persistenceGroup.modules, ['jwt-auth', 'db-adapter']);
1304
+ } finally {
1305
+ fs.rmSync(targetRoot, { recursive: true, force: true });
1306
+ }
1307
+ });
1308
+
1264
1309
  it('applies logger then jwt-auth on db/i18n-disabled scaffold without breaking health controller syntax', () => {
1265
1310
  const targetRoot = mkTmp('forgeon-module-jwt-nodb-noi18n-');
1266
1311
  const projectRoot = path.join(targetRoot, 'demo-jwt-nodb-noi18n');
@@ -120,8 +120,10 @@ function patchHealthController(targetRoot) {
120
120
  }
121
121
 
122
122
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
123
+ content = ensureImportLine(content, "import { Header } from '@nestjs/common';");
123
124
  if (!content.includes("@Get('rate-limit')")) {
124
125
  const method = `
126
+ @Header('Cache-Control', 'no-store, no-cache, must-revalidate')
125
127
  @Get('rate-limit')
126
128
  getRateLimitProbe() {
127
129
  return {
@@ -24,6 +24,25 @@ const PRISMA_AUTH_MIGRATION_TEMPLATE = path.join(
24
24
  '0002_auth_refresh_token_hash',
25
25
  );
26
26
 
27
+ const AUTH_PERSISTENCE_STRATEGIES = [
28
+ {
29
+ id: 'db-prisma',
30
+ capability: 'db-adapter',
31
+ providerLabel: 'db-prisma',
32
+ participants: ['jwt-auth', 'db-adapter'],
33
+ relatedModules: ['jwt-auth', 'db-prisma'],
34
+ description: [
35
+ 'Patch AppModule to wire AUTH_REFRESH_TOKEN_STORE to the current db-adapter implementation (today: PrismaAuthRefreshTokenStore)',
36
+ 'Add apps/api/src/auth/prisma-auth-refresh-token.store.ts',
37
+ 'Extend Prisma User model with refreshTokenHash and add migration 0002_auth_refresh_token_hash',
38
+ 'Update JWT auth README note to reflect db-adapter-backed refresh-token persistence',
39
+ ],
40
+ isDetected: (detected) => detected.dbPrisma,
41
+ isPending: isAuthPersistencePending,
42
+ apply: syncJwtDbPrisma,
43
+ },
44
+ ];
45
+
27
46
  function ensureLineAfter(content, anchorLine, lineToInsert) {
28
47
  if (content.includes(lineToInsert)) {
29
48
  return content;
@@ -95,21 +114,18 @@ const INTEGRATION_GROUPS = [
95
114
  {
96
115
  id: 'auth-persistence',
97
116
  title: 'Auth Persistence Integration',
98
- modules: ['jwt-auth', 'db-prisma'],
99
- description: [
100
- 'Patch AppModule to wire AUTH_REFRESH_TOKEN_STORE to the current db-adapter implementation (today: PrismaAuthRefreshTokenStore)',
101
- 'Add apps/api/src/auth/prisma-auth-refresh-token.store.ts',
102
- 'Extend Prisma User model with refreshTokenHash and add migration 0002_auth_refresh_token_hash',
103
- 'Update JWT auth README note to reflect db-adapter-backed refresh-token persistence',
104
- ],
105
- isAvailable: (detected) => detected.jwtAuth && detected.dbPrisma,
106
- isPending: (rootDir) => isAuthPersistencePending(rootDir),
107
- apply: syncJwtDbPrisma,
117
+ participants: ['jwt-auth', 'db-adapter'],
118
+ relatedModules: ['jwt-auth', 'db-prisma'],
119
+ description: (detected) => getAuthPersistenceDescription(detected),
120
+ isAvailable: (detected) => detected.jwtAuth && hasSingleAuthPersistenceStrategy(detected),
121
+ isPending: (rootDir, detected) => isAuthPersistencePendingForDetected(rootDir, detected),
122
+ apply: applyAuthPersistenceSync,
108
123
  },
109
124
  {
110
125
  id: 'auth-rbac-claims',
111
126
  title: 'Auth Claims Integration',
112
- modules: ['jwt-auth', 'rbac'],
127
+ participants: ['jwt-auth', 'rbac'],
128
+ relatedModules: ['jwt-auth', 'rbac'],
113
129
  description: [
114
130
  'Extend AuthUser with optional permissions in @forgeon/auth-contracts',
115
131
  'Add demo RBAC claims to jwt-auth login and token payloads',
@@ -139,6 +155,73 @@ function detectModules(rootDir) {
139
155
  };
140
156
  }
141
157
 
158
+ function resolveAuthPersistenceStrategy(detected) {
159
+ const matched = AUTH_PERSISTENCE_STRATEGIES.filter((strategy) => strategy.isDetected(detected));
160
+ if (matched.length === 0) {
161
+ return { kind: 'none' };
162
+ }
163
+ if (matched.length > 1) {
164
+ return { kind: 'conflict', strategies: matched };
165
+ }
166
+ return { kind: 'single', strategy: matched[0] };
167
+ }
168
+
169
+ function hasSingleAuthPersistenceStrategy(detected) {
170
+ return resolveAuthPersistenceStrategy(detected).kind === 'single';
171
+ }
172
+
173
+ function getAuthPersistenceDescription(detected) {
174
+ const resolved = resolveAuthPersistenceStrategy(detected);
175
+ if (resolved.kind === 'single') {
176
+ return [...resolved.strategy.description];
177
+ }
178
+ return [
179
+ 'Use the current db-adapter provider strategy to wire refresh-token persistence.',
180
+ 'A supported db-adapter provider must be installed before this integration can apply.',
181
+ ];
182
+ }
183
+
184
+ function isAuthPersistencePendingForDetected(rootDir, detected) {
185
+ const resolved = resolveAuthPersistenceStrategy(detected);
186
+ if (resolved.kind !== 'single') {
187
+ return false;
188
+ }
189
+ return resolved.strategy.isPending(rootDir);
190
+ }
191
+
192
+ function applyAuthPersistenceSync({ rootDir, packageRoot, changedFiles }) {
193
+ const detected = detectModules(rootDir);
194
+ const resolved = resolveAuthPersistenceStrategy(detected);
195
+ if (resolved.kind === 'none') {
196
+ return { applied: false, reason: 'no supported db-adapter provider detected' };
197
+ }
198
+ if (resolved.kind === 'conflict') {
199
+ return { applied: false, reason: 'multiple db-adapter providers detected' };
200
+ }
201
+ return resolved.strategy.apply({ rootDir, packageRoot, changedFiles });
202
+ }
203
+
204
+ function getGroupParticipants(group) {
205
+ return Array.isArray(group.participants) && group.participants.length > 0
206
+ ? group.participants
207
+ : Array.isArray(group.modules)
208
+ ? group.modules
209
+ : [];
210
+ }
211
+
212
+ function getGroupRelatedModules(group) {
213
+ return Array.isArray(group.relatedModules) && group.relatedModules.length > 0
214
+ ? group.relatedModules
215
+ : getGroupParticipants(group);
216
+ }
217
+
218
+ function getGroupDescription(group, detected) {
219
+ if (typeof group.description === 'function') {
220
+ return group.description(detected);
221
+ }
222
+ return Array.isArray(group.description) ? group.description : [];
223
+ }
224
+
142
225
  function syncJwtDbPrisma({ rootDir, packageRoot, changedFiles }) {
143
226
  const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
144
227
  const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
@@ -365,7 +448,7 @@ export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
365
448
  const detected = detectModules(rootDir);
366
449
  const summary = [];
367
450
  const available = INTEGRATION_GROUPS.filter(
368
- (group) => group.isAvailable(detected) && group.isPending(rootDir),
451
+ (group) => group.isAvailable(detected) && group.isPending(rootDir, detected),
369
452
  );
370
453
  const selected = Array.isArray(groupIds)
371
454
  ? available.filter((group) => groupIds.includes(group.id))
@@ -375,7 +458,7 @@ export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
375
458
  summary.push({
376
459
  id: group.id,
377
460
  title: group.title,
378
- modules: group.modules,
461
+ modules: getGroupParticipants(group),
379
462
  result: group.apply({ rootDir, packageRoot, changedFiles }),
380
463
  });
381
464
  }
@@ -385,8 +468,8 @@ export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
385
468
  availableGroups: available.map((group) => ({
386
469
  id: group.id,
387
470
  title: group.title,
388
- modules: [...group.modules],
389
- description: [...group.description],
471
+ modules: [...getGroupParticipants(group)],
472
+ description: [...getGroupDescription(group, detected)],
390
473
  })),
391
474
  changedFiles: [...changedFiles].sort().map((filePath) => path.relative(rootDir, filePath)),
392
475
  };
@@ -398,15 +481,15 @@ export function scanIntegrations({ targetRoot, relatedModuleId = null }) {
398
481
  const available = INTEGRATION_GROUPS.filter(
399
482
  (group) =>
400
483
  group.isAvailable(detected) &&
401
- group.isPending(rootDir) &&
402
- (!relatedModuleId || group.modules.includes(relatedModuleId)),
484
+ group.isPending(rootDir, detected) &&
485
+ (!relatedModuleId || getGroupRelatedModules(group).includes(relatedModuleId)),
403
486
  );
404
487
  return {
405
488
  groups: available.map((group) => ({
406
489
  id: group.id,
407
490
  title: group.title,
408
- modules: [...group.modules],
409
- description: [...group.description],
491
+ modules: [...getGroupParticipants(group)],
492
+ description: [...getGroupDescription(group, detected)],
410
493
  })),
411
494
  };
412
495
  }
@@ -13,7 +13,10 @@ export default function App() {
13
13
  const [networkError, setNetworkError] = useState<string | null>(null);
14
14
 
15
15
  const requestProbe = async (url: string, init?: RequestInit): Promise<ProbeResult> => {
16
- const response = await fetch(url, init);
16
+ const response = await fetch(url, {
17
+ ...(init ?? {}),
18
+ cache: 'no-store',
19
+ });
17
20
  let body: unknown = null;
18
21
 
19
22
  try {
@@ -45,6 +45,15 @@ ALTER TABLE "User"
45
45
  ADD COLUMN "refreshTokenHash" TEXT;
46
46
  `;
47
47
 
48
+ const AUTH_PERSISTENCE_STRATEGIES = [
49
+ {
50
+ id: 'db-prisma',
51
+ providerLabel: 'db-prisma',
52
+ isDetected: (detected) => detected.dbPrisma,
53
+ apply: syncJwtDbPrisma,
54
+ },
55
+ ];
56
+
48
57
  function detectModules(rootDir) {
49
58
  const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
50
59
  const appModuleText = fs.existsSync(appModulePath) ? fs.readFileSync(appModulePath, 'utf8') : '';
@@ -74,6 +83,17 @@ function ensureLineAfter(content, anchorLine, lineToInsert) {
74
83
  return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
75
84
  }
76
85
 
86
+ function resolveAuthPersistenceStrategy(detected) {
87
+ const matched = AUTH_PERSISTENCE_STRATEGIES.filter((strategy) => strategy.isDetected(detected));
88
+ if (matched.length === 0) {
89
+ return { kind: 'none' };
90
+ }
91
+ if (matched.length > 1) {
92
+ return { kind: 'conflict', strategies: matched };
93
+ }
94
+ return { kind: 'single', strategy: matched[0] };
95
+ }
96
+
77
97
  function syncJwtDbPrisma({ rootDir, changedFiles }) {
78
98
  const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
79
99
  const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
@@ -290,16 +310,21 @@ function run() {
290
310
  const changedFiles = new Set();
291
311
  const detected = detectModules(rootDir);
292
312
  const summary = [];
313
+ const authPersistence = resolveAuthPersistenceStrategy(detected);
293
314
 
294
- if (detected.jwtAuth && detected.dbPrisma) {
315
+ if (detected.jwtAuth && authPersistence.kind === 'single') {
295
316
  summary.push({
296
- feature: 'jwt-auth + db-adapter (current provider: db-prisma)',
297
- result: syncJwtDbPrisma({ rootDir, changedFiles }),
317
+ feature: `jwt-auth + db-adapter (current provider: ${authPersistence.strategy.providerLabel})`,
318
+ result: authPersistence.strategy.apply({ rootDir, changedFiles }),
298
319
  });
299
320
  } else {
321
+ const reason =
322
+ authPersistence.kind === 'conflict'
323
+ ? 'multiple db-adapter providers detected'
324
+ : 'required components are not both available';
300
325
  summary.push({
301
326
  feature: 'jwt-auth + db-adapter (current provider: db-prisma)',
302
- result: { applied: false, reason: 'required modules are not both installed' },
327
+ result: { applied: false, reason },
303
328
  });
304
329
  }
305
330
 
@@ -311,7 +336,7 @@ function run() {
311
336
  } else {
312
337
  summary.push({
313
338
  feature: 'jwt-auth + rbac',
314
- result: { applied: false, reason: 'required modules are not both installed' },
339
+ result: { applied: false, reason: 'required components are not both available' },
315
340
  });
316
341
  }
317
342
 
@@ -27,7 +27,8 @@ export default function App() {
27
27
 
28
28
  const requestProbe = async (path: string, init?: RequestInit): Promise<ProbeResult> => {
29
29
  const response = await fetch(`/api${path}${toLangQuery(locale)}`, {
30
- ...init,
30
+ ...(init ?? {}),
31
+ cache: 'no-store',
31
32
  headers: {
32
33
  ...(init?.headers ?? {}),
33
34
  'Accept-Language': locale,