create-forgeon 0.3.3 → 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.3",
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
 
@@ -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;
@@ -97,15 +116,10 @@ const INTEGRATION_GROUPS = [
97
116
  title: 'Auth Persistence Integration',
98
117
  participants: ['jwt-auth', 'db-adapter'],
99
118
  relatedModules: ['jwt-auth', 'db-prisma'],
100
- description: [
101
- 'Patch AppModule to wire AUTH_REFRESH_TOKEN_STORE to the current db-adapter implementation (today: PrismaAuthRefreshTokenStore)',
102
- 'Add apps/api/src/auth/prisma-auth-refresh-token.store.ts',
103
- 'Extend Prisma User model with refreshTokenHash and add migration 0002_auth_refresh_token_hash',
104
- 'Update JWT auth README note to reflect db-adapter-backed refresh-token persistence',
105
- ],
106
- isAvailable: (detected) => detected.jwtAuth && detected.dbPrisma,
107
- isPending: (rootDir) => isAuthPersistencePending(rootDir),
108
- apply: syncJwtDbPrisma,
119
+ description: (detected) => getAuthPersistenceDescription(detected),
120
+ isAvailable: (detected) => detected.jwtAuth && hasSingleAuthPersistenceStrategy(detected),
121
+ isPending: (rootDir, detected) => isAuthPersistencePendingForDetected(rootDir, detected),
122
+ apply: applyAuthPersistenceSync,
109
123
  },
110
124
  {
111
125
  id: 'auth-rbac-claims',
@@ -141,6 +155,52 @@ function detectModules(rootDir) {
141
155
  };
142
156
  }
143
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
+
144
204
  function getGroupParticipants(group) {
145
205
  return Array.isArray(group.participants) && group.participants.length > 0
146
206
  ? group.participants
@@ -155,6 +215,13 @@ function getGroupRelatedModules(group) {
155
215
  : getGroupParticipants(group);
156
216
  }
157
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
+
158
225
  function syncJwtDbPrisma({ rootDir, packageRoot, changedFiles }) {
159
226
  const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
160
227
  const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
@@ -381,7 +448,7 @@ export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
381
448
  const detected = detectModules(rootDir);
382
449
  const summary = [];
383
450
  const available = INTEGRATION_GROUPS.filter(
384
- (group) => group.isAvailable(detected) && group.isPending(rootDir),
451
+ (group) => group.isAvailable(detected) && group.isPending(rootDir, detected),
385
452
  );
386
453
  const selected = Array.isArray(groupIds)
387
454
  ? available.filter((group) => groupIds.includes(group.id))
@@ -391,7 +458,7 @@ export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
391
458
  summary.push({
392
459
  id: group.id,
393
460
  title: group.title,
394
- modules: group.modules,
461
+ modules: getGroupParticipants(group),
395
462
  result: group.apply({ rootDir, packageRoot, changedFiles }),
396
463
  });
397
464
  }
@@ -402,7 +469,7 @@ export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
402
469
  id: group.id,
403
470
  title: group.title,
404
471
  modules: [...getGroupParticipants(group)],
405
- description: [...group.description],
472
+ description: [...getGroupDescription(group, detected)],
406
473
  })),
407
474
  changedFiles: [...changedFiles].sort().map((filePath) => path.relative(rootDir, filePath)),
408
475
  };
@@ -414,7 +481,7 @@ export function scanIntegrations({ targetRoot, relatedModuleId = null }) {
414
481
  const available = INTEGRATION_GROUPS.filter(
415
482
  (group) =>
416
483
  group.isAvailable(detected) &&
417
- group.isPending(rootDir) &&
484
+ group.isPending(rootDir, detected) &&
418
485
  (!relatedModuleId || getGroupRelatedModules(group).includes(relatedModuleId)),
419
486
  );
420
487
  return {
@@ -422,7 +489,7 @@ export function scanIntegrations({ targetRoot, relatedModuleId = null }) {
422
489
  id: group.id,
423
490
  title: group.title,
424
491
  modules: [...getGroupParticipants(group)],
425
- description: [...group.description],
492
+ description: [...getGroupDescription(group, detected)],
426
493
  })),
427
494
  };
428
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 components are not both available' },
327
+ result: { applied: false, reason },
303
328
  });
304
329
  }
305
330
 
@@ -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,