create-forgeon 0.2.0 → 0.2.2

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.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Forgeon project generator CLI",
5
5
  "license": "MIT",
6
6
  "author": "Forgeon",
@@ -84,6 +84,26 @@ function ensureLineBefore(content, anchorLine, lineToInsert) {
84
84
  return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
85
85
  }
86
86
 
87
+ function ensureNestCommonImport(content, importName) {
88
+ const pattern = /import\s*\{([^}]*)\}\s*from '@nestjs\/common';/m;
89
+ const match = content.match(pattern);
90
+ if (!match) {
91
+ return `import { ${importName} } from '@nestjs/common';\n${content}`;
92
+ }
93
+
94
+ const names = match[1]
95
+ .split(',')
96
+ .map((item) => item.trim())
97
+ .filter(Boolean);
98
+
99
+ if (!names.includes(importName)) {
100
+ names.push(importName);
101
+ }
102
+
103
+ const replacement = `import { ${names.join(', ')} } from '@nestjs/common';`;
104
+ return content.replace(pattern, replacement);
105
+ }
106
+
87
107
  function upsertEnvLines(filePath, lines) {
88
108
  let content = '';
89
109
  if (fs.existsSync(filePath)) {
@@ -244,10 +264,13 @@ function patchHealthController(targetRoot) {
244
264
  }
245
265
 
246
266
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
267
+ content = ensureNestCommonImport(content, 'Post');
268
+
247
269
  if (!content.includes("from '@forgeon/db-prisma';")) {
270
+ const nestCommonImport = content.match(/import\s*\{[^}]*\}\s*from '@nestjs\/common';/m)?.[0];
248
271
  const anchor = content.includes("import { I18nService } from 'nestjs-i18n';")
249
272
  ? "import { I18nService } from 'nestjs-i18n';"
250
- : "import { BadRequestException, ConflictException, Controller, Get, Post, Query } from '@nestjs/common';";
273
+ : nestCommonImport;
251
274
  content = ensureLineAfter(content, anchor, "import { PrismaService } from '@forgeon/db-prisma';");
252
275
  }
253
276
 
@@ -301,6 +324,11 @@ function patchWebApp(targetRoot) {
301
324
  }
302
325
 
303
326
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
327
+ content = content
328
+ .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
329
+ .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
330
+ .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
331
+ .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
304
332
 
305
333
  if (!content.includes('dbProbeResult')) {
306
334
  const stateAnchor = ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);';
@@ -314,23 +342,29 @@ function patchWebApp(targetRoot) {
314
342
  }
315
343
 
316
344
  if (!content.includes('Check database (create user)')) {
317
- const buttonAnchor = " <button onClick={() => runProbe(setValidationProbeResult, '/api/health/validation')>";
318
- const buttonAnchorI18n = " <button onClick={() => runProbe(setValidationProbeResult, '/health/validation')>";
319
- const dbButton = content.includes(buttonAnchorI18n)
345
+ const dbButton = content.includes("runProbe(setHealthResult, '/health')")
320
346
  ? " <button onClick={() => runProbe(setDbProbeResult, '/health/db', { method: 'POST' })}>\n Check database (create user)\n </button>"
321
347
  : " <button onClick={() => runProbe(setDbProbeResult, '/api/health/db', { method: 'POST' })}>\n Check database (create user)\n </button>";
322
348
 
323
- if (content.includes(buttonAnchor)) {
324
- content = ensureLineAfter(content, buttonAnchor, dbButton);
325
- } else if (content.includes(buttonAnchorI18n)) {
326
- content = ensureLineAfter(content, buttonAnchorI18n, dbButton);
349
+ const actionsStart = content.indexOf('<div className="actions">');
350
+ if (actionsStart >= 0) {
351
+ const actionsEnd = content.indexOf('\n </div>', actionsStart);
352
+ if (actionsEnd >= 0) {
353
+ content = `${content.slice(0, actionsEnd)}\n${dbButton}${content.slice(actionsEnd)}`;
354
+ }
327
355
  }
328
356
  }
329
357
 
330
358
  if (!content.includes("{renderResult('DB probe response', dbProbeResult)}")) {
331
- const resultAnchor = "{renderResult('Validation probe response', validationProbeResult)}";
332
- if (content.includes(resultAnchor)) {
333
- content = ensureLineAfter(content, resultAnchor, " {renderResult('DB probe response', dbProbeResult)}");
359
+ const dbResultLine = " {renderResult('DB probe response', dbProbeResult)}";
360
+ const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
361
+ if (content.includes(networkLine)) {
362
+ content = content.replace(networkLine, `${dbResultLine}\n${networkLine}`);
363
+ } else {
364
+ const resultAnchor = "{renderResult('Validation probe response', validationProbeResult)}";
365
+ if (content.includes(resultAnchor)) {
366
+ content = ensureLineAfter(content, resultAnchor, dbResultLine);
367
+ }
334
368
  }
335
369
  }
336
370
 
@@ -350,6 +384,7 @@ function patchApiDockerfile(targetRoot) {
350
384
 
351
385
  content = content
352
386
  .replace(/^RUN pnpm --filter @forgeon\/db-prisma build\r?\n?/gm, '')
387
+ .replace(/^RUN pnpm --filter @forgeon\/core build\r?\n?/gm, '')
353
388
  .replace(/^RUN pnpm --filter @forgeon\/api prisma:generate\r?\n?/gm, '')
354
389
  .replace(/^CMD \["node", "apps\/api\/dist\/main\.js"\]\r?\n?/gm, '')
355
390
  .replace(
@@ -357,6 +392,7 @@ function patchApiDockerfile(targetRoot) {
357
392
  '',
358
393
  );
359
394
 
395
+ content = ensureLineBefore(content, 'RUN pnpm --filter @forgeon/api build', 'RUN pnpm --filter @forgeon/core build');
360
396
  content = ensureLineBefore(content, 'RUN pnpm --filter @forgeon/api build', 'RUN pnpm --filter @forgeon/db-prisma build');
361
397
  content = ensureLineBefore(content, 'RUN pnpm --filter @forgeon/api build', 'RUN pnpm --filter @forgeon/api prisma:generate');
362
398
  content = `${content.trimEnd()}\nCMD ["sh", "-c", "pnpm --filter @forgeon/api prisma:migrate:deploy && node apps/api/dist/main.js"]\n`;
@@ -188,23 +188,23 @@ function patchApiDockerfile(targetRoot) {
188
188
  .replace(/^RUN pnpm --filter @forgeon\/i18n-contracts build\r?\n?/gm, '')
189
189
  .replace(/^RUN pnpm --filter @forgeon\/i18n build\r?\n?/gm, '');
190
190
 
191
+ const apiBuildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
192
+ ? 'RUN pnpm --filter @forgeon/api prisma:generate'
193
+ : 'RUN pnpm --filter @forgeon/api build';
194
+
191
195
  content = ensureLineBefore(
192
196
  content,
193
- 'RUN pnpm --filter @forgeon/api prisma:generate',
197
+ apiBuildAnchor,
194
198
  'RUN pnpm --filter @forgeon/core build',
195
199
  );
196
200
  content = ensureLineBefore(
197
201
  content,
198
- content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
199
- ? 'RUN pnpm --filter @forgeon/api prisma:generate'
200
- : 'RUN pnpm --filter @forgeon/api build',
202
+ apiBuildAnchor,
201
203
  'RUN pnpm --filter @forgeon/i18n-contracts build',
202
204
  );
203
205
  content = ensureLineBefore(
204
206
  content,
205
- content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
206
- ? 'RUN pnpm --filter @forgeon/api prisma:generate'
207
- : 'RUN pnpm --filter @forgeon/api build',
207
+ apiBuildAnchor,
208
208
  'RUN pnpm --filter @forgeon/i18n build',
209
209
  );
210
210
 
@@ -2,9 +2,6 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
4
 
5
- const JWT_README_START = '<!-- forgeon:jwt-auth:start -->';
6
- const JWT_README_END = '<!-- forgeon:jwt-auth:end -->';
7
-
8
5
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
9
6
  const source = path.join(packageRoot, 'templates', 'module-presets', 'jwt-auth', relativePath);
10
7
  if (!fs.existsSync(source)) {
@@ -356,30 +353,58 @@ function patchWebApp(targetRoot) {
356
353
  }
357
354
 
358
355
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
356
+ content = content
357
+ .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
358
+ .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
359
+ .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
360
+ .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
361
+
359
362
  if (!content.includes('authProbeResult')) {
360
- content = content.replace(
361
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
362
- ` const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);
363
+ if (content.includes(' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);')) {
364
+ content = content.replace(
365
+ ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
366
+ ` const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);
363
367
  const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);`,
364
- );
368
+ );
369
+ } else if (content.includes(' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);')) {
370
+ content = content.replace(
371
+ ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
372
+ ` const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);
373
+ const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);`,
374
+ );
375
+ }
365
376
  }
366
377
 
367
378
  if (!content.includes('Check JWT auth probe')) {
368
379
  const path = content.includes("runProbe(setHealthResult, '/health')") ? '/health/auth' : '/api/health/auth';
369
- content = content.replace(
370
- /<button onClick=\{\(\) => runProbe\(setErrorProbeResult,[\s\S]*?<\/button>/m,
371
- (match) =>
372
- `${match}
373
- <button onClick={() => runProbe(setAuthProbeResult, '${path}')}>Check JWT auth probe</button>`,
374
- );
380
+ const authButton = ` <button onClick={() => runProbe(setAuthProbeResult, '${path}')}>Check JWT auth probe</button>`;
381
+ const actionsStart = content.indexOf('<div className="actions">');
382
+ if (actionsStart >= 0) {
383
+ const actionsEnd = content.indexOf('\n </div>', actionsStart);
384
+ if (actionsEnd >= 0) {
385
+ content = `${content.slice(0, actionsEnd)}\n${authButton}${content.slice(actionsEnd)}`;
386
+ }
387
+ }
375
388
  }
376
389
 
377
390
  if (!content.includes("renderResult('Auth probe response', authProbeResult)")) {
378
- content = content.replace(
379
- "{renderResult('DB probe response', dbProbeResult)}",
380
- `{renderResult('DB probe response', dbProbeResult)}
391
+ const authResultLine = " {renderResult('Auth probe response', authProbeResult)}";
392
+ const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
393
+ if (content.includes(networkLine)) {
394
+ content = content.replace(networkLine, `${authResultLine}\n${networkLine}`);
395
+ } else if (content.includes("{renderResult('DB probe response', dbProbeResult)}")) {
396
+ content = content.replace(
397
+ "{renderResult('DB probe response', dbProbeResult)}",
398
+ `{renderResult('DB probe response', dbProbeResult)}
381
399
  {renderResult('Auth probe response', authProbeResult)}`,
382
- );
400
+ );
401
+ } else if (content.includes("{renderResult('Validation probe response', validationProbeResult)}")) {
402
+ content = content.replace(
403
+ "{renderResult('Validation probe response', validationProbeResult)}",
404
+ `{renderResult('Validation probe response', validationProbeResult)}
405
+ {renderResult('Auth probe response', authProbeResult)}`,
406
+ );
407
+ }
383
408
  }
384
409
 
385
410
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
@@ -480,8 +505,7 @@ function patchReadme(targetRoot, dbAdapter) {
480
505
  1. install a DB module first (for now: \`create-forgeon add db-prisma --project .\`);
481
506
  2. run \`create-forgeon add jwt-auth --project .\` again to auto-wire the adapter.`;
482
507
 
483
- const section = `${JWT_README_START}
484
- ## JWT Auth Module
508
+ const section = `## JWT Auth Module
485
509
 
486
510
  The jwt-auth add-module provides:
487
511
  - \`@forgeon/auth-contracts\` shared auth routes/types/error codes
@@ -501,13 +525,19 @@ Default routes:
501
525
  - \`POST /api/auth/login\`
502
526
  - \`POST /api/auth/refresh\`
503
527
  - \`POST /api/auth/logout\`
504
- - \`GET /api/auth/me\`
505
- ${JWT_README_END}`;
528
+ - \`GET /api/auth/me\``;
506
529
 
507
530
  let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
508
- const sectionPattern = new RegExp(`${JWT_README_START}[\\s\\S]*?${JWT_README_END}`, 'm');
509
- if (sectionPattern.test(content)) {
510
- content = content.replace(sectionPattern, section);
531
+ const sectionHeading = '## JWT Auth Module';
532
+ if (content.includes(sectionHeading)) {
533
+ const start = content.indexOf(sectionHeading);
534
+ const tail = content.slice(start + sectionHeading.length);
535
+ const nextHeadingMatch = tail.match(/\n##\s+/);
536
+ const end =
537
+ nextHeadingMatch && nextHeadingMatch.index !== undefined
538
+ ? start + sectionHeading.length + nextHeadingMatch.index + 1
539
+ : content.length;
540
+ content = `${content.slice(0, start)}${section}\n\n${content.slice(end).replace(/^\n+/, '')}`;
511
541
  } else if (content.includes('## Prisma In Docker Start')) {
512
542
  content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
513
543
  } else {
@@ -566,6 +596,14 @@ export function applyJwtAuthModule({ packageRoot, targetRoot }) {
566
596
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-contracts'));
567
597
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-api'));
568
598
 
599
+ const swaggerPackagePath = path.join(targetRoot, 'packages', 'swagger', 'package.json');
600
+ const authApiPackagePath = path.join(targetRoot, 'packages', 'auth-api', 'package.json');
601
+ if (fs.existsSync(swaggerPackagePath) && fs.existsSync(authApiPackagePath)) {
602
+ const authApiPackage = JSON.parse(fs.readFileSync(authApiPackagePath, 'utf8'));
603
+ ensureDependency(authApiPackage, '@nestjs/swagger', '^11.2.0');
604
+ writeJson(authApiPackagePath, authApiPackage);
605
+ }
606
+
569
607
  if (supportsPrismaStore) {
570
608
  copyFromPreset(
571
609
  packageRoot,
@@ -23,3 +23,6 @@ If a module can be validated through a safe API call, it must provide:
23
23
  - If probe writes data, it must use clearly marked probe/test records.
24
24
  - Probe should not require hidden setup beyond documented env/dependencies.
25
25
  - `create-forgeon add <module>` must wire both API probe and web probe UI when feasible.
26
+ - Web probes should be appended to the existing probe UI structure in `apps/web/src/App.tsx`:
27
+ - add new action button at the end of `<div className="actions">`
28
+ - add new result block before the `networkError` render block
@@ -54,8 +54,14 @@ function syncJwtSwagger({ rootDir, changedFiles }) {
54
54
  );
55
55
  const loginDtoPath = path.join(rootDir, 'packages', 'auth-api', 'src', 'dto', 'login.dto.ts');
56
56
  const refreshDtoPath = path.join(rootDir, 'packages', 'auth-api', 'src', 'dto', 'refresh.dto.ts');
57
-
58
- if (!fs.existsSync(controllerPath) || !fs.existsSync(loginDtoPath) || !fs.existsSync(refreshDtoPath)) {
57
+ const authApiPackagePath = path.join(rootDir, 'packages', 'auth-api', 'package.json');
58
+
59
+ if (
60
+ !fs.existsSync(controllerPath) ||
61
+ !fs.existsSync(loginDtoPath) ||
62
+ !fs.existsSync(refreshDtoPath) ||
63
+ !fs.existsSync(authApiPackagePath)
64
+ ) {
59
65
  return { applied: false, reason: 'jwt-auth source files are missing' };
60
66
  }
61
67
 
@@ -153,6 +159,16 @@ function syncJwtSwagger({ rootDir, changedFiles }) {
153
159
  changedFiles.add(loginDtoPath);
154
160
  changedFiles.add(refreshDtoPath);
155
161
 
162
+ const authApiPackage = JSON.parse(fs.readFileSync(authApiPackagePath, 'utf8'));
163
+ if (!authApiPackage.dependencies) {
164
+ authApiPackage.dependencies = {};
165
+ }
166
+ if (!authApiPackage.dependencies['@nestjs/swagger']) {
167
+ authApiPackage.dependencies['@nestjs/swagger'] = '^11.2.0';
168
+ fs.writeFileSync(authApiPackagePath, `${JSON.stringify(authApiPackage, null, 2)}\n`, 'utf8');
169
+ changedFiles.add(authApiPackagePath);
170
+ }
171
+
156
172
  return { applied: true };
157
173
  }
158
174