1/* @internal */ 2namespace ts.codefix { 3 registerCodeFix({ 4 errorCodes: [Diagnostics.File_is_a_CommonJS_module_it_may_be_converted_to_an_ES_module.code], 5 getCodeActions(context) { 6 const { sourceFile, program, preferences } = context; 7 const changes = textChanges.ChangeTracker.with(context, changes => { 8 const moduleExportsChangedToDefault = convertFileToEsModule(sourceFile, program.getTypeChecker(), changes, getEmitScriptTarget(program.getCompilerOptions()), getQuotePreference(sourceFile, preferences)); 9 if (moduleExportsChangedToDefault) { 10 for (const importingFile of program.getSourceFiles()) { 11 fixImportOfModuleExports(importingFile, sourceFile, changes, getQuotePreference(importingFile, preferences)); 12 } 13 } 14 }); 15 // No support for fix-all since this applies to the whole file at once anyway. 16 return [createCodeFixActionWithoutFixAll("convertToEsModule", changes, Diagnostics.Convert_to_ES_module)]; 17 }, 18 }); 19 20 function fixImportOfModuleExports(importingFile: SourceFile, exportingFile: SourceFile, changes: textChanges.ChangeTracker, quotePreference: QuotePreference) { 21 for (const moduleSpecifier of importingFile.imports) { 22 const imported = getResolvedModule(importingFile, moduleSpecifier.text, getModeForUsageLocation(importingFile, moduleSpecifier)); 23 if (!imported || imported.resolvedFileName !== exportingFile.fileName) { 24 continue; 25 } 26 27 const importNode = importFromModuleSpecifier(moduleSpecifier); 28 switch (importNode.kind) { 29 case SyntaxKind.ImportEqualsDeclaration: 30 changes.replaceNode(importingFile, importNode, makeImport(importNode.name, /*namedImports*/ undefined, moduleSpecifier, quotePreference)); 31 break; 32 case SyntaxKind.CallExpression: 33 if (isRequireCall(importNode, /*checkArgumentIsStringLiteralLike*/ false)) { 34 changes.replaceNode(importingFile, importNode, factory.createPropertyAccessExpression(getSynthesizedDeepClone(importNode), "default")); 35 } 36 break; 37 } 38 } 39 } 40 41 /** @returns Whether we converted a `module.exports =` to a default export. */ 42 function convertFileToEsModule(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, target: ScriptTarget, quotePreference: QuotePreference): ModuleExportsChanged { 43 const identifiers: Identifiers = { original: collectFreeIdentifiers(sourceFile), additional: new Set() }; 44 const exports = collectExportRenames(sourceFile, checker, identifiers); 45 convertExportsAccesses(sourceFile, exports, changes); 46 let moduleExportsChangedToDefault = false; 47 let useSitesToUnqualify: ESMap<Node, Node> | undefined; 48 // Process variable statements first to collect use sites that need to be updated inside other transformations 49 for (const statement of filter(sourceFile.statements, isVariableStatement)) { 50 const newUseSites = convertVariableStatement(sourceFile, statement, changes, checker, identifiers, target, quotePreference); 51 if (newUseSites) { 52 copyEntries(newUseSites, useSitesToUnqualify ??= new Map()); 53 } 54 } 55 // `convertStatement` will delete entries from `useSitesToUnqualify` when containing statements are replaced 56 for (const statement of filter(sourceFile.statements, s => !isVariableStatement(s))) { 57 const moduleExportsChanged = convertStatement(sourceFile, statement, checker, changes, identifiers, target, exports, useSitesToUnqualify, quotePreference); 58 moduleExportsChangedToDefault = moduleExportsChangedToDefault || moduleExportsChanged; 59 } 60 // Remaining use sites can be changed directly 61 useSitesToUnqualify?.forEach((replacement, original) => { 62 changes.replaceNode(sourceFile, original, replacement); 63 }); 64 65 return moduleExportsChangedToDefault; 66 } 67 68 /** 69 * Contains an entry for each renamed export. 70 * This is necessary because `exports.x = 0;` does not declare a local variable. 71 * Converting this to `export const x = 0;` would declare a local, so we must be careful to avoid shadowing. 72 * If there would be shadowing at either the declaration or at any reference to `exports.x` (now just `x`), we must convert to: 73 * const _x = 0; 74 * export { _x as x }; 75 * This conversion also must place if the exported name is not a valid identifier, e.g. `exports.class = 0;`. 76 */ 77 type ExportRenames = ReadonlyESMap<string, string>; 78 79 function collectExportRenames(sourceFile: SourceFile, checker: TypeChecker, identifiers: Identifiers): ExportRenames { 80 const res = new Map<string, string>(); 81 forEachExportReference(sourceFile, node => { 82 const { text, originalKeywordKind } = node.name; 83 if (!res.has(text) && (originalKeywordKind !== undefined && isNonContextualKeyword(originalKeywordKind) 84 || checker.resolveName(text, node, SymbolFlags.Value, /*excludeGlobals*/ true))) { 85 // Unconditionally add an underscore in case `text` is a keyword. 86 res.set(text, makeUniqueName(`_${text}`, identifiers)); 87 } 88 }); 89 return res; 90 } 91 92 function convertExportsAccesses(sourceFile: SourceFile, exports: ExportRenames, changes: textChanges.ChangeTracker): void { 93 forEachExportReference(sourceFile, (node, isAssignmentLhs) => { 94 if (isAssignmentLhs) { 95 return; 96 } 97 const { text } = node.name; 98 changes.replaceNode(sourceFile, node, factory.createIdentifier(exports.get(text) || text)); 99 }); 100 } 101 102 function forEachExportReference(sourceFile: SourceFile, cb: (node: (PropertyAccessExpression & { name: Identifier }), isAssignmentLhs: boolean) => void): void { 103 sourceFile.forEachChild(function recur(node) { 104 if (isPropertyAccessExpression(node) && isExportsOrModuleExportsOrAlias(sourceFile, node.expression) && isIdentifier(node.name)) { 105 const { parent } = node; 106 cb(node as typeof node & { name: Identifier }, isBinaryExpression(parent) && parent.left === node && parent.operatorToken.kind === SyntaxKind.EqualsToken); 107 } 108 node.forEachChild(recur); 109 }); 110 } 111 112 /** Whether `module.exports =` was changed to `export default` */ 113 type ModuleExportsChanged = boolean; 114 115 function convertStatement( 116 sourceFile: SourceFile, 117 statement: Statement, 118 checker: TypeChecker, 119 changes: textChanges.ChangeTracker, 120 identifiers: Identifiers, 121 target: ScriptTarget, 122 exports: ExportRenames, 123 useSitesToUnqualify: ESMap<Node, Node> | undefined, 124 quotePreference: QuotePreference 125 ): ModuleExportsChanged { 126 switch (statement.kind) { 127 case SyntaxKind.VariableStatement: 128 convertVariableStatement(sourceFile, statement as VariableStatement, changes, checker, identifiers, target, quotePreference); 129 return false; 130 case SyntaxKind.ExpressionStatement: { 131 const { expression } = statement as ExpressionStatement; 132 switch (expression.kind) { 133 case SyntaxKind.CallExpression: { 134 if (isRequireCall(expression, /*checkArgumentIsStringLiteralLike*/ true)) { 135 // For side-effecting require() call, just make a side-effecting import. 136 changes.replaceNode(sourceFile, statement, makeImport(/*name*/ undefined, /*namedImports*/ undefined, expression.arguments[0], quotePreference)); 137 } 138 return false; 139 } 140 case SyntaxKind.BinaryExpression: { 141 const { operatorToken } = expression as BinaryExpression; 142 return operatorToken.kind === SyntaxKind.EqualsToken && convertAssignment(sourceFile, checker, expression as BinaryExpression, changes, exports, useSitesToUnqualify); 143 } 144 } 145 } 146 // falls through 147 default: 148 return false; 149 } 150 } 151 152 function convertVariableStatement( 153 sourceFile: SourceFile, 154 statement: VariableStatement, 155 changes: textChanges.ChangeTracker, 156 checker: TypeChecker, 157 identifiers: Identifiers, 158 target: ScriptTarget, 159 quotePreference: QuotePreference, 160 ): ESMap<Node, Node> | undefined { 161 const { declarationList } = statement; 162 let foundImport = false; 163 const converted = map(declarationList.declarations, decl => { 164 const { name, initializer } = decl; 165 if (initializer) { 166 if (isExportsOrModuleExportsOrAlias(sourceFile, initializer)) { 167 // `const alias = module.exports;` can be removed. 168 foundImport = true; 169 return convertedImports([]); 170 } 171 else if (isRequireCall(initializer, /*checkArgumentIsStringLiteralLike*/ true)) { 172 foundImport = true; 173 return convertSingleImport(name, initializer.arguments[0], checker, identifiers, target, quotePreference); 174 } 175 else if (isPropertyAccessExpression(initializer) && isRequireCall(initializer.expression, /*checkArgumentIsStringLiteralLike*/ true)) { 176 foundImport = true; 177 return convertPropertyAccessImport(name, initializer.name.text, initializer.expression.arguments[0], identifiers, quotePreference); 178 } 179 } 180 // Move it out to its own variable statement. (This will not be used if `!foundImport`) 181 return convertedImports([factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([decl], declarationList.flags))]); 182 }); 183 if (foundImport) { 184 // useNonAdjustedEndPosition to ensure we don't eat the newline after the statement. 185 changes.replaceNodeWithNodes(sourceFile, statement, flatMap(converted, c => c.newImports)); 186 let combinedUseSites: ESMap<Node, Node> | undefined; 187 forEach(converted, c => { 188 if (c.useSitesToUnqualify) { 189 copyEntries(c.useSitesToUnqualify, combinedUseSites ??= new Map()); 190 } 191 }); 192 193 return combinedUseSites; 194 } 195 } 196 197 /** Converts `const name = require("moduleSpecifier").propertyName` */ 198 function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers, quotePreference: QuotePreference): ConvertedImports { 199 switch (name.kind) { 200 case SyntaxKind.ObjectBindingPattern: 201 case SyntaxKind.ArrayBindingPattern: { 202 // `const [a, b] = require("c").d` --> `import { d } from "c"; const [a, b] = d;` 203 const tmp = makeUniqueName(propertyName, identifiers); 204 return convertedImports([ 205 makeSingleImport(tmp, propertyName, moduleSpecifier, quotePreference), 206 makeConst(/*modifiers*/ undefined, name, factory.createIdentifier(tmp)), 207 ]); 208 } 209 case SyntaxKind.Identifier: 210 // `const a = require("b").c` --> `import { c as a } from "./b"; 211 return convertedImports([makeSingleImport(name.text, propertyName, moduleSpecifier, quotePreference)]); 212 default: 213 return Debug.assertNever(name, `Convert to ES module got invalid syntax form ${(name as BindingName).kind}`); 214 } 215 } 216 217 function convertAssignment( 218 sourceFile: SourceFile, 219 checker: TypeChecker, 220 assignment: BinaryExpression, 221 changes: textChanges.ChangeTracker, 222 exports: ExportRenames, 223 useSitesToUnqualify: ESMap<Node, Node> | undefined, 224 ): ModuleExportsChanged { 225 const { left, right } = assignment; 226 if (!isPropertyAccessExpression(left)) { 227 return false; 228 } 229 230 if (isExportsOrModuleExportsOrAlias(sourceFile, left)) { 231 if (isExportsOrModuleExportsOrAlias(sourceFile, right)) { 232 // `const alias = module.exports;` or `module.exports = alias;` can be removed. 233 changes.delete(sourceFile, assignment.parent); 234 } 235 else { 236 const replacement = isObjectLiteralExpression(right) ? tryChangeModuleExportsObject(right, useSitesToUnqualify) 237 : isRequireCall(right, /*checkArgumentIsStringLiteralLike*/ true) ? convertReExportAll(right.arguments[0], checker) 238 : undefined; 239 if (replacement) { 240 changes.replaceNodeWithNodes(sourceFile, assignment.parent, replacement[0]); 241 return replacement[1]; 242 } 243 else { 244 changes.replaceRangeWithText(sourceFile, createRange(left.getStart(sourceFile), right.pos), "export default"); 245 return true; 246 } 247 } 248 } 249 else if (isExportsOrModuleExportsOrAlias(sourceFile, left.expression)) { 250 convertNamedExport(sourceFile, assignment as BinaryExpression & { left: PropertyAccessExpression }, changes, exports); 251 } 252 253 return false; 254 } 255 256 /** 257 * Convert `module.exports = { ... }` to individual exports.. 258 * We can't always do this if the module has interesting members -- then it will be a default export instead. 259 */ 260 function tryChangeModuleExportsObject(object: ObjectLiteralExpression, useSitesToUnqualify: ESMap<Node, Node> | undefined): [readonly Statement[], ModuleExportsChanged] | undefined { 261 const statements = mapAllOrFail(object.properties, prop => { 262 switch (prop.kind) { 263 case SyntaxKind.GetAccessor: 264 case SyntaxKind.SetAccessor: 265 // TODO: Maybe we should handle this? See fourslash test `refactorConvertToEs6Module_export_object_shorthand.ts`. 266 // falls through 267 case SyntaxKind.ShorthandPropertyAssignment: 268 case SyntaxKind.SpreadAssignment: 269 return undefined; 270 case SyntaxKind.PropertyAssignment: 271 return !isIdentifier(prop.name) ? undefined : convertExportsDotXEquals_replaceNode(prop.name.text, prop.initializer, useSitesToUnqualify); 272 case SyntaxKind.MethodDeclaration: 273 return !isIdentifier(prop.name) ? undefined : functionExpressionToDeclaration(prop.name.text, [factory.createToken(SyntaxKind.ExportKeyword)], prop, useSitesToUnqualify); 274 default: 275 Debug.assertNever(prop, `Convert to ES6 got invalid prop kind ${(prop as ObjectLiteralElementLike).kind}`); 276 } 277 }); 278 return statements && [statements, false]; 279 } 280 281 function convertNamedExport( 282 sourceFile: SourceFile, 283 assignment: BinaryExpression & { left: PropertyAccessExpression }, 284 changes: textChanges.ChangeTracker, 285 exports: ExportRenames, 286 ): void { 287 // If "originalKeywordKind" was set, this is e.g. `exports. 288 const { text } = assignment.left.name; 289 const rename = exports.get(text); 290 if (rename !== undefined) { 291 /* 292 const _class = 0; 293 export { _class as class }; 294 */ 295 const newNodes = [ 296 makeConst(/*modifiers*/ undefined, rename, assignment.right), 297 makeExportDeclaration([factory.createExportSpecifier(/*isTypeOnly*/ false, rename, text)]), 298 ]; 299 changes.replaceNodeWithNodes(sourceFile, assignment.parent, newNodes); 300 } 301 else { 302 convertExportsPropertyAssignment(assignment, sourceFile, changes); 303 } 304 } 305 306 function convertReExportAll(reExported: StringLiteralLike, checker: TypeChecker): [readonly Statement[], ModuleExportsChanged] { 307 // `module.exports = require("x");` ==> `export * from "x"; export { default } from "x";` 308 const moduleSpecifier = reExported.text; 309 const moduleSymbol = checker.getSymbolAtLocation(reExported); 310 const exports = moduleSymbol ? moduleSymbol.exports! : emptyMap as ReadonlyCollection<__String>; 311 return exports.has(InternalSymbolName.ExportEquals) ? [[reExportDefault(moduleSpecifier)], true] : 312 !exports.has(InternalSymbolName.Default) ? [[reExportStar(moduleSpecifier)], false] : 313 // If there's some non-default export, must include both `export *` and `export default`. 314 exports.size > 1 ? [[reExportStar(moduleSpecifier), reExportDefault(moduleSpecifier)], true] : [[reExportDefault(moduleSpecifier)], true]; 315 } 316 function reExportStar(moduleSpecifier: string): ExportDeclaration { 317 return makeExportDeclaration(/*exportClause*/ undefined, moduleSpecifier); 318 } 319 function reExportDefault(moduleSpecifier: string): ExportDeclaration { 320 return makeExportDeclaration([factory.createExportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, "default")], moduleSpecifier); 321 } 322 323 function convertExportsPropertyAssignment({ left, right, parent }: BinaryExpression & { left: PropertyAccessExpression }, sourceFile: SourceFile, changes: textChanges.ChangeTracker): void { 324 const name = left.name.text; 325 if ((isFunctionExpression(right) || isArrowFunction(right) || isClassExpression(right)) && (!right.name || right.name.text === name)) { 326 // `exports.f = function() {}` -> `export function f() {}` -- Replace `exports.f = ` with `export `, and insert the name after `function`. 327 changes.replaceRange(sourceFile, { pos: left.getStart(sourceFile), end: right.getStart(sourceFile) }, factory.createToken(SyntaxKind.ExportKeyword), { suffix: " " }); 328 329 if (!right.name) changes.insertName(sourceFile, right, name); 330 331 const semi = findChildOfKind(parent, SyntaxKind.SemicolonToken, sourceFile); 332 if (semi) changes.delete(sourceFile, semi); 333 } 334 else { 335 // `exports.f = function g() {}` -> `export const f = function g() {}` -- just replace `exports.` with `export const ` 336 changes.replaceNodeRangeWithNodes(sourceFile, left.expression, findChildOfKind(left, SyntaxKind.DotToken, sourceFile)!, 337 [factory.createToken(SyntaxKind.ExportKeyword), factory.createToken(SyntaxKind.ConstKeyword)], 338 { joiner: " ", suffix: " " }); 339 } 340 } 341 342 // TODO: GH#22492 this will cause an error if a change has been made inside the body of the node. 343 function convertExportsDotXEquals_replaceNode(name: string | undefined, exported: Expression, useSitesToUnqualify: ESMap<Node, Node> | undefined): Statement { 344 const modifiers = [factory.createToken(SyntaxKind.ExportKeyword)]; 345 switch (exported.kind) { 346 case SyntaxKind.FunctionExpression: { 347 const { name: expressionName } = exported as FunctionExpression; 348 if (expressionName && expressionName.text !== name) { 349 // `exports.f = function g() {}` -> `export const f = function g() {}` 350 return exportConst(); 351 } 352 } 353 354 // falls through 355 case SyntaxKind.ArrowFunction: 356 // `exports.f = function() {}` --> `export function f() {}` 357 return functionExpressionToDeclaration(name, modifiers, exported as FunctionExpression | ArrowFunction, useSitesToUnqualify); 358 case SyntaxKind.ClassExpression: 359 // `exports.C = class {}` --> `export class C {}` 360 return classExpressionToDeclaration(name, modifiers, exported as ClassExpression, useSitesToUnqualify); 361 default: 362 return exportConst(); 363 } 364 365 function exportConst() { 366 // `exports.x = 0;` --> `export const x = 0;` 367 return makeConst(modifiers, factory.createIdentifier(name!), replaceImportUseSites(exported, useSitesToUnqualify)); // TODO: GH#18217 368 } 369 } 370 371 function replaceImportUseSites<T extends Node>(node: T, useSitesToUnqualify: ESMap<Node, Node> | undefined): T; 372 function replaceImportUseSites<T extends Node>(nodes: NodeArray<T>, useSitesToUnqualify: ESMap<Node, Node> | undefined): NodeArray<T>; 373 function replaceImportUseSites<T extends Node>(nodeOrNodes: T | NodeArray<T>, useSitesToUnqualify: ESMap<Node, Node> | undefined) { 374 if (!useSitesToUnqualify || !some(arrayFrom(useSitesToUnqualify.keys()), original => rangeContainsRange(nodeOrNodes, original))) { 375 return nodeOrNodes; 376 } 377 378 return isArray(nodeOrNodes) 379 ? getSynthesizedDeepClonesWithReplacements(nodeOrNodes, /*includeTrivia*/ true, replaceNode) 380 : getSynthesizedDeepCloneWithReplacements(nodeOrNodes, /*includeTrivia*/ true, replaceNode); 381 382 function replaceNode(original: Node) { 383 // We are replacing `mod.SomeExport` wih `SomeExport`, so we only need to look at PropertyAccessExpressions 384 if (original.kind === SyntaxKind.PropertyAccessExpression) { 385 const replacement = useSitesToUnqualify!.get(original); 386 // Remove entry from `useSitesToUnqualify` so the refactor knows it's taken care of by the parent statement we're replacing 387 useSitesToUnqualify!.delete(original); 388 return replacement; 389 } 390 } 391 } 392 393 /** 394 * Converts `const <<name>> = require("x");`. 395 * Returns nodes that will replace the variable declaration for the commonjs import. 396 * May also make use `changes` to remove qualifiers at the use sites of imports, to change `mod.x` to `x`. 397 */ 398 function convertSingleImport( 399 name: BindingName, 400 moduleSpecifier: StringLiteralLike, 401 checker: TypeChecker, 402 identifiers: Identifiers, 403 target: ScriptTarget, 404 quotePreference: QuotePreference, 405 ): ConvertedImports { 406 switch (name.kind) { 407 case SyntaxKind.ObjectBindingPattern: { 408 const importSpecifiers = mapAllOrFail(name.elements, e => 409 e.dotDotDotToken || e.initializer || e.propertyName && !isIdentifier(e.propertyName) || !isIdentifier(e.name) 410 ? undefined 411 : makeImportSpecifier(e.propertyName && e.propertyName.text, e.name.text)); 412 if (importSpecifiers) { 413 return convertedImports([makeImport(/*name*/ undefined, importSpecifiers, moduleSpecifier, quotePreference)]); 414 } 415 } 416 // falls through -- object destructuring has an interesting pattern and must be a variable declaration 417 case SyntaxKind.ArrayBindingPattern: { 418 /* 419 import x from "x"; 420 const [a, b, c] = x; 421 */ 422 const tmp = makeUniqueName(moduleSpecifierToValidIdentifier(moduleSpecifier.text, target), identifiers); 423 return convertedImports([ 424 makeImport(factory.createIdentifier(tmp), /*namedImports*/ undefined, moduleSpecifier, quotePreference), 425 makeConst(/*modifiers*/ undefined, getSynthesizedDeepClone(name), factory.createIdentifier(tmp)), 426 ]); 427 } 428 case SyntaxKind.Identifier: 429 return convertSingleIdentifierImport(name, moduleSpecifier, checker, identifiers, quotePreference); 430 default: 431 return Debug.assertNever(name, `Convert to ES module got invalid name kind ${(name as BindingName).kind}`); 432 } 433 } 434 435 /** 436 * Convert `import x = require("x").` 437 * Also: 438 * - Convert `x.default()` to `x()` to handle ES6 default export 439 * - Converts uses like `x.y()` to `y()` and uses a named import. 440 */ 441 function convertSingleIdentifierImport(name: Identifier, moduleSpecifier: StringLiteralLike, checker: TypeChecker, identifiers: Identifiers, quotePreference: QuotePreference): ConvertedImports { 442 const nameSymbol = checker.getSymbolAtLocation(name); 443 // Maps from module property name to name actually used. (The same if there isn't shadowing.) 444 const namedBindingsNames = new Map<string, string>(); 445 // True if there is some non-property use like `x()` or `f(x)`. 446 let needDefaultImport = false; 447 let useSitesToUnqualify: ESMap<Node, Node> | undefined; 448 449 for (const use of identifiers.original.get(name.text)!) { 450 if (checker.getSymbolAtLocation(use) !== nameSymbol || use === name) { 451 // This was a use of a different symbol with the same name, due to shadowing. Ignore. 452 continue; 453 } 454 455 const { parent } = use; 456 if (isPropertyAccessExpression(parent)) { 457 const { name: { text: propertyName } } = parent; 458 if (propertyName === "default") { 459 needDefaultImport = true; 460 461 const importDefaultName = use.getText(); 462 (useSitesToUnqualify ??= new Map()).set(parent, factory.createIdentifier(importDefaultName)); 463 } 464 else { 465 Debug.assert(parent.expression === use, "Didn't expect expression === use"); // Else shouldn't have been in `collectIdentifiers` 466 let idName = namedBindingsNames.get(propertyName); 467 if (idName === undefined) { 468 idName = makeUniqueName(propertyName, identifiers); 469 namedBindingsNames.set(propertyName, idName); 470 } 471 472 (useSitesToUnqualify ??= new Map()).set(parent, factory.createIdentifier(idName)); 473 } 474 } 475 else { 476 needDefaultImport = true; 477 } 478 } 479 480 const namedBindings = namedBindingsNames.size === 0 ? undefined : arrayFrom(mapIterator(namedBindingsNames.entries(), ([propertyName, idName]) => 481 factory.createImportSpecifier(/*isTypeOnly*/ false, propertyName === idName ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(idName)))); 482 if (!namedBindings) { 483 // If it was unused, ensure that we at least import *something*. 484 needDefaultImport = true; 485 } 486 return convertedImports( 487 [makeImport(needDefaultImport ? getSynthesizedDeepClone(name) : undefined, namedBindings, moduleSpecifier, quotePreference)], 488 useSitesToUnqualify 489 ); 490 } 491 492 // Identifiers helpers 493 494 function makeUniqueName(name: string, identifiers: Identifiers): string { 495 while (identifiers.original.has(name) || identifiers.additional.has(name)) { 496 name = `_${name}`; 497 } 498 identifiers.additional.add(name); 499 return name; 500 } 501 502 /** 503 * Helps us create unique identifiers. 504 * `original` refers to the local variable names in the original source file. 505 * `additional` is any new unique identifiers we've generated. (e.g., we'll generate `_x`, then `__x`.) 506 */ 507 interface Identifiers { 508 readonly original: FreeIdentifiers; 509 // Additional identifiers we've added. Mutable! 510 readonly additional: Set<string>; 511 } 512 513 type FreeIdentifiers = ReadonlyESMap<string, readonly Identifier[]>; 514 function collectFreeIdentifiers(file: SourceFile): FreeIdentifiers { 515 const map = createMultiMap<Identifier>(); 516 forEachFreeIdentifier(file, id => map.add(id.text, id)); 517 return map; 518 } 519 520 /** 521 * A free identifier is an identifier that can be accessed through name lookup as a local variable. 522 * In the expression `x.y`, `x` is a free identifier, but `y` is not. 523 */ 524 function forEachFreeIdentifier(node: Node, cb: (id: Identifier) => void): void { 525 if (isIdentifier(node) && isFreeIdentifier(node)) cb(node); 526 node.forEachChild(child => forEachFreeIdentifier(child, cb)); 527 } 528 529 function isFreeIdentifier(node: Identifier): boolean { 530 const { parent } = node; 531 switch (parent.kind) { 532 case SyntaxKind.PropertyAccessExpression: 533 return (parent as PropertyAccessExpression).name !== node; 534 case SyntaxKind.BindingElement: 535 return (parent as BindingElement).propertyName !== node; 536 case SyntaxKind.ImportSpecifier: 537 return (parent as ImportSpecifier).propertyName !== node; 538 default: 539 return true; 540 } 541 } 542 543 // Node helpers 544 545 function functionExpressionToDeclaration(name: string | undefined, additionalModifiers: readonly Modifier[], fn: FunctionExpression | ArrowFunction | MethodDeclaration, useSitesToUnqualify: ESMap<Node, Node> | undefined): FunctionDeclaration { 546 return factory.createFunctionDeclaration( 547 concatenate(additionalModifiers, getSynthesizedDeepClones(fn.modifiers)), 548 getSynthesizedDeepClone(fn.asteriskToken), 549 name, 550 getSynthesizedDeepClones(fn.typeParameters), 551 getSynthesizedDeepClones(fn.parameters), 552 getSynthesizedDeepClone(fn.type), 553 factory.converters.convertToFunctionBlock(replaceImportUseSites(fn.body!, useSitesToUnqualify))); 554 } 555 556 function classExpressionToDeclaration(name: string | undefined, additionalModifiers: readonly Modifier[], cls: ClassExpression, useSitesToUnqualify: ESMap<Node, Node> | undefined): ClassDeclaration { 557 return factory.createClassDeclaration( 558 concatenate(additionalModifiers, getSynthesizedDeepClones(cls.modifiers)), 559 name, 560 getSynthesizedDeepClones(cls.typeParameters), 561 getSynthesizedDeepClones(cls.heritageClauses), 562 replaceImportUseSites(cls.members, useSitesToUnqualify)); 563 } 564 565 function makeSingleImport(localName: string, propertyName: string, moduleSpecifier: StringLiteralLike, quotePreference: QuotePreference): ImportDeclaration { 566 return propertyName === "default" 567 ? makeImport(factory.createIdentifier(localName), /*namedImports*/ undefined, moduleSpecifier, quotePreference) 568 : makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier, quotePreference); 569 } 570 571 function makeImportSpecifier(propertyName: string | undefined, name: string): ImportSpecifier { 572 return factory.createImportSpecifier(/*isTypeOnly*/ false, propertyName !== undefined && propertyName !== name ? factory.createIdentifier(propertyName) : undefined, factory.createIdentifier(name)); 573 } 574 575 function makeConst(modifiers: readonly Modifier[] | undefined, name: string | BindingName, init: Expression): VariableStatement { 576 return factory.createVariableStatement( 577 modifiers, 578 factory.createVariableDeclarationList( 579 [factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, /*type*/ undefined, init)], 580 NodeFlags.Const)); 581 } 582 583 function makeExportDeclaration(exportSpecifiers: ExportSpecifier[] | undefined, moduleSpecifier?: string): ExportDeclaration { 584 return factory.createExportDeclaration( 585 /*modifiers*/ undefined, 586 /*isTypeOnly*/ false, 587 exportSpecifiers && factory.createNamedExports(exportSpecifiers), 588 moduleSpecifier === undefined ? undefined : factory.createStringLiteral(moduleSpecifier)); 589 } 590 591 interface ConvertedImports { 592 newImports: readonly Node[]; 593 useSitesToUnqualify?: ESMap<Node, Node>; 594 } 595 596 function convertedImports(newImports: readonly Node[], useSitesToUnqualify?: ESMap<Node, Node>): ConvertedImports { 597 return { 598 newImports, 599 useSitesToUnqualify 600 }; 601 } 602} 603