create-forgeon 0.2.1 → 0.2.3
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/bin/create-forgeon.mjs +9 -6
- package/package.json +1 -1
- package/src/cli/add-help.mjs +10 -6
- package/src/cli/help.mjs +13 -9
- package/src/integrations/flow.mjs +118 -0
- package/src/modules/db-prisma.mjs +49 -172
- package/src/modules/executor.test.mjs +97 -10
- package/src/modules/i18n.mjs +10 -134
- package/src/modules/jwt-auth.mjs +94 -294
- package/src/modules/logger.mjs +10 -118
- package/src/modules/shared/patch-utils.mjs +162 -0
- package/src/modules/swagger.mjs +10 -121
- package/src/modules/sync-integrations.mjs +269 -0
- package/src/run-add-module.mjs +8 -42
- package/src/run-scan-integrations.mjs +93 -0
- package/templates/base/docs/AI/ARCHITECTURE.md +17 -0
- package/templates/base/docs/AI/MODULE_CHECKS.md +3 -0
- package/templates/base/docs/AI/MODULE_SPEC.md +4 -0
- package/templates/base/package.json +0 -3
- package/templates/base/scripts/forgeon-sync-integrations.mjs +44 -225
package/bin/create-forgeon.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { runCreateForgeon } from '../src/run-create-forgeon.mjs';
|
|
3
|
-
import { runAddModule } from '../src/run-add-module.mjs';
|
|
2
|
+
import { runCreateForgeon } from '../src/run-create-forgeon.mjs';
|
|
3
|
+
import { runAddModule } from '../src/run-add-module.mjs';
|
|
4
|
+
import { runScanIntegrations } from '../src/run-scan-integrations.mjs';
|
|
4
5
|
|
|
5
6
|
const args = process.argv.slice(2);
|
|
6
7
|
const command = args[0];
|
|
7
8
|
|
|
8
|
-
const task =
|
|
9
|
-
command === 'add'
|
|
10
|
-
? runAddModule(args.slice(1))
|
|
11
|
-
:
|
|
9
|
+
const task =
|
|
10
|
+
command === 'add'
|
|
11
|
+
? runAddModule(args.slice(1))
|
|
12
|
+
: command === 'scan-integrations'
|
|
13
|
+
? runScanIntegrations(args.slice(1))
|
|
14
|
+
: runCreateForgeon(args);
|
|
12
15
|
|
|
13
16
|
task.then(() => {
|
|
14
17
|
if (typeof process.stdin.pause === 'function') {
|
package/package.json
CHANGED
package/src/cli/add-help.mjs
CHANGED
|
@@ -4,9 +4,13 @@ export function printAddHelp() {
|
|
|
4
4
|
Usage:
|
|
5
5
|
npx create-forgeon@latest add <module-id> [options]
|
|
6
6
|
|
|
7
|
-
Options:
|
|
8
|
-
--project <path> Target project path (default: current directory)
|
|
9
|
-
--list List available modules
|
|
10
|
-
-h, --help Show this help
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
Options:
|
|
8
|
+
--project <path> Target project path (default: current directory)
|
|
9
|
+
--list List available modules
|
|
10
|
+
-h, --help Show this help
|
|
11
|
+
|
|
12
|
+
Note:
|
|
13
|
+
Pair integrations are explicit.
|
|
14
|
+
Run "pnpm forgeon:sync-integrations" in the target project after add-module steps.
|
|
15
|
+
`);
|
|
16
|
+
}
|
package/src/cli/help.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export function printHelp() {
|
|
2
2
|
console.log(`create-forgeon
|
|
3
3
|
|
|
4
|
-
Usage:
|
|
5
|
-
npx create-forgeon@latest <project-name> [options]
|
|
6
|
-
npx create-forgeon@latest add <module-id> [options]
|
|
7
|
-
npx create-forgeon@latest add --list
|
|
4
|
+
Usage:
|
|
5
|
+
npx create-forgeon@latest <project-name> [options]
|
|
6
|
+
npx create-forgeon@latest add <module-id> [options]
|
|
7
|
+
npx create-forgeon@latest add --list
|
|
8
|
+
npx create-forgeon@latest scan-integrations [options]
|
|
8
9
|
|
|
9
10
|
Create options:
|
|
10
11
|
--db-prisma <true|false> Enable db-prisma module (default: true)
|
|
@@ -14,8 +15,11 @@ Create options:
|
|
|
14
15
|
-y, --yes Skip prompts and use defaults
|
|
15
16
|
-h, --help Show this help
|
|
16
17
|
|
|
17
|
-
Add options:
|
|
18
|
-
--project <path> Target project path (default: current directory)
|
|
19
|
-
--list List available modules
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
Add options:
|
|
19
|
+
--project <path> Target project path (default: current directory)
|
|
20
|
+
--list List available modules
|
|
21
|
+
|
|
22
|
+
Scan options:
|
|
23
|
+
--project <path> Target project path (default: current directory)
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { promptSelect } from '../cli/prompt-select.mjs';
|
|
2
|
+
import { scanIntegrations, syncIntegrations } from '../modules/sync-integrations.mjs';
|
|
3
|
+
|
|
4
|
+
const ansi = {
|
|
5
|
+
reset: '\x1b[0m',
|
|
6
|
+
yellow: '\x1b[33m',
|
|
7
|
+
green: '\x1b[32m',
|
|
8
|
+
cyan: '\x1b[36m',
|
|
9
|
+
dim: '\x1b[2m',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function colorize(color, text) {
|
|
13
|
+
return `${ansi[color]}${text}${ansi.reset}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function printGroup(group) {
|
|
17
|
+
console.log(`\n${colorize('yellow', `▶ ${group.title}`)}`);
|
|
18
|
+
group.modules.forEach((moduleId, index) => {
|
|
19
|
+
const branch = index === group.modules.length - 1 ? '└─' : '├─';
|
|
20
|
+
console.log(` ${branch} ${colorize('cyan', moduleId)}`);
|
|
21
|
+
});
|
|
22
|
+
console.log(' This will:');
|
|
23
|
+
for (const line of group.description) {
|
|
24
|
+
console.log(` • ${line}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function chooseGroupSelection(groups) {
|
|
29
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
30
|
+
return { kind: 'skip', ids: [] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (groups.length === 1) {
|
|
34
|
+
const picked = await promptSelect({
|
|
35
|
+
message: 'Apply now?',
|
|
36
|
+
defaultValue: 'skip',
|
|
37
|
+
choices: [
|
|
38
|
+
{ label: 'No, skip for now', value: 'skip' },
|
|
39
|
+
{ label: `Yes, apply "${groups[0].title}"`, value: groups[0].id },
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
if (picked === 'skip') {
|
|
43
|
+
return { kind: 'skip', ids: [] };
|
|
44
|
+
}
|
|
45
|
+
return { kind: 'single', ids: [picked] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const choices = groups.map((group) => ({
|
|
49
|
+
label: group.title,
|
|
50
|
+
value: group.id,
|
|
51
|
+
}));
|
|
52
|
+
choices.push({ label: 'Skip for now', value: '__skip' });
|
|
53
|
+
choices.push({ label: 'Sync all pending integrations', value: '__all' });
|
|
54
|
+
|
|
55
|
+
const picked = await promptSelect({
|
|
56
|
+
message: 'Select integration to apply now',
|
|
57
|
+
defaultValue: '__skip',
|
|
58
|
+
choices,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (picked === '__skip') {
|
|
62
|
+
return { kind: 'skip', ids: [] };
|
|
63
|
+
}
|
|
64
|
+
if (picked === '__all') {
|
|
65
|
+
return { kind: 'all', ids: groups.map((group) => group.id) };
|
|
66
|
+
}
|
|
67
|
+
return { kind: 'single', ids: [picked] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runIntegrationFlow({
|
|
71
|
+
targetRoot,
|
|
72
|
+
packageRoot,
|
|
73
|
+
relatedModuleId = null,
|
|
74
|
+
scanMessage = 'Scanning for integrations...',
|
|
75
|
+
}) {
|
|
76
|
+
console.log(scanMessage);
|
|
77
|
+
const scan = scanIntegrations({ targetRoot, relatedModuleId });
|
|
78
|
+
if (scan.groups.length === 0) {
|
|
79
|
+
console.log('No integration groups found.');
|
|
80
|
+
return { scanned: true, applied: false, groups: [] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const groupWord = scan.groups.length === 1 ? 'group' : 'groups';
|
|
84
|
+
console.log(colorize('yellow', `Found ${scan.groups.length} integration ${groupWord}:`));
|
|
85
|
+
for (const group of scan.groups) {
|
|
86
|
+
printGroup(group);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(colorize('dim', 'Command: pnpm forgeon:sync-integrations'));
|
|
90
|
+
const selection = await chooseGroupSelection(scan.groups);
|
|
91
|
+
if (selection.kind === 'skip') {
|
|
92
|
+
console.log('Integration skipped.');
|
|
93
|
+
console.log('Run later with: pnpm forgeon:sync-integrations');
|
|
94
|
+
return { scanned: true, applied: false, groups: scan.groups };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sync = syncIntegrations({ targetRoot, packageRoot, groupIds: selection.ids });
|
|
98
|
+
console.log('[forgeon:sync-integrations] done');
|
|
99
|
+
for (const item of sync.summary) {
|
|
100
|
+
if (item.result.applied) {
|
|
101
|
+
console.log(`- ${item.title}: applied`);
|
|
102
|
+
} else {
|
|
103
|
+
console.log(`- ${item.title}: skipped (${item.result.reason})`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (sync.changedFiles.length > 0) {
|
|
107
|
+
console.log('- changed files:');
|
|
108
|
+
for (const filePath of sync.changedFiles) {
|
|
109
|
+
console.log(` - ${filePath}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { scanned: true, applied: true, groups: scan.groups, sync };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function printModuleAdded(moduleId, docsPath) {
|
|
116
|
+
console.log(colorize('green', `✔ Module added: ${moduleId}`));
|
|
117
|
+
console.log(`- docs: ${docsPath}`);
|
|
118
|
+
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
|
+
import {
|
|
5
|
+
ensureBuildSteps,
|
|
6
|
+
ensureDependency,
|
|
7
|
+
ensureDevDependency,
|
|
8
|
+
ensureLineAfter,
|
|
9
|
+
ensureLineBefore,
|
|
10
|
+
ensureLoadItem,
|
|
11
|
+
ensureNestCommonImport,
|
|
12
|
+
ensureScript,
|
|
13
|
+
ensureValidatorSchema,
|
|
14
|
+
upsertEnvLines,
|
|
15
|
+
} from './shared/patch-utils.mjs';
|
|
4
16
|
|
|
5
17
|
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
6
18
|
const source = path.join(packageRoot, 'templates', 'module-presets', 'db-prisma', relativePath);
|
|
@@ -11,167 +23,6 @@ function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
|
11
23
|
copyRecursive(source, destination);
|
|
12
24
|
}
|
|
13
25
|
|
|
14
|
-
function ensureDependency(packageJson, name, version) {
|
|
15
|
-
if (!packageJson.dependencies) {
|
|
16
|
-
packageJson.dependencies = {};
|
|
17
|
-
}
|
|
18
|
-
packageJson.dependencies[name] = version;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function ensureDevDependency(packageJson, name, version) {
|
|
22
|
-
if (!packageJson.devDependencies) {
|
|
23
|
-
packageJson.devDependencies = {};
|
|
24
|
-
}
|
|
25
|
-
packageJson.devDependencies[name] = version;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function ensureScript(packageJson, name, command) {
|
|
29
|
-
if (!packageJson.scripts) {
|
|
30
|
-
packageJson.scripts = {};
|
|
31
|
-
}
|
|
32
|
-
packageJson.scripts[name] = command;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function ensureBuildSteps(packageJson, scriptName, requiredCommands) {
|
|
36
|
-
if (!packageJson.scripts) {
|
|
37
|
-
packageJson.scripts = {};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const current = packageJson.scripts[scriptName];
|
|
41
|
-
const steps =
|
|
42
|
-
typeof current === 'string' && current.trim().length > 0
|
|
43
|
-
? current
|
|
44
|
-
.split('&&')
|
|
45
|
-
.map((item) => item.trim())
|
|
46
|
-
.filter(Boolean)
|
|
47
|
-
: [];
|
|
48
|
-
|
|
49
|
-
for (const command of requiredCommands) {
|
|
50
|
-
if (!steps.includes(command)) {
|
|
51
|
-
steps.push(command);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (steps.length > 0) {
|
|
56
|
-
packageJson.scripts[scriptName] = steps.join(' && ');
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function ensureLineAfter(content, anchorLine, lineToInsert) {
|
|
61
|
-
if (content.includes(lineToInsert)) {
|
|
62
|
-
return content;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const index = content.indexOf(anchorLine);
|
|
66
|
-
if (index < 0) {
|
|
67
|
-
return `${content.trimEnd()}\n${lineToInsert}\n`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const insertAt = index + anchorLine.length;
|
|
71
|
-
return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function ensureLineBefore(content, anchorLine, lineToInsert) {
|
|
75
|
-
if (content.includes(lineToInsert)) {
|
|
76
|
-
return content;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const index = content.indexOf(anchorLine);
|
|
80
|
-
if (index < 0) {
|
|
81
|
-
return `${content.trimEnd()}\n${lineToInsert}\n`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
|
|
85
|
-
}
|
|
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
|
-
|
|
107
|
-
function upsertEnvLines(filePath, lines) {
|
|
108
|
-
let content = '';
|
|
109
|
-
if (fs.existsSync(filePath)) {
|
|
110
|
-
content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const keys = new Set(
|
|
114
|
-
content
|
|
115
|
-
.split('\n')
|
|
116
|
-
.filter(Boolean)
|
|
117
|
-
.map((line) => line.split('=')[0]),
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
const append = [];
|
|
121
|
-
for (const line of lines) {
|
|
122
|
-
const key = line.split('=')[0];
|
|
123
|
-
if (!keys.has(key)) {
|
|
124
|
-
append.push(line);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const next =
|
|
129
|
-
append.length > 0 ? `${content.trimEnd()}\n${append.join('\n')}\n` : `${content.trimEnd()}\n`;
|
|
130
|
-
fs.writeFileSync(filePath, next.replace(/^\n/, ''), 'utf8');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function ensureLoadItem(content, itemName) {
|
|
134
|
-
const pattern = /load:\s*\[([^\]]*)\]/m;
|
|
135
|
-
const match = content.match(pattern);
|
|
136
|
-
if (!match) {
|
|
137
|
-
return content;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const rawList = match[1];
|
|
141
|
-
const items = rawList
|
|
142
|
-
.split(',')
|
|
143
|
-
.map((item) => item.trim())
|
|
144
|
-
.filter(Boolean);
|
|
145
|
-
|
|
146
|
-
if (!items.includes(itemName)) {
|
|
147
|
-
items.push(itemName);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const next = `load: [${items.join(', ')}]`;
|
|
151
|
-
return content.replace(pattern, next);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function ensureValidatorSchema(content, schemaName) {
|
|
155
|
-
const pattern = /validate:\s*createEnvValidator\(\[([^\]]*)\]\)/m;
|
|
156
|
-
const match = content.match(pattern);
|
|
157
|
-
if (!match) {
|
|
158
|
-
return content;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const rawList = match[1];
|
|
162
|
-
const items = rawList
|
|
163
|
-
.split(',')
|
|
164
|
-
.map((item) => item.trim())
|
|
165
|
-
.filter(Boolean);
|
|
166
|
-
|
|
167
|
-
if (!items.includes(schemaName)) {
|
|
168
|
-
items.push(schemaName);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const next = `validate: createEnvValidator([${items.join(', ')}])`;
|
|
172
|
-
return content.replace(pattern, next);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
26
|
function patchApiPackage(targetRoot) {
|
|
176
27
|
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
177
28
|
if (!fs.existsSync(packagePath)) {
|
|
@@ -285,6 +136,16 @@ function patchHealthController(targetRoot) {
|
|
|
285
136
|
private readonly prisma: PrismaService,
|
|
286
137
|
) {`;
|
|
287
138
|
content = content.replace(original, next);
|
|
139
|
+
} else {
|
|
140
|
+
const classAnchor = 'export class HealthController {';
|
|
141
|
+
if (content.includes(classAnchor)) {
|
|
142
|
+
content = content.replace(
|
|
143
|
+
classAnchor,
|
|
144
|
+
`${classAnchor}
|
|
145
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
146
|
+
`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
288
149
|
}
|
|
289
150
|
}
|
|
290
151
|
|
|
@@ -310,7 +171,12 @@ function patchHealthController(targetRoot) {
|
|
|
310
171
|
if (translateIndex > -1) {
|
|
311
172
|
content = `${content.slice(0, translateIndex).trimEnd()}\n\n${dbMethod}\n${content.slice(translateIndex)}`;
|
|
312
173
|
} else {
|
|
313
|
-
|
|
174
|
+
const classEnd = content.lastIndexOf('\n}');
|
|
175
|
+
if (classEnd >= 0) {
|
|
176
|
+
content = `${content.slice(0, classEnd).trimEnd()}\n\n${dbMethod}\n${content.slice(classEnd)}`;
|
|
177
|
+
} else {
|
|
178
|
+
content = `${content.trimEnd()}\n${dbMethod}\n`;
|
|
179
|
+
}
|
|
314
180
|
}
|
|
315
181
|
}
|
|
316
182
|
|
|
@@ -324,6 +190,11 @@ function patchWebApp(targetRoot) {
|
|
|
324
190
|
}
|
|
325
191
|
|
|
326
192
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
193
|
+
content = content
|
|
194
|
+
.replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
|
|
195
|
+
.replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
|
|
196
|
+
.replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
|
|
197
|
+
.replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
|
|
327
198
|
|
|
328
199
|
if (!content.includes('dbProbeResult')) {
|
|
329
200
|
const stateAnchor = ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);';
|
|
@@ -337,23 +208,29 @@ function patchWebApp(targetRoot) {
|
|
|
337
208
|
}
|
|
338
209
|
|
|
339
210
|
if (!content.includes('Check database (create user)')) {
|
|
340
|
-
const
|
|
341
|
-
const buttonAnchorI18n = " <button onClick={() => runProbe(setValidationProbeResult, '/health/validation')>";
|
|
342
|
-
const dbButton = content.includes(buttonAnchorI18n)
|
|
211
|
+
const dbButton = content.includes("runProbe(setHealthResult, '/health')")
|
|
343
212
|
? " <button onClick={() => runProbe(setDbProbeResult, '/health/db', { method: 'POST' })}>\n Check database (create user)\n </button>"
|
|
344
213
|
: " <button onClick={() => runProbe(setDbProbeResult, '/api/health/db', { method: 'POST' })}>\n Check database (create user)\n </button>";
|
|
345
214
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
215
|
+
const actionsStart = content.indexOf('<div className="actions">');
|
|
216
|
+
if (actionsStart >= 0) {
|
|
217
|
+
const actionsEnd = content.indexOf('\n </div>', actionsStart);
|
|
218
|
+
if (actionsEnd >= 0) {
|
|
219
|
+
content = `${content.slice(0, actionsEnd)}\n${dbButton}${content.slice(actionsEnd)}`;
|
|
220
|
+
}
|
|
350
221
|
}
|
|
351
222
|
}
|
|
352
223
|
|
|
353
224
|
if (!content.includes("{renderResult('DB probe response', dbProbeResult)}")) {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
225
|
+
const dbResultLine = " {renderResult('DB probe response', dbProbeResult)}";
|
|
226
|
+
const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
|
|
227
|
+
if (content.includes(networkLine)) {
|
|
228
|
+
content = content.replace(networkLine, `${dbResultLine}\n${networkLine}`);
|
|
229
|
+
} else {
|
|
230
|
+
const resultAnchor = "{renderResult('Validation probe response', validationProbeResult)}";
|
|
231
|
+
if (content.includes(resultAnchor)) {
|
|
232
|
+
content = ensureLineAfter(content, resultAnchor, dbResultLine);
|
|
233
|
+
}
|
|
357
234
|
}
|
|
358
235
|
}
|
|
359
236
|
|
|
@@ -5,6 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { addModule } from './executor.mjs';
|
|
8
|
+
import { syncIntegrations } from './sync-integrations.mjs';
|
|
8
9
|
import { scaffoldProject } from '../core/scaffold.mjs';
|
|
9
10
|
|
|
10
11
|
function mkTmp(prefix) {
|
|
@@ -400,7 +401,6 @@ describe('addModule', () => {
|
|
|
400
401
|
assert.match(rootPackage, /"i18n:check"/);
|
|
401
402
|
assert.match(rootPackage, /"i18n:types"/);
|
|
402
403
|
assert.match(rootPackage, /"i18n:add"/);
|
|
403
|
-
assert.match(rootPackage, /"ts-morph":/);
|
|
404
404
|
|
|
405
405
|
const i18nAddScriptPath = path.join(projectRoot, 'scripts', 'i18n-add.mjs');
|
|
406
406
|
assert.equal(fs.existsSync(i18nAddScriptPath), true);
|
|
@@ -893,7 +893,7 @@ describe('addModule', () => {
|
|
|
893
893
|
}
|
|
894
894
|
});
|
|
895
895
|
|
|
896
|
-
it('applies jwt-auth with db-prisma
|
|
896
|
+
it('applies jwt-auth with db-prisma as stateless first, then wires persistence via explicit sync', () => {
|
|
897
897
|
const targetRoot = mkTmp('forgeon-module-jwt-db-');
|
|
898
898
|
const projectRoot = path.join(targetRoot, 'demo-jwt-db');
|
|
899
899
|
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
@@ -918,6 +918,14 @@ describe('addModule', () => {
|
|
|
918
918
|
});
|
|
919
919
|
|
|
920
920
|
assert.equal(result.applied, true);
|
|
921
|
+
assertJwtAuthWiring(projectRoot, false);
|
|
922
|
+
|
|
923
|
+
const syncResult = syncIntegrations({ targetRoot: projectRoot, packageRoot });
|
|
924
|
+
const dbPair = syncResult.summary.find((item) => item.id === 'auth-persistence');
|
|
925
|
+
assert.ok(dbPair);
|
|
926
|
+
assert.equal(dbPair.result.applied, true);
|
|
927
|
+
assert.equal(syncResult.changedFiles.length > 0, true);
|
|
928
|
+
|
|
921
929
|
assertJwtAuthWiring(projectRoot, true);
|
|
922
930
|
|
|
923
931
|
const storeFile = path.join(
|
|
@@ -955,15 +963,11 @@ describe('addModule', () => {
|
|
|
955
963
|
}
|
|
956
964
|
});
|
|
957
965
|
|
|
958
|
-
it('applies jwt-auth without db and
|
|
966
|
+
it('applies jwt-auth without db and keeps stateless fallback until pair sync is available', () => {
|
|
959
967
|
const targetRoot = mkTmp('forgeon-module-jwt-nodb-');
|
|
960
968
|
const projectRoot = path.join(targetRoot, 'demo-jwt-nodb');
|
|
961
969
|
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
962
970
|
|
|
963
|
-
const originalError = console.error;
|
|
964
|
-
const warnings = [];
|
|
965
|
-
console.error = (...args) => warnings.push(args.join(' '));
|
|
966
|
-
|
|
967
971
|
try {
|
|
968
972
|
scaffoldProject({
|
|
969
973
|
templateRoot,
|
|
@@ -996,10 +1000,93 @@ describe('addModule', () => {
|
|
|
996
1000
|
assert.match(readme, /refresh token persistence: disabled/);
|
|
997
1001
|
assert.match(readme, /create-forgeon add db-prisma/);
|
|
998
1002
|
|
|
999
|
-
assert.equal(warnings.length > 0, true);
|
|
1000
|
-
assert.match(warnings.join('\n'), /jwt-auth installed without persistent refresh token store/);
|
|
1001
1003
|
} finally {
|
|
1002
|
-
|
|
1004
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it('applies logger then jwt-auth on db/i18n-disabled scaffold without breaking health controller syntax', () => {
|
|
1009
|
+
const targetRoot = mkTmp('forgeon-module-jwt-nodb-noi18n-');
|
|
1010
|
+
const projectRoot = path.join(targetRoot, 'demo-jwt-nodb-noi18n');
|
|
1011
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1012
|
+
|
|
1013
|
+
try {
|
|
1014
|
+
scaffoldProject({
|
|
1015
|
+
templateRoot,
|
|
1016
|
+
packageRoot,
|
|
1017
|
+
targetRoot: projectRoot,
|
|
1018
|
+
projectName: 'demo-jwt-nodb-noi18n',
|
|
1019
|
+
frontend: 'react',
|
|
1020
|
+
db: 'prisma',
|
|
1021
|
+
dbPrismaEnabled: false,
|
|
1022
|
+
i18nEnabled: false,
|
|
1023
|
+
proxy: 'caddy',
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
addModule({
|
|
1027
|
+
moduleId: 'logger',
|
|
1028
|
+
targetRoot: projectRoot,
|
|
1029
|
+
packageRoot,
|
|
1030
|
+
});
|
|
1031
|
+
addModule({
|
|
1032
|
+
moduleId: 'jwt-auth',
|
|
1033
|
+
targetRoot: projectRoot,
|
|
1034
|
+
packageRoot,
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
const healthController = fs.readFileSync(
|
|
1038
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
1039
|
+
'utf8',
|
|
1040
|
+
);
|
|
1041
|
+
assert.match(healthController, /constructor\(private readonly authService: AuthService\)/);
|
|
1042
|
+
assert.match(healthController, /@Get\('auth'\)/);
|
|
1043
|
+
assert.match(healthController, /return this\.authService\.getProbeStatus\(\);/);
|
|
1044
|
+
|
|
1045
|
+
const classStart = healthController.indexOf('export class HealthController {');
|
|
1046
|
+
const classEnd = healthController.lastIndexOf('\n}');
|
|
1047
|
+
const authProbe = healthController.indexOf("@Get('auth')");
|
|
1048
|
+
assert.equal(classStart > -1, true);
|
|
1049
|
+
assert.equal(classEnd > classStart, true);
|
|
1050
|
+
assert.equal(authProbe > classStart && authProbe < classEnd, true);
|
|
1051
|
+
} finally {
|
|
1052
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it('applies swagger then jwt-auth without forcing swagger dependency in auth-api', () => {
|
|
1057
|
+
const targetRoot = mkTmp('forgeon-module-jwt-swagger-');
|
|
1058
|
+
const projectRoot = path.join(targetRoot, 'demo-jwt-swagger');
|
|
1059
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1060
|
+
|
|
1061
|
+
try {
|
|
1062
|
+
scaffoldProject({
|
|
1063
|
+
templateRoot,
|
|
1064
|
+
packageRoot,
|
|
1065
|
+
targetRoot: projectRoot,
|
|
1066
|
+
projectName: 'demo-jwt-swagger',
|
|
1067
|
+
frontend: 'react',
|
|
1068
|
+
db: 'prisma',
|
|
1069
|
+
dbPrismaEnabled: false,
|
|
1070
|
+
i18nEnabled: false,
|
|
1071
|
+
proxy: 'caddy',
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
addModule({
|
|
1075
|
+
moduleId: 'swagger',
|
|
1076
|
+
targetRoot: projectRoot,
|
|
1077
|
+
packageRoot,
|
|
1078
|
+
});
|
|
1079
|
+
addModule({
|
|
1080
|
+
moduleId: 'jwt-auth',
|
|
1081
|
+
targetRoot: projectRoot,
|
|
1082
|
+
packageRoot,
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
const authApiPackage = JSON.parse(
|
|
1086
|
+
fs.readFileSync(path.join(projectRoot, 'packages', 'auth-api', 'package.json'), 'utf8'),
|
|
1087
|
+
);
|
|
1088
|
+
assert.equal(Object.hasOwn(authApiPackage.dependencies ?? {}, '@nestjs/swagger'), false);
|
|
1089
|
+
} finally {
|
|
1003
1090
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1004
1091
|
}
|
|
1005
1092
|
});
|