UNPKG

babel-plugin-node-cjs-interop

Version:

A babel plugin to fix the default import interoperability issue in Node.js

345 lines (325 loc) 14.3 kB
import { declare } from "@babel/helper-plugin-utils"; import { getPackageName } from "./package-name.js"; import { validateOptions } from "./options.js"; const statePrefix = "import-interop"; export default declare((api, options) => { api.assertVersion("^7.0.0 || ^8.0.0"); const { types: t } = api; validateOptions(options); return { name: "babel-plugin-node-cjs-interop", visitor: { ImportDeclaration(path, state) { var _path$node$specifiers; const pkgType = applicableSourceType(path.node.source.value, options); if (!pkgType) return; // Should have been removed by transform-typescript. Just in case. if (path.node.importKind === "type") return; if (isCjsAnnotated(path.node)) return; if (path.node.specifiers.length === 0) return; const replaceMap = new Map(); const existingNsImport = (_path$node$specifiers = path.node.specifiers.find(specifier => specifier.type === "ImportNamespaceSpecifier")) === null || _path$node$specifiers === void 0 ? void 0 : _path$node$specifiers.local; const nsImport = existingNsImport ?? path.scope.generateUidIdentifier("ns"); for (const specifier of path.node.specifiers) { let expr; if (specifier.type === "ImportDefaultSpecifier") { // ns.defalut expr = t.memberExpression(t.cloneNode(nsImport), t.identifier("default")); } else if (specifier.type === "ImportSpecifier") { if (specifier.imported.type === "StringLiteral") { // ns["named"] expr = t.memberExpression(t.cloneNode(nsImport), t.cloneNode(specifier.imported), true); } else { // ns.named expr = t.memberExpression(t.cloneNode(nsImport), t.cloneNode(specifier.imported)); } } else if (specifier.type === "ImportNamespaceSpecifier") { // No need to replace continue; } else { const { type } = specifier; // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unknown specifier type: ${type}`); } replaceMap.set(specifier.local.name, { scope: path.scope, expr }); } const importHelper = getImportHelper(t, path, state, pkgType, options.useRuntime ?? false); // import ... from "source"; // -> // import * as moduleOrig from "source"; // const module = _interopImportCJSNamespace(moduleOrig); const importOriginalName = path.scope.generateUidIdentifier("nsOrig"); const program = path.scope.getProgramParent().path; program.unshiftContainer("body", t.variableDeclaration("const", [t.variableDeclarator(nsImport, t.callExpression(t.cloneNode(importHelper), [t.cloneNode(importOriginalName), ...(pkgType !== "ts-twisted" && options.loose ? [t.booleanLiteral(true)] : [])]))])); const newImport = t.cloneNode(path.node); newImport.specifiers = [t.importNamespaceSpecifier(importOriginalName)]; path.replaceWith(newImport); annotateAsCjs(t, path.node); if (replaceMap.size > 0) { path.parentPath.traverse({ Identifier(path) { const replacement = replaceMap.get(path.node.name); if (!replacement) return; if (!isReferencedValueIdentifier(path)) return; const binding = path.scope.getBinding(path.node.name); if (!binding || binding.scope === replacement.scope) { replaceIdentifier(t, path, t.cloneNode(replacement.expr)); } }, JSXIdentifier(path) { const replacement = replaceMap.get(path.node.name); if (!replacement) return; if (!isReferencedValueIdentifier(path)) return; const binding = path.scope.getBinding(path.node.name); if (!binding || binding.scope === replacement.scope) { replaceIdentifier(t, path, t.cloneNode(replacement.expr)); } } }); } }, ExportNamedDeclaration(path) { if (!path.node.source) return; const pkgType = applicableSourceType(path.node.source.value, options); if (!pkgType) return; // Should have been removed by transform-typescript. Just in case. if (path.node.exportKind === "type") return; if (isCjsAnnotated(path.node)) return; if (path.node.specifiers.length === 0) return; throw path.buildCodeFrameError("babel-plugin-node-cjs-interop: cannot transform export declarations"); }, ExportAllDeclaration(path) { if (!path.node.source) return; const pkgType = applicableSourceType(path.node.source.value, options); if (!pkgType) return; // Should have been removed by transform-typescript. Just in case. if (path.node.exportKind === "type") return; if (isCjsAnnotated(path.node)) return; throw path.buildCodeFrameError("babel-plugin-node-cjs-interop: cannot transform export declarations"); }, // createImportExpressions === false; Babel 7 default CallExpression(path, state) { if (path.node.callee.type === "Import") { processImportExpression(path, path.get("arguments")[0], state); } }, // createImportExpressions === true; Babel 8 default ImportExpression(path, state) { processImportExpression(path, path.get("source"), state); } } }; function processImportExpression(path, sourcePath, state) { if (sourcePath.node.type !== "StringLiteral") return; const pkgType = applicableSourceType(sourcePath.node.value, options); if (!pkgType) return; if (isCjsAnnotated(path.node)) return; const awaitPath = path.parentPath; if (awaitPath.isAwaitExpression() && awaitPath.node.argument === path.node) { if (isCjsAnnotated(awaitPath.node)) return; const importHelper = getImportHelper(t, path, state, pkgType, options.useRuntime ?? false); const origNode = awaitPath.node; // await import("source") // -> _interopImportCJSNamespace(await import("source")) awaitPath.replaceWith(t.callExpression(t.cloneNode(importHelper), [awaitPath.node, ...(pkgType !== "ts-twisted" && options.loose ? [t.booleanLiteral(true)] : [])])); annotateAsCjs(t, origNode); return; } else { const importHelper = getImportHelper(t, path, state, pkgType, options.useRuntime ?? false); const origNode = path.node; // import("source") // -> import("source").then((ns) => _interopImportCJSNamespace(ns)) // NOTE: strictly speaking, it does not preserve scheduling semantics of the Promise; // the transform delays the execution by one cycle of microtask queue. // We could add other heuristics for cases like import("source").then(...) // but ultimately we cannot do so when we are not sure where the Promise goes to. // As the effect is fairly minor (do not depend on microtask cycle count!), // we just leave it as is for now. path.replaceWith(t.callExpression(t.memberExpression( // Explicit parentheses to get room for the #__CJS__ comment t.parenthesizedExpression(path.node), t.identifier("then"), false, false), [t.arrowFunctionExpression([t.identifier("ns")], t.callExpression(t.cloneNode(importHelper), [t.identifier("ns"), ...(pkgType !== "ts-twisted" && options.loose ? [t.booleanLiteral(true)] : [])]))])); annotateAsCjs(t, origNode); return; } } }); function getImportHelper(t, path, state, pkgType, useRuntime) { const key = pkgType === "ts-twisted" ? `${statePrefix}/importHelperT` : `${statePrefix}/importHelper`; let helper = state.get(key); if (helper) return helper; const helperName = pkgType === "ts-twisted" ? "interopImportCJSNamespaceT" : "interopImportCJSNamespace"; const scope = path.scope.getProgramParent(); helper = scope.generateUidIdentifier(helperName); if (useRuntime) { // import { // interopImportCJSNamespace as _interopImportCJSNamespace, // } from "node-cjs-interop"; scope.path.unshiftContainer("body", t.importDeclaration([t.importSpecifier(t.cloneNode(helper), t.identifier(helperName))], t.stringLiteral("node-cjs-interop"))); state.set(key, helper); return helper; } const ns = t.identifier("ns"); const loose = t.identifier("loose"); const nsDefault = t.memberExpression(t.cloneNode(ns), t.identifier("default")); if (pkgType === "ts-twisted") { // function interopImportCJSNamespaceT(ns) { // return ns.default && ns.default.__esModule ? ns : { // ...ns, // default: ns // }; // } scope.path.unshiftContainer("body", t.functionDeclaration(t.cloneNode(helper), [t.cloneNode(ns)], t.blockStatement([t.returnStatement(t.conditionalExpression(t.logicalExpression("&&", t.cloneNode(nsDefault), t.memberExpression(t.cloneNode(nsDefault), t.identifier("__esModule"))), t.cloneNode(ns), t.objectExpression([t.spreadElement(t.cloneNode(ns)), t.objectProperty(t.identifier("default"), t.cloneNode(ns), false, false)])))]))); } else { // function interopImportCJSNamespace(ns, loose) { // return (loose || ns.__esModule) && ns.default && ns.default.__esModule ? ns.default : ns; // } scope.path.unshiftContainer("body", t.functionDeclaration(t.cloneNode(helper), [t.cloneNode(ns), t.cloneNode(loose)], t.blockStatement([t.returnStatement(t.conditionalExpression(t.logicalExpression("&&", t.logicalExpression("&&", t.logicalExpression("||", t.cloneNode(loose), t.memberExpression(t.cloneNode(ns), t.identifier("__esModule"))), t.cloneNode(nsDefault)), t.memberExpression(t.cloneNode(nsDefault), t.identifier("__esModule"))), t.cloneNode(nsDefault), t.cloneNode(ns)))]))); } state.set(key, helper); return helper; } const CJS_ANNOTATION = "#__CJS__"; const isCjsAnnotated = ({ leadingComments }) => !!leadingComments && leadingComments.some(comment => /#__CJS__/.test(comment.value)); function annotateAsCjs(t, node) { if (isCjsAnnotated(node)) { return; } t.addComment(node, "leading", CJS_ANNOTATION); } function applicableSourceType(source, options) { const { packages = [], packagesT = [] } = options; const sourcePackage = getPackageName(source); if (sourcePackage === undefined) return undefined; if (packages.includes(sourcePackage)) { return "normal"; } else if (packagesT.includes(sourcePackage)) { return "ts-twisted"; } return undefined; } function replaceIdentifier(t, path, replacement) { if (path.isJSXIdentifier()) { path.replaceWith(toJSXReference(t, replacement)); return; } if ((path.parentPath.isCallExpression({ callee: path.node }) || path.parentPath.isOptionalCallExpression({ callee: path.node }) || path.parentPath.isTaggedTemplateExpression({ tag: path.node })) && t.isMemberExpression(replacement)) { path.replaceWith(t.sequenceExpression([t.numericLiteral(0), replacement])); } else { path.replaceWith(replacement); } } function toJSXReference(t, expr) { if (t.isIdentifier(expr)) { return t.inherits(t.jsxIdentifier(expr.name), expr); } else if (t.isMemberExpression(expr)) { if (!t.isIdentifier(expr.property) || expr.computed) throw new Error("Not an identifier reference"); const property = t.inherits(t.jsxIdentifier(expr.property.name), expr.property); return t.inherits(t.jsxMemberExpression(toJSXReference(t, expr.object), property), expr); } else { throw new Error("Not a chain of identifiers"); } } function isReferencedValueIdentifier(path) { if (!path.isReferencedIdentifier()) return false; const { node, parent } = path; switch (parent.type) { // Most usages in types fall into this category. case "TSTypeReference": return false; // no: type T = typeof NODE; case "TSTypeQuery": return false; // no: type T = NODE.T; // no: type T = T.NODE; case "TSQualifiedName": return false; // no: type T = (NODE) => void // no: type T = new (NODE) => void // no: type T = { (NODE): void; } // no: type T = { new (NODE): void; } case "TSFunctionType": case "TSConstructorType": case "TSCallSignatureDeclaration": case "TSConstructSignatureDeclaration": return false; // no: type T = { NODE(): void; }; // perhaps: type T = { [NODE](): void; }; // perhaps: type T = { foo(NODE): void; }; case "TSMethodSignature": return false; // no: type T = { NODE }; // perhaps: type T = { [NODE] }; case "TSPropertySignature": return false; // Equivalent to FunctionDeclaration // no: declare function NODE(); // no: declare function foo(NODE); case "TSDeclareFunction": return false; // Equivalent to ClassMethod // no: class C { NODE(); } // perhaps: class C { [NODE](); } // no: class C { foo(NODE); } case "TSDeclareMethod": return false; // no: function foo(x): NODE is T; case "TSTypePredicate": return false; // no: class C { constructor(readonly NODE) {} } case "TSParameterProperty": return false; // no: type T = [NODE: T]; case "TSNamedTupleMember": return false; // no: interface I extends NODE {} case "TSExpressionWithTypeArguments": return false; // no: import NODE = require("foo"); case "TSImportEqualsDeclaration": return false; // no: export as namespace NODE; case "TSNamespaceExportDeclaration": return false; // no: type T = { [NODE: string]: T }; case "TSIndexSignature": return false; // no: type NODE = {}; case "TSTypeAliasDeclaration": return false; // no: interface NODE {} case "TSInterfaceDeclaration": return false; // no: namespace NODE {} case "TSModuleDeclaration": return false; // no: enum NODE {} case "TSEnumDeclaration": return false; // no: enum E { NODE } // yes: enum E { A = NODE } case "TSEnumMember": return parent.initializer === node; } return true; }