diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json
index 9b30a51a07b2a..16dac1ac07c36 100644
--- a/src/compiler/diagnosticMessages.json
+++ b/src/compiler/diagnosticMessages.json
@@ -6598,7 +6598,14 @@
"category": "Message",
"code": 90058
},
-
+ "Export '{0}' from module '{1}'": {
+ "category": "Message",
+ "code": 90059
+ },
+ "Export all missing members from modules": {
+ "category": "Message",
+ "code": 90060
+ },
"Convert function to an ES2015 class": {
"category": "Message",
"code": 95001
diff --git a/src/services/codefixes/fixImportNonExportedMember.ts b/src/services/codefixes/fixImportNonExportedMember.ts
new file mode 100644
index 0000000000000..6c957a41900fc
--- /dev/null
+++ b/src/services/codefixes/fixImportNonExportedMember.ts
@@ -0,0 +1,204 @@
+/* @internal */
+namespace ts.codefix {
+ const fixId = "importNonExportedMember";
+
+ const errorCodes = [
+ Diagnostics.Module_0_declares_1_locally_but_it_is_not_exported.code,
+ ];
+
+ registerCodeFix({
+ errorCodes,
+
+ getCodeActions(context) {
+ const { sourceFile } = context;
+
+ const info = getInfo(sourceFile, context, context.span.start);
+
+ if (!info || info.originSourceFile.isDeclarationFile) {
+ return undefined;
+ }
+
+ const changes = textChanges.ChangeTracker.with(context, (t) =>
+ doChange(t, info.originSourceFile, info.node)
+ );
+
+ return [
+ createCodeFixAction(
+ /*fixName*/ fixId,
+ changes,
+ /*description*/ [
+ Diagnostics.Export_0_from_module_1,
+ info.node.text,
+ showModuleSpecifier(info.importDecl),
+ ],
+ fixId,
+ /*fixAllDescription*/ Diagnostics.Export_all_missing_members_from_modules
+ ),
+ ];
+ },
+
+ fixIds: [fixId],
+
+ getAllCodeActions: (context) =>
+ codeFixAll(context, errorCodes, (changes, diag) => {
+ const info = getInfo(diag.file, context, diag.start);
+
+ if (info) {
+ doChange(changes, info.originSourceFile, info.node);
+ }
+ }),
+ });
+
+ interface Info {
+ readonly node: Identifier;
+
+ readonly importDecl: ImportDeclaration;
+
+ readonly originSourceFile: SourceFile;
+ }
+
+ function getInfo(
+ sourceFile: SourceFile,
+ context: CodeFixContext | CodeFixAllContext,
+ pos: number
+ ): Info | undefined {
+ const node = getTokenAtPosition(sourceFile, pos);
+
+ if (!isIdentifier(node)) {
+ return;
+ }
+
+ const importDecl = findAncestor(node, isImportDeclaration);
+
+ if (!importDecl || !isStringLiteralLike(importDecl.moduleSpecifier)) {
+ return undefined;
+ }
+
+ const resolvedModule = getResolvedModule(
+ sourceFile,
+ /*moduleName*/ importDecl.moduleSpecifier.text,
+ /*mode*/ undefined
+ );
+
+ if (!resolvedModule) {
+ return undefined;
+ }
+
+ const originSourceFile = context.program.getSourceFile(
+ resolvedModule.resolvedFileName
+ );
+
+ if (!originSourceFile) {
+ return undefined;
+ }
+
+ return { node, importDecl, originSourceFile };
+ }
+
+ function sortSpecifiers(
+ specifiers: ExportSpecifier[]
+ ): readonly ExportSpecifier[] {
+ return stableSort(specifiers, (s1, s2) =>
+ compareStringsCaseInsensitive(
+ (s1.propertyName || s1.name).text,
+ (s2.propertyName || s2.name).text
+ )
+ );
+ }
+
+ const isVariableDeclarationListWith1Element = (
+ node: Node
+ ): node is VariableDeclarationList =>
+ isVariableDeclarationList(node) && node.declarations.length === 1;
+
+ function doChange(
+ changes: textChanges.ChangeTracker,
+ sourceFile: SourceFile,
+ node: Identifier
+ ): void {
+ const moduleSymbol = sourceFile.localSymbol ?? sourceFile.symbol;
+
+ const localSymbol = moduleSymbol.valueDeclaration?.locals?.get(
+ node.escapedText
+ );
+
+ if (localSymbol === undefined) {
+ return;
+ }
+
+ // Node we need to export is a function, class, or variable declaration which can have `export` prepended
+ if (
+ localSymbol.valueDeclaration !== undefined &&
+ (isDeclarationStatement(localSymbol.valueDeclaration) ||
+ isVariableStatement(localSymbol.valueDeclaration))
+ ) {
+ const node = localSymbol.valueDeclaration;
+
+ return changes.insertExportModifier(sourceFile, node);
+ }
+ else if (
+ localSymbol.valueDeclaration &&
+ isVariableDeclarationListWith1Element(
+ localSymbol.valueDeclaration.parent
+ )
+ ) {
+ const node = localSymbol.valueDeclaration.parent;
+
+ return changes.insertExportModifier(sourceFile, node);
+ }
+
+ // In all other cases the node should be exported via `export {a}`
+ // Search for an export statement we can use
+ for (const namedExportDeclaration of sourceFile.statements) {
+ if (
+ isExportDeclaration(namedExportDeclaration) &&
+ namedExportDeclaration.exportClause &&
+ isNamedExports(namedExportDeclaration.exportClause) &&
+ !namedExportDeclaration.isTypeOnly && // don't use `export type {...}`
+ namedExportDeclaration.moduleSpecifier === undefined // don't use `export {...} from`
+ ) {
+ return changes.replaceNode(
+ sourceFile,
+ namedExportDeclaration,
+ factory.updateExportDeclaration(
+ namedExportDeclaration,
+ /*modifiers*/ undefined,
+ /*isTypeOnly*/ false,
+ /*exportClause*/ factory.updateNamedExports(
+ namedExportDeclaration.exportClause,
+ /*elements*/ sortSpecifiers(
+ namedExportDeclaration.exportClause.elements.concat(
+ factory.createExportSpecifier(
+ /*isTypeOnly*/ false,
+ /*propertyName*/ undefined,
+ node
+ )
+ )
+ )
+ ),
+ /*moduleSpecifier*/ undefined,
+ /*assertClause*/ undefined
+ )
+ );
+ }
+ }
+
+ // If we won't find an existing `export` statement we can use, create one
+ return changes.insertNodeAtEndOfScope(
+ sourceFile,
+ sourceFile,
+ factory.createExportDeclaration(
+ /*modifiers*/ undefined,
+ /*isTypeOnly*/ false,
+ /*exportClause*/ factory.createNamedExports([
+ factory.createExportSpecifier(
+ /*isTypeOnly*/ false,
+ /*propertyName*/ undefined,
+ node
+ ),
+ ]),
+ /*moduleSpecifier*/ undefined
+ )
+ );
+ }
+}
diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts
index 59f22c8a9fbe0..ff7abe3973636 100644
--- a/src/services/textChanges.ts
+++ b/src/services/textChanges.ts
@@ -780,8 +780,20 @@ namespace ts.textChanges {
}
}
- public insertExportModifier(sourceFile: SourceFile, node: DeclarationStatement | VariableStatement): void {
- this.insertText(sourceFile, node.getStart(sourceFile), "export ");
+ public insertExportModifier(
+ sourceFile: SourceFile,
+ node: DeclarationStatement | VariableStatement | VariableDeclarationList
+ ): void {
+ Debug.assert(
+ /*expression*/ !(isVariableDeclarationList(node) && node.declarations.length !== 1),
+ /*message*/ "Only allow adding export modifier to variable lists with 1 element"
+ );
+
+ this.insertText(
+ sourceFile,
+ node.getStart(sourceFile, /*includeJsDocComment*/ false),
+ "export "
+ );
}
public insertImportSpecifierAtIndex(sourceFile: SourceFile, importSpecifier: ImportSpecifier, namedImports: NamedImports, index: number) {
diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json
index cef6b9139698a..87b6c01ffc754 100644
--- a/src/services/tsconfig.json
+++ b/src/services/tsconfig.json
@@ -115,6 +115,7 @@
"codefixes/convertConstToLet.ts",
"codefixes/fixExpectedComma.ts",
"codefixes/fixAddVoidToPromise.ts",
+ "codefixes/fixImportNonExportedMember.ts",
"refactors/convertExport.ts",
"refactors/convertImport.ts",
"refactors/convertToOptionalChainExpression.ts",
@@ -134,6 +135,6 @@
"transform.ts",
"shims.ts",
"globalThisShim.ts",
- "exportAsModule.ts"
+ "exportAsModule.ts",
]
}
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all.ts
new file mode 100644
index 0000000000000..87fe4311a9cc6
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all.ts
@@ -0,0 +1,23 @@
+///
+
+// @Filename: /a.ts
+////declare function foo(): any;
+////declare function bar(): any;
+////declare function zoo(): any;
+////export { zoo }
+
+// @Filename: /b.ts
+////import { foo, bar } from "./a";
+
+goTo.file("/b.ts");
+verify.codeFixAll({
+ fixId: "importNonExportedMember",
+ fixAllDescription:
+ ts.Diagnostics.Export_all_missing_members_from_modules.message,
+ newFileContent: {
+ "/a.ts": `export declare function foo(): any;
+export declare function bar(): any;
+declare function zoo(): any;
+export { zoo }`,
+ },
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_class_inAList.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_class_inAList.ts
new file mode 100644
index 0000000000000..88faee12ce68b
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_class_inAList.ts
@@ -0,0 +1,24 @@
+///
+// @Filename: /a.ts
+////class a{}, class b{};
+////export let c = 3;
+
+// @Filename: /b.ts
+////import { a, b, c } from "./a"
+
+goTo.file("/b.ts");
+verify.codeFixAvailable([
+ { description: `Export 'a' from module './a'` },
+ { description: `Export 'b' from module './a'` },
+ { description: `Remove import from './a'` },
+]);
+
+// Can export class in list
+verify.codeFix({
+ index: 1,
+ description: `Export 'b' from module './a'`,
+ newFileContent: {
+ "/a.ts": `class a{}, export class b{};
+export let c = 3;`,
+ },
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_class_onItsOwn.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_class_onItsOwn.ts
new file mode 100644
index 0000000000000..c369c0013c4df
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_class_onItsOwn.ts
@@ -0,0 +1,22 @@
+///
+// @Filename: /a.ts
+////class a{};
+////export let b = 2;
+
+// @Filename: /b.ts
+////import { a, b } from "./a"
+
+goTo.file("/b.ts");
+verify.codeFixAvailable([
+ { description: `Export 'a' from module './a'` },
+ { description: `Remove import from './a'` },
+]);
+// Can export a class
+verify.codeFix({
+ index: 0,
+ description: `Export 'a' from module './a'`,
+ newFileContent: {
+ "/a.ts": `export class a{};
+export let b = 2;`,
+ },
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_function_withDeclare.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_function_withDeclare.ts
new file mode 100644
index 0000000000000..740b7c6e56446
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_function_withDeclare.ts
@@ -0,0 +1,31 @@
+///
+// @Filename: /a.ts
+////declare function zoo(): any;
+////export { zoo };
+
+// @Filename: /b.ts
+////declare function foo(): any;
+////function bar(): any;
+////export { foo };
+
+// @Filename: /c.ts
+////import { zoo } from "./a";
+////import { bar } from "./b";
+
+goTo.file("/c.ts");
+// Recognises that importing from a file with a `declare` is ok if its exported
+verify.codeFixAvailable([
+ { description: `Export 'bar' from module './b'` },
+ { description: `Remove import from './a'` },
+ { description: `Remove import from './b'` },
+]);
+// Exports a function with a `declare` correctly
+verify.codeFix({
+ index: 0,
+ description: `Export 'bar' from module './b'`,
+ newFileContent: {
+ "/b.ts": `declare function foo(): any;
+export function bar(): any;
+export { foo };`,
+ },
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_jsdoc_onClass.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_jsdoc_onClass.ts
new file mode 100644
index 0000000000000..d590b39f174fe
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_jsdoc_onClass.ts
@@ -0,0 +1,29 @@
+///
+// @Filename: /a.ts
+/////**
+//// * baz
+//// */
+////class a{};
+////export let b = 3;
+
+// @Filename: /b.ts
+////import { a, b } from "./a"
+
+goTo.file("/b.ts");
+verify.codeFixAvailable([
+ { description: `Export 'a' from module './a'` },
+ { description: `Remove import from './a'` },
+]);
+
+// Doesn't clobber jsdoc on classes
+verify.codeFix({
+ index: 0,
+ description: `Export 'a' from module './a'`,
+ newFileContent: {
+ "/a.ts": `/**
+ * baz
+ */
+export class a{};
+export let b = 3;`,
+ },
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_jsdoc_onFunction.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_jsdoc_onFunction.ts
new file mode 100644
index 0000000000000..e85b1685f188f
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_jsdoc_onFunction.ts
@@ -0,0 +1,29 @@
+///
+// @Filename: /a.ts
+/////**
+//// * foo
+//// */
+////function a(){};
+////export let b = 3;
+
+// @Filename: /b.ts
+////import { a, b } from "./a"
+
+goTo.file("/b.ts");
+verify.codeFixAvailable([
+ { description: `Export 'a' from module './a'` },
+ { description: `Remove import from './a'` },
+]);
+
+// Doesn't clobber jsdoc on functions
+verify.codeFix({
+ index: 0,
+ description: `Export 'a' from module './a'`,
+ newFileContent: {
+ "/a.ts": `/**
+ * foo
+ */
+export function a(){};
+export let b = 3;`,
+ },
+});
\ No newline at end of file
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_jsdoc_onVariable.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_jsdoc_onVariable.ts
new file mode 100644
index 0000000000000..fe8cd30bb1795
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_jsdoc_onVariable.ts
@@ -0,0 +1,30 @@
+///
+// @Filename: /a.ts
+/////**
+//// * bar
+//// */
+////let a = 4;
+////export let b = 3;
+
+// @Filename: /b.ts
+////import { a, b } from "./a"
+
+goTo.file("/b.ts");
+verify.codeFixAvailable([
+ { description: `Export 'a' from module './a'` },
+ { description: `Remove import from './a'` },
+]);
+
+
+// Doesn't clobber jsdoc on variables
+verify.codeFix({
+ index: 0,
+ description: `Export 'a' from module './a'`,
+ newFileContent: {
+ "/a.ts": `/**
+ * bar
+ */
+export let a = 4;
+export let b = 3;`,
+ },
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_variable_inAList.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_variable_inAList.ts
new file mode 100644
index 0000000000000..103e29ec6db05
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_variable_inAList.ts
@@ -0,0 +1,27 @@
+///
+
+// @Filename: /a.ts
+////let a = 1, b = 2, c = 3;
+////export let d = 4;
+
+// @Filename: /b.ts
+////import { a } from "./a"
+
+goTo.file("/b.ts");
+verify.codeFixAvailable([
+ { description: `Export 'a' from module './a'` },
+ { description: `Remove import from './a'` },
+]);
+
+// Can fix a variable in a list (adds a named export)
+verify.codeFix({
+ index: 0,
+ description: `Export 'a' from module './a'`,
+ newFileContent: {
+ "/a.ts": `let a = 1, b = 2, c = 3;
+export let d = 4;
+
+export { a };
+`,
+ },
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_variable_onItsOwn.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_variable_onItsOwn.ts
new file mode 100644
index 0000000000000..fef6d78bb68f7
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_variable_onItsOwn.ts
@@ -0,0 +1,24 @@
+///
+// @Filename: /a.ts
+////let a = 4;
+////export let b = 2;
+
+// @Filename: /b.ts
+////import { a, b } from "./a"
+
+
+goTo.file("/b.ts");
+verify.codeFixAvailable([
+ { description: `Export 'a' from module './a'` },
+ { description: `Remove import from './a'` },
+]);
+
+// Can fix a single variable
+verify.codeFix({
+ index: 0,
+ description: `Export 'a' from module './a'`,
+ newFileContent: {
+ "/a.ts": `export let a = 4;
+export let b = 2;`,
+ },
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_will_useExistingExportStatement.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_will_useExistingExportStatement.ts
new file mode 100644
index 0000000000000..669f228cdb0e2
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_will_useExistingExportStatement.ts
@@ -0,0 +1,28 @@
+///
+// @Filename: /a.ts
+////let a = 1, b = 2, c = 3;
+////let d = 4;
+////export function whatever() {
+////}
+////export { d }
+
+// @Filename: /b.ts
+////import { a, d } from "./a"
+
+goTo.file("/b.ts");
+verify.codeFixAvailable([
+ { description: `Export 'a' from module './a'` },
+ { description: `Remove import from './a'` },
+]);
+// Can export from list using an existing export
+verify.codeFix({
+ index: 0,
+ description: `Export 'a' from module './a'`,
+ newFileContent: {
+ "/a.ts": `let a = 1, b = 2, c = 3;
+let d = 4;
+export function whatever() {
+}
+export { a, d };`,
+ },
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_wont_applyToModuleDotExports.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_wont_applyToModuleDotExports.ts
new file mode 100644
index 0000000000000..89bee7093b507
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_wont_applyToModuleDotExports.ts
@@ -0,0 +1,10 @@
+///
+// @Filename: /node_modules/foo/index.d.ts
+////let a = 0
+////module.exports = 0;
+
+// @Filename: /a.ts
+////import { a } from "foo";
+
+// Won't apply fix if `module.exports` is used
+verify.not.codeFixAvailable();
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_wont_clobberExportAs.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_wont_clobberExportAs.ts
new file mode 100644
index 0000000000000..86d95aa8b6a44
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_wont_clobberExportAs.ts
@@ -0,0 +1,28 @@
+///
+// @Filename: /a.ts
+////export let a = 1;
+
+// @Filename: /b.ts
+////let b = 2, c = 3;
+////export * as a from "./a";
+
+// @Filename: /c.ts
+////import { a, b } from "./b"
+
+// Doesn't use/clobber `export * as a from`
+goTo.file("/c.ts");
+verify.codeFixAvailable([
+ { description: `Export 'b' from module './b'` },
+ { description: `Remove import from './b'` },
+]);
+verify.codeFix({
+ index: 0,
+ description: `Export 'b' from module './b'`,
+ newFileContent: {
+ "/b.ts": `let b = 2, c = 3;
+export * as a from "./a";
+
+export { b };
+`,
+ },
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_wont_clobberExportFrom.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_wont_clobberExportFrom.ts
new file mode 100644
index 0000000000000..8e13d92c97922
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_wont_clobberExportFrom.ts
@@ -0,0 +1,28 @@
+///
+// @Filename: /a.ts
+////export let a = 1;
+
+// @Filename: /b.ts
+////let b = 2, c = 3;
+////export { a } from "./a";
+
+// @Filename: /c.ts
+////import { a, b } from "./b"
+
+// Doesn't use/clobber `export {...} from`
+goTo.file("/c.ts");
+verify.codeFixAvailable([
+ { description: `Export 'b' from module './b'` },
+ { description: `Remove import from './b'` },
+]);
+verify.codeFix({
+ index: 0,
+ description: `Export 'b' from module './b'`,
+ newFileContent: {
+ "/b.ts": `let b = 2, c = 3;
+export { a } from "./a";
+
+export { b };
+`,
+ },
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_wont_clobberExportType.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_wont_clobberExportType.ts
new file mode 100644
index 0000000000000..f2a33b00f0821
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_wont_clobberExportType.ts
@@ -0,0 +1,27 @@
+///
+// @Filename: /a.ts
+////let a = 1, b = 2;
+////type c = {foo: string};
+////export type { c };
+
+// @Filename: /b.ts
+////import { a } from "./a"
+
+// Doesn't use/clobber `export type {...}`
+goTo.file("/b.ts");
+verify.codeFixAvailable([
+ { description: `Export 'a' from module './a'` },
+ { description: `Remove import from './a'` },
+]);
+verify.codeFix({
+ index: 0,
+ description: `Export 'a' from module './a'`,
+ newFileContent: {
+ "/a.ts": `let a = 1, b = 2;
+type c = {foo: string};
+export type { c };
+
+export { a };
+`,
+ },
+});