@fluojs/cli 2.0.1 → 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.
Files changed (118) hide show
  1. package/README.ko.md +169 -21
  2. package/README.md +172 -21
  3. package/dist/cli.d.ts +5 -4
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +127 -35
  6. package/dist/commands/diagnostics.d.ts +35 -0
  7. package/dist/commands/diagnostics.d.ts.map +1 -1
  8. package/dist/commands/diagnostics.js +60 -0
  9. package/dist/commands/inspect.d.ts +5 -1
  10. package/dist/commands/inspect.d.ts.map +1 -1
  11. package/dist/commands/inspect.js +84 -17
  12. package/dist/commands/migrate.d.ts.map +1 -1
  13. package/dist/commands/migrate.js +27 -15
  14. package/dist/commands/migration-transform-tokens.d.ts +15 -0
  15. package/dist/commands/migration-transform-tokens.d.ts.map +1 -0
  16. package/dist/commands/migration-transform-tokens.js +41 -0
  17. package/dist/commands/new.d.ts.map +1 -1
  18. package/dist/commands/new.js +29 -9
  19. package/dist/commands/output-path-safety.d.ts +14 -0
  20. package/dist/commands/output-path-safety.d.ts.map +1 -0
  21. package/dist/commands/output-path-safety.js +34 -0
  22. package/dist/commands/scripts.js +2 -2
  23. package/dist/commands/typegen-artifact.d.ts +54 -0
  24. package/dist/commands/typegen-artifact.d.ts.map +1 -0
  25. package/dist/commands/typegen-artifact.js +117 -0
  26. package/dist/commands/typegen-generation-child.d.ts +2 -0
  27. package/dist/commands/typegen-generation-child.d.ts.map +1 -0
  28. package/dist/commands/typegen-generation-child.js +59 -0
  29. package/dist/commands/typegen-generation-process.d.ts +46 -0
  30. package/dist/commands/typegen-generation-process.d.ts.map +1 -0
  31. package/dist/commands/typegen-generation-process.js +131 -0
  32. package/dist/commands/typegen-generation-protocol.d.ts +16 -0
  33. package/dist/commands/typegen-generation-protocol.d.ts.map +1 -0
  34. package/dist/commands/typegen-generation-protocol.js +35 -0
  35. package/dist/commands/typegen-isolated-source.d.ts +10 -0
  36. package/dist/commands/typegen-isolated-source.d.ts.map +1 -0
  37. package/dist/commands/typegen-isolated-source.js +75 -0
  38. package/dist/commands/typegen-options.d.ts +20 -0
  39. package/dist/commands/typegen-options.d.ts.map +1 -0
  40. package/dist/commands/typegen-options.js +71 -0
  41. package/dist/commands/typegen-source.d.ts +59 -0
  42. package/dist/commands/typegen-source.d.ts.map +1 -0
  43. package/dist/commands/typegen-source.js +183 -0
  44. package/dist/commands/typegen-watch.d.ts +46 -0
  45. package/dist/commands/typegen-watch.d.ts.map +1 -0
  46. package/dist/commands/typegen-watch.js +212 -0
  47. package/dist/commands/typegen.d.ts +29 -0
  48. package/dist/commands/typegen.d.ts.map +1 -0
  49. package/dist/commands/typegen.js +119 -0
  50. package/dist/dev-runner/node-restart-runner.d.ts.map +1 -1
  51. package/dist/dev-runner/node-restart-runner.js +54 -7
  52. package/dist/fixtures/inspect-react-app.module.d.ts +4 -0
  53. package/dist/fixtures/inspect-react-app.module.d.ts.map +1 -0
  54. package/dist/fixtures/inspect-react-app.module.js +32 -0
  55. package/dist/fixtures/typegen-react-app.module.d.ts +4 -0
  56. package/dist/fixtures/typegen-react-app.module.d.ts.map +1 -0
  57. package/dist/fixtures/typegen-react-app.module.js +33 -0
  58. package/dist/index.d.ts +6 -5
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +3 -2
  61. package/dist/new/install.js +1 -1
  62. package/dist/new/package-spec-resolver.d.ts.map +1 -1
  63. package/dist/new/package-spec-resolver.js +50 -59
  64. package/dist/new/prompt.d.ts +1 -0
  65. package/dist/new/prompt.d.ts.map +1 -1
  66. package/dist/new/prompt.js +40 -12
  67. package/dist/new/published-internal-dependencies.d.ts +20 -0
  68. package/dist/new/published-internal-dependencies.d.ts.map +1 -0
  69. package/dist/new/published-internal-dependencies.js +20 -0
  70. package/dist/new/react-vite-ssr-scaffold.d.ts +19 -0
  71. package/dist/new/react-vite-ssr-scaffold.d.ts.map +1 -0
  72. package/dist/new/react-vite-ssr-scaffold.js +108 -0
  73. package/dist/new/resolver.d.ts +2 -2
  74. package/dist/new/resolver.d.ts.map +1 -1
  75. package/dist/new/resolver.js +5 -1
  76. package/dist/new/scaffold.d.ts.map +1 -1
  77. package/dist/new/scaffold.js +62 -31
  78. package/dist/new/starter-profiles.d.ts +8 -4
  79. package/dist/new/starter-profiles.d.ts.map +1 -1
  80. package/dist/new/starter-profiles.js +52 -18
  81. package/dist/new/templates/react-vite-ssr/README.md.ejs +74 -0
  82. package/dist/new/templates/react-vite-ssr/playwright.config.ts.ejs +26 -0
  83. package/dist/new/templates/react-vite-ssr/src/app.test.ts.ejs +82 -0
  84. package/dist/new/templates/react-vite-ssr/src/app.ts.ejs +91 -0
  85. package/dist/new/templates/react-vite-ssr/src/entry-client.tsx.ejs +29 -0
  86. package/dist/new/templates/react-vite-ssr/src/entry-server.tsx.ejs +52 -0
  87. package/dist/new/templates/react-vite-ssr/src/load-manifest.test.ts.ejs +55 -0
  88. package/dist/new/templates/react-vite-ssr/src/load-manifest.ts.ejs +53 -0
  89. package/dist/new/templates/react-vite-ssr/src/main.ts.ejs +18 -0
  90. package/dist/new/templates/react-vite-ssr/src/page.tsx.ejs +43 -0
  91. package/dist/new/templates/react-vite-ssr/src/react-app.test.tsx.ejs +67 -0
  92. package/dist/new/templates/react-vite-ssr/src/react-app.tsx.ejs +46 -0
  93. package/dist/new/templates/react-vite-ssr/src/styles.css.ejs +25 -0
  94. package/dist/new/templates/react-vite-ssr/src/styles.d.ts.ejs +1 -0
  95. package/dist/new/templates/react-vite-ssr/tests/production-hydration.spec.ts.ejs +54 -0
  96. package/dist/new/templates/react-vite-ssr/tsconfig.json.ejs +16 -0
  97. package/dist/new/templates/react-vite-ssr/vite.client.config.ts.ejs +24 -0
  98. package/dist/new/templates/react-vite-ssr/vite.server.config.ts.ejs +17 -0
  99. package/dist/new/templates/react-vite-ssr/vitest.config.ts.ejs +10 -0
  100. package/dist/new/types.d.ts +4 -0
  101. package/dist/new/types.d.ts.map +1 -1
  102. package/dist/public-typegen.d.ts +14 -0
  103. package/dist/public-typegen.d.ts.map +1 -0
  104. package/dist/public-typegen.js +17 -0
  105. package/dist/run-cli.d.ts +2 -1
  106. package/dist/run-cli.d.ts.map +1 -1
  107. package/dist/studio/sidecar.d.ts.map +1 -1
  108. package/dist/studio/sidecar.js +35 -9
  109. package/dist/transforms/nestjs-migrate.d.ts +4 -1
  110. package/dist/transforms/nestjs-migrate.d.ts.map +1 -1
  111. package/dist/transforms/nestjs-migrate.js +625 -65
  112. package/dist/typegen-contract.d.ts +10 -0
  113. package/dist/typegen-contract.d.ts.map +1 -0
  114. package/dist/typegen-contract.js +9 -0
  115. package/dist/usage.d.ts +6 -0
  116. package/dist/usage.d.ts.map +1 -1
  117. package/dist/usage.js +47 -0
  118. 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(false, undefined, ts.factory.createIdentifier(binding.local));
141
+ return ts.factory.createImportSpecifier(binding.isTypeOnly, undefined, ts.factory.createIdentifier(binding.local));
140
142
  }
141
- return ts.factory.createImportSpecifier(false, ts.factory.createIdentifier(binding.imported), ts.factory.createIdentifier(binding.local));
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 updatedClause = ts.factory.updateImportClause(importClause, importClause.isTypeOnly, importClause.name, bindings.length > 0 ? ts.factory.createNamedImports(bindings.map(createImportSpecifier)) : undefined);
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
- const existingBindings = getImportBindings(statement);
171
- for (const binding of existingBindings) {
172
- deduped.set(toBindingKey(binding), binding);
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 = [...deduped.values()].sort((left, right) => left.local.localeCompare(right.local));
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 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));
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 rewriteImports(source, filePath) {
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 sourceFile = parseSource(source, filePath);
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 createCalls = new Map();
440
- const listenCalls = new Map();
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 && !ts.isObjectLiteralExpression(callExpression.arguments[1])) {
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: 'Unsupported NestFactory.create adapter-specific startup form.',
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
- const inspect = node => {
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
- 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') {
482
- createCalls.set(node.name.text, initializer);
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
- listenCalls.set(node.expression.expression.text, node);
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, inspect);
1028
+ ts.forEachChild(node, inspectListens);
489
1029
  };
490
- inspect(sourceFile);
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
- if (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'NestFactory' && node.expression.name.text === 'create') {
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
- let nextArgs = [...node.arguments];
501
- const ownerEntry = [...createCalls.entries()].find(([, callExpression]) => callExpression.pos === node.pos && callExpression.end === node.end);
502
- if (ownerEntry) {
503
- const [appVariable] = ownerEntry;
504
- const listenCall = listenCalls.get(appVariable);
505
- if (listenCall && listenCall.arguments.length === 1) {
506
- const [portExpression] = listenCall.arguments;
507
- if (nextArgs.length === 1) {
508
- nextArgs = [nextArgs[0], ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment('port', portExpression)], true)];
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
- const appVariable = node.expression.expression.text;
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
- const removed = removeImportBinding(nextSource, filePath, '@nestjs/core', 'NestFactory');
546
- nextSource = removed.source;
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: withRuntimeImport !== source,
554
- source: withRuntimeImport,
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 === 'Inject') {
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');