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
|
@@ -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
|
-
:
|
|
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
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
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`;
|
package/src/modules/i18n.mjs
CHANGED
|
@@ -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
|
-
|
|
197
|
+
apiBuildAnchor,
|
|
194
198
|
'RUN pnpm --filter @forgeon/core build',
|
|
195
199
|
);
|
|
196
200
|
content = ensureLineBefore(
|
|
197
201
|
content,
|
|
198
|
-
|
|
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
|
-
|
|
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
|
|
package/src/modules/jwt-auth.mjs
CHANGED
|
@@ -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 =
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
379
|
-
"{
|
|
380
|
-
|
|
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 =
|
|
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
|
|
509
|
-
if (
|
|
510
|
-
|
|
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
|
-
|
|
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
|
|