diff --git a/src/babel/transformation/modules/_default.js b/src/babel/transformation/modules/_default.js index 5b7ea1ab88..d563e51bb6 100644 --- a/src/babel/transformation/modules/_default.js +++ b/src/babel/transformation/modules/_default.js @@ -1,78 +1,16 @@ import * as messages from "../../messages"; +import Remaps from "./lib/remaps"; import extend from "lodash/object/extend"; import object from "../../helpers/object"; import * as util from "../../util"; import * as t from "../../types"; -var remapVisitor = { - enter(node) { - if (node._skipModulesRemap) { - return this.skip(); - } - }, - - ReferencedIdentifier(node, parent, scope, formatter) { - var remap = formatter.internalRemap[node.name]; - - if (remap && node !== remap) { - if (!scope.hasBinding(node.name) || scope.bindingIdentifierEquals(node.name, formatter.localImports[node.name])) { - if (this.key === "callee" && this.parentPath.isCallExpression()) { - return t.sequenceExpression([t.literal(0), remap]); - } else { - return remap; - } - } - } - }, - - AssignmentExpression: { - exit(node, parent, scope, formatter) { - if (!node._ignoreModulesRemap) { - var exported = formatter.getExport(node.left, scope); - if (exported) { - return formatter.remapExportAssignment(node, exported); - } - } - } - }, - - UpdateExpression(node, parent, scope, formatter) { - var exported = formatter.getExport(node.argument, scope); - if (!exported) return; - - this.skip(); - - // expand to long file assignment expression - var assign = t.assignmentExpression(node.operator[0] + "=", node.argument, t.literal(1)); - - // remap this assignment expression - var remapped = formatter.remapExportAssignment(assign, exported); - - // we don't need to change the result - if (t.isExpressionStatement(parent) || node.prefix) { - return remapped; - } - - var nodes = []; - nodes.push(remapped); - - var operator; - if (node.operator === "--") { - operator = "+"; - } else { // "++" - operator = "-"; - } - nodes.push(t.binaryExpression(operator, node.argument, t.literal(1))); - - return t.sequenceExpression(nodes); - } -}; - var metadataVisitor = { ModuleDeclaration: { enter(node, parent, scope, formatter) { if (node.source) { node.source.value = formatter.file.resolveModuleSource(node.source.value); + formatter.addScope(this); } } }, @@ -218,18 +156,27 @@ var metadataVisitor = { } }, - Scope() { - this.skip(); + Scope(node, parent, scope, formatter) { + if (!formatter.isLoose()) { + this.skip(); + } } }; export default class DefaultFormatter { constructor(file) { - this.internalRemap = object(); - this.defaultIds = object(); - this.scope = file.scope; - this.file = file; - this.ids = object(); + // object containg all module sources with the scope that they're contained in + this.sourceScopes = object(); + + // ids for use in module ids + this.defaultIds = object(); + this.ids = object(); + + // contains reference aliases for live bindings + this.remaps = new Remaps(file, this); + + this.scope = file.scope; + this.file = file; this.hasNonDefaultExports = false; @@ -243,6 +190,18 @@ export default class DefaultFormatter { this.getMetadata(); } + addScope(path) { + var source = path.node.source && path.node.source.value; + if (!source) return; + + var existingScope = this.sourceScopes[source]; + if (existingScope && existingScope !== path.scope) { + throw path.errorWithNode(messages.get("modulesDuplicateDeclarations")); + } + + this.sourceScopes[source] = path.scope; + } + isModuleType(node, type) { var modules = this.file.dynamicImportTypes[type]; return modules && modules.indexOf(node) >= 0; @@ -264,12 +223,14 @@ export default class DefaultFormatter { break; } } - if (has) this.file.path.traverse(metadataVisitor, this); + if (has || this.isLoose()) { + this.file.path.traverse(metadataVisitor, this); + } } remapAssignments() { if (this.hasLocalExports || this.hasLocalImports) { - this.file.path.traverse(remapVisitor, this); + this.remaps.run(); } } diff --git a/src/babel/transformation/modules/amd.js b/src/babel/transformation/modules/amd.js index c6932d8574..a660373731 100644 --- a/src/babel/transformation/modules/amd.js +++ b/src/babel/transformation/modules/amd.js @@ -72,7 +72,7 @@ export default class AMDFormatter extends DefaultFormatter { this.getExternalReference(node); } - importSpecifier(specifier, node, nodes) { + importSpecifier(specifier, node, nodes, scope) { var key = node.source.value; var ref = this.getExternalReference(node); @@ -90,7 +90,7 @@ export default class AMDFormatter extends DefaultFormatter { // import * as bar from "foo"; } else if (!includes(this.file.dynamicImported, node) && t.isSpecifierDefault(specifier) && !this.noInteropRequireImport) { // import foo from "foo"; - var uid = this.scope.generateUidIdentifier(specifier.local.name); + var uid = scope.generateUidIdentifier(specifier.local.name); nodes.push(t.variableDeclaration("var", [ t.variableDeclarator(uid, t.callExpression(this.file.addHelper("interop-require-default"), [ref])) ])); @@ -102,7 +102,7 @@ export default class AMDFormatter extends DefaultFormatter { ref = t.memberExpression(ref, imported); } - this.internalRemap[specifier.local.name] = ref; + this.remaps.add(scope, specifier.local.name, ref); } exportSpecifier(specifier, node, nodes) { diff --git a/src/babel/transformation/modules/common.js b/src/babel/transformation/modules/common.js index 2fd23d5af1..9293d88b46 100644 --- a/src/babel/transformation/modules/common.js +++ b/src/babel/transformation/modules/common.js @@ -37,7 +37,7 @@ export default class CommonJSFormatter extends DefaultFormatter { } } - importSpecifier(specifier, node, nodes) { + importSpecifier(specifier, node, nodes, scope) { var variableName = specifier.local; var ref = this.getExternalReference(node, nodes); @@ -47,9 +47,9 @@ export default class CommonJSFormatter extends DefaultFormatter { if (this.isModuleType(node, "absolute")) { // absolute module reference } else if (this.isModuleType(node, "absoluteDefault")) { - this.internalRemap[variableName.name] = ref; + this.remaps.add(scope, variableName.name, ref); } else if (this.noInteropRequireImport) { - this.internalRemap[variableName.name] = t.memberExpression(ref, t.identifier("default")); + this.remaps.add(scope, variableName.name, t.memberExpression(ref, t.identifier("default"))); } else { var uid = this.scope.generateUidIdentifierBasedOnNode(node, "import"); @@ -57,7 +57,7 @@ export default class CommonJSFormatter extends DefaultFormatter { t.variableDeclarator(uid, t.callExpression(this.file.addHelper("interop-require-default"), [ref])) ])); - this.internalRemap[variableName.name] = t.memberExpression(uid, t.identifier("default")); + this.remaps.add(scope, variableName.name, t.memberExpression(uid, t.identifier("default"))); } } else { if (t.isImportNamespaceSpecifier(specifier)) { @@ -71,7 +71,7 @@ export default class CommonJSFormatter extends DefaultFormatter { ])); } else { // import { foo } from "foo"; - this.internalRemap[variableName.name] = t.memberExpression(ref, specifier.imported); + this.remaps.add(scope, variableName.name, t.memberExpression(ref, specifier.imported)); } } } diff --git a/src/babel/transformation/modules/lib/remaps.js b/src/babel/transformation/modules/lib/remaps.js new file mode 100644 index 0000000000..a02ffd1766 --- /dev/null +++ b/src/babel/transformation/modules/lib/remaps.js @@ -0,0 +1,119 @@ +import * as t from "../../../types"; + +var remapVisitor = { + enter(node) { + if (node._skipModulesRemap) { + return this.skip(); + } + }, + + ReferencedIdentifier(node, parent, scope, remaps) { + var remap = remaps.get(scope, node.name); + if (!remap || node === remap) return; + + if (!scope.hasBinding(node.name) || + scope.bindingIdentifierEquals(node.name, remaps.formatter.localImports[node.name])) { + if (this.key === "callee" && this.parentPath.isCallExpression()) { + return t.sequenceExpression([t.literal(0), remap]); + } else { + return remap; + } + } + }, + + AssignmentExpression: { + exit(node, parent, scope, { formatter }) { + if (!node._ignoreModulesRemap) { + var exported = formatter.getExport(node.left, scope); + if (exported) { + return formatter.remapExportAssignment(node, exported); + } + } + } + }, + + UpdateExpression(node, parent, scope, { formatter }) { + var exported = formatter.getExport(node.argument, scope); + if (!exported) return; + + this.skip(); + + // expand to long file assignment expression + var assign = t.assignmentExpression(node.operator[0] + "=", node.argument, t.literal(1)); + + // remap this assignment expression + var remapped = formatter.remapExportAssignment(assign, exported); + + // we don't need to change the result + if (t.isExpressionStatement(parent) || node.prefix) { + return remapped; + } + + var nodes = []; + nodes.push(remapped); + + var operator; + if (node.operator === "--") { + operator = "+"; + } else { // "++" + operator = "-"; + } + nodes.push(t.binaryExpression(operator, node.argument, t.literal(1))); + + return t.sequenceExpression(nodes); + } +}; +export default class Remaps { + constructor(file, formatter) { + this.formatter = formatter; + this.file = file; + } + + run() { + this.file.path.traverse(remapVisitor, this); + } + + _getKey(name) { + return `${name}:moduleRemap`; + } + + get(scope, name) { + return scope.getData(this._getKey(name)); + } + + add(scope, name, val) { + if (this.all) { + this.all.push({ + name, + scope, + node: val + }); + } + + return scope.setData(this._getKey(name), val); + } + + remove(scope, name) { + return scope.removeData(this._getKey(name)); + } + + /** + * These methods are used by the system module formatter who needs access to all the remaps + * so it can process them into it's specific setter method. We don't do this by default since + * no other module formatters need access to this. + */ + + getAll() { + return this.all; + } + + clearAll() { + if (this.all) { + for (var remap of (this.all: Array)) { + remap.scope.removeData(this._getKey(remap.name)); + } + } + + this.all = []; + } +} diff --git a/src/babel/transformation/modules/system.js b/src/babel/transformation/modules/system.js index afcb86a003..da4b410aec 100644 --- a/src/babel/transformation/modules/system.js +++ b/src/babel/transformation/modules/system.js @@ -82,6 +82,8 @@ export default class SystemFormatter extends AMDFormatter { this.exportIdentifier = file.scope.generateUidIdentifier("export"); this.noInteropRequireExport = true; this.noInteropRequireImport = true; + + this.remaps.clearAll(); } _addImportSource(node, exportNode) { @@ -137,13 +139,13 @@ export default class SystemFormatter extends AMDFormatter { importSpecifier(specifier, node, nodes) { AMDFormatter.prototype.importSpecifier.apply(this, arguments); - for (var name in this.internalRemap) { + for (var remap of (this.remaps.getAll(): Array)) { nodes.push(t.variableDeclaration("var", [ - t.variableDeclarator(t.identifier(name), this.internalRemap[name]) + t.variableDeclarator(t.identifier(remap.name), remap.node) ])); } - this.internalRemap = object(); + this.remaps.clearAll(); this._addImportSource(last(nodes), node); } diff --git a/src/babel/transformation/transformers/es6/modules.js b/src/babel/transformation/transformers/es6/modules.js index e83a1454c6..ba653750fb 100644 --- a/src/babel/transformation/transformers/es6/modules.js +++ b/src/babel/transformation/transformers/es6/modules.js @@ -21,10 +21,10 @@ export var visitor = { if (node.specifiers.length) { for (var specifier of (node.specifiers: Array)) { - file.moduleFormatter.importSpecifier(specifier, node, nodes, parent); + file.moduleFormatter.importSpecifier(specifier, node, nodes, scope); } } else { - file.moduleFormatter.importDeclaration(node, nodes, parent); + file.moduleFormatter.importDeclaration(node, nodes, scope); } if (nodes.length === 1) { @@ -37,14 +37,14 @@ export var visitor = { ExportAllDeclaration(node, parent, scope, file) { var nodes = []; - file.moduleFormatter.exportAllDeclaration(node, nodes, parent); + file.moduleFormatter.exportAllDeclaration(node, nodes, scope); keepBlockHoist(node, nodes); return nodes; }, ExportDefaultDeclaration(node, parent, scope, file) { var nodes = []; - file.moduleFormatter.exportDeclaration(node, nodes, parent); + file.moduleFormatter.exportDeclaration(node, nodes, scope); keepBlockHoist(node, nodes); return nodes; }, @@ -63,10 +63,10 @@ export var visitor = { declar.init = declar.init || t.identifier("undefined"); } - file.moduleFormatter.exportDeclaration(node, nodes, parent); + file.moduleFormatter.exportDeclaration(node, nodes, scope); } else if (node.specifiers) { for (let i = 0; i < node.specifiers.length; i++) { - file.moduleFormatter.exportSpecifier(node.specifiers[i], node, nodes, parent); + file.moduleFormatter.exportSpecifier(node.specifiers[i], node, nodes, scope); } } diff --git a/src/babel/traversal/scope/index.js b/src/babel/traversal/scope/index.js index 7415a30f08..2a69c855ca 100644 --- a/src/babel/traversal/scope/index.js +++ b/src/babel/traversal/scope/index.js @@ -598,6 +598,38 @@ export default class Scope { } } + /** + * Set some arbitrary data on the current scope. + */ + + setData(key, val) { + return this.data[key] = val; + } + + /** + * Recursively walk up scope tree looking for the data `key`. + */ + + getData(key) { + var scope = this; + do { + var data = scope.data[key]; + if (data != null) return data; + } while(scope = scope.parent); + } + /** + * Recursively walk up scope tree looking for the data `key` and if it exists, + * remove it. + */ + + removeData(key) { + var scope = this; + do { + var data = scope.data[key]; + if (data != null) scope.data[key] = null; + } while(scope = scope.parent); + } + /** * Description */ @@ -623,6 +655,7 @@ export default class Scope { bindings: object(), globals: object(), uids: object(), + data: object(), }; extend(this, info); diff --git a/test/core/fixtures/transformation/es6.modules-common/loose-collision/actual.js b/test/core/fixtures/transformation/es6.modules-common/loose-collision/actual.js new file mode 100644 index 0000000000..89934708a6 --- /dev/null +++ b/test/core/fixtures/transformation/es6.modules-common/loose-collision/actual.js @@ -0,0 +1,5 @@ +if (true) { + import foo from "foo"; +} else { + import bar from "foo"; +} diff --git a/test/core/fixtures/transformation/es6.modules-common/loose-collision/options.json b/test/core/fixtures/transformation/es6.modules-common/loose-collision/options.json new file mode 100644 index 0000000000..1d2da25030 --- /dev/null +++ b/test/core/fixtures/transformation/es6.modules-common/loose-collision/options.json @@ -0,0 +1,4 @@ +{ + "throws": "Duplicate module declarations with the same source but in different scopes", + "loose": "es6.modules" +} diff --git a/test/core/fixtures/transformation/es6.modules-common/loose/actual.js b/test/core/fixtures/transformation/es6.modules-common/loose/actual.js index 0c650f9545..6d06248e3c 100644 --- a/test/core/fixtures/transformation/es6.modules-common/loose/actual.js +++ b/test/core/fixtures/transformation/es6.modules-common/loose/actual.js @@ -1 +1,7 @@ export var foo = 5; + +if (true) { + import bar from "bar"; +} + +bar; diff --git a/test/core/fixtures/transformation/es6.modules-common/loose/expected.js b/test/core/fixtures/transformation/es6.modules-common/loose/expected.js index 7c5d4df47b..cd9f50ef3d 100644 --- a/test/core/fixtures/transformation/es6.modules-common/loose/expected.js +++ b/test/core/fixtures/transformation/es6.modules-common/loose/expected.js @@ -2,4 +2,12 @@ exports.__esModule = true; var foo = 5; -exports.foo = foo; \ No newline at end of file + +exports.foo = foo; +if (true) { + var _bar = require("bar"); + + var _bar2 = babelHelpers.interopRequireDefault(_bar); +} + +bar;