@fluojs/cli 1.1.0 → 3.0.0
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/README.ko.md +183 -22
- package/README.md +186 -22
- package/dist/cli.d.ts +5 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +128 -35
- package/dist/commands/diagnostics.d.ts +35 -0
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +60 -0
- package/dist/commands/generate.d.ts +1 -49
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +1 -214
- package/dist/commands/inspect.d.ts +5 -7
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +85 -65
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +27 -15
- package/dist/commands/migration-transform-tokens.d.ts +15 -0
- package/dist/commands/migration-transform-tokens.d.ts.map +1 -0
- package/dist/commands/migration-transform-tokens.js +41 -0
- package/dist/commands/new.d.ts +0 -6
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +42 -96
- package/dist/commands/output-path-safety.d.ts +14 -0
- package/dist/commands/output-path-safety.d.ts.map +1 -0
- package/dist/commands/output-path-safety.js +34 -0
- package/dist/commands/scripts.d.ts +1 -1
- package/dist/commands/scripts.d.ts.map +1 -1
- package/dist/commands/scripts.js +13 -7
- package/dist/commands/typegen-artifact.d.ts +54 -0
- package/dist/commands/typegen-artifact.d.ts.map +1 -0
- package/dist/commands/typegen-artifact.js +117 -0
- package/dist/commands/typegen-generation-child.d.ts +2 -0
- package/dist/commands/typegen-generation-child.d.ts.map +1 -0
- package/dist/commands/typegen-generation-child.js +59 -0
- package/dist/commands/typegen-generation-process.d.ts +46 -0
- package/dist/commands/typegen-generation-process.d.ts.map +1 -0
- package/dist/commands/typegen-generation-process.js +131 -0
- package/dist/commands/typegen-generation-protocol.d.ts +16 -0
- package/dist/commands/typegen-generation-protocol.d.ts.map +1 -0
- package/dist/commands/typegen-generation-protocol.js +35 -0
- package/dist/commands/typegen-isolated-source.d.ts +10 -0
- package/dist/commands/typegen-isolated-source.d.ts.map +1 -0
- package/dist/commands/typegen-isolated-source.js +75 -0
- package/dist/commands/typegen-options.d.ts +20 -0
- package/dist/commands/typegen-options.d.ts.map +1 -0
- package/dist/commands/typegen-options.js +71 -0
- package/dist/commands/typegen-source.d.ts +59 -0
- package/dist/commands/typegen-source.d.ts.map +1 -0
- package/dist/commands/typegen-source.js +183 -0
- package/dist/commands/typegen-watch.d.ts +46 -0
- package/dist/commands/typegen-watch.d.ts.map +1 -0
- package/dist/commands/typegen-watch.js +212 -0
- package/dist/commands/typegen.d.ts +29 -0
- package/dist/commands/typegen.d.ts.map +1 -0
- package/dist/commands/typegen.js +119 -0
- package/dist/dev-runner/node-restart-runner.d.ts +6 -0
- package/dist/dev-runner/node-restart-runner.d.ts.map +1 -1
- package/dist/dev-runner/node-restart-runner.js +64 -13
- package/dist/fixtures/inspect-react-app.module.d.ts +4 -0
- package/dist/fixtures/inspect-react-app.module.d.ts.map +1 -0
- package/dist/fixtures/inspect-react-app.module.js +32 -0
- package/dist/fixtures/typegen-react-app.module.d.ts +4 -0
- package/dist/fixtures/typegen-react-app.module.d.ts.map +1 -0
- package/dist/fixtures/typegen-react-app.module.js +33 -0
- package/dist/generate-command.d.ts +50 -0
- package/dist/generate-command.d.ts.map +1 -0
- package/dist/generate-command.js +214 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/new/install.js +1 -1
- package/dist/new/package-spec-resolver.d.ts.map +1 -1
- package/dist/new/package-spec-resolver.js +50 -59
- package/dist/new/prompt.d.ts +1 -0
- package/dist/new/prompt.d.ts.map +1 -1
- package/dist/new/prompt.js +40 -12
- package/dist/new/published-internal-dependencies.d.ts +20 -0
- package/dist/new/published-internal-dependencies.d.ts.map +1 -0
- package/dist/new/published-internal-dependencies.js +20 -0
- package/dist/new/react-vite-ssr-scaffold.d.ts +19 -0
- package/dist/new/react-vite-ssr-scaffold.d.ts.map +1 -0
- package/dist/new/react-vite-ssr-scaffold.js +108 -0
- package/dist/new/resolver.d.ts +2 -2
- package/dist/new/resolver.d.ts.map +1 -1
- package/dist/new/resolver.js +5 -1
- package/dist/new/scaffold.d.ts.map +1 -1
- package/dist/new/scaffold.js +177 -54
- package/dist/new/starter-profiles.d.ts +8 -4
- package/dist/new/starter-profiles.d.ts.map +1 -1
- package/dist/new/starter-profiles.js +52 -18
- package/dist/new/templates/react-vite-ssr/README.md.ejs +74 -0
- package/dist/new/templates/react-vite-ssr/playwright.config.ts.ejs +26 -0
- package/dist/new/templates/react-vite-ssr/src/app.test.ts.ejs +82 -0
- package/dist/new/templates/react-vite-ssr/src/app.ts.ejs +91 -0
- package/dist/new/templates/react-vite-ssr/src/entry-client.tsx.ejs +29 -0
- package/dist/new/templates/react-vite-ssr/src/entry-server.tsx.ejs +52 -0
- package/dist/new/templates/react-vite-ssr/src/load-manifest.test.ts.ejs +55 -0
- package/dist/new/templates/react-vite-ssr/src/load-manifest.ts.ejs +53 -0
- package/dist/new/templates/react-vite-ssr/src/main.ts.ejs +18 -0
- package/dist/new/templates/react-vite-ssr/src/page.tsx.ejs +43 -0
- package/dist/new/templates/react-vite-ssr/src/react-app.test.tsx.ejs +67 -0
- package/dist/new/templates/react-vite-ssr/src/react-app.tsx.ejs +46 -0
- package/dist/new/templates/react-vite-ssr/src/styles.css.ejs +25 -0
- package/dist/new/templates/react-vite-ssr/src/styles.d.ts.ejs +1 -0
- package/dist/new/templates/react-vite-ssr/tests/production-hydration.spec.ts.ejs +54 -0
- package/dist/new/templates/react-vite-ssr/tsconfig.json.ejs +16 -0
- package/dist/new/templates/react-vite-ssr/vite.client.config.ts.ejs +24 -0
- package/dist/new/templates/react-vite-ssr/vite.server.config.ts.ejs +17 -0
- package/dist/new/templates/react-vite-ssr/vitest.config.ts.ejs +10 -0
- package/dist/new/types.d.ts +4 -2
- package/dist/new/types.d.ts.map +1 -1
- package/dist/public-generate.d.ts +2 -0
- package/dist/public-generate.d.ts.map +1 -0
- package/dist/public-generate.js +1 -0
- package/dist/public-inspect.d.ts +13 -0
- package/dist/public-inspect.d.ts.map +1 -0
- package/dist/public-inspect.js +16 -0
- package/dist/public-new.d.ts +13 -0
- package/dist/public-new.d.ts.map +1 -0
- package/dist/public-new.js +16 -0
- package/dist/public-typegen.d.ts +14 -0
- package/dist/public-typegen.d.ts.map +1 -0
- package/dist/public-typegen.js +17 -0
- package/dist/run-cli.d.ts +2 -1
- package/dist/run-cli.d.ts.map +1 -1
- package/dist/studio/sidecar.d.ts.map +1 -1
- package/dist/studio/sidecar.js +106 -14
- package/dist/transforms/nestjs-migrate.d.ts +4 -1
- package/dist/transforms/nestjs-migrate.d.ts.map +1 -1
- package/dist/transforms/nestjs-migrate.js +625 -65
- package/dist/typegen-contract.d.ts +10 -0
- package/dist/typegen-contract.d.ts.map +1 -0
- package/dist/typegen-contract.js +9 -0
- package/dist/usage.d.ts +19 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +178 -0
- package/package.json +19 -10
|
@@ -14,7 +14,7 @@ export const MIGRATION_TRANSFORMS = ['imports', 'injectable', 'scope', 'bootstra
|
|
|
14
14
|
/**
|
|
15
15
|
* Provides the warning categories value.
|
|
16
16
|
*/
|
|
17
|
-
export const WARNING_CATEGORIES = ['inject-token', 'request-dto', 'pipe-converter', 'bootstrap-unsupported', 'testing-unsupported', 'import-unsupported', 'injectable-options', 'tsconfig-parse', 'bootstrap-port'];
|
|
17
|
+
export const WARNING_CATEGORIES = ['inject-token', 'inject-token-unsupported', 'request-dto', 'pipe-converter', 'bootstrap-unsupported', 'testing-unsupported', 'import-unsupported', 'injectable-options', 'tsconfig-parse', 'bootstrap-port'];
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Defines the warning category type.
|
|
@@ -26,6 +26,7 @@ export const WARNING_CATEGORIES = ['inject-token', 'request-dto', 'pipe-converte
|
|
|
26
26
|
|
|
27
27
|
const WARNING_CATEGORY_LABEL = {
|
|
28
28
|
'inject-token': 'DI token migration (@Inject)',
|
|
29
|
+
'inject-token-unsupported': 'Unsupported constructor injection',
|
|
29
30
|
'request-dto': 'Request DTO migration (handler parameter decorators)',
|
|
30
31
|
'pipe-converter': 'Pipe/converter migration',
|
|
31
32
|
'bootstrap-unsupported': 'Unsupported bootstrap variant',
|
|
@@ -106,8 +107,8 @@ const NEST_COMMON_TO_FLUO = {
|
|
|
106
107
|
const REQUEST_DTO_DECORATORS = new Set(['Body', 'Param', 'Query']);
|
|
107
108
|
const TRANSFORM_KIND_LABEL = {
|
|
108
109
|
bootstrap: 'bootstrap rewrite',
|
|
109
|
-
imports: 'import rewriting',
|
|
110
110
|
injectable: '@Injectable removal',
|
|
111
|
+
imports: 'import rewriting',
|
|
111
112
|
scope: 'scope mapping',
|
|
112
113
|
testing: 'testing rewrite',
|
|
113
114
|
tsconfig: 'tsconfig rewrite'
|
|
@@ -131,14 +132,15 @@ function getImportBindings(importDeclaration) {
|
|
|
131
132
|
}
|
|
132
133
|
return importClause.namedBindings.elements.map(element => ({
|
|
133
134
|
imported: (element.propertyName ?? element.name).text,
|
|
135
|
+
isTypeOnly: importClause.isTypeOnly || element.isTypeOnly,
|
|
134
136
|
local: element.name.text
|
|
135
137
|
}));
|
|
136
138
|
}
|
|
137
139
|
function createImportSpecifier(binding) {
|
|
138
140
|
if (binding.imported === binding.local) {
|
|
139
|
-
return ts.factory.createImportSpecifier(
|
|
141
|
+
return ts.factory.createImportSpecifier(binding.isTypeOnly, undefined, ts.factory.createIdentifier(binding.local));
|
|
140
142
|
}
|
|
141
|
-
return ts.factory.createImportSpecifier(
|
|
143
|
+
return ts.factory.createImportSpecifier(binding.isTypeOnly, ts.factory.createIdentifier(binding.imported), ts.factory.createIdentifier(binding.local));
|
|
142
144
|
}
|
|
143
145
|
function toBindingKey(binding) {
|
|
144
146
|
return `${binding.imported}::${binding.local}`;
|
|
@@ -151,34 +153,48 @@ function updateNamedImports(importDeclaration, bindings) {
|
|
|
151
153
|
if (bindings.length === 0 && !importClause.name) {
|
|
152
154
|
return undefined;
|
|
153
155
|
}
|
|
154
|
-
const
|
|
156
|
+
const isTypeOnlyClause = !importClause.name && bindings.length > 0 && bindings.every(binding => binding.isTypeOnly);
|
|
157
|
+
const updatedClause = ts.factory.updateImportClause(importClause, isTypeOnlyClause, importClause.name, bindings.length > 0 ? ts.factory.createNamedImports(bindings.map(binding => createImportSpecifier({
|
|
158
|
+
...binding,
|
|
159
|
+
isTypeOnly: isTypeOnlyClause ? false : binding.isTypeOnly
|
|
160
|
+
}))) : undefined);
|
|
155
161
|
return ts.factory.updateImportDeclaration(importDeclaration, importDeclaration.modifiers, updatedClause, importDeclaration.moduleSpecifier, importDeclaration.attributes);
|
|
156
162
|
}
|
|
157
|
-
function mergeNamedImport(statements, moduleSpecifier, newBindings) {
|
|
163
|
+
function mergeNamedImport(statements, moduleSpecifier, newBindings, preserveTypeOnlyImport = false) {
|
|
158
164
|
if (newBindings.length === 0) {
|
|
159
165
|
return statements;
|
|
160
166
|
}
|
|
161
|
-
const deduped = new Map();
|
|
162
|
-
for (const binding of newBindings) {
|
|
163
|
-
deduped.set(toBindingKey(binding), binding);
|
|
164
|
-
}
|
|
165
167
|
let merged = false;
|
|
166
168
|
const updated = statements.map(statement => {
|
|
167
|
-
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== moduleSpecifier) {
|
|
169
|
+
if (merged || !ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== moduleSpecifier || preserveTypeOnlyImport && statement.importClause?.isTypeOnly) {
|
|
168
170
|
return statement;
|
|
169
171
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
if (!statement.importClause?.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
173
|
+
return statement;
|
|
174
|
+
}
|
|
175
|
+
const bindings = new Map();
|
|
176
|
+
for (const binding of getImportBindings(statement)) {
|
|
177
|
+
bindings.set(toBindingKey(binding), binding);
|
|
178
|
+
}
|
|
179
|
+
for (const binding of newBindings) {
|
|
180
|
+
const existing = bindings.get(toBindingKey(binding));
|
|
181
|
+
if (!existing || !binding.isTypeOnly) {
|
|
182
|
+
bindings.set(toBindingKey(binding), binding);
|
|
183
|
+
}
|
|
173
184
|
}
|
|
174
185
|
merged = true;
|
|
175
|
-
const mergedBindings = [...
|
|
186
|
+
const mergedBindings = [...bindings.values()].sort((left, right) => left.local.localeCompare(right.local));
|
|
176
187
|
return updateNamedImports(statement, mergedBindings) ?? statement;
|
|
177
188
|
});
|
|
178
189
|
if (merged) {
|
|
179
190
|
return updated;
|
|
180
191
|
}
|
|
181
|
-
const
|
|
192
|
+
const bindings = [...newBindings].sort((left, right) => left.local.localeCompare(right.local));
|
|
193
|
+
const isTypeOnlyClause = bindings.every(binding => binding.isTypeOnly);
|
|
194
|
+
const importDeclaration = ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(isTypeOnlyClause, undefined, ts.factory.createNamedImports(bindings.map(binding => createImportSpecifier({
|
|
195
|
+
...binding,
|
|
196
|
+
isTypeOnly: isTypeOnlyClause ? false : binding.isTypeOnly
|
|
197
|
+
})))), ts.factory.createStringLiteral(moduleSpecifier));
|
|
182
198
|
const importIndexes = updated.map((statement, index) => ({
|
|
183
199
|
index,
|
|
184
200
|
isImport: ts.isImportDeclaration(statement)
|
|
@@ -227,11 +243,42 @@ function removeImportBinding(source, sourceFilePath, moduleSpecifier, importedNa
|
|
|
227
243
|
source: printSourceFile(sourceFile, statements)
|
|
228
244
|
};
|
|
229
245
|
}
|
|
230
|
-
function
|
|
246
|
+
function removeNestFactoryValueImports(source, sourceFilePath) {
|
|
247
|
+
const sourceFile = parseSource(source, sourceFilePath);
|
|
248
|
+
let changed = false;
|
|
249
|
+
const statements = [];
|
|
250
|
+
for (const statement of sourceFile.statements) {
|
|
251
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== '@nestjs/core') {
|
|
252
|
+
statements.push(statement);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const bindings = getImportBindings(statement);
|
|
256
|
+
const filtered = bindings.filter(binding => binding.imported !== 'NestFactory' || binding.isTypeOnly);
|
|
257
|
+
if (filtered.length !== bindings.length) {
|
|
258
|
+
changed = true;
|
|
259
|
+
}
|
|
260
|
+
const updated = updateNamedImports(statement, filtered);
|
|
261
|
+
if (updated) {
|
|
262
|
+
statements.push(updated);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!changed) {
|
|
266
|
+
return {
|
|
267
|
+
changed: false,
|
|
268
|
+
source
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
changed: true,
|
|
273
|
+
source: printSourceFile(sourceFile, statements)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function rewriteImports(source, filePath, allowedNestImports) {
|
|
231
277
|
const sourceFile = parseSource(source, filePath);
|
|
232
278
|
const warnings = [];
|
|
233
279
|
const additions = new Map();
|
|
234
280
|
const statements = [];
|
|
281
|
+
const hasNestInjectParameter = hasNestInjectParameterDecorator(sourceFile, getNestInjectLocalNames(sourceFile));
|
|
235
282
|
let touched = false;
|
|
236
283
|
for (const statement of sourceFile.statements) {
|
|
237
284
|
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
@@ -250,6 +297,18 @@ function rewriteImports(source, filePath) {
|
|
|
250
297
|
}
|
|
251
298
|
const remaining = [];
|
|
252
299
|
for (const binding of getImportBindings(statement)) {
|
|
300
|
+
if (binding.isTypeOnly && allowedNestImports) {
|
|
301
|
+
remaining.push(binding);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (allowedNestImports && !allowedNestImports.has(binding.imported)) {
|
|
305
|
+
remaining.push(binding);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (binding.imported === 'Inject' && hasNestInjectParameter) {
|
|
309
|
+
remaining.push(binding);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
253
312
|
const targetModule = NEST_COMMON_TO_FLUO[binding.imported];
|
|
254
313
|
if (!targetModule) {
|
|
255
314
|
remaining.push(binding);
|
|
@@ -259,6 +318,7 @@ function rewriteImports(source, filePath) {
|
|
|
259
318
|
const moduleBindings = additions.get(targetModule) ?? [];
|
|
260
319
|
moduleBindings.push({
|
|
261
320
|
imported: binding.imported,
|
|
321
|
+
isTypeOnly: binding.isTypeOnly,
|
|
262
322
|
local: binding.local
|
|
263
323
|
});
|
|
264
324
|
additions.set(targetModule, moduleBindings);
|
|
@@ -277,7 +337,7 @@ function rewriteImports(source, filePath) {
|
|
|
277
337
|
}
|
|
278
338
|
let nextStatements = statements;
|
|
279
339
|
for (const [moduleSpecifier, bindings] of additions.entries()) {
|
|
280
|
-
nextStatements = mergeNamedImport(nextStatements, moduleSpecifier, bindings);
|
|
340
|
+
nextStatements = mergeNamedImport(nextStatements, moduleSpecifier, bindings, !!allowedNestImports);
|
|
281
341
|
}
|
|
282
342
|
const nextSource = printSourceFile(sourceFile, nextStatements);
|
|
283
343
|
return {
|
|
@@ -304,6 +364,331 @@ function hasDecoratorNamed(modifiers, name) {
|
|
|
304
364
|
}
|
|
305
365
|
return modifiers.some(modifier => ts.isDecorator(modifier) && isDecoratorNamed(modifier, name));
|
|
306
366
|
}
|
|
367
|
+
function getNestInjectLocalNames(sourceFile) {
|
|
368
|
+
const localNames = new Set();
|
|
369
|
+
for (const statement of sourceFile.statements) {
|
|
370
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== '@nestjs/common' || statement.importClause?.isTypeOnly || !statement.importClause?.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
374
|
+
if (!element.isTypeOnly && (element.propertyName ?? element.name).text === 'Inject') {
|
|
375
|
+
localNames.add(element.name.text);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return localNames;
|
|
380
|
+
}
|
|
381
|
+
function getFluoInjectLocalNames(sourceFile) {
|
|
382
|
+
const localNames = new Set();
|
|
383
|
+
for (const statement of sourceFile.statements) {
|
|
384
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== '@fluojs/core' || statement.importClause?.isTypeOnly || !statement.importClause?.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
388
|
+
if (!element.isTypeOnly && (element.propertyName ?? element.name).text === 'Inject') {
|
|
389
|
+
localNames.add(element.name.text);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return localNames;
|
|
394
|
+
}
|
|
395
|
+
function getFluoInjectNamespaceLocalNames(sourceFile) {
|
|
396
|
+
const localNames = new Set();
|
|
397
|
+
for (const statement of sourceFile.statements) {
|
|
398
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== '@fluojs/core' || statement.importClause?.isTypeOnly || !statement.importClause?.namedBindings || !ts.isNamespaceImport(statement.importClause.namedBindings)) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
localNames.add(statement.importClause.namedBindings.name.text);
|
|
402
|
+
}
|
|
403
|
+
return localNames;
|
|
404
|
+
}
|
|
405
|
+
function getNestInjectDecoratorLocalName(decorator, localNames) {
|
|
406
|
+
if (!ts.isCallExpression(decorator.expression) || !ts.isIdentifier(decorator.expression.expression)) {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
const localName = decorator.expression.expression.text;
|
|
410
|
+
return localNames.has(localName) ? localName : undefined;
|
|
411
|
+
}
|
|
412
|
+
function hasNestInjectDecorator(modifiers, localNames) {
|
|
413
|
+
if (!modifiers) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
return modifiers.some(modifier => ts.isDecorator(modifier) && !!getNestInjectDecoratorLocalName(modifier, localNames));
|
|
417
|
+
}
|
|
418
|
+
function getFluoInjectClassDecorator(modifiers, localNames, namespaceLocalNames) {
|
|
419
|
+
if (!modifiers) {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
for (const modifier of modifiers) {
|
|
423
|
+
if (!ts.isDecorator(modifier) || !ts.isCallExpression(modifier.expression)) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const decoratorTarget = modifier.expression.expression;
|
|
427
|
+
if (ts.isIdentifier(decoratorTarget) && localNames.has(decoratorTarget.text) || ts.isPropertyAccessExpression(decoratorTarget) && ts.isIdentifier(decoratorTarget.expression) && namespaceLocalNames.has(decoratorTarget.expression.text) && decoratorTarget.name.text === 'Inject') {
|
|
428
|
+
return {
|
|
429
|
+
decorator: modifier,
|
|
430
|
+
expression: modifier.expression
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
function hasNestInjectParameterDecorator(sourceFile, localNames) {
|
|
437
|
+
let found = false;
|
|
438
|
+
const visit = node => {
|
|
439
|
+
if (found) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (ts.isParameter(node) && hasNestInjectDecorator(node.modifiers, localNames)) {
|
|
443
|
+
found = true;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
ts.forEachChild(node, visit);
|
|
447
|
+
};
|
|
448
|
+
visit(sourceFile);
|
|
449
|
+
return found;
|
|
450
|
+
}
|
|
451
|
+
function getRuntimeValueNames(sourceFile) {
|
|
452
|
+
const names = new Set();
|
|
453
|
+
for (const statement of sourceFile.statements) {
|
|
454
|
+
if ((ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement) || ts.isFunctionDeclaration(statement)) && statement.name) {
|
|
455
|
+
names.add(statement.name.text);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (ts.isVariableStatement(statement)) {
|
|
459
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
460
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
461
|
+
names.add(declaration.name.text);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return names;
|
|
467
|
+
}
|
|
468
|
+
function getCollidingTypeValueNames(sourceFile, runtimeValueNames) {
|
|
469
|
+
const names = new Set();
|
|
470
|
+
for (const statement of sourceFile.statements) {
|
|
471
|
+
if ((ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) && runtimeValueNames.has(statement.name.text)) {
|
|
472
|
+
names.add(statement.name.text);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return names;
|
|
476
|
+
}
|
|
477
|
+
function getTypeOnlyImportNames(sourceFile) {
|
|
478
|
+
const names = new Set();
|
|
479
|
+
for (const statement of sourceFile.statements) {
|
|
480
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (statement.importClause.name && statement.importClause.isTypeOnly) {
|
|
484
|
+
names.add(statement.importClause.name.text);
|
|
485
|
+
}
|
|
486
|
+
if (!statement.importClause.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
490
|
+
if (statement.importClause.isTypeOnly || element.isTypeOnly) {
|
|
491
|
+
names.add(element.name.text);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return names;
|
|
496
|
+
}
|
|
497
|
+
function getInScopeTypeParameterNames(node) {
|
|
498
|
+
const names = new Set();
|
|
499
|
+
for (let current = node; current; current = current.parent) {
|
|
500
|
+
if (!ts.isClassDeclaration(current) && !ts.isFunctionLike(current)) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
for (const typeParameter of current.typeParameters ?? []) {
|
|
504
|
+
names.add(typeParameter.name.text);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return names;
|
|
508
|
+
}
|
|
509
|
+
function createInjectionTokenFromType(type, runtimeValueNames, collidingTypeValueNames, typeOnlyImportNames, inScopeTypeParameterNames) {
|
|
510
|
+
if (!type || !ts.isTypeReferenceNode(type) || !ts.isIdentifier(type.typeName) || !runtimeValueNames.has(type.typeName.text) || collidingTypeValueNames.has(type.typeName.text) || typeOnlyImportNames.has(type.typeName.text) || inScopeTypeParameterNames.has(type.typeName.text)) {
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
return ts.factory.createIdentifier(type.typeName.text);
|
|
514
|
+
}
|
|
515
|
+
function getExistingInjectTokens(expression) {
|
|
516
|
+
if (expression.arguments.length !== 1 || !ts.isArrayLiteralExpression(expression.arguments[0])) {
|
|
517
|
+
return expression.arguments;
|
|
518
|
+
}
|
|
519
|
+
if (expression.arguments[0].elements.some(element => !ts.isExpression(element))) {
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
return expression.arguments[0].elements.filter(ts.isExpression);
|
|
523
|
+
}
|
|
524
|
+
function getConstructorInjectionTokens(classDeclaration, nestInjectLocalNames, runtimeValueNames, collidingTypeValueNames, typeOnlyImportNames, existingInjectTokens, hasExistingInjectDecorator) {
|
|
525
|
+
const constructorDeclaration = classDeclaration.members.find(member => ts.isConstructorDeclaration(member) && member.body !== undefined);
|
|
526
|
+
if (!constructorDeclaration) {
|
|
527
|
+
return {
|
|
528
|
+
kind: 'none'
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
const parameters = constructorDeclaration.parameters;
|
|
532
|
+
const hasInjectParameter = parameters.some(parameter => hasNestInjectDecorator(parameter.modifiers, nestInjectLocalNames));
|
|
533
|
+
if (!hasInjectParameter) {
|
|
534
|
+
return {
|
|
535
|
+
kind: 'none'
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (hasNestInjectDecorator(classDeclaration.modifiers, nestInjectLocalNames)) {
|
|
539
|
+
return {
|
|
540
|
+
kind: 'unsafe',
|
|
541
|
+
node: classDeclaration
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (hasExistingInjectDecorator && !existingInjectTokens) {
|
|
545
|
+
return {
|
|
546
|
+
kind: 'unsafe',
|
|
547
|
+
node: classDeclaration
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const tokens = [];
|
|
551
|
+
const inScopeTypeParameterNames = getInScopeTypeParameterNames(classDeclaration);
|
|
552
|
+
let decoratorLocalName;
|
|
553
|
+
for (const [index, parameter] of parameters.entries()) {
|
|
554
|
+
if (parameter.dotDotDotToken || parameter.questionToken || parameter.initializer || !ts.isIdentifier(parameter.name)) {
|
|
555
|
+
return {
|
|
556
|
+
kind: 'unsafe',
|
|
557
|
+
node: parameter
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const decorators = parameter.modifiers?.filter(ts.isDecorator) ?? [];
|
|
561
|
+
const injectDecorators = decorators.map(decorator => ({
|
|
562
|
+
decorator,
|
|
563
|
+
localName: getNestInjectDecoratorLocalName(decorator, nestInjectLocalNames)
|
|
564
|
+
})).filter(entry => !!entry.localName);
|
|
565
|
+
if (decorators.length !== injectDecorators.length || injectDecorators.length > 1) {
|
|
566
|
+
return {
|
|
567
|
+
kind: 'unsafe',
|
|
568
|
+
node: parameter
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const [injectDecorator] = injectDecorators;
|
|
572
|
+
if (injectDecorator) {
|
|
573
|
+
const expression = injectDecorator.decorator.expression;
|
|
574
|
+
if (!ts.isCallExpression(expression) || expression.arguments.length !== 1) {
|
|
575
|
+
return {
|
|
576
|
+
kind: 'unsafe',
|
|
577
|
+
node: parameter
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
const [token] = expression.arguments;
|
|
581
|
+
if (!token) {
|
|
582
|
+
return {
|
|
583
|
+
kind: 'unsafe',
|
|
584
|
+
node: parameter
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
tokens.push(token);
|
|
588
|
+
decoratorLocalName ??= injectDecorator.localName;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const existingToken = existingInjectTokens?.[index];
|
|
592
|
+
if (existingToken) {
|
|
593
|
+
tokens.push(existingToken);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const token = createInjectionTokenFromType(parameter.type, runtimeValueNames, collidingTypeValueNames, typeOnlyImportNames, inScopeTypeParameterNames);
|
|
597
|
+
if (!token) {
|
|
598
|
+
return {
|
|
599
|
+
kind: 'unsafe',
|
|
600
|
+
node: parameter
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
tokens.push(token);
|
|
604
|
+
}
|
|
605
|
+
if (!decoratorLocalName) {
|
|
606
|
+
return {
|
|
607
|
+
kind: 'unsafe',
|
|
608
|
+
node: constructorDeclaration
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
decoratorLocalName,
|
|
613
|
+
kind: 'safe',
|
|
614
|
+
tokens
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function rewriteConstructorInjectTokens(source, filePath) {
|
|
618
|
+
const sourceFile = parseSource(source, filePath);
|
|
619
|
+
const warnings = [];
|
|
620
|
+
const nestInjectLocalNames = getNestInjectLocalNames(sourceFile);
|
|
621
|
+
const fluoInjectLocalNames = getFluoInjectLocalNames(sourceFile);
|
|
622
|
+
const fluoInjectNamespaceLocalNames = getFluoInjectNamespaceLocalNames(sourceFile);
|
|
623
|
+
const runtimeValueNames = getRuntimeValueNames(sourceFile);
|
|
624
|
+
const collidingTypeValueNames = getCollidingTypeValueNames(sourceFile, runtimeValueNames);
|
|
625
|
+
const typeOnlyImportNames = getTypeOnlyImportNames(sourceFile);
|
|
626
|
+
const safeClasses = new Map();
|
|
627
|
+
let hasUnsafeConstructor = false;
|
|
628
|
+
const inspect = node => {
|
|
629
|
+
if (ts.isClassDeclaration(node)) {
|
|
630
|
+
const existingInjectDecorator = getFluoInjectClassDecorator(node.modifiers, fluoInjectLocalNames, fluoInjectNamespaceLocalNames);
|
|
631
|
+
const existingInjectTokens = existingInjectDecorator ? getExistingInjectTokens(existingInjectDecorator.expression) : undefined;
|
|
632
|
+
const result = getConstructorInjectionTokens(node, nestInjectLocalNames, runtimeValueNames, collidingTypeValueNames, typeOnlyImportNames, existingInjectTokens, !!existingInjectDecorator);
|
|
633
|
+
if (result.kind === 'safe') {
|
|
634
|
+
safeClasses.set(node, result);
|
|
635
|
+
}
|
|
636
|
+
if (result.kind === 'unsafe') {
|
|
637
|
+
hasUnsafeConstructor = true;
|
|
638
|
+
warnings.push(buildWarning(filePath, sourceFile, result.node, 'inject-token-unsupported', 'Unable to safely convert constructor injection. Keep the NestJS constructor unchanged and manually add class-level @Inject(...) tokens in constructor parameter order.'));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
ts.forEachChild(node, inspect);
|
|
642
|
+
};
|
|
643
|
+
inspect(sourceFile);
|
|
644
|
+
if (safeClasses.size === 0) {
|
|
645
|
+
return {
|
|
646
|
+
changed: false,
|
|
647
|
+
source,
|
|
648
|
+
warnings
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const generatedFluoInjectLocalName = hasUnsafeConstructor ? [...fluoInjectLocalNames][0] ?? 'FluoInject' : undefined;
|
|
652
|
+
const transformer = context => {
|
|
653
|
+
const visit = node => {
|
|
654
|
+
if (ts.isClassDeclaration(node)) {
|
|
655
|
+
const injection = safeClasses.get(node);
|
|
656
|
+
if (injection) {
|
|
657
|
+
const members = node.members.map(member => {
|
|
658
|
+
if (!ts.isConstructorDeclaration(member)) {
|
|
659
|
+
return member;
|
|
660
|
+
}
|
|
661
|
+
const parameters = member.parameters.map(parameter => {
|
|
662
|
+
const modifiers = parameter.modifiers?.filter(modifier => !ts.isDecorator(modifier) || !getNestInjectDecoratorLocalName(modifier, nestInjectLocalNames));
|
|
663
|
+
return ts.factory.updateParameterDeclaration(parameter, modifiers, parameter.dotDotDotToken, parameter.name, parameter.questionToken, parameter.type, parameter.initializer);
|
|
664
|
+
});
|
|
665
|
+
return ts.factory.updateConstructorDeclaration(member, member.modifiers, parameters, member.body);
|
|
666
|
+
});
|
|
667
|
+
const existingInjectDecorator = getFluoInjectClassDecorator(node.modifiers, fluoInjectLocalNames, fluoInjectNamespaceLocalNames);
|
|
668
|
+
const modifiers = existingInjectDecorator ? node.modifiers?.map(modifier => modifier === existingInjectDecorator.decorator ? ts.factory.updateDecorator(existingInjectDecorator.decorator, ts.factory.updateCallExpression(existingInjectDecorator.expression, existingInjectDecorator.expression.expression, existingInjectDecorator.expression.typeArguments, injection.tokens)) : modifier) : [ts.factory.createDecorator(ts.factory.createCallExpression(ts.factory.createIdentifier(generatedFluoInjectLocalName ?? injection.decoratorLocalName), undefined, injection.tokens)), ...(node.modifiers ?? [])];
|
|
669
|
+
return ts.factory.updateClassDeclaration(node, modifiers, node.name, node.typeParameters, node.heritageClauses, members);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return ts.visitEachChild(node, visit, context);
|
|
673
|
+
};
|
|
674
|
+
return node => ts.visitNode(node, visit);
|
|
675
|
+
};
|
|
676
|
+
const transformed = ts.transform(sourceFile, [transformer]).transformed[0];
|
|
677
|
+
let nextSource = printer.printFile(transformed);
|
|
678
|
+
if (generatedFluoInjectLocalName && fluoInjectLocalNames.size === 0) {
|
|
679
|
+
const nextSourceFile = parseSource(nextSource, filePath);
|
|
680
|
+
nextSource = printSourceFile(nextSourceFile, mergeNamedImport([...nextSourceFile.statements], '@fluojs/core', [{
|
|
681
|
+
imported: 'Inject',
|
|
682
|
+
isTypeOnly: false,
|
|
683
|
+
local: generatedFluoInjectLocalName
|
|
684
|
+
}], true));
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
changed: true,
|
|
688
|
+
source: nextSource,
|
|
689
|
+
warnings
|
|
690
|
+
};
|
|
691
|
+
}
|
|
307
692
|
function hasConflictingScopeImport(sourceFile) {
|
|
308
693
|
for (const statement of sourceFile.statements) {
|
|
309
694
|
if (!ts.isImportDeclaration(statement) || !statement.importClause?.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
@@ -420,6 +805,7 @@ function rewriteInjectableAndScope(source, filePath, options) {
|
|
|
420
805
|
const nextSourceFile = parseSource(nextSource, filePath);
|
|
421
806
|
nextSource = printSourceFile(nextSourceFile, mergeNamedImport([...nextSourceFile.statements], '@fluojs/core', [{
|
|
422
807
|
imported: 'Scope',
|
|
808
|
+
isTypeOnly: false,
|
|
423
809
|
local: scopeDecoratorName
|
|
424
810
|
}]));
|
|
425
811
|
}
|
|
@@ -433,14 +819,25 @@ function rewriteInjectableAndScope(source, filePath, options) {
|
|
|
433
819
|
warnings
|
|
434
820
|
};
|
|
435
821
|
}
|
|
436
|
-
function rewriteBootstrap(source, filePath) {
|
|
437
|
-
const
|
|
822
|
+
function rewriteBootstrap(source, filePath, bootstrapPlatform) {
|
|
823
|
+
const compilerOptions = {
|
|
824
|
+
module: ts.ModuleKind.ESNext,
|
|
825
|
+
noLib: true,
|
|
826
|
+
noResolve: true,
|
|
827
|
+
target: ts.ScriptTarget.ES2022
|
|
828
|
+
};
|
|
829
|
+
const compilerHost = ts.createCompilerHost(compilerOptions, true);
|
|
830
|
+
const getSourceFile = compilerHost.getSourceFile.bind(compilerHost);
|
|
831
|
+
compilerHost.getSourceFile = (path, languageVersion, onError, shouldCreateNewSourceFile) => path === filePath ? parseSource(source, filePath) : getSourceFile(path, languageVersion, onError, shouldCreateNewSourceFile);
|
|
832
|
+
const program = ts.createProgram([filePath], compilerOptions, compilerHost);
|
|
833
|
+
const sourceFile = program.getSourceFile(filePath) ?? parseSource(source, filePath);
|
|
834
|
+
const checker = program.getTypeChecker();
|
|
438
835
|
const warnings = [];
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
const portFoldedApps = new Set();
|
|
836
|
+
const bootstrapBindings = new Map();
|
|
837
|
+
const portFoldedListenCallKeys = new Set();
|
|
442
838
|
const rewrittenCreateCallKeys = new Set();
|
|
443
839
|
const warnedCreateCallKeys = new Set();
|
|
840
|
+
let usesExpressAdapter = false;
|
|
444
841
|
function toCallKey(callExpression) {
|
|
445
842
|
return `${callExpression.pos}:${callExpression.end}`;
|
|
446
843
|
}
|
|
@@ -452,6 +849,77 @@ function rewriteBootstrap(source, filePath) {
|
|
|
452
849
|
warnedCreateCallKeys.add(key);
|
|
453
850
|
warnings.push(buildWarning(filePath, sourceFile, callExpression, 'bootstrap-unsupported', `${reason} Keep this Nest bootstrap path and migrate manually.`));
|
|
454
851
|
}
|
|
852
|
+
function getSupportedListenCall(binding, createCall) {
|
|
853
|
+
const appListenCalls = binding.listenCalls;
|
|
854
|
+
if (appListenCalls.length === 0) {
|
|
855
|
+
return {
|
|
856
|
+
category: 'bootstrap-unsupported',
|
|
857
|
+
node: createCall,
|
|
858
|
+
reason: 'Unable to find an app.listen(...) call for this NestFactory.create result.'
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
const [listenCall] = appListenCalls;
|
|
862
|
+
if (!listenCall) {
|
|
863
|
+
return {
|
|
864
|
+
category: 'bootstrap-unsupported',
|
|
865
|
+
node: createCall,
|
|
866
|
+
reason: 'Unable to resolve an app.listen(...) call for this NestFactory.create result.'
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
if (appListenCalls.length > 1) {
|
|
870
|
+
return {
|
|
871
|
+
category: 'bootstrap-unsupported',
|
|
872
|
+
node: listenCall,
|
|
873
|
+
reason: 'Multiple app.listen(...) calls are associated with this NestFactory.create result.'
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
if (hasEscapedBindingUse(binding)) {
|
|
877
|
+
return {
|
|
878
|
+
category: 'bootstrap-unsupported',
|
|
879
|
+
node: createCall,
|
|
880
|
+
reason: 'Unable to prove that this NestFactory.create result owns the app.listen(...) call.'
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const [portExpression] = listenCall.arguments;
|
|
884
|
+
if (listenCall.arguments.length !== 1 || !portExpression || !ts.isNumericLiteral(portExpression)) {
|
|
885
|
+
return {
|
|
886
|
+
category: 'bootstrap-port',
|
|
887
|
+
node: listenCall,
|
|
888
|
+
reason: 'Expected exactly one numeric port argument for app.listen(...).'
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
listenCall,
|
|
893
|
+
portExpression
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
function enclosingFunction(node) {
|
|
897
|
+
let current = node.parent;
|
|
898
|
+
while (current) {
|
|
899
|
+
if (ts.isFunctionLike(current)) {
|
|
900
|
+
return current;
|
|
901
|
+
}
|
|
902
|
+
current = current.parent;
|
|
903
|
+
}
|
|
904
|
+
return undefined;
|
|
905
|
+
}
|
|
906
|
+
function hasEscapedBindingUse(binding) {
|
|
907
|
+
let escaped = false;
|
|
908
|
+
const [listenCall] = binding.listenCalls;
|
|
909
|
+
const listenReceiver = listenCall && ts.isPropertyAccessExpression(listenCall.expression) ? listenCall.expression.expression : undefined;
|
|
910
|
+
const inspectBindingUse = node => {
|
|
911
|
+
if (ts.isIdentifier(node) && checker.getSymbolAtLocation(node) === binding.symbol && node !== binding.declaration.name && node !== listenReceiver) {
|
|
912
|
+
escaped = true;
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
ts.forEachChild(node, inspectBindingUse);
|
|
916
|
+
};
|
|
917
|
+
inspectBindingUse(binding.functionOwner);
|
|
918
|
+
return escaped;
|
|
919
|
+
}
|
|
920
|
+
function warnUnsupportedListen(node, category, reason) {
|
|
921
|
+
warnings.push(buildWarning(filePath, sourceFile, node, category, `${reason} Keep this Nest bootstrap path and migrate manually.`));
|
|
922
|
+
}
|
|
455
923
|
function isSupportedCreateCall(callExpression) {
|
|
456
924
|
if (callExpression.typeArguments && callExpression.typeArguments.length > 0) {
|
|
457
925
|
return {
|
|
@@ -465,9 +933,21 @@ function rewriteBootstrap(source, filePath) {
|
|
|
465
933
|
supported: false
|
|
466
934
|
};
|
|
467
935
|
}
|
|
468
|
-
if (callExpression.arguments.length === 2
|
|
936
|
+
if (callExpression.arguments.length === 2) {
|
|
937
|
+
if (ts.isObjectLiteralExpression(callExpression.arguments[1])) {
|
|
938
|
+
return {
|
|
939
|
+
reason: 'NestFactory.create options object has no documented Fluo mapping.',
|
|
940
|
+
supported: false
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
944
|
+
reason: 'NestFactory.create uses an adapter-specific startup form. Run adapter-independent transforms with --skip bootstrap, then replace the NestJS adapter with a Fluo platform adapter passed to FluoFactory.create(..., { adapter }) before calling listen().',
|
|
945
|
+
supported: false
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
if (bootstrapPlatform !== 'express') {
|
|
469
949
|
return {
|
|
470
|
-
reason: '
|
|
950
|
+
reason: 'Select a Fluo platform adapter before rewriting this bootstrap. For Express, rerun with --platform express.',
|
|
471
951
|
supported: false
|
|
472
952
|
};
|
|
473
953
|
}
|
|
@@ -475,56 +955,115 @@ function rewriteBootstrap(source, filePath) {
|
|
|
475
955
|
supported: true
|
|
476
956
|
};
|
|
477
957
|
}
|
|
478
|
-
|
|
958
|
+
function createExpressAdapter(options) {
|
|
959
|
+
usesExpressAdapter = true;
|
|
960
|
+
return ts.factory.createCallExpression(ts.factory.createIdentifier('createExpressAdapter'), undefined, options ? [options] : []);
|
|
961
|
+
}
|
|
962
|
+
function isNestFactoryValueImportReference(identifier) {
|
|
963
|
+
const symbol = checker.getSymbolAtLocation(identifier);
|
|
964
|
+
return Boolean(symbol?.declarations?.some(declaration => {
|
|
965
|
+
if (!ts.isImportSpecifier(declaration) || declaration.isTypeOnly) {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
const importClause = declaration.parent.parent;
|
|
969
|
+
const importDeclaration = importClause.parent;
|
|
970
|
+
return !importClause.isTypeOnly && ts.isImportDeclaration(importDeclaration) && ts.isStringLiteral(importDeclaration.moduleSpecifier) && importDeclaration.moduleSpecifier.text === '@nestjs/core' && (declaration.propertyName ?? declaration.name).text === 'NestFactory';
|
|
971
|
+
}));
|
|
972
|
+
}
|
|
973
|
+
function nestFactoryCreateCall(node) {
|
|
974
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || !ts.isIdentifier(node.expression.expression) || node.expression.name.text !== 'create') {
|
|
975
|
+
return undefined;
|
|
976
|
+
}
|
|
977
|
+
return {
|
|
978
|
+
call: node,
|
|
979
|
+
receiver: node.expression.expression
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
function hasUnconvertedNestFactoryValueReference() {
|
|
983
|
+
let found = false;
|
|
984
|
+
const inspect = node => {
|
|
985
|
+
if (ts.isIdentifier(node) && isNestFactoryValueImportReference(node) && !ts.isImportSpecifier(node.parent)) {
|
|
986
|
+
const createCall = nestFactoryCreateCall(node.parent.parent);
|
|
987
|
+
const isRewrittenCreateReceiver = createCall && createCall.receiver === node && rewrittenCreateCallKeys.has(toCallKey(createCall.call));
|
|
988
|
+
if (!isRewrittenCreateReceiver) {
|
|
989
|
+
found = true;
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
ts.forEachChild(node, inspect);
|
|
994
|
+
};
|
|
995
|
+
inspect(sourceFile);
|
|
996
|
+
return found;
|
|
997
|
+
}
|
|
998
|
+
const inspectCreates = node => {
|
|
479
999
|
if (ts.isVariableDeclaration(node) && node.initializer) {
|
|
480
1000
|
const initializer = ts.isAwaitExpression(node.initializer) ? node.initializer.expression : node.initializer;
|
|
481
|
-
|
|
482
|
-
|
|
1001
|
+
const createCall = nestFactoryCreateCall(initializer);
|
|
1002
|
+
if (ts.isIdentifier(node.name) && createCall && isNestFactoryValueImportReference(createCall.receiver) && ts.isVariableDeclarationList(node.parent) && (node.parent.flags & ts.NodeFlags.Const) !== 0) {
|
|
1003
|
+
const functionOwner = enclosingFunction(node);
|
|
1004
|
+
const symbol = checker.getSymbolAtLocation(node.name);
|
|
1005
|
+
if (functionOwner && symbol) {
|
|
1006
|
+
bootstrapBindings.set(toCallKey(createCall.call), {
|
|
1007
|
+
declaration: node,
|
|
1008
|
+
functionOwner,
|
|
1009
|
+
listenCalls: [],
|
|
1010
|
+
name: node.name.text,
|
|
1011
|
+
symbol
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
483
1014
|
}
|
|
484
1015
|
}
|
|
1016
|
+
ts.forEachChild(node, inspectCreates);
|
|
1017
|
+
};
|
|
1018
|
+
const inspectListens = node => {
|
|
485
1019
|
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.name.text === 'listen') {
|
|
486
|
-
|
|
1020
|
+
const functionOwner = enclosingFunction(node);
|
|
1021
|
+
const symbol = checker.getSymbolAtLocation(node.expression.expression);
|
|
1022
|
+
for (const binding of bootstrapBindings.values()) {
|
|
1023
|
+
if (binding.symbol === symbol && binding.functionOwner === functionOwner) {
|
|
1024
|
+
binding.listenCalls.push(node);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
487
1027
|
}
|
|
488
|
-
ts.forEachChild(node,
|
|
1028
|
+
ts.forEachChild(node, inspectListens);
|
|
489
1029
|
};
|
|
490
|
-
|
|
1030
|
+
inspectCreates(sourceFile);
|
|
1031
|
+
inspectListens(sourceFile);
|
|
491
1032
|
const transformer = context => {
|
|
492
1033
|
const visit = node => {
|
|
493
1034
|
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
494
|
-
|
|
1035
|
+
const createCall = nestFactoryCreateCall(node);
|
|
1036
|
+
if (createCall) {
|
|
1037
|
+
if (!isNestFactoryValueImportReference(createCall.receiver)) {
|
|
1038
|
+
return node;
|
|
1039
|
+
}
|
|
495
1040
|
const support = isSupportedCreateCall(node);
|
|
496
1041
|
if (!support.supported) {
|
|
497
1042
|
warnUnsupportedCreate(node, support.reason);
|
|
498
1043
|
return node;
|
|
499
1044
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
portFoldedApps.add(appVariable);
|
|
510
|
-
} else if (nextArgs.length === 2 && ts.isObjectLiteralExpression(nextArgs[1])) {
|
|
511
|
-
const hasPort = nextArgs[1].properties.some(property => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'port');
|
|
512
|
-
if (!hasPort) {
|
|
513
|
-
nextArgs = [nextArgs[0], ts.factory.updateObjectLiteralExpression(nextArgs[1], [...nextArgs[1].properties, ts.factory.createPropertyAssignment('port', portExpression)])];
|
|
514
|
-
portFoldedApps.add(appVariable);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (!portFoldedApps.has(appVariable)) {
|
|
518
|
-
warnings.push(buildWarning(filePath, sourceFile, node, 'bootstrap-port', 'Unable to move listen() port argument into FluoFactory.create options. Review bootstrap manually.'));
|
|
519
|
-
}
|
|
520
|
-
}
|
|
1045
|
+
const binding = bootstrapBindings.get(toCallKey(node));
|
|
1046
|
+
if (!binding) {
|
|
1047
|
+
warnUnsupportedCreate(node, 'Unable to associate this NestFactory.create call with a named application variable and app.listen(...) call.');
|
|
1048
|
+
return node;
|
|
1049
|
+
}
|
|
1050
|
+
const supportedListenCall = getSupportedListenCall(binding, node);
|
|
1051
|
+
if ('reason' in supportedListenCall) {
|
|
1052
|
+
warnUnsupportedListen(supportedListenCall.node, supportedListenCall.category, supportedListenCall.reason);
|
|
1053
|
+
return node;
|
|
521
1054
|
}
|
|
1055
|
+
const [rootModule] = node.arguments;
|
|
1056
|
+
if (!rootModule) {
|
|
1057
|
+
warnUnsupportedCreate(node, 'NestFactory.create requires a root module argument.');
|
|
1058
|
+
return node;
|
|
1059
|
+
}
|
|
1060
|
+
const nextArgs = [rootModule, ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment('adapter', createExpressAdapter(ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment('port', supportedListenCall.portExpression)], true)))], true)];
|
|
1061
|
+
portFoldedListenCallKeys.add(toCallKey(supportedListenCall.listenCall));
|
|
522
1062
|
rewrittenCreateCallKeys.add(toCallKey(node));
|
|
523
1063
|
return ts.factory.updateCallExpression(node, ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('FluoFactory'), ts.factory.createIdentifier('create')), undefined, nextArgs);
|
|
524
1064
|
}
|
|
525
1065
|
if (ts.isIdentifier(node.expression.expression) && node.expression.name.text === 'listen') {
|
|
526
|
-
|
|
527
|
-
if (createCalls.has(appVariable) && node.arguments.length > 0 && portFoldedApps.has(appVariable)) {
|
|
1066
|
+
if (node.arguments.length > 0 && portFoldedListenCallKeys.has(toCallKey(node))) {
|
|
528
1067
|
return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, []);
|
|
529
1068
|
}
|
|
530
1069
|
}
|
|
@@ -542,16 +1081,24 @@ function rewriteBootstrap(source, filePath) {
|
|
|
542
1081
|
};
|
|
543
1082
|
}
|
|
544
1083
|
let nextSource = printer.printFile(transformed);
|
|
545
|
-
|
|
546
|
-
|
|
1084
|
+
if (!hasUnconvertedNestFactoryValueReference()) {
|
|
1085
|
+
const removed = removeNestFactoryValueImports(nextSource, filePath);
|
|
1086
|
+
nextSource = removed.source;
|
|
1087
|
+
}
|
|
547
1088
|
const nextSourceFile = parseSource(nextSource, filePath);
|
|
548
1089
|
const withRuntimeImport = printSourceFile(nextSourceFile, mergeNamedImport([...nextSourceFile.statements], '@fluojs/runtime', [{
|
|
549
1090
|
imported: 'FluoFactory',
|
|
1091
|
+
isTypeOnly: false,
|
|
550
1092
|
local: 'FluoFactory'
|
|
551
1093
|
}]));
|
|
1094
|
+
const withPlatformImport = usesExpressAdapter ? printSourceFile(parseSource(withRuntimeImport, filePath), mergeNamedImport([...parseSource(withRuntimeImport, filePath).statements], '@fluojs/platform-express', [{
|
|
1095
|
+
imported: 'createExpressAdapter',
|
|
1096
|
+
isTypeOnly: false,
|
|
1097
|
+
local: 'createExpressAdapter'
|
|
1098
|
+
}])) : withRuntimeImport;
|
|
552
1099
|
return {
|
|
553
|
-
changed:
|
|
554
|
-
source:
|
|
1100
|
+
changed: withPlatformImport !== source,
|
|
1101
|
+
source: withPlatformImport,
|
|
555
1102
|
warnings
|
|
556
1103
|
};
|
|
557
1104
|
}
|
|
@@ -663,6 +1210,7 @@ function rewriteTesting(source, filePath) {
|
|
|
663
1210
|
const nextSourceFile = parseSource(nextSource, filePath);
|
|
664
1211
|
nextSource = printSourceFile(nextSourceFile, mergeNamedImport([...nextSourceFile.statements], '@fluojs/testing', [{
|
|
665
1212
|
imported: 'createTestingModule',
|
|
1213
|
+
isTypeOnly: false,
|
|
666
1214
|
local: 'createTestingModule'
|
|
667
1215
|
}]));
|
|
668
1216
|
const withFluoImportSourceFile = parseSource(nextSource, filePath);
|
|
@@ -760,8 +1308,9 @@ function rewriteBaseUrlPaths(paths, baseUrl) {
|
|
|
760
1308
|
});
|
|
761
1309
|
return Object.fromEntries(rewrittenEntries);
|
|
762
1310
|
}
|
|
763
|
-
function detectManualFollowUps(source, filePath) {
|
|
1311
|
+
function detectManualFollowUps(source, filePath, includeInjectTokenWarning) {
|
|
764
1312
|
const sourceFile = parseSource(source, filePath);
|
|
1313
|
+
const nestInjectLocalNames = getNestInjectLocalNames(sourceFile);
|
|
765
1314
|
const warnings = [];
|
|
766
1315
|
let hasRequestDecoratorWarning = false;
|
|
767
1316
|
let hasInjectParameterWarning = false;
|
|
@@ -773,7 +1322,7 @@ function detectManualFollowUps(source, filePath) {
|
|
|
773
1322
|
continue;
|
|
774
1323
|
}
|
|
775
1324
|
const decoratorName = modifier.expression.expression.text;
|
|
776
|
-
if (!hasInjectParameterWarning && decoratorName
|
|
1325
|
+
if (includeInjectTokenWarning && !hasInjectParameterWarning && nestInjectLocalNames.has(decoratorName)) {
|
|
777
1326
|
hasInjectParameterWarning = true;
|
|
778
1327
|
warnings.push(buildWarning(filePath, sourceFile, modifier, 'inject-token', 'Constructor @Inject(TOKEN) parameter decorators need manual migration to class-level @Inject(TOKEN, ...) syntax.'));
|
|
779
1328
|
}
|
|
@@ -838,10 +1387,21 @@ function gatherTargetFiles(targetPath, includeTsconfig) {
|
|
|
838
1387
|
}
|
|
839
1388
|
return collected.sort((left, right) => left.localeCompare(right));
|
|
840
1389
|
}
|
|
841
|
-
function runTypeScriptTransforms(source, filePath, enabledTransforms) {
|
|
1390
|
+
function runTypeScriptTransforms(source, filePath, enabledTransforms, bootstrapPlatform) {
|
|
842
1391
|
let nextSource = source;
|
|
843
1392
|
const appliedTransforms = [];
|
|
844
1393
|
const warnings = [];
|
|
1394
|
+
if (enabledTransforms.has('injectable')) {
|
|
1395
|
+
const rewritten = rewriteConstructorInjectTokens(nextSource, filePath);
|
|
1396
|
+
nextSource = rewritten.source;
|
|
1397
|
+
warnings.push(...rewritten.warnings);
|
|
1398
|
+
if (rewritten.changed) {
|
|
1399
|
+
appliedTransforms.push('injectable');
|
|
1400
|
+
const rewrittenImports = rewriteImports(nextSource, filePath, new Set(['Inject']));
|
|
1401
|
+
nextSource = rewrittenImports.source;
|
|
1402
|
+
warnings.push(...rewrittenImports.warnings);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
845
1405
|
if (enabledTransforms.has('imports')) {
|
|
846
1406
|
const rewritten = rewriteImports(nextSource, filePath);
|
|
847
1407
|
nextSource = rewritten.source;
|
|
@@ -867,7 +1427,7 @@ function runTypeScriptTransforms(source, filePath, enabledTransforms) {
|
|
|
867
1427
|
}
|
|
868
1428
|
}
|
|
869
1429
|
if (enabledTransforms.has('bootstrap')) {
|
|
870
|
-
const rewritten = rewriteBootstrap(nextSource, filePath);
|
|
1430
|
+
const rewritten = rewriteBootstrap(nextSource, filePath, bootstrapPlatform);
|
|
871
1431
|
nextSource = rewritten.source;
|
|
872
1432
|
warnings.push(...rewritten.warnings);
|
|
873
1433
|
if (rewritten.changed) {
|
|
@@ -882,7 +1442,7 @@ function runTypeScriptTransforms(source, filePath, enabledTransforms) {
|
|
|
882
1442
|
appliedTransforms.push('testing');
|
|
883
1443
|
}
|
|
884
1444
|
}
|
|
885
|
-
warnings.push(...detectManualFollowUps(source, filePath));
|
|
1445
|
+
warnings.push(...detectManualFollowUps(source, filePath, !enabledTransforms.has('injectable')));
|
|
886
1446
|
return {
|
|
887
1447
|
appliedTransforms: [...new Set(appliedTransforms)],
|
|
888
1448
|
source: nextSource,
|
|
@@ -921,7 +1481,7 @@ export function runNestJsMigration(options) {
|
|
|
921
1481
|
});
|
|
922
1482
|
continue;
|
|
923
1483
|
}
|
|
924
|
-
const rewritten = runTypeScriptTransforms(source, filePath, options.enabledTransforms);
|
|
1484
|
+
const rewritten = runTypeScriptTransforms(source, filePath, options.enabledTransforms, options.bootstrapPlatform);
|
|
925
1485
|
const changed = rewritten.source !== source;
|
|
926
1486
|
if (changed && options.apply) {
|
|
927
1487
|
writeFileSync(filePath, rewritten.source, 'utf8');
|