create-forgeon 0.2.2 → 0.2.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/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 +26 -172
- package/src/modules/executor.test.mjs +150 -10
- package/src/modules/i18n.mjs +29 -150
- package/src/modules/jwt-auth.mjs +37 -297
- package/src/modules/logger.mjs +10 -118
- package/src/modules/shared/patch-utils.mjs +237 -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_SPEC.md +4 -0
- package/templates/base/package.json +0 -3
- package/templates/base/scripts/forgeon-sync-integrations.mjs +44 -241
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,20 @@
|
|
|
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
|
+
ensureClassMember,
|
|
7
|
+
ensureDependency,
|
|
8
|
+
ensureDevDependency,
|
|
9
|
+
ensureImportLine,
|
|
10
|
+
ensureLineAfter,
|
|
11
|
+
ensureLineBefore,
|
|
12
|
+
ensureLoadItem,
|
|
13
|
+
ensureNestCommonImport,
|
|
14
|
+
ensureScript,
|
|
15
|
+
ensureValidatorSchema,
|
|
16
|
+
upsertEnvLines,
|
|
17
|
+
} from './shared/patch-utils.mjs';
|
|
4
18
|
|
|
5
19
|
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
6
20
|
const source = path.join(packageRoot, 'templates', 'module-presets', 'db-prisma', relativePath);
|
|
@@ -11,167 +25,6 @@ function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
|
11
25
|
copyRecursive(source, destination);
|
|
12
26
|
}
|
|
13
27
|
|
|
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
28
|
function patchApiPackage(targetRoot) {
|
|
176
29
|
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
177
30
|
if (!fs.existsSync(packagePath)) {
|
|
@@ -267,11 +120,7 @@ function patchHealthController(targetRoot) {
|
|
|
267
120
|
content = ensureNestCommonImport(content, 'Post');
|
|
268
121
|
|
|
269
122
|
if (!content.includes("from '@forgeon/db-prisma';")) {
|
|
270
|
-
|
|
271
|
-
const anchor = content.includes("import { I18nService } from 'nestjs-i18n';")
|
|
272
|
-
? "import { I18nService } from 'nestjs-i18n';"
|
|
273
|
-
: nestCommonImport;
|
|
274
|
-
content = ensureLineAfter(content, anchor, "import { PrismaService } from '@forgeon/db-prisma';");
|
|
123
|
+
content = ensureImportLine(content, "import { PrismaService } from '@forgeon/db-prisma';");
|
|
275
124
|
}
|
|
276
125
|
|
|
277
126
|
if (!content.includes('private readonly prisma: PrismaService')) {
|
|
@@ -285,6 +134,16 @@ function patchHealthController(targetRoot) {
|
|
|
285
134
|
private readonly prisma: PrismaService,
|
|
286
135
|
) {`;
|
|
287
136
|
content = content.replace(original, next);
|
|
137
|
+
} else {
|
|
138
|
+
const classAnchor = 'export class HealthController {';
|
|
139
|
+
if (content.includes(classAnchor)) {
|
|
140
|
+
content = content.replace(
|
|
141
|
+
classAnchor,
|
|
142
|
+
`${classAnchor}
|
|
143
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
144
|
+
`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
288
147
|
}
|
|
289
148
|
}
|
|
290
149
|
|
|
@@ -306,12 +165,7 @@ function patchHealthController(targetRoot) {
|
|
|
306
165
|
};
|
|
307
166
|
}
|
|
308
167
|
`;
|
|
309
|
-
|
|
310
|
-
if (translateIndex > -1) {
|
|
311
|
-
content = `${content.slice(0, translateIndex).trimEnd()}\n\n${dbMethod}\n${content.slice(translateIndex)}`;
|
|
312
|
-
} else {
|
|
313
|
-
content = `${content.trimEnd()}\n${dbMethod}\n`;
|
|
314
|
-
}
|
|
168
|
+
content = ensureClassMember(content, 'HealthController', dbMethod, { beforeNeedle: 'private translate(' });
|
|
315
169
|
}
|
|
316
170
|
|
|
317
171
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
@@ -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,146 @@ 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('keeps health controller valid for add sequence jwt-auth -> logger -> swagger -> i18n -> db-prisma on db/i18n-disabled scaffold', () => {
|
|
1057
|
+
const targetRoot = mkTmp('forgeon-module-seq-health-valid-');
|
|
1058
|
+
const projectRoot = path.join(targetRoot, 'demo-seq-health-valid');
|
|
1059
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1060
|
+
|
|
1061
|
+
try {
|
|
1062
|
+
scaffoldProject({
|
|
1063
|
+
templateRoot,
|
|
1064
|
+
packageRoot,
|
|
1065
|
+
targetRoot: projectRoot,
|
|
1066
|
+
projectName: 'demo-seq-health-valid',
|
|
1067
|
+
frontend: 'react',
|
|
1068
|
+
db: 'prisma',
|
|
1069
|
+
dbPrismaEnabled: false,
|
|
1070
|
+
i18nEnabled: false,
|
|
1071
|
+
proxy: 'caddy',
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
for (const moduleId of ['jwt-auth', 'logger', 'swagger', 'i18n', 'db-prisma']) {
|
|
1075
|
+
addModule({ moduleId, targetRoot: projectRoot, packageRoot });
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const healthController = fs.readFileSync(
|
|
1079
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
1080
|
+
'utf8',
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
const classStart = healthController.indexOf('export class HealthController {');
|
|
1084
|
+
const classEnd = healthController.lastIndexOf('\n}');
|
|
1085
|
+
assert.equal(classStart > -1, true);
|
|
1086
|
+
assert.equal(classEnd > classStart, true);
|
|
1087
|
+
|
|
1088
|
+
const imports = [...healthController.matchAll(/^import\s.+;$/gm)];
|
|
1089
|
+
assert.equal(imports.length > 0, true);
|
|
1090
|
+
for (const importLine of imports) {
|
|
1091
|
+
assert.equal(importLine.index < classStart, true);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const authProbe = healthController.indexOf("@Get('auth')");
|
|
1095
|
+
const dbProbe = healthController.indexOf("@Post('db')");
|
|
1096
|
+
const translateMethod = healthController.indexOf('private translate(');
|
|
1097
|
+
assert.equal(authProbe > classStart && authProbe < classEnd, true);
|
|
1098
|
+
assert.equal(dbProbe > classStart && dbProbe < classEnd, true);
|
|
1099
|
+
assert.equal(translateMethod > classStart && translateMethod < classEnd, true);
|
|
1100
|
+
|
|
1101
|
+
assert.match(healthController, /private readonly authService: AuthService/);
|
|
1102
|
+
assert.match(healthController, /private readonly i18n: I18nService/);
|
|
1103
|
+
assert.match(healthController, /private readonly prisma: PrismaService/);
|
|
1104
|
+
} finally {
|
|
1105
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
it('applies swagger then jwt-auth without forcing swagger dependency in auth-api', () => {
|
|
1110
|
+
const targetRoot = mkTmp('forgeon-module-jwt-swagger-');
|
|
1111
|
+
const projectRoot = path.join(targetRoot, 'demo-jwt-swagger');
|
|
1112
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1113
|
+
|
|
1114
|
+
try {
|
|
1115
|
+
scaffoldProject({
|
|
1116
|
+
templateRoot,
|
|
1117
|
+
packageRoot,
|
|
1118
|
+
targetRoot: projectRoot,
|
|
1119
|
+
projectName: 'demo-jwt-swagger',
|
|
1120
|
+
frontend: 'react',
|
|
1121
|
+
db: 'prisma',
|
|
1122
|
+
dbPrismaEnabled: false,
|
|
1123
|
+
i18nEnabled: false,
|
|
1124
|
+
proxy: 'caddy',
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
addModule({
|
|
1128
|
+
moduleId: 'swagger',
|
|
1129
|
+
targetRoot: projectRoot,
|
|
1130
|
+
packageRoot,
|
|
1131
|
+
});
|
|
1132
|
+
addModule({
|
|
1133
|
+
moduleId: 'jwt-auth',
|
|
1134
|
+
targetRoot: projectRoot,
|
|
1135
|
+
packageRoot,
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
const authApiPackage = JSON.parse(
|
|
1139
|
+
fs.readFileSync(path.join(projectRoot, 'packages', 'auth-api', 'package.json'), 'utf8'),
|
|
1140
|
+
);
|
|
1141
|
+
assert.equal(Object.hasOwn(authApiPackage.dependencies ?? {}, '@nestjs/swagger'), false);
|
|
1142
|
+
} finally {
|
|
1003
1143
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1004
1144
|
}
|
|
1005
1145
|
});
|