1/* @internal */ 2namespace ts.refactor { 3 const refactorName = "Convert export"; 4 5 const defaultToNamedAction = { 6 name: "Convert default export to named export", 7 description: Diagnostics.Convert_default_export_to_named_export.message, 8 kind: "refactor.rewrite.export.named" 9 }; 10 const namedToDefaultAction = { 11 name: "Convert named export to default export", 12 description: Diagnostics.Convert_named_export_to_default_export.message, 13 kind: "refactor.rewrite.export.default" 14 }; 15 16 registerRefactor(refactorName, { 17 kinds: [ 18 defaultToNamedAction.kind, 19 namedToDefaultAction.kind 20 ], 21 getAvailableActions: function getRefactorActionsToConvertBetweenNamedAndDefaultExports(context): readonly ApplicableRefactorInfo[] { 22 const info = getInfo(context, context.triggerReason === "invoked"); 23 if (!info) return emptyArray; 24 25 if (!isRefactorErrorInfo(info)) { 26 const action = info.wasDefault ? defaultToNamedAction : namedToDefaultAction; 27 return [{ name: refactorName, description: action.description, actions: [action] }]; 28 } 29 30 if (context.preferences.provideRefactorNotApplicableReason) { 31 return [ 32 { name: refactorName, description: Diagnostics.Convert_default_export_to_named_export.message, actions: [ 33 { ...defaultToNamedAction, notApplicableReason: info.error }, 34 { ...namedToDefaultAction, notApplicableReason: info.error }, 35 ]} 36 ]; 37 } 38 39 return emptyArray; 40 }, 41 getEditsForAction: function getRefactorEditsToConvertBetweenNamedAndDefaultExports(context, actionName): RefactorEditInfo { 42 Debug.assert(actionName === defaultToNamedAction.name || actionName === namedToDefaultAction.name, "Unexpected action name"); 43 const info = getInfo(context); 44 Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info"); 45 const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, info, t, context.cancellationToken)); 46 return { edits, renameFilename: undefined, renameLocation: undefined }; 47 }, 48 }); 49 50 // If a VariableStatement, will have exactly one VariableDeclaration, with an Identifier for a name. 51 type ExportToConvert = FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | NamespaceDeclaration | TypeAliasDeclaration | VariableStatement | ExportAssignment; 52 interface ExportInfo { 53 readonly exportNode: ExportToConvert; 54 readonly exportName: Identifier; // This is exportNode.name except for VariableStatement_s. 55 readonly wasDefault: boolean; 56 readonly exportingModuleSymbol: Symbol; 57 } 58 59 function getInfo(context: RefactorContext, considerPartialSpans = true): ExportInfo | RefactorErrorInfo | undefined { 60 const { file, program } = context; 61 const span = getRefactorContextSpan(context); 62 const token = getTokenAtPosition(file, span.start); 63 const exportNode = !!(token.parent && getSyntacticModifierFlags(token.parent) & ModifierFlags.Export) && considerPartialSpans ? token.parent : getParentNodeInSpan(token, file, span); 64 if (!exportNode || (!isSourceFile(exportNode.parent) && !(isModuleBlock(exportNode.parent) && isAmbientModule(exportNode.parent.parent)))) { 65 return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_export_statement) }; 66 } 67 68 const checker = program.getTypeChecker(); 69 const exportingModuleSymbol = getExportingModuleSymbol(exportNode, checker); 70 const flags = getSyntacticModifierFlags(exportNode) || ((isExportAssignment(exportNode) && !exportNode.isExportEquals) ? ModifierFlags.ExportDefault : ModifierFlags.None); 71 72 const wasDefault = !!(flags & ModifierFlags.Default); 73 // If source file already has a default export, don't offer refactor. 74 if (!(flags & ModifierFlags.Export) || !wasDefault && exportingModuleSymbol.exports!.has(InternalSymbolName.Default)) { 75 return { error: getLocaleSpecificMessage(Diagnostics.This_file_already_has_a_default_export) }; 76 } 77 78 const noSymbolError = (id: Node) => 79 (isIdentifier(id) && checker.getSymbolAtLocation(id)) ? undefined 80 : { error: getLocaleSpecificMessage(Diagnostics.Can_only_convert_named_export) }; 81 82 switch (exportNode.kind) { 83 case SyntaxKind.FunctionDeclaration: 84 case SyntaxKind.ClassDeclaration: 85 case SyntaxKind.InterfaceDeclaration: 86 case SyntaxKind.EnumDeclaration: 87 case SyntaxKind.TypeAliasDeclaration: 88 case SyntaxKind.ModuleDeclaration: { 89 const node = exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration; 90 if (!node.name) return undefined; 91 return noSymbolError(node.name) 92 || { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol }; 93 } 94 case SyntaxKind.VariableStatement: { 95 const vs = exportNode as VariableStatement; 96 // Must be `export const x = something;`. 97 if (!(vs.declarationList.flags & NodeFlags.Const) || vs.declarationList.declarations.length !== 1) { 98 return undefined; 99 } 100 const decl = first(vs.declarationList.declarations); 101 if (!decl.initializer) return undefined; 102 Debug.assert(!wasDefault, "Can't have a default flag here"); 103 return noSymbolError(decl.name) 104 || { exportNode: vs, exportName: decl.name as Identifier, wasDefault, exportingModuleSymbol }; 105 } 106 case SyntaxKind.ExportAssignment: { 107 const node = exportNode as ExportAssignment; 108 if (node.isExportEquals) return undefined; 109 return noSymbolError(node.expression) 110 || { exportNode: node, exportName: node.expression as Identifier, wasDefault, exportingModuleSymbol }; 111 } 112 default: 113 return undefined; 114 } 115 } 116 117 function doChange(exportingSourceFile: SourceFile, program: Program, info: ExportInfo, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { 118 changeExport(exportingSourceFile, info, changes, program.getTypeChecker()); 119 changeImports(program, info, changes, cancellationToken); 120 } 121 122 function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: ExportInfo, changes: textChanges.ChangeTracker, checker: TypeChecker): void { 123 if (wasDefault) { 124 if (isExportAssignment(exportNode) && !exportNode.isExportEquals) { 125 const exp = exportNode.expression as Identifier; 126 const spec = makeExportSpecifier(exp.text, exp.text); 127 changes.replaceNode(exportingSourceFile, exportNode, factory.createExportDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false, factory.createNamedExports([spec]))); 128 } 129 else { 130 changes.delete(exportingSourceFile, Debug.checkDefined(findModifier(exportNode, SyntaxKind.DefaultKeyword), "Should find a default keyword in modifier list")); 131 } 132 } 133 else { 134 const exportKeyword = Debug.checkDefined(findModifier(exportNode, SyntaxKind.ExportKeyword), "Should find an export keyword in modifier list"); 135 switch (exportNode.kind) { 136 case SyntaxKind.FunctionDeclaration: 137 case SyntaxKind.ClassDeclaration: 138 case SyntaxKind.InterfaceDeclaration: 139 changes.insertNodeAfter(exportingSourceFile, exportKeyword, factory.createToken(SyntaxKind.DefaultKeyword)); 140 break; 141 case SyntaxKind.VariableStatement: 142 // If 'x' isn't used in this file and doesn't have type definition, `export const x = 0;` --> `export default 0;` 143 const decl = first(exportNode.declarationList.declarations); 144 if (!FindAllReferences.Core.isSymbolReferencedInFile(exportName, checker, exportingSourceFile) && !decl.type) { 145 // We checked in `getInfo` that an initializer exists. 146 changes.replaceNode(exportingSourceFile, exportNode, factory.createExportDefault(Debug.checkDefined(decl.initializer, "Initializer was previously known to be present"))); 147 break; 148 } 149 // falls through 150 case SyntaxKind.EnumDeclaration: 151 case SyntaxKind.TypeAliasDeclaration: 152 case SyntaxKind.ModuleDeclaration: 153 // `export type T = number;` -> `type T = number; export default T;` 154 changes.deleteModifier(exportingSourceFile, exportKeyword); 155 changes.insertNodeAfter(exportingSourceFile, exportNode, factory.createExportDefault(factory.createIdentifier(exportName.text))); 156 break; 157 default: 158 Debug.fail(`Unexpected exportNode kind ${(exportNode as ExportToConvert).kind}`); 159 } 160 } 161 } 162 163 function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: ExportInfo, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { 164 const checker = program.getTypeChecker(); 165 const exportSymbol = Debug.checkDefined(checker.getSymbolAtLocation(exportName), "Export name should resolve to a symbol"); 166 FindAllReferences.Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName.text, wasDefault, ref => { 167 if (exportName === ref) return; 168 const importingSourceFile = ref.getSourceFile(); 169 if (wasDefault) { 170 changeDefaultToNamedImport(importingSourceFile, ref, changes, exportName.text); 171 } 172 else { 173 changeNamedToDefaultImport(importingSourceFile, ref, changes); 174 } 175 }); 176 } 177 178 function changeDefaultToNamedImport(importingSourceFile: SourceFile, ref: Identifier, changes: textChanges.ChangeTracker, exportName: string): void { 179 const { parent } = ref; 180 switch (parent.kind) { 181 case SyntaxKind.PropertyAccessExpression: 182 // `a.default` --> `a.foo` 183 changes.replaceNode(importingSourceFile, ref, factory.createIdentifier(exportName)); 184 break; 185 case SyntaxKind.ImportSpecifier: 186 case SyntaxKind.ExportSpecifier: { 187 const spec = parent as ImportSpecifier | ExportSpecifier; 188 // `default as foo` --> `foo`, `default as bar` --> `foo as bar` 189 changes.replaceNode(importingSourceFile, spec, makeImportSpecifier(exportName, spec.name.text)); 190 break; 191 } 192 case SyntaxKind.ImportClause: { 193 const clause = parent as ImportClause; 194 Debug.assert(clause.name === ref, "Import clause name should match provided ref"); 195 const spec = makeImportSpecifier(exportName, ref.text); 196 const { namedBindings } = clause; 197 if (!namedBindings) { 198 // `import foo from "./a";` --> `import { foo } from "./a";` 199 changes.replaceNode(importingSourceFile, ref, factory.createNamedImports([spec])); 200 } 201 else if (namedBindings.kind === SyntaxKind.NamespaceImport) { 202 // `import foo, * as a from "./a";` --> `import * as a from ".a/"; import { foo } from "./a";` 203 changes.deleteRange(importingSourceFile, { pos: ref.getStart(importingSourceFile), end: namedBindings.getStart(importingSourceFile) }); 204 const quotePreference = isStringLiteral(clause.parent.moduleSpecifier) ? quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : QuotePreference.Double; 205 const newImport = makeImport(/*default*/ undefined, [makeImportSpecifier(exportName, ref.text)], clause.parent.moduleSpecifier, quotePreference); 206 changes.insertNodeAfter(importingSourceFile, clause.parent, newImport); 207 } 208 else { 209 // `import foo, { bar } from "./a"` --> `import { bar, foo } from "./a";` 210 changes.delete(importingSourceFile, ref); 211 changes.insertNodeAtEndOfList(importingSourceFile, namedBindings.elements, spec); 212 } 213 break; 214 } 215 case SyntaxKind.ImportType: 216 const importTypeNode = parent as ImportTypeNode; 217 changes.replaceNode(importingSourceFile, parent, factory.createImportTypeNode(importTypeNode.argument, importTypeNode.assertions, factory.createIdentifier(exportName), importTypeNode.typeArguments, importTypeNode.isTypeOf)); 218 break; 219 default: 220 Debug.failBadSyntaxKind(parent); 221 } 222 } 223 224 function changeNamedToDefaultImport(importingSourceFile: SourceFile, ref: Identifier, changes: textChanges.ChangeTracker): void { 225 const parent = ref.parent as PropertyAccessExpression | ImportSpecifier | ExportSpecifier; 226 switch (parent.kind) { 227 case SyntaxKind.PropertyAccessExpression: 228 // `a.foo` --> `a.default` 229 changes.replaceNode(importingSourceFile, ref, factory.createIdentifier("default")); 230 break; 231 case SyntaxKind.ImportSpecifier: { 232 // `import { foo } from "./a";` --> `import foo from "./a";` 233 // `import { foo as bar } from "./a";` --> `import bar from "./a";` 234 const defaultImport = factory.createIdentifier(parent.name.text); 235 if (parent.parent.elements.length === 1) { 236 changes.replaceNode(importingSourceFile, parent.parent, defaultImport); 237 } 238 else { 239 changes.delete(importingSourceFile, parent); 240 changes.insertNodeBefore(importingSourceFile, parent.parent, defaultImport); 241 } 242 break; 243 } 244 case SyntaxKind.ExportSpecifier: { 245 // `export { foo } from "./a";` --> `export { default as foo } from "./a";` 246 // `export { foo as bar } from "./a";` --> `export { default as bar } from "./a";` 247 // `export { foo as default } from "./a";` --> `export { default } from "./a";` 248 // (Because `export foo from "./a";` isn't valid syntax.) 249 changes.replaceNode(importingSourceFile, parent, makeExportSpecifier("default", parent.name.text)); 250 break; 251 } 252 default: 253 Debug.assertNever(parent, `Unexpected parent kind ${(parent as Node).kind}`); 254 } 255 256 } 257 258 function makeImportSpecifier(propertyName: string, name: string): ImportSpecifier { 259 return factory.createImportSpecifier(/*isTypeOnly*/ false, propertyName === name ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name)); 260 } 261 262 function makeExportSpecifier(propertyName: string, name: string): ExportSpecifier { 263 return factory.createExportSpecifier(/*isTypeOnly*/ false, propertyName === name ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name)); 264 } 265 266 function getExportingModuleSymbol(node: Node, checker: TypeChecker) { 267 const parent = node.parent; 268 if (isSourceFile(parent)) { 269 return parent.symbol; 270 } 271 const symbol = parent.parent.symbol; 272 if (symbol.valueDeclaration && isExternalModuleAugmentation(symbol.valueDeclaration)) { 273 return checker.getMergedSymbol(symbol); 274 } 275 return symbol; 276 } 277} 278