Skip to content

Commit

Permalink
Export an empty named export declaration to imply ESM
Browse files Browse the repository at this point in the history
  • Loading branch information
Will Binns-Smith committed May 18, 2021
1 parent 229a548 commit 8589908
Show file tree
Hide file tree
Showing 27 changed files with 162 additions and 92 deletions.
214 changes: 122 additions & 92 deletions packages/babel-plugin-transform-typescript/src/index.ts
Expand Up @@ -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 IMPLICITLY_ESM = new WeakSet();
const PARSED_PARAMS = new WeakSet();

function isGlobalType(path, name) {
const program = path.find(path => path.isProgram()).node;
Expand Down Expand Up @@ -175,118 +179,135 @@ 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 pragmaFragImportName = fileJsxPragmaFrag || jsxPragmaFrag;
if (pragmaFragImportName) {
[pragmaFragImportName] = pragmaFragImportName.split(".");
}
let pragmaImportName = fileJsxPragma || jsxPragma;
if (pragmaImportName) {
[pragmaImportName] = pragmaImportName.split(".");
}

// remove type imports
for (let stmt of path.get("body")) {
if (stmt.isImportDeclaration()) {
if (stmt.node.importKind === "type") {
stmt.remove();
continue;
}
let pragmaFragImportName = fileJsxPragmaFrag || jsxPragmaFrag;
if (pragmaFragImportName) {
[pragmaFragImportName] = pragmaFragImportName.split(".");
}

// 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) {
// remove type imports
for (let stmt of path.get("body")) {
if (stmt.isImportDeclaration()) {
if (stmt.node.importKind === "type") {
stmt.remove();
continue;
}

let allElided = true;
const importsToRemove: NodePath<t.Node>[] = [];

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) {
IMPLICITLY_ESM.add(path.node);
} 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) {
IMPLICITLY_ESM.add(path.node);
continue;
}
}

if (allElided) {
stmt.remove();
} else {
for (const importPath of importsToRemove) {
importPath.remove();
let allElided = true;
const importsToRemove: NodePath<t.Node>[] = [];

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;
IMPLICITLY_ESM.add(path.node);
}
}

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())
}
},
exit(path) {
if (
!IMPLICITLY_ESM.has(path.node) &&
path.node.sourceType === "module"
) {
registerGlobalType(path.scope, stmt.node.id.name);
// 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 (path.node.exportKind === "type") {
path.remove();
return;
Expand All @@ -307,24 +328,33 @@ export default declare((api, opts) => {
)
) {
path.remove();
return;
}

IMPLICITLY_ESM.add(state.file.ast.program);
},

ExportSpecifier(path) {
ExportSpecifier(path, state) {
// remove type exports
if (!path.parent.source && isGlobalType(path, path.node.local.name)) {
path.remove();
return;
}

IMPLICITLY_ESM.add(state.file.ast.program);
},

ExportDefaultDeclaration(path) {
ExportDefaultDeclaration(path, state) {
// 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;
}

IMPLICITLY_ESM.add(state.file.ast.program);
},

TSDeclareFunction(path) {
Expand Down
@@ -1 +1,3 @@
try {} catch (e) {}

export {};
@@ -1 +1,3 @@
; // Otherwise-empty file

export {};
@@ -1 +1,3 @@
; // Otherwise-empty file

export {};
@@ -1 +1,2 @@
;
export {};
@@ -1 +1,2 @@
;
export {};
@@ -1,2 +1,3 @@
import local from "source";
local();
export {};
@@ -1 +1,2 @@
const x = 0;
export {};
@@ -1 +1,2 @@
const x = 0;
export {};
@@ -1 +1,2 @@
const x = 0;
export {};
@@ -1 +1,2 @@
const x = 0;
export {};
@@ -1 +1,2 @@
const x = 0;
export {};
Expand Up @@ -5,3 +5,4 @@ var Enum;
})(Enum || (Enum = {}));

;
export {};
@@ -1,2 +1,3 @@
// TODO: This should not be removed
;
export {};
@@ -1 +1,2 @@
;
export {};
@@ -1,3 +1,4 @@
const obj = {
A: 'foo'
};
export {};
Expand Up @@ -37,3 +37,5 @@ for (let s of strings) {
console.log(`"${s}" - ${validators[name].isAcceptable(s) ? "matches" : "does not match"} ${name}`);
}
}

export {};
Expand Up @@ -3,3 +3,5 @@ class A {}
(function (_A) {
const B = _A.B = 1;
})(A || (A = {}));

export {};
Expand Up @@ -7,3 +7,5 @@ var A;
(function (_A) {
const B = _A.B = 1;
})(A || (A = {}));

export {};
Expand Up @@ -137,3 +137,5 @@ let N;

(function (_flatMap) {})(flatMap || (flatMap = {}));
})(N || (N = {}));

export {};
Expand Up @@ -21,3 +21,5 @@ let src;
_ns2.foo = foo;
})(ns2 || (ns2 = _src.ns2 || (_src.ns2 = {})));
})(src || (src = {}));

export {};
Expand Up @@ -3,3 +3,5 @@ let N;
(function (_N) {})(N || (N = {}));

(function (_N2) {})(N || (N = {}));

export {};
Expand Up @@ -11,3 +11,5 @@ let N;

(function (_M2) {})(M2 || (M2 = _N.M2 || (_N.M2 = {})));
})(N || (N = {}));

export {};
Expand Up @@ -22,3 +22,5 @@ let N;
} = C;
_N.e = e, _N.c = c, _N.d = d;
})(N || (N = {}));

export {};
Expand Up @@ -47,3 +47,5 @@ class A {}
L[L["M"] = 19] = "M";
})(L || (L = {}));
})(A || (A = {}));

export {};
Expand Up @@ -27,3 +27,5 @@ let N;
_N9._N = _N;
})(N || (N = _N2.N || (_N2.N = {})));
})(N || (N = {}));

export {};
@@ -1,3 +1,5 @@
let N;

(function (_N) {})(N || (N = {}));

export {};

0 comments on commit 8589908

Please sign in to comment.