@fluojs/cli 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.ko.md +155 -0
- package/README.md +155 -0
- package/bin/fluo.mjs +5 -0
- package/dist/cli.d.ts +37 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +292 -0
- package/dist/commands/generate.d.ts +40 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +134 -0
- package/dist/commands/inspect.d.ts +30 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +221 -0
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +173 -0
- package/dist/commands/new.d.ts +45 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +353 -0
- package/dist/generator-types.d.ts +21 -0
- package/dist/generator-types.d.ts.map +1 -0
- package/dist/generator-types.js +1 -0
- package/dist/generators/controller.d.ts +3 -0
- package/dist/generators/controller.d.ts.map +1 -0
- package/dist/generators/controller.js +22 -0
- package/dist/generators/guard.d.ts +3 -0
- package/dist/generators/guard.d.ts.map +1 -0
- package/dist/generators/guard.js +15 -0
- package/dist/generators/interceptor.d.ts +3 -0
- package/dist/generators/interceptor.d.ts.map +1 -0
- package/dist/generators/interceptor.js +15 -0
- package/dist/generators/manifest.d.ts +121 -0
- package/dist/generators/manifest.d.ts.map +1 -0
- package/dist/generators/manifest.js +130 -0
- package/dist/generators/middleware.d.ts +3 -0
- package/dist/generators/middleware.d.ts.map +1 -0
- package/dist/generators/middleware.js +15 -0
- package/dist/generators/module.d.ts +6 -0
- package/dist/generators/module.d.ts.map +1 -0
- package/dist/generators/module.js +143 -0
- package/dist/generators/render.d.ts +2 -0
- package/dist/generators/render.d.ts.map +1 -0
- package/dist/generators/render.js +17 -0
- package/dist/generators/repository.d.ts +3 -0
- package/dist/generators/repository.d.ts.map +1 -0
- package/dist/generators/repository.js +29 -0
- package/dist/generators/request-dto.d.ts +3 -0
- package/dist/generators/request-dto.d.ts.map +1 -0
- package/dist/generators/request-dto.js +17 -0
- package/dist/generators/response-dto.d.ts +3 -0
- package/dist/generators/response-dto.d.ts.map +1 -0
- package/dist/generators/response-dto.js +17 -0
- package/dist/generators/service.d.ts +3 -0
- package/dist/generators/service.d.ts.map +1 -0
- package/dist/generators/service.js +22 -0
- package/dist/generators/templates/controller.test.ts.ejs +21 -0
- package/dist/generators/templates/controller.ts.ejs +29 -0
- package/dist/generators/templates/guard.ts.ejs +7 -0
- package/dist/generators/templates/interceptor.ts.ejs +7 -0
- package/dist/generators/templates/middleware.ts.ejs +11 -0
- package/dist/generators/templates/module.ts.ejs +9 -0
- package/dist/generators/templates/repository.slice.test.ts.ejs +15 -0
- package/dist/generators/templates/repository.test.ts.ejs +9 -0
- package/dist/generators/templates/repository.ts.ejs +10 -0
- package/dist/generators/templates/request-dto.ts.ejs +9 -0
- package/dist/generators/templates/response-dto.ts.ejs +3 -0
- package/dist/generators/templates/service.test.ts.ejs +21 -0
- package/dist/generators/templates/service.ts.ejs +24 -0
- package/dist/generators/utils.d.ts +4 -0
- package/dist/generators/utils.d.ts.map +1 -0
- package/dist/generators/utils.js +18 -0
- package/dist/help.d.ts +8 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +16 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/new/install.d.ts +51 -0
- package/dist/new/install.d.ts.map +1 -0
- package/dist/new/install.js +140 -0
- package/dist/new/package-spec-resolver.d.ts +4 -0
- package/dist/new/package-spec-resolver.d.ts.map +1 -0
- package/dist/new/package-spec-resolver.js +397 -0
- package/dist/new/prompt.d.ts +56 -0
- package/dist/new/prompt.d.ts.map +1 -0
- package/dist/new/prompt.js +278 -0
- package/dist/new/resolver.d.ts +32 -0
- package/dist/new/resolver.d.ts.map +1 -0
- package/dist/new/resolver.js +93 -0
- package/dist/new/scaffold.d.ts +14 -0
- package/dist/new/scaffold.d.ts.map +1 -0
- package/dist/new/scaffold.js +2010 -0
- package/dist/new/starter-profiles.d.ts +91 -0
- package/dist/new/starter-profiles.d.ts.map +1 -0
- package/dist/new/starter-profiles.js +347 -0
- package/dist/new/types.d.ts +63 -0
- package/dist/new/types.d.ts.map +1 -0
- package/dist/new/types.js +1 -0
- package/dist/registry.d.ts +10 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +30 -0
- package/dist/transforms/nestjs-migrate.d.ts +33 -0
- package/dist/transforms/nestjs-migrate.d.ts.map +1 -0
- package/dist/transforms/nestjs-migrate.js +891 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, extname, posix, resolve } from 'node:path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
export const MIGRATION_TRANSFORMS = ['imports', 'injectable', 'scope', 'bootstrap', 'testing', 'tsconfig'];
|
|
5
|
+
export const WARNING_CATEGORIES = ['inject-token', 'request-dto', 'pipe-converter', 'bootstrap-unsupported', 'testing-unsupported', 'import-unsupported', 'injectable-options', 'tsconfig-parse', 'bootstrap-port'];
|
|
6
|
+
const WARNING_CATEGORY_LABEL = {
|
|
7
|
+
'inject-token': 'DI token migration (@Inject)',
|
|
8
|
+
'request-dto': 'Request DTO migration (handler parameter decorators)',
|
|
9
|
+
'pipe-converter': 'Pipe/converter migration',
|
|
10
|
+
'bootstrap-unsupported': 'Unsupported bootstrap variant',
|
|
11
|
+
'testing-unsupported': 'Unsupported testing pattern',
|
|
12
|
+
'import-unsupported': 'Unsupported import form',
|
|
13
|
+
'injectable-options': '@Injectable options removed',
|
|
14
|
+
'tsconfig-parse': 'tsconfig parse failure',
|
|
15
|
+
'bootstrap-port': 'Bootstrap port folding issue'
|
|
16
|
+
};
|
|
17
|
+
export function getWarningCategoryLabel(category) {
|
|
18
|
+
return WARNING_CATEGORY_LABEL[category];
|
|
19
|
+
}
|
|
20
|
+
export function groupWarningsByCategory(warnings) {
|
|
21
|
+
const groups = new Map();
|
|
22
|
+
for (const warning of warnings) {
|
|
23
|
+
const existing = groups.get(warning.category) ?? [];
|
|
24
|
+
existing.push(warning);
|
|
25
|
+
groups.set(warning.category, existing);
|
|
26
|
+
}
|
|
27
|
+
return groups;
|
|
28
|
+
}
|
|
29
|
+
const printer = ts.createPrinter({
|
|
30
|
+
newLine: ts.NewLineKind.LineFeed
|
|
31
|
+
});
|
|
32
|
+
const NEST_COMMON_TO_FLUO = {
|
|
33
|
+
Body: '@fluojs/http',
|
|
34
|
+
ConflictException: '@fluojs/http',
|
|
35
|
+
Controller: '@fluojs/http',
|
|
36
|
+
Delete: '@fluojs/http',
|
|
37
|
+
ForbiddenException: '@fluojs/http',
|
|
38
|
+
Get: '@fluojs/http',
|
|
39
|
+
Header: '@fluojs/http',
|
|
40
|
+
Headers: '@fluojs/http',
|
|
41
|
+
HttpCode: '@fluojs/http',
|
|
42
|
+
HttpException: '@fluojs/http',
|
|
43
|
+
Inject: '@fluojs/core',
|
|
44
|
+
Module: '@fluojs/core',
|
|
45
|
+
NotFoundException: '@fluojs/http',
|
|
46
|
+
Param: '@fluojs/http',
|
|
47
|
+
Patch: '@fluojs/http',
|
|
48
|
+
Post: '@fluojs/http',
|
|
49
|
+
Put: '@fluojs/http',
|
|
50
|
+
Query: '@fluojs/http',
|
|
51
|
+
Req: '@fluojs/http',
|
|
52
|
+
Res: '@fluojs/http',
|
|
53
|
+
Scope: '@fluojs/core',
|
|
54
|
+
UnauthorizedException: '@fluojs/http',
|
|
55
|
+
UseGuards: '@fluojs/http',
|
|
56
|
+
UseInterceptors: '@fluojs/http'
|
|
57
|
+
};
|
|
58
|
+
const REQUEST_DTO_DECORATORS = new Set(['Body', 'Param', 'Query']);
|
|
59
|
+
const TRANSFORM_KIND_LABEL = {
|
|
60
|
+
bootstrap: 'bootstrap rewrite',
|
|
61
|
+
imports: 'import rewriting',
|
|
62
|
+
injectable: '@Injectable removal',
|
|
63
|
+
scope: 'scope mapping',
|
|
64
|
+
testing: 'testing rewrite',
|
|
65
|
+
tsconfig: 'tsconfig rewrite'
|
|
66
|
+
};
|
|
67
|
+
function parseSource(source, filePath) {
|
|
68
|
+
return ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
69
|
+
}
|
|
70
|
+
function buildWarning(filePath, sourceFile, node, category, message) {
|
|
71
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
72
|
+
return {
|
|
73
|
+
category,
|
|
74
|
+
filePath,
|
|
75
|
+
line,
|
|
76
|
+
message
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function getImportBindings(importDeclaration) {
|
|
80
|
+
const importClause = importDeclaration.importClause;
|
|
81
|
+
if (!importClause?.namedBindings || !ts.isNamedImports(importClause.namedBindings)) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
return importClause.namedBindings.elements.map(element => ({
|
|
85
|
+
imported: (element.propertyName ?? element.name).text,
|
|
86
|
+
local: element.name.text
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
function createImportSpecifier(binding) {
|
|
90
|
+
if (binding.imported === binding.local) {
|
|
91
|
+
return ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(binding.local));
|
|
92
|
+
}
|
|
93
|
+
return ts.factory.createImportSpecifier(false, ts.factory.createIdentifier(binding.imported), ts.factory.createIdentifier(binding.local));
|
|
94
|
+
}
|
|
95
|
+
function toBindingKey(binding) {
|
|
96
|
+
return `${binding.imported}::${binding.local}`;
|
|
97
|
+
}
|
|
98
|
+
function updateNamedImports(importDeclaration, bindings) {
|
|
99
|
+
const importClause = importDeclaration.importClause;
|
|
100
|
+
if (!importClause?.namedBindings || !ts.isNamedImports(importClause.namedBindings)) {
|
|
101
|
+
return importDeclaration;
|
|
102
|
+
}
|
|
103
|
+
if (bindings.length === 0 && !importClause.name) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const updatedClause = ts.factory.updateImportClause(importClause, importClause.isTypeOnly, importClause.name, bindings.length > 0 ? ts.factory.createNamedImports(bindings.map(createImportSpecifier)) : undefined);
|
|
107
|
+
return ts.factory.updateImportDeclaration(importDeclaration, importDeclaration.modifiers, updatedClause, importDeclaration.moduleSpecifier, importDeclaration.attributes);
|
|
108
|
+
}
|
|
109
|
+
function mergeNamedImport(statements, moduleSpecifier, newBindings) {
|
|
110
|
+
if (newBindings.length === 0) {
|
|
111
|
+
return statements;
|
|
112
|
+
}
|
|
113
|
+
const deduped = new Map();
|
|
114
|
+
for (const binding of newBindings) {
|
|
115
|
+
deduped.set(toBindingKey(binding), binding);
|
|
116
|
+
}
|
|
117
|
+
let merged = false;
|
|
118
|
+
const updated = statements.map(statement => {
|
|
119
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== moduleSpecifier) {
|
|
120
|
+
return statement;
|
|
121
|
+
}
|
|
122
|
+
const existingBindings = getImportBindings(statement);
|
|
123
|
+
for (const binding of existingBindings) {
|
|
124
|
+
deduped.set(toBindingKey(binding), binding);
|
|
125
|
+
}
|
|
126
|
+
merged = true;
|
|
127
|
+
const mergedBindings = [...deduped.values()].sort((left, right) => left.local.localeCompare(right.local));
|
|
128
|
+
return updateNamedImports(statement, mergedBindings) ?? statement;
|
|
129
|
+
});
|
|
130
|
+
if (merged) {
|
|
131
|
+
return updated;
|
|
132
|
+
}
|
|
133
|
+
const importDeclaration = ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([...deduped.values()].sort((left, right) => left.local.localeCompare(right.local)).map(createImportSpecifier))), ts.factory.createStringLiteral(moduleSpecifier));
|
|
134
|
+
const importIndexes = updated.map((statement, index) => ({
|
|
135
|
+
index,
|
|
136
|
+
isImport: ts.isImportDeclaration(statement)
|
|
137
|
+
})).filter(entry => entry.isImport).map(entry => entry.index);
|
|
138
|
+
if (importIndexes.length === 0) {
|
|
139
|
+
return [importDeclaration, ...updated];
|
|
140
|
+
}
|
|
141
|
+
const insertIndex = importIndexes[importIndexes.length - 1] + 1;
|
|
142
|
+
return [...updated.slice(0, insertIndex), importDeclaration, ...updated.slice(insertIndex)];
|
|
143
|
+
}
|
|
144
|
+
function printSourceFile(sourceFile, statements) {
|
|
145
|
+
const updated = ts.factory.updateSourceFile(sourceFile, statements);
|
|
146
|
+
return printer.printFile(updated);
|
|
147
|
+
}
|
|
148
|
+
function removeImportBinding(source, sourceFilePath, moduleSpecifier, importedName) {
|
|
149
|
+
const sourceFile = parseSource(source, sourceFilePath);
|
|
150
|
+
let changed = false;
|
|
151
|
+
const statements = [];
|
|
152
|
+
for (const statement of sourceFile.statements) {
|
|
153
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== moduleSpecifier) {
|
|
154
|
+
statements.push(statement);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const bindings = getImportBindings(statement);
|
|
158
|
+
if (bindings.length === 0) {
|
|
159
|
+
statements.push(statement);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const filtered = bindings.filter(binding => binding.imported !== importedName);
|
|
163
|
+
if (filtered.length !== bindings.length) {
|
|
164
|
+
changed = true;
|
|
165
|
+
}
|
|
166
|
+
const updated = updateNamedImports(statement, filtered);
|
|
167
|
+
if (updated) {
|
|
168
|
+
statements.push(updated);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!changed) {
|
|
172
|
+
return {
|
|
173
|
+
changed: false,
|
|
174
|
+
source
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
changed: true,
|
|
179
|
+
source: printSourceFile(sourceFile, statements)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function rewriteImports(source, filePath) {
|
|
183
|
+
const sourceFile = parseSource(source, filePath);
|
|
184
|
+
const warnings = [];
|
|
185
|
+
const additions = new Map();
|
|
186
|
+
const statements = [];
|
|
187
|
+
let touched = false;
|
|
188
|
+
for (const statement of sourceFile.statements) {
|
|
189
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
190
|
+
statements.push(statement);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (statement.moduleSpecifier.text !== '@nestjs/common') {
|
|
194
|
+
statements.push(statement);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const importClause = statement.importClause;
|
|
198
|
+
if (!importClause?.namedBindings || !ts.isNamedImports(importClause.namedBindings)) {
|
|
199
|
+
warnings.push(buildWarning(filePath, sourceFile, statement, 'import-unsupported', 'Unsupported Nest import form detected. Review this import manually.'));
|
|
200
|
+
statements.push(statement);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const remaining = [];
|
|
204
|
+
for (const binding of getImportBindings(statement)) {
|
|
205
|
+
const targetModule = NEST_COMMON_TO_FLUO[binding.imported];
|
|
206
|
+
if (!targetModule) {
|
|
207
|
+
remaining.push(binding);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
touched = true;
|
|
211
|
+
const moduleBindings = additions.get(targetModule) ?? [];
|
|
212
|
+
moduleBindings.push({
|
|
213
|
+
imported: binding.imported,
|
|
214
|
+
local: binding.local
|
|
215
|
+
});
|
|
216
|
+
additions.set(targetModule, moduleBindings);
|
|
217
|
+
}
|
|
218
|
+
const updated = updateNamedImports(statement, remaining);
|
|
219
|
+
if (updated) {
|
|
220
|
+
statements.push(updated);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!touched) {
|
|
224
|
+
return {
|
|
225
|
+
changed: false,
|
|
226
|
+
source,
|
|
227
|
+
warnings
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
let nextStatements = statements;
|
|
231
|
+
for (const [moduleSpecifier, bindings] of additions.entries()) {
|
|
232
|
+
nextStatements = mergeNamedImport(nextStatements, moduleSpecifier, bindings);
|
|
233
|
+
}
|
|
234
|
+
const nextSource = printSourceFile(sourceFile, nextStatements);
|
|
235
|
+
return {
|
|
236
|
+
changed: nextSource !== source,
|
|
237
|
+
source: nextSource,
|
|
238
|
+
warnings
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function isDecoratorNamed(decorator, name) {
|
|
242
|
+
if (!ts.isCallExpression(decorator.expression)) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
return ts.isIdentifier(decorator.expression.expression) && decorator.expression.expression.text === name;
|
|
246
|
+
}
|
|
247
|
+
function hasScopeDecorator(modifiers) {
|
|
248
|
+
if (!modifiers) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return modifiers.some(modifier => ts.isDecorator(modifier) && isDecoratorNamed(modifier, 'Scope'));
|
|
252
|
+
}
|
|
253
|
+
function hasDecoratorNamed(modifiers, name) {
|
|
254
|
+
if (!modifiers) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
return modifiers.some(modifier => ts.isDecorator(modifier) && isDecoratorNamed(modifier, name));
|
|
258
|
+
}
|
|
259
|
+
function hasConflictingScopeImport(sourceFile) {
|
|
260
|
+
for (const statement of sourceFile.statements) {
|
|
261
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause?.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text === '@fluojs/core') {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (statement.importClause.namedBindings.elements.some(element => element.name.text === 'Scope')) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
function readInjectableScope(argument) {
|
|
274
|
+
if (!argument || !ts.isObjectLiteralExpression(argument)) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
for (const property of argument.properties) {
|
|
278
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (!ts.isIdentifier(property.name) || property.name.text !== 'scope') {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const value = property.initializer.getText();
|
|
285
|
+
if (value === 'Scope.REQUEST') {
|
|
286
|
+
return 'request';
|
|
287
|
+
}
|
|
288
|
+
if (value === 'Scope.TRANSIENT') {
|
|
289
|
+
return 'transient';
|
|
290
|
+
}
|
|
291
|
+
if (value === 'Scope.DEFAULT') {
|
|
292
|
+
return 'singleton';
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
function rewriteInjectableAndScope(source, filePath, options) {
|
|
298
|
+
if (!options.removeInjectable && !options.rewriteScope || !source.includes('@Injectable')) {
|
|
299
|
+
return {
|
|
300
|
+
changed: false,
|
|
301
|
+
source,
|
|
302
|
+
warnings: []
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const sourceFile = parseSource(source, filePath);
|
|
306
|
+
const warnings = [];
|
|
307
|
+
let hasStructuralChange = false;
|
|
308
|
+
let addedScopeDecorator = false;
|
|
309
|
+
const scopeDecoratorName = hasConflictingScopeImport(sourceFile) ? 'FluoScope' : 'Scope';
|
|
310
|
+
const transformer = context => {
|
|
311
|
+
const visit = node => {
|
|
312
|
+
if (!ts.isClassDeclaration(node) || !node.modifiers) {
|
|
313
|
+
return ts.visitEachChild(node, visit, context);
|
|
314
|
+
}
|
|
315
|
+
let classUpdated = false;
|
|
316
|
+
let sawInjectableDecorator = false;
|
|
317
|
+
let mappedScope;
|
|
318
|
+
const nextModifiers = [];
|
|
319
|
+
for (const modifier of node.modifiers) {
|
|
320
|
+
if (!ts.isDecorator(modifier) || !ts.isCallExpression(modifier.expression)) {
|
|
321
|
+
nextModifiers.push(modifier);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (!ts.isIdentifier(modifier.expression.expression) || modifier.expression.expression.text !== 'Injectable') {
|
|
325
|
+
nextModifiers.push(modifier);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
sawInjectableDecorator = true;
|
|
329
|
+
const [firstArgument] = modifier.expression.arguments;
|
|
330
|
+
if (options.rewriteScope) {
|
|
331
|
+
mappedScope = readInjectableScope(firstArgument);
|
|
332
|
+
}
|
|
333
|
+
if (!options.removeInjectable) {
|
|
334
|
+
nextModifiers.push(modifier);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
classUpdated = true;
|
|
338
|
+
hasStructuralChange = true;
|
|
339
|
+
if (firstArgument && ts.isObjectLiteralExpression(firstArgument)) {
|
|
340
|
+
const unsupportedProperties = firstArgument.properties.filter(property => !ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) || property.name.text !== 'scope');
|
|
341
|
+
if (unsupportedProperties.length > 0) {
|
|
342
|
+
warnings.push(buildWarning(filePath, sourceFile, modifier, 'injectable-options', '@Injectable options other than scope were removed. Verify behavior manually.'));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (!sawInjectableDecorator) {
|
|
347
|
+
return ts.visitEachChild(node, visit, context);
|
|
348
|
+
}
|
|
349
|
+
if (options.rewriteScope && mappedScope && !hasScopeDecorator(nextModifiers) && !hasDecoratorNamed(nextModifiers, scopeDecoratorName)) {
|
|
350
|
+
classUpdated = true;
|
|
351
|
+
hasStructuralChange = true;
|
|
352
|
+
addedScopeDecorator = true;
|
|
353
|
+
nextModifiers.unshift(ts.factory.createDecorator(ts.factory.createCallExpression(ts.factory.createIdentifier(scopeDecoratorName), undefined, [ts.factory.createStringLiteral(mappedScope)])));
|
|
354
|
+
}
|
|
355
|
+
if (!classUpdated) {
|
|
356
|
+
return ts.visitEachChild(node, visit, context);
|
|
357
|
+
}
|
|
358
|
+
return ts.factory.updateClassDeclaration(node, nextModifiers.length > 0 ? nextModifiers : undefined, node.name, node.typeParameters, node.heritageClauses, node.members);
|
|
359
|
+
};
|
|
360
|
+
return node => ts.visitNode(node, visit);
|
|
361
|
+
};
|
|
362
|
+
const transformed = ts.transform(sourceFile, [transformer]).transformed[0];
|
|
363
|
+
if (!hasStructuralChange) {
|
|
364
|
+
return {
|
|
365
|
+
changed: false,
|
|
366
|
+
source,
|
|
367
|
+
warnings
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
let nextSource = printer.printFile(transformed);
|
|
371
|
+
if (options.rewriteScope && addedScopeDecorator) {
|
|
372
|
+
const nextSourceFile = parseSource(nextSource, filePath);
|
|
373
|
+
nextSource = printSourceFile(nextSourceFile, mergeNamedImport([...nextSourceFile.statements], '@fluojs/core', [{
|
|
374
|
+
imported: 'Scope',
|
|
375
|
+
local: scopeDecoratorName
|
|
376
|
+
}]));
|
|
377
|
+
}
|
|
378
|
+
if (options.removeInjectable) {
|
|
379
|
+
const withoutInjectableImport = removeImportBinding(nextSource, filePath, '@nestjs/common', 'Injectable');
|
|
380
|
+
nextSource = withoutInjectableImport.source;
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
changed: nextSource !== source,
|
|
384
|
+
source: nextSource,
|
|
385
|
+
warnings
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function rewriteBootstrap(source, filePath) {
|
|
389
|
+
const sourceFile = parseSource(source, filePath);
|
|
390
|
+
const warnings = [];
|
|
391
|
+
const createCalls = new Map();
|
|
392
|
+
const listenCalls = new Map();
|
|
393
|
+
const portFoldedApps = new Set();
|
|
394
|
+
const rewrittenCreateCallKeys = new Set();
|
|
395
|
+
const warnedCreateCallKeys = new Set();
|
|
396
|
+
function toCallKey(callExpression) {
|
|
397
|
+
return `${callExpression.pos}:${callExpression.end}`;
|
|
398
|
+
}
|
|
399
|
+
function warnUnsupportedCreate(callExpression, reason) {
|
|
400
|
+
const key = toCallKey(callExpression);
|
|
401
|
+
if (warnedCreateCallKeys.has(key)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
warnedCreateCallKeys.add(key);
|
|
405
|
+
warnings.push(buildWarning(filePath, sourceFile, callExpression, 'bootstrap-unsupported', `${reason} Keep this Nest bootstrap path and migrate manually.`));
|
|
406
|
+
}
|
|
407
|
+
function isSupportedCreateCall(callExpression) {
|
|
408
|
+
if (callExpression.typeArguments && callExpression.typeArguments.length > 0) {
|
|
409
|
+
return {
|
|
410
|
+
reason: 'Unsupported NestFactory.create type-argument usage.',
|
|
411
|
+
supported: false
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (callExpression.arguments.length === 0 || callExpression.arguments.length > 2) {
|
|
415
|
+
return {
|
|
416
|
+
reason: 'Unsupported NestFactory.create argument shape.',
|
|
417
|
+
supported: false
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (callExpression.arguments.length === 2 && !ts.isObjectLiteralExpression(callExpression.arguments[1])) {
|
|
421
|
+
return {
|
|
422
|
+
reason: 'Unsupported NestFactory.create adapter-specific startup form.',
|
|
423
|
+
supported: false
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
supported: true
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const inspect = node => {
|
|
431
|
+
if (ts.isVariableDeclaration(node) && node.initializer) {
|
|
432
|
+
const initializer = ts.isAwaitExpression(node.initializer) ? node.initializer.expression : node.initializer;
|
|
433
|
+
if (ts.isIdentifier(node.name) && ts.isCallExpression(initializer) && ts.isPropertyAccessExpression(initializer.expression) && ts.isIdentifier(initializer.expression.expression) && initializer.expression.expression.text === 'NestFactory' && initializer.expression.name.text === 'create') {
|
|
434
|
+
createCalls.set(node.name.text, initializer);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.name.text === 'listen') {
|
|
438
|
+
listenCalls.set(node.expression.expression.text, node);
|
|
439
|
+
}
|
|
440
|
+
ts.forEachChild(node, inspect);
|
|
441
|
+
};
|
|
442
|
+
inspect(sourceFile);
|
|
443
|
+
const transformer = context => {
|
|
444
|
+
const visit = node => {
|
|
445
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
446
|
+
if (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'NestFactory' && node.expression.name.text === 'create') {
|
|
447
|
+
const support = isSupportedCreateCall(node);
|
|
448
|
+
if (!support.supported) {
|
|
449
|
+
warnUnsupportedCreate(node, support.reason);
|
|
450
|
+
return node;
|
|
451
|
+
}
|
|
452
|
+
let nextArgs = [...node.arguments];
|
|
453
|
+
const ownerEntry = [...createCalls.entries()].find(([, callExpression]) => callExpression.pos === node.pos && callExpression.end === node.end);
|
|
454
|
+
if (ownerEntry) {
|
|
455
|
+
const [appVariable] = ownerEntry;
|
|
456
|
+
const listenCall = listenCalls.get(appVariable);
|
|
457
|
+
if (listenCall && listenCall.arguments.length === 1) {
|
|
458
|
+
const [portExpression] = listenCall.arguments;
|
|
459
|
+
if (nextArgs.length === 1) {
|
|
460
|
+
nextArgs = [nextArgs[0], ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment('port', portExpression)], true)];
|
|
461
|
+
portFoldedApps.add(appVariable);
|
|
462
|
+
} else if (nextArgs.length === 2 && ts.isObjectLiteralExpression(nextArgs[1])) {
|
|
463
|
+
const hasPort = nextArgs[1].properties.some(property => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'port');
|
|
464
|
+
if (!hasPort) {
|
|
465
|
+
nextArgs = [nextArgs[0], ts.factory.updateObjectLiteralExpression(nextArgs[1], [...nextArgs[1].properties, ts.factory.createPropertyAssignment('port', portExpression)])];
|
|
466
|
+
portFoldedApps.add(appVariable);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!portFoldedApps.has(appVariable)) {
|
|
470
|
+
warnings.push(buildWarning(filePath, sourceFile, node, 'bootstrap-port', 'Unable to move listen() port argument into FluoFactory.create options. Review bootstrap manually.'));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
rewrittenCreateCallKeys.add(toCallKey(node));
|
|
475
|
+
return ts.factory.updateCallExpression(node, ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('FluoFactory'), ts.factory.createIdentifier('create')), undefined, nextArgs);
|
|
476
|
+
}
|
|
477
|
+
if (ts.isIdentifier(node.expression.expression) && node.expression.name.text === 'listen') {
|
|
478
|
+
const appVariable = node.expression.expression.text;
|
|
479
|
+
if (createCalls.has(appVariable) && node.arguments.length > 0 && portFoldedApps.has(appVariable)) {
|
|
480
|
+
return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, []);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return ts.visitEachChild(node, visit, context);
|
|
485
|
+
};
|
|
486
|
+
return node => ts.visitNode(node, visit);
|
|
487
|
+
};
|
|
488
|
+
const transformed = ts.transform(sourceFile, [transformer]).transformed[0];
|
|
489
|
+
if (rewrittenCreateCallKeys.size === 0) {
|
|
490
|
+
return {
|
|
491
|
+
changed: false,
|
|
492
|
+
source,
|
|
493
|
+
warnings
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
let nextSource = printer.printFile(transformed);
|
|
497
|
+
const removed = removeImportBinding(nextSource, filePath, '@nestjs/core', 'NestFactory');
|
|
498
|
+
nextSource = removed.source;
|
|
499
|
+
const nextSourceFile = parseSource(nextSource, filePath);
|
|
500
|
+
const withRuntimeImport = printSourceFile(nextSourceFile, mergeNamedImport([...nextSourceFile.statements], '@fluojs/runtime', [{
|
|
501
|
+
imported: 'FluoFactory',
|
|
502
|
+
local: 'FluoFactory'
|
|
503
|
+
}]));
|
|
504
|
+
return {
|
|
505
|
+
changed: withRuntimeImport !== source,
|
|
506
|
+
source: withRuntimeImport,
|
|
507
|
+
warnings
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function rewriteTesting(source, filePath) {
|
|
511
|
+
const sourceFile = parseSource(source, filePath);
|
|
512
|
+
const warnings = [];
|
|
513
|
+
let convertedCalls = 0;
|
|
514
|
+
const supportedBuilderMethods = new Set(['compile', 'overrideProvider', 'overrideProviders', 'overrideGuard', 'overrideInterceptor', 'overrideFilter', 'overrideModule']);
|
|
515
|
+
const convertTestingMetadata = callExpression => {
|
|
516
|
+
if (callExpression.arguments.length !== 1) {
|
|
517
|
+
return {
|
|
518
|
+
warning: 'Unsupported Test.createTestingModule call shape. Expected exactly one metadata object argument.'
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const [metadataArgument] = callExpression.arguments;
|
|
522
|
+
if (!metadataArgument || !ts.isObjectLiteralExpression(metadataArgument)) {
|
|
523
|
+
return {
|
|
524
|
+
warning: 'Unsupported Test.createTestingModule metadata shape. Expected an object literal.'
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
const properties = metadataArgument.properties;
|
|
528
|
+
const propertyAssignments = properties.filter(ts.isPropertyAssignment);
|
|
529
|
+
const unsupportedPropertyKinds = properties.some(property => !ts.isPropertyAssignment(property));
|
|
530
|
+
if (unsupportedPropertyKinds) {
|
|
531
|
+
return {
|
|
532
|
+
warning: 'Unsupported Test.createTestingModule metadata shape. Manual migration required.'
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const getProperty = name => {
|
|
536
|
+
for (const property of propertyAssignments) {
|
|
537
|
+
if (ts.isIdentifier(property.name) && property.name.text === name) {
|
|
538
|
+
return property;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return undefined;
|
|
542
|
+
};
|
|
543
|
+
const rootModuleProperty = getProperty('rootModule');
|
|
544
|
+
if (rootModuleProperty) {
|
|
545
|
+
if (propertyAssignments.length !== 1) {
|
|
546
|
+
return {
|
|
547
|
+
warning: 'Unsupported Test.createTestingModule metadata shape. Keep only rootModule for automatic rewrite.'
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
convertedArgument: ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment('rootModule', rootModuleProperty.initializer)], true)
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
const importsProperty = getProperty('imports');
|
|
555
|
+
if (!importsProperty || propertyAssignments.length !== 1 || !ts.isArrayLiteralExpression(importsProperty.initializer) || importsProperty.initializer.elements.length !== 1) {
|
|
556
|
+
return {
|
|
557
|
+
warning: 'Unsupported Test.createTestingModule metadata shape. Expected { imports: [RootModule] } or { rootModule: RootModule }.'
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const [rootModuleExpression] = importsProperty.initializer.elements;
|
|
561
|
+
return {
|
|
562
|
+
convertedArgument: ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment('rootModule', rootModuleExpression)], true)
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
const hasNestTestCreateCall = file => {
|
|
566
|
+
let found = false;
|
|
567
|
+
const inspect = node => {
|
|
568
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'Test' && node.expression.name.text === 'createTestingModule') {
|
|
569
|
+
found = true;
|
|
570
|
+
}
|
|
571
|
+
if (!found) {
|
|
572
|
+
ts.forEachChild(node, inspect);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
inspect(file);
|
|
576
|
+
return found;
|
|
577
|
+
};
|
|
578
|
+
const transformer = context => {
|
|
579
|
+
const visit = node => {
|
|
580
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'Test' && node.expression.name.text === 'createTestingModule') {
|
|
581
|
+
let cursor = node;
|
|
582
|
+
while (true) {
|
|
583
|
+
if (ts.isPropertyAccessExpression(cursor.parent) && cursor.parent.expression === cursor && ts.isCallExpression(cursor.parent.parent) && cursor.parent.parent.expression === cursor.parent) {
|
|
584
|
+
const methodName = cursor.parent.name.text;
|
|
585
|
+
if (!supportedBuilderMethods.has(methodName)) {
|
|
586
|
+
warnings.push(buildWarning(filePath, sourceFile, cursor.parent, 'testing-unsupported', `Unsupported testing builder method "${methodName}" after Test.createTestingModule. Keep Nest testing chain and migrate manually.`));
|
|
587
|
+
return node;
|
|
588
|
+
}
|
|
589
|
+
cursor = cursor.parent.parent;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
const conversion = convertTestingMetadata(node);
|
|
595
|
+
if ('warning' in conversion) {
|
|
596
|
+
warnings.push(buildWarning(filePath, sourceFile, node, 'testing-unsupported', `${conversion.warning} Keep Nest testing metadata and migrate this test manually.`));
|
|
597
|
+
return node;
|
|
598
|
+
}
|
|
599
|
+
convertedCalls += 1;
|
|
600
|
+
return ts.factory.updateCallExpression(node, ts.factory.createIdentifier('createTestingModule'), node.typeArguments, [conversion.convertedArgument]);
|
|
601
|
+
}
|
|
602
|
+
return ts.visitEachChild(node, visit, context);
|
|
603
|
+
};
|
|
604
|
+
return node => ts.visitNode(node, visit);
|
|
605
|
+
};
|
|
606
|
+
const transformed = ts.transform(sourceFile, [transformer]).transformed[0];
|
|
607
|
+
if (convertedCalls === 0) {
|
|
608
|
+
return {
|
|
609
|
+
changed: false,
|
|
610
|
+
source,
|
|
611
|
+
warnings
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
let nextSource = printer.printFile(transformed);
|
|
615
|
+
const nextSourceFile = parseSource(nextSource, filePath);
|
|
616
|
+
nextSource = printSourceFile(nextSourceFile, mergeNamedImport([...nextSourceFile.statements], '@fluojs/testing', [{
|
|
617
|
+
imported: 'createTestingModule',
|
|
618
|
+
local: 'createTestingModule'
|
|
619
|
+
}]));
|
|
620
|
+
const withFluoImportSourceFile = parseSource(nextSource, filePath);
|
|
621
|
+
if (!hasNestTestCreateCall(withFluoImportSourceFile)) {
|
|
622
|
+
const removedTest = removeImportBinding(nextSource, filePath, '@nestjs/testing', 'Test');
|
|
623
|
+
nextSource = removedTest.source;
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
changed: nextSource !== source,
|
|
627
|
+
source: nextSource,
|
|
628
|
+
warnings
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function rewriteTsconfig(source, filePath) {
|
|
632
|
+
try {
|
|
633
|
+
const parsed = JSON.parse(source);
|
|
634
|
+
if (!parsed.compilerOptions) {
|
|
635
|
+
return {
|
|
636
|
+
changed: false,
|
|
637
|
+
source,
|
|
638
|
+
warnings: []
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const nextCompilerOptions = {
|
|
642
|
+
...parsed.compilerOptions
|
|
643
|
+
};
|
|
644
|
+
const baseUrl = typeof nextCompilerOptions.baseUrl === 'string' ? nextCompilerOptions.baseUrl : undefined;
|
|
645
|
+
const rewrittenPaths = rewriteBaseUrlPaths(nextCompilerOptions.paths, baseUrl);
|
|
646
|
+
const hadExperimentalDecorators = 'experimentalDecorators' in nextCompilerOptions;
|
|
647
|
+
const hadDecoratorMetadata = 'emitDecoratorMetadata' in nextCompilerOptions;
|
|
648
|
+
const hadBaseUrl = typeof nextCompilerOptions.baseUrl === 'string';
|
|
649
|
+
const shouldDropBaseUrl = hadBaseUrl && !!baseUrl && (normalizeBaseUrl(baseUrl) === '.' || !!rewrittenPaths);
|
|
650
|
+
delete nextCompilerOptions.experimentalDecorators;
|
|
651
|
+
delete nextCompilerOptions.emitDecoratorMetadata;
|
|
652
|
+
if (rewrittenPaths) {
|
|
653
|
+
nextCompilerOptions.paths = rewrittenPaths;
|
|
654
|
+
}
|
|
655
|
+
if (shouldDropBaseUrl) {
|
|
656
|
+
delete nextCompilerOptions.baseUrl;
|
|
657
|
+
}
|
|
658
|
+
if (!hadExperimentalDecorators && !hadDecoratorMetadata && !rewrittenPaths && !shouldDropBaseUrl) {
|
|
659
|
+
return {
|
|
660
|
+
changed: false,
|
|
661
|
+
source,
|
|
662
|
+
warnings: []
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
changed: true,
|
|
667
|
+
source: `${JSON.stringify({
|
|
668
|
+
...parsed,
|
|
669
|
+
compilerOptions: nextCompilerOptions
|
|
670
|
+
}, null, 2)}\n`,
|
|
671
|
+
warnings: []
|
|
672
|
+
};
|
|
673
|
+
} catch {
|
|
674
|
+
return {
|
|
675
|
+
changed: false,
|
|
676
|
+
source,
|
|
677
|
+
warnings: [{
|
|
678
|
+
category: 'tsconfig-parse',
|
|
679
|
+
filePath,
|
|
680
|
+
line: 1,
|
|
681
|
+
message: 'Failed to parse tsconfig.json. Rewrite it manually.'
|
|
682
|
+
}]
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function normalizeBaseUrl(baseUrl) {
|
|
687
|
+
const normalized = posix.normalize(baseUrl.replace(/\\/g, '/'));
|
|
688
|
+
return normalized === '' ? '.' : normalized;
|
|
689
|
+
}
|
|
690
|
+
function rewriteBaseUrlPathTarget(target, baseUrl) {
|
|
691
|
+
if (target === '') {
|
|
692
|
+
return target;
|
|
693
|
+
}
|
|
694
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
695
|
+
if (normalizedBaseUrl === '.') {
|
|
696
|
+
return target;
|
|
697
|
+
}
|
|
698
|
+
if (/^(?:[A-Za-z]:|\/|[a-z]+:)/.test(target)) {
|
|
699
|
+
return target;
|
|
700
|
+
}
|
|
701
|
+
return posix.join(normalizedBaseUrl, target.replace(/\\/g, '/'));
|
|
702
|
+
}
|
|
703
|
+
function rewriteBaseUrlPaths(paths, baseUrl) {
|
|
704
|
+
if (!baseUrl || !paths || Array.isArray(paths) || typeof paths !== 'object') {
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
707
|
+
const rewrittenEntries = Object.entries(paths).map(([alias, targets]) => {
|
|
708
|
+
if (!Array.isArray(targets) || !targets.every(target => typeof target === 'string')) {
|
|
709
|
+
return [alias, targets];
|
|
710
|
+
}
|
|
711
|
+
return [alias, targets.map(target => rewriteBaseUrlPathTarget(target, baseUrl))];
|
|
712
|
+
});
|
|
713
|
+
return Object.fromEntries(rewrittenEntries);
|
|
714
|
+
}
|
|
715
|
+
function detectManualFollowUps(source, filePath) {
|
|
716
|
+
const sourceFile = parseSource(source, filePath);
|
|
717
|
+
const warnings = [];
|
|
718
|
+
let hasRequestDecoratorWarning = false;
|
|
719
|
+
let hasInjectParameterWarning = false;
|
|
720
|
+
let hasPipesWarning = false;
|
|
721
|
+
const visit = node => {
|
|
722
|
+
if (ts.isParameter(node) && node.modifiers) {
|
|
723
|
+
for (const modifier of node.modifiers) {
|
|
724
|
+
if (!ts.isDecorator(modifier) || !ts.isCallExpression(modifier.expression) || !ts.isIdentifier(modifier.expression.expression)) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
const decoratorName = modifier.expression.expression.text;
|
|
728
|
+
if (!hasInjectParameterWarning && decoratorName === 'Inject') {
|
|
729
|
+
hasInjectParameterWarning = true;
|
|
730
|
+
warnings.push(buildWarning(filePath, sourceFile, modifier, 'inject-token', 'Constructor @Inject(TOKEN) parameter decorators need manual migration to class-level @Inject(TOKEN, ...) syntax.'));
|
|
731
|
+
}
|
|
732
|
+
if (!hasRequestDecoratorWarning && REQUEST_DTO_DECORATORS.has(decoratorName)) {
|
|
733
|
+
hasRequestDecoratorWarning = true;
|
|
734
|
+
warnings.push(buildWarning(filePath, sourceFile, modifier, 'request-dto', 'Handler parameter decorators should be reviewed for RequestDto + DTO field decorator migration.'));
|
|
735
|
+
}
|
|
736
|
+
if (!hasPipesWarning && decoratorName === 'UsePipes') {
|
|
737
|
+
hasPipesWarning = true;
|
|
738
|
+
warnings.push(buildWarning(filePath, sourceFile, modifier, 'pipe-converter', 'Detected @UsePipes usage. Migrate transform/pipe logic to converters + RequestDto validation.'));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (!hasPipesWarning && ts.isIdentifier(node) && /(?:ValidationPipe|Parse\w*Pipe)$/.test(node.text)) {
|
|
743
|
+
hasPipesWarning = true;
|
|
744
|
+
warnings.push(buildWarning(filePath, sourceFile, node, 'pipe-converter', 'Detected Nest pipe usage. Review converter migration manually.'));
|
|
745
|
+
}
|
|
746
|
+
ts.forEachChild(node, visit);
|
|
747
|
+
};
|
|
748
|
+
visit(sourceFile);
|
|
749
|
+
return warnings;
|
|
750
|
+
}
|
|
751
|
+
function gatherTargetFiles(targetPath, includeTsconfig) {
|
|
752
|
+
const resolvedPath = resolve(targetPath);
|
|
753
|
+
const stats = statSync(resolvedPath);
|
|
754
|
+
if (stats.isFile()) {
|
|
755
|
+
const extension = extname(resolvedPath);
|
|
756
|
+
if (extension === '.ts' || extension === '.tsx') {
|
|
757
|
+
return [resolvedPath];
|
|
758
|
+
}
|
|
759
|
+
if (includeTsconfig && basename(resolvedPath) === 'tsconfig.json') {
|
|
760
|
+
return [resolvedPath];
|
|
761
|
+
}
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
const collected = [];
|
|
765
|
+
const queue = [resolvedPath];
|
|
766
|
+
while (queue.length > 0) {
|
|
767
|
+
const current = queue.pop();
|
|
768
|
+
if (!current) {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
for (const entry of readdirSync(current, {
|
|
772
|
+
withFileTypes: true
|
|
773
|
+
})) {
|
|
774
|
+
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'coverage') {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const absolutePath = resolve(current, entry.name);
|
|
778
|
+
if (entry.isDirectory()) {
|
|
779
|
+
queue.push(absolutePath);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
|
|
783
|
+
collected.push(absolutePath);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (includeTsconfig && entry.isFile() && entry.name === 'tsconfig.json') {
|
|
787
|
+
collected.push(absolutePath);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return collected.sort((left, right) => left.localeCompare(right));
|
|
792
|
+
}
|
|
793
|
+
function runTypeScriptTransforms(source, filePath, enabledTransforms) {
|
|
794
|
+
let nextSource = source;
|
|
795
|
+
const appliedTransforms = [];
|
|
796
|
+
const warnings = [];
|
|
797
|
+
if (enabledTransforms.has('imports')) {
|
|
798
|
+
const rewritten = rewriteImports(nextSource, filePath);
|
|
799
|
+
nextSource = rewritten.source;
|
|
800
|
+
warnings.push(...rewritten.warnings);
|
|
801
|
+
if (rewritten.changed) {
|
|
802
|
+
appliedTransforms.push('imports');
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (enabledTransforms.has('injectable') || enabledTransforms.has('scope')) {
|
|
806
|
+
const rewritten = rewriteInjectableAndScope(nextSource, filePath, {
|
|
807
|
+
removeInjectable: enabledTransforms.has('injectable'),
|
|
808
|
+
rewriteScope: enabledTransforms.has('scope')
|
|
809
|
+
});
|
|
810
|
+
nextSource = rewritten.source;
|
|
811
|
+
warnings.push(...rewritten.warnings);
|
|
812
|
+
if (rewritten.changed) {
|
|
813
|
+
if (enabledTransforms.has('injectable')) {
|
|
814
|
+
appliedTransforms.push('injectable');
|
|
815
|
+
}
|
|
816
|
+
if (enabledTransforms.has('scope')) {
|
|
817
|
+
appliedTransforms.push('scope');
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (enabledTransforms.has('bootstrap')) {
|
|
822
|
+
const rewritten = rewriteBootstrap(nextSource, filePath);
|
|
823
|
+
nextSource = rewritten.source;
|
|
824
|
+
warnings.push(...rewritten.warnings);
|
|
825
|
+
if (rewritten.changed) {
|
|
826
|
+
appliedTransforms.push('bootstrap');
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (enabledTransforms.has('testing')) {
|
|
830
|
+
const rewritten = rewriteTesting(nextSource, filePath);
|
|
831
|
+
nextSource = rewritten.source;
|
|
832
|
+
warnings.push(...rewritten.warnings);
|
|
833
|
+
if (rewritten.changed) {
|
|
834
|
+
appliedTransforms.push('testing');
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
warnings.push(...detectManualFollowUps(source, filePath));
|
|
838
|
+
return {
|
|
839
|
+
appliedTransforms: [...new Set(appliedTransforms)],
|
|
840
|
+
source: nextSource,
|
|
841
|
+
warnings
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
export function runNestJsMigration(options) {
|
|
845
|
+
const resolvedTargetPath = resolve(options.targetPath);
|
|
846
|
+
if (!existsSync(resolvedTargetPath)) {
|
|
847
|
+
throw new Error(`Migration target does not exist: ${resolvedTargetPath}`);
|
|
848
|
+
}
|
|
849
|
+
const includeTsconfig = options.enabledTransforms.has('tsconfig');
|
|
850
|
+
const files = gatherTargetFiles(resolvedTargetPath, includeTsconfig);
|
|
851
|
+
const fileResults = [];
|
|
852
|
+
for (const filePath of files) {
|
|
853
|
+
const source = readFileSync(filePath, 'utf8');
|
|
854
|
+
const isTsconfig = basename(filePath) === 'tsconfig.json';
|
|
855
|
+
if (isTsconfig) {
|
|
856
|
+
const rewritten = rewriteTsconfig(source, filePath);
|
|
857
|
+
const changed = rewritten.changed;
|
|
858
|
+
if (changed && options.apply) {
|
|
859
|
+
writeFileSync(filePath, rewritten.source, 'utf8');
|
|
860
|
+
}
|
|
861
|
+
fileResults.push({
|
|
862
|
+
appliedTransforms: changed ? ['tsconfig'] : [],
|
|
863
|
+
changed,
|
|
864
|
+
filePath,
|
|
865
|
+
warnings: rewritten.warnings
|
|
866
|
+
});
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const rewritten = runTypeScriptTransforms(source, filePath, options.enabledTransforms);
|
|
870
|
+
const changed = rewritten.source !== source;
|
|
871
|
+
if (changed && options.apply) {
|
|
872
|
+
writeFileSync(filePath, rewritten.source, 'utf8');
|
|
873
|
+
}
|
|
874
|
+
fileResults.push({
|
|
875
|
+
appliedTransforms: rewritten.appliedTransforms,
|
|
876
|
+
changed,
|
|
877
|
+
filePath,
|
|
878
|
+
warnings: rewritten.warnings
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
apply: options.apply,
|
|
883
|
+
changedFiles: fileResults.filter(result => result.changed).length,
|
|
884
|
+
scannedFiles: files.length,
|
|
885
|
+
warningCount: fileResults.reduce((total, result) => total + result.warnings.length, 0),
|
|
886
|
+
fileResults
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
export function renderTransformList(kinds) {
|
|
890
|
+
return kinds.map(kind => `${kind} (${TRANSFORM_KIND_LABEL[kind]})`).join(', ');
|
|
891
|
+
}
|