diff --git a/packages/babel-core/src/transformation/internal-plugins/block-hoist.js b/packages/babel-core/src/transformation/internal-plugins/block-hoist.js index 4813b18d22..1682670314 100644 --- a/packages/babel-core/src/transformation/internal-plugins/block-hoist.js +++ b/packages/babel-core/src/transformation/internal-plugins/block-hoist.js @@ -10,6 +10,7 @@ export default { * - 1 Default node position * - 2 Priority over normal nodes * - 3 We want this to be at the **very** top + * - 4 Reserved for the helpers used to implement module imports. */ name: "internal.blockHoist", diff --git a/packages/babel-helper-module-imports/.npmignore b/packages/babel-helper-module-imports/.npmignore new file mode 100644 index 0000000000..f980694583 --- /dev/null +++ b/packages/babel-helper-module-imports/.npmignore @@ -0,0 +1,3 @@ +src +test +*.log diff --git a/packages/babel-helper-module-imports/README.md b/packages/babel-helper-module-imports/README.md new file mode 100644 index 0000000000..e508593ae8 --- /dev/null +++ b/packages/babel-helper-module-imports/README.md @@ -0,0 +1,27 @@ +# babel-helper-module-imports + +## Usage + +### Adding a named impport + +``` +import { addNamed } from "babel-helper-module-imports"; + +export default function({ types: t }) { + return { + visitor: { + ReferencedIdentifier(path) { + let importName = this.importName; + if (importName) { + importName = t.cloneDeep(importName); + } else { + // require('bluebird').coroutine + importName = this.importName = addName(path, 'coroutine', 'bluebird'); + } + + path.replaceWith(importName); + } + }, + }; +} +``` diff --git a/packages/babel-helper-module-imports/package.json b/packages/babel-helper-module-imports/package.json new file mode 100644 index 0000000000..e9db64557a --- /dev/null +++ b/packages/babel-helper-module-imports/package.json @@ -0,0 +1,14 @@ +{ + "name": "babel-helper-module-imports", + "version": "7.0.0-beta.2", + "description": "Babel helper functions for inserting module loads", + "author": "Logan Smyth ", + "homepage": "https://babeljs.io/", + "license": "MIT", + "repository": "https://github.com/babel/babel/tree/master/packages/babel-helper-module-imports", + "main": "lib/index.js", + "dependencies": { + "babel-types": "7.0.0-beta.2", + "lodash": "^4.2.0" + } +} diff --git a/packages/babel-helper-module-imports/src/import-builder.js b/packages/babel-helper-module-imports/src/import-builder.js new file mode 100644 index 0000000000..bbde6fdef9 --- /dev/null +++ b/packages/babel-helper-module-imports/src/import-builder.js @@ -0,0 +1,142 @@ +import assert from "assert"; +import * as t from "babel-types"; + +/** + * A class to track and accumulate mutations to the AST that will eventually + * output a new require/import statement list. + */ +export default class ImportBuilder { + _statements = []; + _resultName = null; + + _scope = null; + _file = null; + + constructor(importedSource, scope, file) { + this._scope = scope; + this._file = file; + this._importedSource = importedSource; + } + + done() { + return { + statements: this._statements, + resultName: this._resultName, + }; + } + + import() { + const importedSource = this._file.resolveModuleSource(this._importedSource); + + this._statements.push( + t.importDeclaration([], t.stringLiteral(importedSource)), + ); + return this; + } + + require() { + const importedSource = this._file.resolveModuleSource(this._importedSource); + + this._statements.push( + t.expressionStatement( + t.callExpression(t.identifier("require"), [ + t.stringLiteral(importedSource), + ]), + ), + ); + return this; + } + + namespace(name) { + name = this._scope.generateUidIdentifier(name); + + const statement = this._statements[this._statements.length - 1]; + assert(statement.type === "ImportDeclaration"); + assert(statement.specifiers.length === 0); + statement.specifiers = [t.importNamespaceSpecifier(name)]; + this._resultName = t.clone(name); + return this; + } + default(name) { + name = this._scope.generateUidIdentifier(name); + const statement = this._statements[this._statements.length - 1]; + assert(statement.type === "ImportDeclaration"); + assert(statement.specifiers.length === 0); + statement.specifiers = [t.importDefaultSpecifier(name)]; + this._resultName = t.clone(name); + return this; + } + named(name, importName) { + if (importName === "default") return this.default(name); + + name = this._scope.generateUidIdentifier(name); + const statement = this._statements[this._statements.length - 1]; + assert(statement.type === "ImportDeclaration"); + assert(statement.specifiers.length === 0); + statement.specifiers = [t.importSpecifier(name, t.identifier(importName))]; + this._resultName = t.clone(name); + return this; + } + + var(name) { + name = this._scope.generateUidIdentifier(name); + let statement = this._statements[this._statements.length - 1]; + if (statement.type !== "ExpressionStatement") { + assert(this._resultName); + statement = t.expressionStatement(this._resultName); + this._statements.push(statement); + } + this._statements[ + this._statements.length - 1 + ] = t.variableDeclaration("var", [ + t.variableDeclarator(name, statement.expression), + ]); + this._resultName = t.clone(name); + return this; + } + + defaultInterop() { + return this._interop(this._file.addHelper("interopRequireDefault")); + } + wildcardInterop() { + return this._interop(this._file.addHelper("interopRequireWildcard")); + } + + _interop(callee) { + const statement = this._statements[this._statements.length - 1]; + if (statement.type === "ExpressionStatement") { + statement.expression = t.callExpression(callee, [statement.expression]); + } else if (statement.type === "VariableDeclaration") { + assert(statement.declarations.length === 1); + statement.declarations[0].init = t.callExpression(callee, [ + statement.declarations[0].init, + ]); + } else { + assert.fail("Unexpected type."); + } + return this; + } + + prop(name) { + const statement = this._statements[this._statements.length - 1]; + if (statement.type === "ExpressionStatement") { + statement.expression = t.memberExpression( + statement.expression, + t.identifier(name), + ); + } else if (statement.type === "VariableDeclaration") { + assert(statement.declarations.length === 1); + statement.declarations[0].init = t.memberExpression( + statement.declarations[0].init, + t.identifier(name), + ); + } else { + assert.fail("Unexpected type:" + statement.type); + } + return this; + } + + read(name) { + this._resultName = t.memberExpression(this._resultName, t.identifier(name)); + } +} diff --git a/packages/babel-helper-module-imports/src/import-injector.js b/packages/babel-helper-module-imports/src/import-injector.js new file mode 100644 index 0000000000..f8326c1abd --- /dev/null +++ b/packages/babel-helper-module-imports/src/import-injector.js @@ -0,0 +1,425 @@ +import assert from "assert"; +import * as t from "babel-types"; + +import ImportBuilder from "./import-builder"; +import isModule from "./is-module"; + +export type ImportOptions = { + /** + * The module being referenced. + */ + importedSource: string | null, + + /** + * The type of module being imported: + * + * * 'es6' - An ES6 module. + * * 'commonjs' - A CommonJS module. (Default) + */ + importedType: "es6" | "commonjs", + + /** + * The type of interop behavior for namespace/default/named when loading + * CommonJS modules. + * + * ## 'babel' (Default) + * + * Load using Babel's interop. + * + * If '.__esModule' is true, treat as 'compiled', else: + * + * * Namespace: A copy of the module.exports with .default + * populated by the module.exports object. + * * Default: The module.exports value. + * * Named: The .named property of module.exports. + * + * The 'ensureLiveReference' has no effect on the liveness of these. + * + * ## 'compiled' + * + * Assume the module is ES6 compiled to CommonJS. Useful to avoid injecting + * interop logic if you are confident that the module is a certain format. + * + * * Namespace: The root module.exports object. + * * Default: The .default property of the namespace. + * * Named: The .named property of the namespace. + * + * Will return erroneous results if the imported module is _not_ compiled + * from ES6 with Babel. + * + * ## 'uncompiled' + * + * Assume the module is _not_ ES6 compiled to CommonJS. Used a simplified + * access pattern that doesn't require additional function calls. + * + * Will return erroneous results if the imported module _is_ compiled + * from ES6 with Babel. + * + * * Namespace: The module.exports object. + * * Default: The module.exports object. + * * Named: The .named property of module.exports. + */ + importedInterop: "babel" | "node" | "compiled" | "uncompiled", + + /** + * The type of CommonJS interop included in the environment that will be + * loading the output code. + * + * * 'babel' - CommonJS modules load with Babel's interop. (Default) + * * 'node' - CommonJS modules load with Node's interop. + * + * See descriptions in 'importedInterop' for more details. + */ + importingInterop: "babel" | "node", + + /** + * Define whether we explicitly care that the import be a live reference. + * Only applies when importing default and named imports, not the namespace. + * + * * true - Force imported values to be live references. + * * false - No particular requirements. Keeps the code simplest. (Default) + */ + ensureLiveReference: boolean, + + /** + * Define if we explicitly care that the result not be a property reference. + * + * * true - Force calls to exclude context. Useful if the value is going to + * be used as function callee. + * * false - No particular requirements for context of the access. (Default) + */ + ensureNoContext: boolean, +}; + +/** + * A general helper classes add imports via transforms. See README for usage. + */ +export default class ImportInjector { + /** + * The path used for manipulation. + */ + _programPath: NodePath; + + /** + * The scope used to generate unique variable names. + */ + _programScope; + + /** + * The file used to inject helpers and resolve paths. + */ + _file; + + /** + * The default options to use with this instance when imports are added. + */ + _defaultOpts: ImportOptions = { + importedSource: null, + importedType: "commonjs", + importedInterop: "babel", + importingInterop: "babel", + ensureLiveReference: false, + ensureNoContext: false, + }; + + constructor(path, importedSource, opts) { + const programPath = path.find(p => p.isProgram()); + + this._programPath = programPath; + this._programScope = programPath.scope; + this._file = programPath.hub.file; + + this._defaultOpts = this._applyDefaults(importedSource, opts, true); + } + + addDefault(importedSourceIn, opts) { + return this.addNamed("default", importedSourceIn, opts); + } + + addNamed(importName, importedSourceIn, opts) { + assert(typeof importName === "string"); + + return this._generateImport( + this._applyDefaults(importedSourceIn, opts), + importName, + ); + } + + addNamespace(importedSourceIn, opts) { + return this._generateImport( + this._applyDefaults(importedSourceIn, opts), + null, + ); + } + + addSideEffect(importedSourceIn, opts) { + return this._generateImport( + this._applyDefaults(importedSourceIn, opts), + false, + ); + } + + _applyDefaults(importedSource, opts, isInit = false) { + const optsList = []; + if (typeof importedSource === "string") { + optsList.push({ importedSource }); + optsList.push(opts); + } else { + assert(!opts, "Unexpected secondary arguments."); + + optsList.push(importedSource); + } + + const newOpts = Object.assign({}, this._defaultOpts); + for (const opts of optsList) { + if (!opts) continue; + Object.keys(newOpts).forEach(key => { + if (opts[key] !== undefined) newOpts[key] = opts[key]; + }); + + if (!isInit) { + if (opts.nameHint !== undefined) newOpts.nameHint = opts.nameHint; + if (opts.blockHoist !== undefined) newOpts.blockHoist = opts.blockHoist; + } + } + return newOpts; + } + + _generateImport(opts, importName) { + const isDefault = importName === "default"; + const isNamed = !!importName && !isDefault; + const isNamespace = importName === null; + + const { + importedSource, + importedType, + importedInterop, + importingInterop, + ensureLiveReference, + ensureNoContext, + + // Provide a hint for generateUidIdentifier for the local variable name + // to use for the import, if the code will generate a simple assignment + // to a variable. + nameHint = importName, + + // Not meant for public usage. Allows code that absolutely must control + // ordering to set a specific hoist value on the import nodes. + blockHoist, + } = opts; + + const isMod = isModule(this._programPath, true); + const isModuleForNode = isMod && importingInterop === "node"; + const isModuleForBabel = isMod && importingInterop === "babel"; + + const builder = new ImportBuilder( + importedSource, + this._programScope, + this._file, + ); + + if (importedType === "es6") { + if (!isModuleForNode && !isModuleForBabel) { + throw new Error("Cannot import an ES6 module from CommonJS"); + } + + // import * as namespace from ''; namespace + // import def from ''; def + // import { named } from ''; named + builder.import(); + if (isNamespace) { + builder.namespace("namespace"); + } else if (isDefault || isNamed) { + builder.named(nameHint, importName); + } + } else if (importedType !== "commonjs") { + throw new Error(`Unexpected interopType "${importedType}"`); + } else if (importedInterop === "babel") { + if (isModuleForNode) { + // import _tmp from ''; var namespace = interopRequireWildcard(_tmp); namespace + // import _tmp from ''; var def = interopRequireDefault(_tmp).default; def + // import _tmp from ''; _tmp.named + + builder.import(); + if (isNamespace) { + builder + .default("es6Default") + .var(nameHint || "namespace") + .wildcardInterop(); + } else if (isDefault) { + if (ensureLiveReference) { + builder + .default("es6Default") + .var("namespace") + .defaultInterop() + .read("default"); + } else { + builder + .default("es6Default") + .var(nameHint) + .defaultInterop() + .prop(importName); + } + } else if (isNamed) { + builder.default("es6Default").read(importName); + } + } else if (isModuleForBabel) { + // import * as namespace from ''; namespace + // import def from ''; def + // import { named } from ''; named + builder.import(); + if (isNamespace) { + builder.namespace("namespace"); + } else if (isDefault || isNamed) { + builder.named(nameHint, importName); + } + } else { + // var namespace = interopRequireWildcard(require('')); + // var def = interopRequireDefault(require('')).default; def + // var named = require('').named; named + builder.require(); + if (isNamespace) { + builder.var("namespace").wildcardInterop(); + } else if ((isDefault || isNamed) && ensureLiveReference) { + builder.var("namespace").read(importName); + + if (isDefault) builder.defaultInterop(); + } else if (isDefault) { + builder + .var(nameHint) + .defaultInterop() + .prop(importName); + } else if (isNamed) { + builder.var(nameHint).prop(importName); + } + } + } else if (importedInterop === "compiled") { + if (isModuleForNode) { + // import namespace from ''; namespace + // import namespace from ''; namespace.default + // import namespace from ''; namespace.named + + builder.import(); + if (isNamespace) { + builder.default("namespace"); + } else if (isDefault || isNamed) { + builder.default("namespace").read(importName); + } + } else if (isModuleForBabel) { + // import * as namespace from ''; namespace + // import def from ''; def + // import { named } from ''; named + // Note: These lookups will break if the module has no __esModule set, + // hence the warning that 'compiled' will not work on standard CommonJS. + + builder.import(); + if (isNamespace) { + builder.namespace("namespace"); + } else if (isDefault || isNamed) { + builder.named(nameHint, importName); + } + } else { + // var namespace = require(''); namespace + // var namespace = require(''); namespace.default + // var namespace = require(''); namespace.named + // var named = require('').named; + + builder.require(); + if (isNamespace) { + builder.var("namespace"); + } else if (isDefault || isNamed) { + if (ensureLiveReference) { + builder.var("namespace").read(importName); + } else { + builder.prop(importName).var(nameHint); + } + } + } + } else if (importedInterop === "uncompiled") { + if (isDefault && ensureLiveReference) { + throw new Error("No live reference for commonjs default"); + } + + if (isModuleForNode) { + // import namespace from ''; namespace + // import def from ''; def; + // import namespace from ''; namespace.named + + builder.import(); + if (isNamespace) { + builder.default("namespace"); + } else if (isDefault) { + builder.default(nameHint); + } else if (isNamed) { + builder.default("namespace").read(importName); + } + } else if (isModuleForBabel) { + // import namespace from ''; + // import def from ''; + // import { named } from ''; named; + // Note: These lookups will break if the module has __esModule set, + // hence the warning that 'uncompiled' will not work on ES6 transpiled + // to CommonJS. + + builder.import(); + if (isNamespace) { + builder.default("namespace"); + } else if (isDefault) { + builder.default(nameHint); + } else if (isNamed) { + builder.named(nameHint, importName); + } + } else { + // var namespace = require(''); namespace + // var def = require(''); def + // var namespace = require(''); namespace.named + // var named = require('').named; + + builder.require(); + if (isNamespace) { + builder.var("namespace"); + } else if (isDefault) { + builder.var(nameHint); + } else if (isNamed) { + if (ensureLiveReference) { + builder.var("namespace").read(importName); + } else { + builder.var(nameHint).prop(importName); + } + } + } + } else { + throw new Error(`Unknown importedInterop "${importedInterop}".`); + } + + const { statements, resultName } = builder.done(); + + this._insertStatements(statements, blockHoist); + + if ( + (isDefault || isNamed) && + ensureNoContext && + resultName.type !== "Identifier" + ) { + return t.sequenceExpression([t.numericLiteral(0), resultName]); + } + return resultName; + } + + _insertStatements(statements, blockHoist = 3) { + statements.forEach(node => { + node._blockHoist = blockHoist; + }); + + const targetPath = this._programPath.get("body").filter(p => { + const val = p.node._blockHoist; + return Number.isFinite(val) && val < 4; + })[0]; + + if (targetPath) { + targetPath.insertBefore(statements); + } else { + this._programPath.unshiftContainer("body", statements); + } + } +} diff --git a/packages/babel-helper-module-imports/src/index.js b/packages/babel-helper-module-imports/src/index.js new file mode 100644 index 0000000000..722631adce --- /dev/null +++ b/packages/babel-helper-module-imports/src/index.js @@ -0,0 +1,21 @@ +import ImportInjector from "./import-injector"; + +export { ImportInjector }; + +export { default as isModule } from "./is-module"; + +export function addDefault(path, importedSource, opts) { + return new ImportInjector(path).addDefault(importedSource, opts); +} + +export function addNamed(path, name, importedSource, opts) { + return new ImportInjector(path).addNamed(name, importedSource, opts); +} + +export function addNamespace(path, importedSource, opts) { + return new ImportInjector(path).addNamespace(importedSource, opts); +} + +export function addSideEffect(path, importedSource, opts) { + return new ImportInjector(path).addSideEffect(importedSource, opts); +} diff --git a/packages/babel-helper-module-imports/src/is-module.js b/packages/babel-helper-module-imports/src/is-module.js new file mode 100644 index 0000000000..8e20fb2472 --- /dev/null +++ b/packages/babel-helper-module-imports/src/is-module.js @@ -0,0 +1,31 @@ +/** + * A small utility to check if a file qualifies as a module, based on a few + * possible conditions. + */ +export default function isModule( + path: NodePath, + requireUnambiguous: boolean = false, +) { + const { sourceType } = path.node; + if (sourceType !== "module" && sourceType !== "script") { + throw path.buildCodeFrameError( + `Unknown sourceType "${sourceType}", cannot transform.`, + ); + } + + const filename = path.hub.file.opts.filename; + if (/\.mjs$/.test(filename)) { + requireUnambiguous = false; + } + + return ( + path.node.sourceType === "module" && + (!requireUnambiguous || isUnambiguousModule(path)) + ); +} + +// This approach is not ideal. It is here to preserve compatibility for now, +// but really this should just return true or be deleted. +function isUnambiguousModule(path) { + return path.get("body").some(p => p.isModuleDeclaration()); +} diff --git a/packages/babel-helper-module-imports/test/index.js b/packages/babel-helper-module-imports/test/index.js new file mode 100644 index 0000000000..c60490da1c --- /dev/null +++ b/packages/babel-helper-module-imports/test/index.js @@ -0,0 +1,1100 @@ +import chai from "chai"; +import * as babel from "babel-core"; + +import { ImportInjector } from "../"; + +function test(sourceType, opts, initializer, expectedCode) { + if (typeof opts === "function") { + expectedCode = initializer; + initializer = opts; + opts = null; + } + + const result = babel.transform("", { + sourceType, + filename: "example" + (sourceType === "module" ? ".mjs" : ".js"), + babelrc: false, + plugins: [ + function({ types: t }) { + return { + pre(file) { + file.set("helpersNamespace", t.identifier("babelHelpers")); + }, + visitor: { + Program(path) { + const manager = new ImportInjector(path, opts); + + const ref = initializer(manager); + if (ref) path.pushContainer("body", t.expressionStatement(ref)); + }, + }, + }; + }, + ], + }); + + chai + .expect(result.code.replace(/\s+/g, " ").trim()) + .to.equal((expectedCode || "").replace(/\s+/g, " ").trim()); +} +const testScript = test.bind(undefined, "script"); +const testModule = test.bind(undefined, "module"); + +describe("babel-helper-module-imports", () => { + describe("namespace import", () => { + const addNamespace = opts => m => m.addNamespace("source", opts); + + describe("loading an ES6 module", () => { + const importedType = "es6"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedType }, + addNamespace(), + ` + import * as _namespace from "source"; + _namespace; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedType }, + addNamespace(), + ` + import * as _namespace from "source"; + _namespace; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + chai + .expect(() => { + testScript({ importedType }, addNamespace()); + }) + .to.throw(Error, "Cannot import an ES6 module from CommonJS"); + }); + }); + }); + + describe("loading CommonJS with 'uncompiled'", () => { + const importedInterop = "uncompiled"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamespace(), + ` + import _namespace from "source"; + _namespace; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamespace(), + ` + import _namespace from "source"; + _namespace; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addNamespace(), + ` + var _namespace = require("source"); + _namespace; + `, + ); + }); + }); + }); + + describe("loading CommonJS with 'compiled'", () => { + const importedInterop = "compiled"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamespace(), + ` + import _namespace from "source"; + _namespace; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamespace(), + ` + import * as _namespace from "source"; + _namespace; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addNamespace(), + ` + var _namespace = require("source"); + _namespace; + `, + ); + }); + }); + }); + + describe("loading CommonJS with 'babel'", () => { + const importedInterop = "babel"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamespace(), + ` + import _es6Default from "source"; + var _namespace = babelHelpers.interopRequireWildcard(_es6Default); + _namespace; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamespace(), + ` + import * as _namespace from "source"; + _namespace; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addNamespace(), + ` + var _namespace = babelHelpers.interopRequireWildcard(require("source")); + _namespace; + `, + ); + }); + }); + }); + }); + + describe("default imports", () => { + const addDefault = opts => m => m.addDefault("source", opts); + + describe("loading an ES6 module", () => { + const importedType = "es6"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedType }, + addDefault(), + ` + import _default from "source"; + _default; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedType }, + addDefault({ nameHint: "hintedName" }), + ` + import _hintedName from "source"; + _hintedName; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedType }, + addDefault(), + ` + import _default from "source"; + _default; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedType }, + addDefault({ nameHint: "hintedName" }), + ` + import _hintedName from "source"; + _hintedName; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + chai + .expect(() => { + testScript({ importedType }, addDefault()); + }) + .to.throw(Error, "Cannot import an ES6 module from CommonJS"); + }); + }); + }); + + describe("loading CommonJS with 'uncompiled'", () => { + const importedInterop = "uncompiled"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addDefault(), + ` + import _default from "source"; + _default; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedInterop }, + addDefault({ nameHint: "hintedName" }), + ` + import _hintedName from "source"; + _hintedName; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addDefault(), + ` + import _default from "source"; + _default; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedInterop }, + addDefault({ nameHint: "hintedName" }), + ` + import _hintedName from "source"; + _hintedName; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addDefault(), + ` + var _default = require("source"); + _default; + `, + ); + }); + + it("should import with a name hint", () => { + testScript( + { importedInterop }, + addDefault({ nameHint: "hintedName" }), + ` + var _hintedName = require("source"); + _hintedName; + `, + ); + }); + + it("should fail to import with force-enabled liveness", () => { + chai + .expect(() => { + testScript( + { importedInterop, ensureLiveReference: true }, + addDefault(), + ); + }) + .to.throw(Error, "No live reference for commonjs default"); + }); + }); + }); + + describe("loading CommonJS with 'compiled'", () => { + const importedInterop = "compiled"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addDefault(), + ` + import _namespace from "source"; + _namespace.default; + `, + ); + }); + + it("should import with a force-disabled context", () => { + testModule( + { importingInterop, importedInterop, ensureNoContext: true }, + addDefault(), + ` + import _namespace from "source"; + 0, _namespace.default; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addDefault(), + ` + import _default from "source"; + _default; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedInterop }, + addDefault({ nameHint: "hintedName" }), + ` + import _hintedName from "source"; + _hintedName; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addDefault(), + ` + var _default = require("source").default; + _default; + `, + ); + }); + + it("should import with a name hint", () => { + testScript( + { importedInterop }, + addDefault({ nameHint: "hintedName" }), + ` + var _hintedName = require("source").default; + _hintedName; + `, + ); + }); + + it("should import with force-enabled liveness", () => { + testScript( + { importedInterop, ensureLiveReference: true }, + addDefault(), + ` + var _namespace = require("source"); + _namespace.default; + `, + ); + }); + }); + }); + + describe("loading CommonJS with 'babel'", () => { + const importedInterop = "babel"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addDefault(), + ` + import _es6Default from "source"; + var _default = babelHelpers.interopRequireDefault(_es6Default).default; + _default; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedInterop }, + addDefault({ nameHint: "hintedName" }), + ` + import _es6Default from "source"; + var _hintedName = babelHelpers.interopRequireDefault(_es6Default).default; + _hintedName; + `, + ); + }); + + it("should import with force-enabled liveness", () => { + testModule( + { importingInterop, importedInterop, ensureLiveReference: true }, + addDefault(), + ` + import _es6Default from "source"; + var _namespace = babelHelpers.interopRequireDefault(_es6Default); + _namespace.default; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addDefault(), + ` + import _default from "source"; + _default; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedInterop }, + addDefault({ nameHint: "hintedName" }), + ` + import _hintedName from "source"; + _hintedName; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addDefault(), + ` + var _default = babelHelpers.interopRequireDefault(require("source")).default; + _default; + `, + ); + }); + + it("should import with a name hint", () => { + testScript( + { importedInterop }, + addDefault({ nameHint: "hintedName" }), + ` + var _hintedName = babelHelpers.interopRequireDefault(require("source")).default; + _hintedName; + `, + ); + }); + + it("should import with force-enabled liveness", () => { + testScript( + { importedInterop, ensureLiveReference: true }, + addDefault(), + ` + var _namespace = babelHelpers.interopRequireDefault(require("source")); + _namespace.default; + `, + ); + }); + }); + }); + }); + + describe("named imports", () => { + const addNamed = opts => m => m.addNamed("read", "source", opts); + + describe("loading an ES6 module", () => { + const importedType = "es6"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedType }, + addNamed(), + ` + import { read as _read } from "source"; + _read; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedType }, + addNamed({ nameHint: "hintedName" }), + ` + import { read as _hintedName } from "source"; + _hintedName; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedType }, + addNamed(), + ` + import { read as _read } from "source"; + _read; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedType }, + addNamed({ nameHint: "hintedName" }), + ` + import { read as _hintedName } from "source"; + _hintedName; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + chai + .expect(() => { + testScript({ importedType }, addNamed()); + }) + .to.throw(Error, "Cannot import an ES6 module from CommonJS"); + }); + }); + }); + + describe("loading CommonJS with 'uncompiled'", () => { + const importedInterop = "uncompiled"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamed(), + ` + import _namespace from "source"; + _namespace.read; + `, + ); + }); + + it("should import with a force-disabled context", () => { + testModule( + { importingInterop, importedInterop, ensureNoContext: true }, + addNamed(), + ` + import _namespace from "source"; + 0, _namespace.read; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamed(), + ` + import { read as _read } from "source"; + _read; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedInterop }, + addNamed({ nameHint: "hintedName" }), + ` + import { read as _hintedName } from "source"; + _hintedName; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addNamed(), + ` + var _read = require("source").read; + _read; + `, + ); + }); + + it("should import with a name hint", () => { + testScript( + { importedInterop }, + addNamed({ nameHint: "hintedName" }), + ` + var _hintedName = require("source").read; + _hintedName; + `, + ); + }); + + it("should import with force-enabled liveness", () => { + testScript( + { importedInterop, ensureLiveReference: true }, + addNamed(), + ` + var _namespace = require("source"); + _namespace.read; + `, + ); + }); + }); + }); + + describe("loading CommonJS with 'compiled'", () => { + const importedInterop = "compiled"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamed(), + ` + import _namespace from "source"; + _namespace.read; + `, + ); + }); + + it("should import with a force-disabled context", () => { + testModule( + { importingInterop, importedInterop, ensureNoContext: true }, + addNamed(), + ` + import _namespace from "source"; + 0, _namespace.read; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamed(), + ` + import { read as _read } from "source"; + _read; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedInterop }, + addNamed({ nameHint: "hintedName" }), + ` + import { read as _hintedName } from "source"; + _hintedName; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addNamed(), + ` + var _read = require("source").read; + _read; + `, + ); + }); + + it("should import with a name hint", () => { + testScript( + { importedInterop }, + addNamed({ nameHint: "hintedName" }), + ` + var _hintedName = require("source").read; + _hintedName; + `, + ); + }); + + it("should import with force-enabled liveness", () => { + testScript( + { importedInterop, ensureLiveReference: true }, + addNamed(), + ` + var _namespace = require("source"); + _namespace.read; + `, + ); + }); + }); + }); + + describe("loading CommonJS with 'babel'", () => { + const importedInterop = "babel"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamed(), + ` + import _es6Default from "source"; + _es6Default.read; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addNamed(), + ` + import { read as _read } from "source"; + _read; + `, + ); + }); + + it("should import with a name hint", () => { + testModule( + { importingInterop, importedInterop }, + addNamed({ nameHint: "hintedName" }), + ` + import { read as _hintedName } from "source"; + _hintedName; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addNamed(), + ` + var _read = require("source").read; + _read; + `, + ); + }); + + it("should import with a name hint", () => { + testScript( + { importedInterop }, + addNamed({ nameHint: "hintedName" }), + ` + var _hintedName = require("source").read; + _hintedName; + `, + ); + }); + + it("should import with force-enabled liveness", () => { + testScript( + { importedInterop, ensureLiveReference: true }, + addNamed(), + ` + var _namespace = require("source"); + _namespace.read; + `, + ); + }); + }); + }); + }); + + describe("side-effectful imports", () => { + const addSideEffect = opts => m => m.addSideEffect("source", opts); + + describe("loading an ES6 module", () => { + const importedType = "es6"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedType }, + addSideEffect(), + ` + import "source"; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedType }, + addSideEffect(), + ` + import "source"; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + chai + .expect(() => { + testScript({ importedType }, addSideEffect()); + }) + .to.throw(Error, "Cannot import an ES6 module from CommonJS"); + }); + }); + }); + + describe("loading CommonJS with 'uncompiled'", () => { + const importedInterop = "uncompiled"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addSideEffect(), + ` + import "source"; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addSideEffect(), + ` + import "source"; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addSideEffect(), + ` + require("source"); + `, + ); + }); + }); + }); + + describe("loading CommonJS with 'compiled'", () => { + const importedInterop = "compiled"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addSideEffect(), + ` + import "source"; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addSideEffect(), + ` + import "source"; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addSideEffect(), + ` + require("source"); + `, + ); + }); + }); + }); + + describe("loading CommonJS with 'babel'", () => { + const importedInterop = "babel"; + + describe("using Node's interop", () => { + const importingInterop = "node"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addSideEffect(), + ` + import "source"; + `, + ); + }); + }); + + describe("using Babel's interop", () => { + const importingInterop = "babel"; + + it("should import", () => { + testModule( + { importingInterop, importedInterop }, + addSideEffect(), + ` + import "source"; + `, + ); + }); + }); + + describe("using a CommonJS loader", () => { + it("should import", () => { + testScript( + { importedInterop }, + addSideEffect(), + ` + require("source"); + `, + ); + }); + }); + }); + }); +}); diff --git a/packages/babel-helper-module-transforms/package.json b/packages/babel-helper-module-transforms/package.json index 86b75232a9..f989c44eee 100644 --- a/packages/babel-helper-module-transforms/package.json +++ b/packages/babel-helper-module-transforms/package.json @@ -8,6 +8,7 @@ "repository": "https://github.com/babel/babel/tree/master/packages/babel-helper-module-transforms", "main": "lib/index.js", "dependencies": { + "babel-helper-module-imports": "7.0.0-beta.2", "babel-template": "7.0.0-beta.2", "babel-types": "7.0.0-beta.2", "lodash": "^4.2.0" diff --git a/packages/babel-helper-module-transforms/src/index.js b/packages/babel-helper-module-transforms/src/index.js index f868c01fc3..0055273bc5 100644 --- a/packages/babel-helper-module-transforms/src/index.js +++ b/packages/babel-helper-module-transforms/src/index.js @@ -3,6 +3,8 @@ import * as t from "babel-types"; import template from "babel-template"; import chunk from "lodash/chunk"; +import { isModule } from "babel-helper-module-imports"; + import rewriteThis from "./rewrite-this"; import rewriteLiveReferences from "./rewrite-live-references"; import normalizeAndLoadModuleMetadata, { @@ -10,32 +12,7 @@ import normalizeAndLoadModuleMetadata, { isSideEffectImport, } from "./normalize-and-load-metadata"; -export { hasExports, isSideEffectImport }; - -export function isModule(path: NodePath, requireUnambiguous: boolean = false) { - const { sourceType } = path.node; - if (sourceType !== "module" && sourceType !== "script") { - throw path.buildCodeFrameError( - `Unknown sourceType "${sourceType}", cannot transform.`, - ); - } - - const filename = path.hub.file.opts.filename; - if (/\.mjs$/.test(filename)) { - requireUnambiguous = false; - } - - return ( - path.node.sourceType === "module" && - (!requireUnambiguous || isUnambiguousModule(path)) - ); -} - -// This approach is not ideal. It is here to preserve compatibility for now, -// but really this should just return true or be deleted. -function isUnambiguousModule(path) { - return path.get("body").some(p => p.isModuleDeclaration()); -} +export { hasExports, isSideEffectImport, isModule }; /** * Perform all of the generic ES6 module rewriting needed to handle initial