Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement importInterop: "node" option for module transforms #12838

Merged
merged 4 commits into from Apr 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 24 additions & 9 deletions packages/babel-helper-module-transforms/src/index.ts
Expand Up @@ -6,9 +6,10 @@ import { isModule } from "@babel/helper-module-imports";

import rewriteThis from "./rewrite-this";
import rewriteLiveReferences from "./rewrite-live-references";
import normalizeAndLoadModuleMetadata, {
import normalizeModuleAndLoadMetadata, {
hasExports,
isSideEffectImport,
validateImportInteropOption,
} from "./normalize-and-load-metadata";
import type {
InteropType,
Expand Down Expand Up @@ -38,6 +39,7 @@ export function rewriteModuleStatementsAndPrepareHeader(
allowTopLevelThis,
strictMode,
noInterop,
importInterop = noInterop ? "none" : "babel",
lazy,
esNamespaceOnly,

Expand All @@ -49,18 +51,20 @@ export function rewriteModuleStatementsAndPrepareHeader(
allowTopLevelThis?;
strictMode;
loose?;
importInterop?: "none" | "babel" | "node";
noInterop?;
lazy?;
esNamespaceOnly?;
constantReexports?;
enumerableModuleMeta?;
},
) {
validateImportInteropOption(importInterop);
assert(isModule(path), "Cannot process module statements in a script");
path.node.sourceType = "script";

const meta = normalizeAndLoadModuleMetadata(path, exportName, {
noInterop,
const meta = normalizeModuleAndLoadMetadata(path, exportName, {
importInterop,
initializeReexports: constantReexports,
lazy,
esNamespaceOnly,
Expand Down Expand Up @@ -128,6 +132,15 @@ export function wrapInterop(
return null;
}

if (type === "node-namespace") {
return t.callExpression(
programPath.hub.addHelper("interopRequireWildcard"),
[expr, t.booleanLiteral(true)],
);
} else if (type === "node-default") {
return null;
}

let helper;
if (type === "default") {
helper = "interopRequireDefault";
Expand Down Expand Up @@ -227,16 +240,18 @@ const buildReexportsFromMeta = (

const { stringSpecifiers } = meta;
return Array.from(metadata.reexports, ([exportName, importName]) => {
let NAMESPACE_IMPORT;
if (stringSpecifiers.has(importName)) {
let NAMESPACE_IMPORT: t.Expression = t.cloneNode(namespace);
if (importName === "default" && metadata.interop === "node-default") {
// Nothing, it's ok as-is
} else if (stringSpecifiers.has(importName)) {
NAMESPACE_IMPORT = t.memberExpression(
t.cloneNode(namespace),
NAMESPACE_IMPORT,
t.stringLiteral(importName),
true,
);
} else {
NAMESPACE_IMPORT = NAMESPACE_IMPORT = t.memberExpression(
t.cloneNode(namespace),
NAMESPACE_IMPORT = t.memberExpression(
NAMESPACE_IMPORT,
t.identifier(importName),
);
}
Expand All @@ -245,7 +260,7 @@ const buildReexportsFromMeta = (
EXPORT_NAME: exportName,
NAMESPACE_IMPORT,
};
if (constantReexports) {
if (constantReexports || t.isIdentifier(NAMESPACE_IMPORT)) {
if (stringSpecifiers.has(exportName)) {
return ReexportTemplate.constantComputed(astNodes);
} else {
Expand Down
Expand Up @@ -21,7 +21,12 @@ export interface ModuleMetadata {
stringSpecifiers: Set<string>;
}

export type InteropType = "default" | "namespace" | "none";
export type InteropType =
| "default" // Babel interop for default-only imports
| "namespace" // Babel interop for namespace or default+named imports
| "node-default" // Node.js interop for default-only imports
| "node-namespace" // Node.js interop for namespace or default+named imports
| "none"; // No interop, or named-only imports

export interface SourceModuleMetadata {
// A unique variable name to use for this namespace object. Centralized for simplicity.
Expand Down Expand Up @@ -68,19 +73,42 @@ export function isSideEffectImport(source: SourceModuleMetadata) {
);
}

export function validateImportInteropOption(
importInterop: any,
): importInterop is "none" | "babel" | "node" | Function {
if (
typeof importInterop !== "function" &&
importInterop !== "none" &&
importInterop !== "babel" &&
importInterop !== "node"
) {
throw new Error(
`.importInterop must be one of "none", "babel", "node", or a function returning one of those values (received ${importInterop}).`,
);
}
return importInterop;
}

function resolveImportInterop(importInterop, source) {
if (typeof importInterop === "function") {
return validateImportInteropOption(importInterop(source));
}
return importInterop;
}

/**
* Remove all imports and exports from the file, and return all metadata
* needed to reconstruct the module's behavior.
*/
export default function normalizeModuleAndLoadMetadata(
programPath: NodePath<t.Program>,
exportName?: string,
exportName: string,
{
noInterop = false,
importInterop,
initializeReexports = false,
lazy = false,
esNamespaceOnly = false,
} = {},
},
): ModuleMetadata {
if (!exportName) {
exportName = programPath.scope.generateUidIdentifier("exports").name;
Expand All @@ -105,16 +133,24 @@ export default function normalizeModuleAndLoadMetadata(
metadata.name = metadata.importsNamespace.values().next().value;
}

if (noInterop) metadata.interop = "none";
else if (esNamespaceOnly) {
const resolvedInterop = resolveImportInterop(
importInterop,
metadata.source,
);

if (resolvedInterop === "none") {
metadata.interop = "none";
} else if (resolvedInterop === "node" && metadata.interop === "namespace") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is metadata.interop?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It tells what interop function we need for the import:

  • default -> default
  • named -> none
  • default+named -> namespace
  • namespace -> namespace

Then in this function we specialize between "babel interop" and "node interop".

metadata.interop = "node-namespace";
} else if (resolvedInterop === "node" && metadata.interop === "default") {
metadata.interop = "node-default";
} else if (esNamespaceOnly && metadata.interop === "namespace") {
// Both the default and namespace interops pass through __esModule
// objects, but the namespace interop is used to enable Babel's
// destructuring-like interop behavior for normal CommonJS.
// Since some tooling has started to remove that behavior, we expose
// it as the `esNamespace` option.
if (metadata.interop === "namespace") {
metadata.interop = "default";
}
metadata.interop = "default";
}
}

Expand Down Expand Up @@ -199,6 +235,8 @@ function getModuleMetadata(
reexportAll: null,

lazy: false,

source,
};
sourceData.set(source, data);
}
Expand Down
Expand Up @@ -96,6 +96,10 @@ export default function rewriteLiveReferences(
let namespace: t.Expression = t.identifier(meta.name);
if (meta.lazy) namespace = t.callExpression(namespace, []);

if (importName === "default" && meta.interop === "node-default") {
return namespace;
}

const computed = metadata.stringSpecifiers.has(importName);

return t.memberExpression(
Expand Down
20 changes: 11 additions & 9 deletions packages/babel-helpers/src/helpers.js
Expand Up @@ -618,33 +618,35 @@ helpers.interopRequireDefault = helper("7.0.0-beta.0")`
}
`;

helpers.interopRequireWildcard = helper("7.0.0-beta.0")`
function _getRequireWildcardCache() {
helpers.interopRequireWildcard = helper("7.14.0")`
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;

var cache = new WeakMap();
_getRequireWildcardCache = function () { return cache; };
return cache;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}

export default function _interopRequireWildcard(obj) {
if (obj && obj.__esModule) {
export default function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}

if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj }
}

var cache = _getRequireWildcardCache();
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}

var newObj = {};
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
Expand Down
3 changes: 2 additions & 1 deletion packages/babel-plugin-transform-modules-amd/package.json
Expand Up @@ -26,6 +26,7 @@
},
"devDependencies": {
"@babel/core": "workspace:*",
"@babel/helper-plugin-test-runner": "workspace:*"
"@babel/helper-plugin-test-runner": "workspace:*",
"@babel/plugin-external-helpers": "workspace:*"
}
}
9 changes: 8 additions & 1 deletion packages/babel-plugin-transform-modules-amd/src/index.js
Expand Up @@ -38,7 +38,13 @@ function injectWrapper(path, wrapper) {
export default declare((api, options) => {
api.assertVersion(7);

const { allowTopLevelThis, strict, strictMode, noInterop } = options;
const {
allowTopLevelThis,
strict,
strictMode,
importInterop,
noInterop,
} = options;

const constantReexports =
api.assumption("constantReexports") ?? options.loose;
Expand Down Expand Up @@ -114,6 +120,7 @@ export default declare((api, options) => {
strict,
strictMode,
allowTopLevelThis,
importInterop,
noInterop,
},
);
Expand Down
@@ -0,0 +1 @@
export { default } from 'dep';
@@ -0,0 +1,8 @@
define(["exports", "dep"], function (_exports, _dep) {
"use strict";

Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = _dep;
});
@@ -0,0 +1 @@
export { default, name } from 'dep';
@@ -0,0 +1,20 @@
define(["exports", "dep"], function (_exports, _dep) {
"use strict";

Object.defineProperty(_exports, "__esModule", {
value: true
});
Object.defineProperty(_exports, "default", {
enumerable: true,
get: function () {
return _dep.default;
}
});
Object.defineProperty(_exports, "name", {
enumerable: true,
get: function () {
return _dep.name;
}
});
_dep = babelHelpers.interopRequireWildcard(_dep, true);
});
@@ -0,0 +1 @@
export { name } from 'dep';
@@ -0,0 +1,13 @@
define(["exports", "dep"], function (_exports, _dep) {
"use strict";

Object.defineProperty(_exports, "__esModule", {
value: true
});
Object.defineProperty(_exports, "name", {
enumerable: true,
get: function () {
return _dep.name;
}
});
});
@@ -0,0 +1,3 @@
import foo from "foo";

foo();
@@ -0,0 +1,5 @@
define(["foo"], function (_foo) {
"use strict";

_foo();
});
@@ -0,0 +1,4 @@
import foo, { named } from "foo";

foo();
named();
@@ -0,0 +1,7 @@
define(["foo"], function (_foo) {
"use strict";

_foo = babelHelpers.interopRequireWildcard(_foo, true);
(0, _foo.default)();
(0, _foo.named)();
});
@@ -0,0 +1,3 @@
import { named } from "foo";

named();
@@ -0,0 +1,5 @@
define(["foo"], function (_foo) {
"use strict";

(0, _foo.named)();
});
@@ -0,0 +1,4 @@
import * as foo from 'foo';

foo.bar();
foo.baz();
@@ -0,0 +1,7 @@
define(["foo"], function (foo) {
"use strict";

foo = babelHelpers.interopRequireWildcard(foo, true);
foo.bar();
foo.baz();
});
@@ -0,0 +1,3 @@
{
"plugins": [["transform-modules-amd", { "importInterop": "node" }]]
}
@@ -0,0 +1 @@
export { default } from 'foo';
@@ -0,0 +1,13 @@
define(["exports", "foo"], function (_exports, _foo) {
"use strict";

Object.defineProperty(_exports, "__esModule", {
value: true
});
Object.defineProperty(_exports, "default", {
enumerable: true,
get: function () {
return _foo.default;
}
});
});
@@ -0,0 +1,3 @@
import foo from "foo";

foo();