create-forgeon 0.2.3 → 0.2.5
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
|
@@ -3,8 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
4
|
import {
|
|
5
5
|
ensureBuildSteps,
|
|
6
|
+
ensureClassMember,
|
|
6
7
|
ensureDependency,
|
|
7
8
|
ensureDevDependency,
|
|
9
|
+
ensureImportLine,
|
|
8
10
|
ensureLineAfter,
|
|
9
11
|
ensureLineBefore,
|
|
10
12
|
ensureLoadItem,
|
|
@@ -118,11 +120,7 @@ function patchHealthController(targetRoot) {
|
|
|
118
120
|
content = ensureNestCommonImport(content, 'Post');
|
|
119
121
|
|
|
120
122
|
if (!content.includes("from '@forgeon/db-prisma';")) {
|
|
121
|
-
|
|
122
|
-
const anchor = content.includes("import { I18nService } from 'nestjs-i18n';")
|
|
123
|
-
? "import { I18nService } from 'nestjs-i18n';"
|
|
124
|
-
: nestCommonImport;
|
|
125
|
-
content = ensureLineAfter(content, anchor, "import { PrismaService } from '@forgeon/db-prisma';");
|
|
123
|
+
content = ensureImportLine(content, "import { PrismaService } from '@forgeon/db-prisma';");
|
|
126
124
|
}
|
|
127
125
|
|
|
128
126
|
if (!content.includes('private readonly prisma: PrismaService')) {
|
|
@@ -167,17 +165,7 @@ function patchHealthController(targetRoot) {
|
|
|
167
165
|
};
|
|
168
166
|
}
|
|
169
167
|
`;
|
|
170
|
-
|
|
171
|
-
if (translateIndex > -1) {
|
|
172
|
-
content = `${content.slice(0, translateIndex).trimEnd()}\n\n${dbMethod}\n${content.slice(translateIndex)}`;
|
|
173
|
-
} else {
|
|
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
|
-
}
|
|
180
|
-
}
|
|
168
|
+
content = ensureClassMember(content, 'HealthController', dbMethod, { beforeNeedle: 'private translate(' });
|
|
181
169
|
}
|
|
182
170
|
|
|
183
171
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
@@ -1053,6 +1053,59 @@ describe('addModule', () => {
|
|
|
1053
1053
|
}
|
|
1054
1054
|
});
|
|
1055
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
|
+
|
|
1056
1109
|
it('applies swagger then jwt-auth without forcing swagger dependency in auth-api', () => {
|
|
1057
1110
|
const targetRoot = mkTmp('forgeon-module-jwt-swagger-');
|
|
1058
1111
|
const projectRoot = path.join(targetRoot, 'demo-jwt-swagger');
|
package/src/modules/i18n.mjs
CHANGED
|
@@ -3,7 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
4
|
import {
|
|
5
5
|
ensureBuildSteps,
|
|
6
|
+
ensureClassMember,
|
|
6
7
|
ensureDependency,
|
|
8
|
+
ensureImportLine,
|
|
7
9
|
ensureLineAfter,
|
|
8
10
|
ensureLineBefore,
|
|
9
11
|
ensureLoadItem,
|
|
@@ -288,19 +290,7 @@ function patchHealthController(targetRoot) {
|
|
|
288
290
|
|
|
289
291
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
290
292
|
if (!content.includes("from 'nestjs-i18n';")) {
|
|
291
|
-
|
|
292
|
-
content = ensureLineAfter(
|
|
293
|
-
content,
|
|
294
|
-
"import { PrismaService } from '@forgeon/db-prisma';",
|
|
295
|
-
"import { I18nService } from 'nestjs-i18n';",
|
|
296
|
-
);
|
|
297
|
-
} else {
|
|
298
|
-
content = ensureLineAfter(
|
|
299
|
-
content,
|
|
300
|
-
"import { BadRequestException, ConflictException, Controller, Get, Post, Query } from '@nestjs/common';",
|
|
301
|
-
"import { I18nService } from 'nestjs-i18n';",
|
|
302
|
-
);
|
|
303
|
-
}
|
|
293
|
+
content = ensureImportLine(content, "import { I18nService } from 'nestjs-i18n';");
|
|
304
294
|
}
|
|
305
295
|
|
|
306
296
|
if (!content.includes('private readonly i18n: I18nService')) {
|
|
@@ -308,11 +298,22 @@ function patchHealthController(targetRoot) {
|
|
|
308
298
|
if (constructorMatch) {
|
|
309
299
|
const original = constructorMatch[0];
|
|
310
300
|
const inner = constructorMatch[1].trimEnd();
|
|
311
|
-
const
|
|
312
|
-
const
|
|
301
|
+
const normalizedInner = inner.replace(/,\s*$/, '');
|
|
302
|
+
const separator = normalizedInner.length > 0 ? ',' : '';
|
|
303
|
+
const next = `constructor(${normalizedInner}${separator}
|
|
313
304
|
private readonly i18n: I18nService,
|
|
314
305
|
) {`;
|
|
315
306
|
content = content.replace(original, next);
|
|
307
|
+
} else {
|
|
308
|
+
const classAnchor = 'export class HealthController {';
|
|
309
|
+
if (content.includes(classAnchor)) {
|
|
310
|
+
content = content.replace(
|
|
311
|
+
classAnchor,
|
|
312
|
+
`${classAnchor}
|
|
313
|
+
constructor(private readonly i18n: I18nService) {}
|
|
314
|
+
`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
316
317
|
}
|
|
317
318
|
}
|
|
318
319
|
|
|
@@ -323,9 +324,10 @@ function patchHealthController(targetRoot) {
|
|
|
323
324
|
return typeof value === 'string' ? value : key;
|
|
324
325
|
}
|
|
325
326
|
`;
|
|
326
|
-
content =
|
|
327
|
+
content = ensureClassMember(content, 'HealthController', translateMethod);
|
|
327
328
|
}
|
|
328
329
|
|
|
330
|
+
content = content.replace(/getHealth\(\)/g, "getHealth(@Query('lang') lang?: string)");
|
|
329
331
|
content = content.replace(
|
|
330
332
|
/getHealth\(@Query\('lang'\)\s*_lang\?:\s*string\)/g,
|
|
331
333
|
"getHealth(@Query('lang') lang?: string)",
|
|
@@ -339,6 +341,7 @@ function patchHealthController(targetRoot) {
|
|
|
339
341
|
"getValidationProbe(@Query('value') value?: string, @Query('lang') lang?: string)",
|
|
340
342
|
);
|
|
341
343
|
content = content.replace(/message:\s*'OK',/g, "message: this.translate('common.actions.ok', lang),");
|
|
344
|
+
content = content.replace(/i18n:\s*'disabled',/g, "i18n: 'en',");
|
|
342
345
|
content = content.replace(/i18n:\s*'English',/g, "i18n: 'en',");
|
|
343
346
|
|
|
344
347
|
content = content.replace(
|
package/src/modules/jwt-auth.mjs
CHANGED
|
@@ -3,7 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
4
|
import {
|
|
5
5
|
ensureBuildSteps,
|
|
6
|
+
ensureClassMember,
|
|
6
7
|
ensureDependency,
|
|
8
|
+
ensureImportLine,
|
|
7
9
|
ensureLineAfter,
|
|
8
10
|
ensureLineBefore,
|
|
9
11
|
ensureLoadItem,
|
|
@@ -117,20 +119,7 @@ function patchHealthController(targetRoot) {
|
|
|
117
119
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
118
120
|
|
|
119
121
|
if (!content.includes("from '@forgeon/auth-api';")) {
|
|
120
|
-
|
|
121
|
-
if (content.includes("import { PrismaService } from '@forgeon/db-prisma';")) {
|
|
122
|
-
content = ensureLineAfter(
|
|
123
|
-
content,
|
|
124
|
-
"import { PrismaService } from '@forgeon/db-prisma';",
|
|
125
|
-
"import { AuthService } from '@forgeon/auth-api';",
|
|
126
|
-
);
|
|
127
|
-
} else {
|
|
128
|
-
content = ensureLineAfter(
|
|
129
|
-
content,
|
|
130
|
-
nestCommonImport ?? "import { Controller, Get } from '@nestjs/common';",
|
|
131
|
-
"import { AuthService } from '@forgeon/auth-api';",
|
|
132
|
-
);
|
|
133
|
-
}
|
|
122
|
+
content = ensureImportLine(content, "import { AuthService } from '@forgeon/auth-api';");
|
|
134
123
|
}
|
|
135
124
|
|
|
136
125
|
if (!content.includes('private readonly authService: AuthService')) {
|
|
@@ -164,19 +153,8 @@ function patchHealthController(targetRoot) {
|
|
|
164
153
|
return this.authService.getProbeStatus();
|
|
165
154
|
}
|
|
166
155
|
`;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
} else if (content.includes('private translate(')) {
|
|
170
|
-
const index = content.indexOf('private translate(');
|
|
171
|
-
content = `${content.slice(0, index).trimEnd()}\n\n${method}\n${content.slice(index)}`;
|
|
172
|
-
} else {
|
|
173
|
-
const classEnd = content.lastIndexOf('\n}');
|
|
174
|
-
if (classEnd >= 0) {
|
|
175
|
-
content = `${content.slice(0, classEnd).trimEnd()}\n\n${method}\n${content.slice(classEnd)}`;
|
|
176
|
-
} else {
|
|
177
|
-
content = `${content.trimEnd()}\n${method}\n`;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
156
|
+
const beforeNeedle = content.includes("@Post('db')") ? "@Post('db')" : 'private translate(';
|
|
157
|
+
content = ensureClassMember(content, 'HealthController', method, { beforeNeedle });
|
|
180
158
|
}
|
|
181
159
|
|
|
182
160
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
@@ -73,6 +73,81 @@ export function ensureLineBefore(content, anchorLine, lineToInsert) {
|
|
|
73
73
|
return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export function ensureImportLine(content, importLine) {
|
|
77
|
+
if (content.includes(importLine)) {
|
|
78
|
+
return content;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const importMatches = [...content.matchAll(/^import\s.+;$/gm)];
|
|
82
|
+
if (importMatches.length === 0) {
|
|
83
|
+
return `${importLine}\n${content}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const lastImport = importMatches.at(-1);
|
|
87
|
+
const insertAt = lastImport.index + lastImport[0].length;
|
|
88
|
+
return `${content.slice(0, insertAt)}\n${importLine}${content.slice(insertAt)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findClassRange(content, className) {
|
|
92
|
+
const classPattern = new RegExp(`export\\s+class\\s+${className}\\b`);
|
|
93
|
+
const classMatch = classPattern.exec(content);
|
|
94
|
+
if (!classMatch) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const openBrace = content.indexOf('{', classMatch.index);
|
|
99
|
+
if (openBrace < 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let depth = 0;
|
|
104
|
+
for (let index = openBrace; index < content.length; index += 1) {
|
|
105
|
+
const char = content[index];
|
|
106
|
+
if (char === '{') {
|
|
107
|
+
depth += 1;
|
|
108
|
+
} else if (char === '}') {
|
|
109
|
+
depth -= 1;
|
|
110
|
+
if (depth === 0) {
|
|
111
|
+
return {
|
|
112
|
+
classStart: classMatch.index,
|
|
113
|
+
bodyStart: openBrace + 1,
|
|
114
|
+
classEnd: index,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function ensureClassMember(content, className, memberCode, options = {}) {
|
|
124
|
+
const member = memberCode.trim();
|
|
125
|
+
if (member.length === 0) {
|
|
126
|
+
return content;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const range = findClassRange(content, className);
|
|
130
|
+
if (!range) {
|
|
131
|
+
return `${content.trimEnd()}\n${member}\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const classBody = content.slice(range.bodyStart, range.classEnd);
|
|
135
|
+
if (classBody.includes(member)) {
|
|
136
|
+
return content;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let insertAt = range.classEnd;
|
|
140
|
+
const beforeNeedle = options.beforeNeedle;
|
|
141
|
+
if (typeof beforeNeedle === 'string' && beforeNeedle.length > 0) {
|
|
142
|
+
const needleIndex = classBody.indexOf(beforeNeedle);
|
|
143
|
+
if (needleIndex >= 0) {
|
|
144
|
+
insertAt = range.bodyStart + needleIndex;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return `${content.slice(0, insertAt)}\n\n${member}\n${content.slice(insertAt)}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
76
151
|
export function upsertEnvLines(filePath, lines) {
|
|
77
152
|
let content = '';
|
|
78
153
|
if (fs.existsSync(filePath)) {
|
|
@@ -72,12 +72,12 @@ const INTEGRATION_GROUPS = [
|
|
|
72
72
|
{
|
|
73
73
|
id: 'auth-persistence',
|
|
74
74
|
title: 'Auth Persistence Integration',
|
|
75
|
-
modules: ['jwt-auth', 'db-prisma'
|
|
75
|
+
modules: ['jwt-auth', 'db-prisma'],
|
|
76
76
|
description: [
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'Extend Prisma User model with refreshTokenHash',
|
|
80
|
-
'
|
|
77
|
+
'Patch AppModule to wire AUTH_REFRESH_TOKEN_STORE with PrismaAuthRefreshTokenStore',
|
|
78
|
+
'Add apps/api/src/auth/prisma-auth-refresh-token.store.ts',
|
|
79
|
+
'Extend Prisma User model with refreshTokenHash and add migration 0002_auth_refresh_token_hash',
|
|
80
|
+
'Update JWT auth README note about refresh-token persistence',
|
|
81
81
|
],
|
|
82
82
|
isAvailable: (detected) => detected.jwtAuth && detected.dbPrisma,
|
|
83
83
|
isPending: (rootDir) => isAuthPersistencePending(rootDir),
|