From 97f9a7e810d4fbc5c986994ab520bdd7794b6015 Mon Sep 17 00:00:00 2001 From: Will Binns-Smith Date: Fri, 14 May 2021 12:56:23 -0700 Subject: [PATCH] Export an empty named export declaration to imply ESM --- .../src/index.ts | 219 ++++++++++-------- .../fixtures/declarations/erased/output.mjs | 2 + .../declarations/nested-namespace/output.mjs | 2 + .../exports/export-type-from/output.mjs | 1 + .../fixtures/exports/export-type/output.mjs | 1 + .../fixtures/imports/elide-preact/output.mjs | 1 + .../fixtures/imports/elide-react/output.mjs | 1 + .../fixtures/imports/elide-typeof/output.mjs | 1 + .../imports/elision-qualifiedname/output.mjs | 1 + .../imports/elision-rename/output.mjs | 1 + .../test/fixtures/imports/enum-id/output.mjs | 1 + .../import-type-not-removed/output.mjs | 1 + .../fixtures/imports/import-type/output.mjs | 1 + .../imports/property-signature/output.mjs | 1 + .../fixtures/namespace/canonical/output.mjs | 2 + 15 files changed, 145 insertions(+), 91 deletions(-) diff --git a/packages/babel-plugin-transform-typescript/src/index.ts b/packages/babel-plugin-transform-typescript/src/index.ts index aec840965bd9..5b7a0babc36d 100644 --- a/packages/babel-plugin-transform-typescript/src/index.ts +++ b/packages/babel-plugin-transform-typescript/src/index.ts @@ -21,8 +21,12 @@ function isInType(path) { } } -const PARSED_PARAMS = new WeakSet(); const GLOBAL_TYPES = new WeakMap(); +// Track programs which contain imports/exports of values, so that we can include +// empty exports for programs that do not, but were parsed as modules. This allows +// tools to infer unamibiguously that results are ESM. +const NEEDS_EXPLICIT_ESM = new WeakMap(); +const PARSED_PARAMS = new WeakSet(); function isGlobalType(path, name) { const program = path.find(path => path.isProgram()).node; @@ -175,118 +179,140 @@ export default declare((api, opts) => { Identifier: visitPattern, RestElement: visitPattern, - Program(path, state) { - const { file } = state; - let fileJsxPragma = null; - let fileJsxPragmaFrag = null; + Program: { + enter(path, state) { + const { file } = state; + let fileJsxPragma = null; + let fileJsxPragmaFrag = null; - if (!GLOBAL_TYPES.has(path.node)) { - GLOBAL_TYPES.set(path.node, new Set()); - } + if (!GLOBAL_TYPES.has(path.node)) { + GLOBAL_TYPES.set(path.node, new Set()); + } - if (file.ast.comments) { - for (const comment of file.ast.comments) { - const jsxMatches = JSX_PRAGMA_REGEX.exec(comment.value); - if (jsxMatches) { - if (jsxMatches[1]) { - // isFragment - fileJsxPragmaFrag = jsxMatches[2]; - } else { - fileJsxPragma = jsxMatches[2]; + if (file.ast.comments) { + for (const comment of file.ast.comments) { + const jsxMatches = JSX_PRAGMA_REGEX.exec(comment.value); + if (jsxMatches) { + if (jsxMatches[1]) { + // isFragment + fileJsxPragmaFrag = jsxMatches[2]; + } else { + fileJsxPragma = jsxMatches[2]; + } } } } - } - let pragmaImportName = fileJsxPragma || jsxPragma; - if (pragmaImportName) { - [pragmaImportName] = pragmaImportName.split("."); - } + let pragmaImportName = fileJsxPragma || jsxPragma; + if (pragmaImportName) { + [pragmaImportName] = pragmaImportName.split("."); + } - let pragmaFragImportName = fileJsxPragmaFrag || jsxPragmaFrag; - if (pragmaFragImportName) { - [pragmaFragImportName] = pragmaFragImportName.split("."); - } + let pragmaFragImportName = fileJsxPragmaFrag || jsxPragmaFrag; + if (pragmaFragImportName) { + [pragmaFragImportName] = pragmaFragImportName.split("."); + } - // remove type imports - for (let stmt of path.get("body")) { - if (stmt.isImportDeclaration()) { - if (stmt.node.importKind === "type") { - stmt.remove(); - continue; - } + // remove type imports + for (let stmt of path.get("body")) { + if (stmt.isImportDeclaration()) { + if (!NEEDS_EXPLICIT_ESM.has(state.file.ast.program)) { + NEEDS_EXPLICIT_ESM.set(state.file.ast.program, true); + } - // If onlyRemoveTypeImports is `true`, only remove type-only imports - // and exports introduced in TypeScript 3.8. - if (!onlyRemoveTypeImports) { - // Note: this will allow both `import { } from "m"` and `import "m";`. - // In TypeScript, the former would be elided. - if (stmt.node.specifiers.length === 0) { + if (stmt.node.importKind === "type") { + stmt.remove(); continue; } - let allElided = true; - const importsToRemove: NodePath[] = []; - - for (const specifier of stmt.node.specifiers) { - const binding = stmt.scope.getBinding(specifier.local.name); - - // The binding may not exist if the import node was explicitly - // injected by another plugin. Currently core does not do a good job - // of keeping scope bindings synchronized with the AST. For now we - // just bail if there is no binding, since chances are good that if - // the import statement was injected then it wasn't a typescript type - // import anyway. - if ( - binding && - isImportTypeOnly({ - binding, - programPath: path, - pragmaImportName, - pragmaFragImportName, - }) - ) { - importsToRemove.push(binding.path); - } else { - allElided = false; + // If onlyRemoveTypeImports is `true`, only remove type-only imports + // and exports introduced in TypeScript 3.8. + if (onlyRemoveTypeImports) { + NEEDS_EXPLICIT_ESM.set(path.node, false); + } else { + // Note: this will allow both `import { } from "m"` and `import "m";`. + // In TypeScript, the former would be elided. + if (stmt.node.specifiers.length === 0) { + NEEDS_EXPLICIT_ESM.set(path.node, false); + continue; } - } - if (allElided) { - stmt.remove(); - } else { - for (const importPath of importsToRemove) { - importPath.remove(); + let allElided = true; + const importsToRemove: NodePath[] = []; + + for (const specifier of stmt.node.specifiers) { + const binding = stmt.scope.getBinding(specifier.local.name); + + // The binding may not exist if the import node was explicitly + // injected by another plugin. Currently core does not do a good job + // of keeping scope bindings synchronized with the AST. For now we + // just bail if there is no binding, since chances are good that if + // the import statement was injected then it wasn't a typescript type + // import anyway. + if ( + binding && + isImportTypeOnly({ + binding, + programPath: path, + pragmaImportName, + pragmaFragImportName, + }) + ) { + importsToRemove.push(binding.path); + } else { + allElided = false; + NEEDS_EXPLICIT_ESM.set(path.node, false); + } + } + + if (allElided) { + stmt.remove(); + } else { + for (const importPath of importsToRemove) { + importPath.remove(); + } } } - } - continue; - } + continue; + } - if (stmt.isExportDeclaration()) { - stmt = stmt.get("declaration"); - } + if (stmt.isExportDeclaration()) { + stmt = stmt.get("declaration"); + } - if (stmt.isVariableDeclaration({ declare: true })) { - for (const name of Object.keys(stmt.getBindingIdentifiers())) { - registerGlobalType(path.scope, name); + if (stmt.isVariableDeclaration({ declare: true })) { + for (const name of Object.keys(stmt.getBindingIdentifiers())) { + registerGlobalType(path.scope, name); + } + } else if ( + stmt.isTSTypeAliasDeclaration() || + stmt.isTSDeclareFunction() || + stmt.isTSInterfaceDeclaration() || + stmt.isClassDeclaration({ declare: true }) || + stmt.isTSEnumDeclaration({ declare: true }) || + (stmt.isTSModuleDeclaration({ declare: true }) && + stmt.get("id").isIdentifier()) + ) { + registerGlobalType(path.scope, stmt.node.id.name); } - } else if ( - stmt.isTSTypeAliasDeclaration() || - stmt.isTSDeclareFunction() || - stmt.isTSInterfaceDeclaration() || - stmt.isClassDeclaration({ declare: true }) || - stmt.isTSEnumDeclaration({ declare: true }) || - (stmt.isTSModuleDeclaration({ declare: true }) && - stmt.get("id").isIdentifier()) - ) { - registerGlobalType(path.scope, stmt.node.id.name); } - } + }, + exit(path) { + if (NEEDS_EXPLICIT_ESM.get(path.node)) { + // If there are no remaining value exports, this file can no longer + // be inferred to be ESM. Leave behind an empty export declaration + // so it can be. + path.pushContainer("body", t.exportNamedDeclaration()); + } + }, }, - ExportNamedDeclaration(path) { + ExportNamedDeclaration(path, state) { + if (!NEEDS_EXPLICIT_ESM.has(state.file.ast.program)) { + NEEDS_EXPLICIT_ESM.set(state.file.ast.program, true); + } + if (path.node.exportKind === "type") { path.remove(); return; @@ -307,7 +333,10 @@ export default declare((api, opts) => { ) ) { path.remove(); + return; } + + NEEDS_EXPLICIT_ESM.set(state.file.ast.program, false); }, ExportSpecifier(path) { @@ -317,14 +346,22 @@ export default declare((api, opts) => { } }, - ExportDefaultDeclaration(path) { + ExportDefaultDeclaration(path, state) { + if (!NEEDS_EXPLICIT_ESM.has(state.file.ast.program)) { + NEEDS_EXPLICIT_ESM.set(state.file.ast.program, true); + } + // remove whole declaration if it's exporting a TS type if ( t.isIdentifier(path.node.declaration) && isGlobalType(path, path.node.declaration.name) ) { path.remove(); + + return; } + + NEEDS_EXPLICIT_ESM.set(state.file.ast.program, false); }, TSDeclareFunction(path) { diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/declarations/erased/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/declarations/erased/output.mjs index dab5d3d1c154..b7786bef8d9d 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/declarations/erased/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/declarations/erased/output.mjs @@ -1 +1,3 @@ ; // Otherwise-empty file + +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/declarations/nested-namespace/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/declarations/nested-namespace/output.mjs index dab5d3d1c154..b7786bef8d9d 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/declarations/nested-namespace/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/declarations/nested-namespace/output.mjs @@ -1 +1,3 @@ ; // Otherwise-empty file + +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/exports/export-type-from/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/exports/export-type-from/output.mjs index 092bc2b04126..95da36c2f396 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/exports/export-type-from/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/exports/export-type-from/output.mjs @@ -1 +1,2 @@ ; +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/exports/export-type/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/exports/export-type/output.mjs index 092bc2b04126..95da36c2f396 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/exports/export-type/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/exports/export-type/output.mjs @@ -1 +1,2 @@ ; +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-preact/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-preact/output.mjs index 0773757932ed..a8ee57e62871 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-preact/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-preact/output.mjs @@ -1 +1,2 @@ const x = 0; +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-react/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-react/output.mjs index 0773757932ed..a8ee57e62871 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-react/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-react/output.mjs @@ -1 +1,2 @@ const x = 0; +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-typeof/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-typeof/output.mjs index 0773757932ed..a8ee57e62871 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-typeof/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/imports/elide-typeof/output.mjs @@ -1 +1,2 @@ const x = 0; +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/imports/elision-qualifiedname/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/imports/elision-qualifiedname/output.mjs index 0773757932ed..a8ee57e62871 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/imports/elision-qualifiedname/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/imports/elision-qualifiedname/output.mjs @@ -1 +1,2 @@ const x = 0; +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/imports/elision-rename/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/imports/elision-rename/output.mjs index 0773757932ed..a8ee57e62871 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/imports/elision-rename/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/imports/elision-rename/output.mjs @@ -1 +1,2 @@ const x = 0; +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/imports/enum-id/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/imports/enum-id/output.mjs index 628c6ba0ff86..63d68f19ffe6 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/imports/enum-id/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/imports/enum-id/output.mjs @@ -5,3 +5,4 @@ var Enum; })(Enum || (Enum = {})); ; +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/imports/import-type-not-removed/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/imports/import-type-not-removed/output.mjs index 3e83039477c8..d3b5fc3d17c6 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/imports/import-type-not-removed/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/imports/import-type-not-removed/output.mjs @@ -1,2 +1,3 @@ // TODO: This should not be removed ; +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/imports/import-type/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/imports/import-type/output.mjs index 092bc2b04126..95da36c2f396 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/imports/import-type/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/imports/import-type/output.mjs @@ -1 +1,2 @@ ; +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/imports/property-signature/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/imports/property-signature/output.mjs index cba06b741a87..f88bedaa1f22 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/imports/property-signature/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/imports/property-signature/output.mjs @@ -1,3 +1,4 @@ const obj = { A: 'foo' }; +export {}; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/namespace/canonical/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/namespace/canonical/output.mjs index f6993c98b8f4..889b59b131c5 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/namespace/canonical/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/namespace/canonical/output.mjs @@ -37,3 +37,5 @@ for (let s of strings) { console.log(`"${s}" - ${validators[name].isAcceptable(s) ? "matches" : "does not match"} ${name}`); } } + +export {};