提交 1df643f2 编写于 作者: D Devon Govett

Wrap module if needed, or move to correct position for side effects

上级 532267bf
......@@ -32,6 +32,7 @@ class JSConcatPackager extends Packager {
this.contents = '';
this.lineOffset = 1;
this.needsPrelude = false;
this.assetOffsets = new Map();
for (let asset of this.bundle.assets) {
// If this module is referenced by another JS bundle, it needs to be exposed externally.
......@@ -147,6 +148,9 @@ class JSConcatPackager extends Packager {
}
}
let shouldWrap = [...asset.parentDeps].some(dep => dep.shouldWrap);
asset.cacheData.shouldWrap = shouldWrap;
if (this.bundle.entryAsset === asset && this.externalModules.size > 0) {
js = `
function $parcel$entry() {
......@@ -157,6 +161,7 @@ class JSConcatPackager extends Packager {
js = js.trim() + '\n';
let startOffset = this.contents.length;
this.bundle.addOffset(asset, this.lineOffset + 1);
this.write(
`\n// ASSET: ${asset.id} - ${path.relative(
......@@ -165,6 +170,9 @@ class JSConcatPackager extends Packager {
)}\n${js}`,
map && map.lineCount ? map.lineCount : undefined
);
let endOffset = this.contents.length;
this.assetOffsets.set(asset.id, [startOffset, endOffset]);
}
getBundleSpecifier(bundle) {
......
......@@ -15,12 +15,12 @@ const DEFAULT_INTEROP_TEMPLATE = template(
const THROW_TEMPLATE = template('$parcel$missingModule(MODULE)');
const REQUIRE_TEMPLATE = template('require(ID)');
const WRAP_TEMPLATE = template(`
var MODULE_EXECUTED = false;
function NAME() {
if (!MODULE) {
if (!MODULE_EXECUTED) {
MODULE_EXECUTED = true;
BODY;
}
return MODULE;
}
`);
......@@ -33,6 +33,8 @@ module.exports = packager => {
// Share $parcel$interopDefault variables between modules
let interops = new Map();
let imports = new Map();
let wrapped = new Map();
let referenced = new Set();
let assets = Array.from(addedAssets).reduce((acc, asset) => {
acc[asset.id] = asset;
......@@ -165,54 +167,65 @@ module.exports = packager => {
}
}
function collectReferencedStatments(scope, name, before, seen = new Set()) {
let binding = scope.getBinding(name);
if (!binding) {
return seen;
function getStatementsInRange(program, start, end) {
return program
.get('body')
.filter(
statement => statement.node.end >= start && statement.node.start <= end
);
}
function wrapModule(path, mod) {
// Find all statements in the module
let program = path.findParent(p => p.isProgram());
let [start, end] = packager.assetOffsets.get(mod.id);
let statements = getStatementsInRange(program, start, end);
if (!statements.length) {
return;
}
// Go through all references to this binding and collect any
// statements with references prior to the given position.
for (let ref of [
binding.path,
...binding.constantViolations,
...binding.referencePaths
]) {
// if (ref.node.start < before) {
let statement = getTopStatement(ref, scope);
if (statement && !seen.has(statement)) {
seen.add(statement);
let bindings = statement.getBindingIdentifiers();
for (let name in bindings) {
collectReferencedStatments(scope, name, before, seen);
}
let scope = path.scope.getProgramParent();
let body = [];
for (let ref of statements) {
let node = ref.node;
// Hoist all declarations out of the function wrapper
// so that they can be referenced by other modules directly.
if (ref.isVariableDeclaration()) {
let ids = ref.getBindingIdentifierPaths();
for (let name in ids) {
scope.push({id: t.identifier(name)});
let p = ids[name];
let right = p.parentPath.isVariableDeclarator()
? p.parent.init
: p.parent;
body.push(
t.assignmentExpression(
'=',
t.identifier(name),
t.toExpression(right)
)
);
}
// }
} else {
body.push(ref.node);
}
}
return seen;
}
function getTopStatement(path, scope) {
return path.findParent(p => p.isStatement() && p.scope === scope);
}
let wrapper = WRAP_TEMPLATE({
NAME: t.identifier(`$${mod.id}$init`),
MODULE_EXECUTED: t.identifier(`$${mod.id}$executed`),
BODY: body
});
function isCircular(asset, id, seen = new Set) {
if (seen.has(asset)) return false;
seen.add(asset);
statements[0].insertBefore(wrapper);
for (let dep of asset.depAssets.values()) {
if (dep.id === id || isCircular(dep, id, seen)) {
return true;
}
for (let p of statements) {
p.remove();
}
return false;
}
let wrapped = new Set;
traverse(ast, {
CallExpression(path) {
let {arguments: args, callee} = path.node;
......@@ -253,62 +266,35 @@ module.exports = packager => {
let name = `$${mod.id}$exports`;
node = t.identifier(replacements.get(name) || name);
if (isCircular(mod, id.value) && !wrapped.has(name)) {
console.log('CIRCULAR', mod.name, id.value)
// CommonJS allows circular dependencies.
// Find all references of this module prior to this `require` call, and move them just before it.
// This way the dependent module will have access to everything this module had at the time it was required.
let scope = path.scope.getProgramParent();
let binding = scope.getBinding(name);
let referenced = collectReferencedStatments(
scope,
name,
path.node.start
);
// let statement = getTopStatement(path, scope);
// let statement = path.getStatementParent();
// console.log(statement, path.node)
if (referenced.size) {
let ref = [...referenced];
let body = [];
for (let ref of referenced) {
let node = ref.node;
if (ref.isDeclaration()) {
let ids = ref.getBindingIdentifierPaths();
for (let name in ids) {
scope.push({id: t.identifier(name)});
let p = ids[name];
let right = p.parentPath.isVariableDeclarator() ? p.parent.init : p.parent;
body.push(t.assignmentExpression('=', t.identifier(name), right));
}
} else {
body.push(ref.node);
}
}
let wrapper = WRAP_TEMPLATE({
NAME: t.identifier(`$${mod.id}$init`),
MODULE: node,
BODY: body
});
// console.log(wrapper)
ref[0].insertBefore(wrapper);
// path.findParent(p => p.isProgram()).unshiftContainer('body', [wrapper])
for (let p of referenced) {
// let node = p.node;
p.remove();
// statement.insertBefore(node);
}
wrapped.add(name);
// We need to wrap the module in a function when a require
// call happens inside a non top-level scope, e.g. in a
// function, if statement, or conditional expression.
if (mod.cacheData.shouldWrap) {
if (!wrapped.has(mod)) {
wrapped.set(mod, path);
}
}
if (wrapped.has(name)) {
node = t.callExpression(t.identifier(`$${mod.id}$init`), []);
node = t.sequenceExpression([
t.callExpression(t.identifier(`$${mod.id}$init`), []),
node
]);
// Otherwise, if this is the first reference to the module,
// we may need to move the actual module code just before.
// This is necessary to support side effects prior to require calls,
// which need to occur in the correct order, along with circular dependencies.
} else if (!referenced.has(name)) {
let program = path.findParent(p => p.isProgram());
let [start, end] = packager.assetOffsets.get(mod.id);
let statements = getStatementsInRange(program, start, end);
let nodes = statements.map(p => p.node);
for (let p of statements) {
p.remove();
}
path.getStatementParent().insertBefore(nodes);
referenced.add(name);
}
} else {
node = REQUIRE_TEMPLATE({ID: t.numericLiteral(mod.id)}).expression;
......@@ -357,7 +343,7 @@ module.exports = packager => {
// This allows us to potentially replace accesses to e.g. `x.foo` with
// a variable like `$id$export$foo` later, avoiding the exports object altogether.
let {id, init} = path.node;
if (!t.isIdentifier(init) ) {
if (!t.isIdentifier(init)) {
return;
}
......@@ -454,6 +440,9 @@ module.exports = packager => {
}
let match = name.match(EXPORTS_RE);
if (match) {
referenced.add(name);
}
// If it's an undefined $id$exports identifier.
if (match && !path.scope.hasBinding(name)) {
......@@ -463,6 +452,10 @@ module.exports = packager => {
Program: {
// A small optimization to remove unused CommonJS exports as sometimes Uglify doesn't remove them.
exit(path) {
for (let [mod, path] of wrapped) {
wrapModule(path, mod);
}
treeShake(path.scope);
if (packager.options.minify) {
......
......@@ -35,6 +35,7 @@ module.exports = {
asset.cacheData.wildcards = asset.cacheData.wildcards || [];
asset.cacheData.sideEffects =
asset._package && asset._package.sideEffects;
asset.cacheData.shouldWrap = false;
let shouldWrap = false;
path.traverse({
......@@ -283,6 +284,20 @@ module.exports = {
return;
}
// If this require call does not occur in the top-level, e.g. in a function
// or inside an if statement, or if it might potentially happen conditionally,
// the module must be wrapped in a function so that the module execution order is correct.
let parent = path.getStatementParent().parentPath;
let bail = path.findParent(
p =>
p.isConditionalExpression() ||
p.isLogicalExpression() ||
p.isSequenceExpression()
);
if (!parent.isProgram() || bail) {
asset.dependencies.get(args[0].value).shouldWrap = true;
}
// Generate a variable name based on the current asset id and the module name to require.
// This will be replaced by the final variable name of the resolved asset in the packager.
path.replaceWith(
......
function p() {
return require('./b');
}
module.exports.foo = 'foo'
module.exports = p();
module.exports = require('./b');
console.log('hi')
module.exports = require('./a').foo + ' bar'
output('a');
if (b) {
require('./b');
}
output('c');
output('a');
require('./b');
output('c');
\ No newline at end of file
output('a');
function x() {
return require('./b');
}
output('c');
x();
......@@ -585,7 +585,7 @@ describe('scope hoisting', function() {
assert.deepEqual(output, {foo: 2, bar: 2, baz: 2});
});
it.only('supports circular dependencies', async function() {
it('supports circular dependencies', async function() {
let b = await bundle(
__dirname + '/integration/scope-hoisting/commonjs/require-circular/a.js'
);
......@@ -594,6 +594,77 @@ describe('scope hoisting', function() {
assert.equal(output, 'foo bar');
});
it('executes modules in the correct order', async function() {
let b = await bundle(
__dirname +
'/integration/scope-hoisting/commonjs/require-execution-order/a.js'
);
let out = [];
await run(b, {
output(o) {
out.push(o);
}
});
assert.deepEqual(out, ['a', 'b', 'c']);
});
it('supports conditional requires', async function() {
let b = await bundle(
__dirname +
'/integration/scope-hoisting/commonjs/require-conditional/a.js'
);
let out = [];
await run(b, {
b: false,
output(o) {
out.push(o);
}
});
assert.deepEqual(out, ['a', 'c']);
out = [];
await run(b, {
b: true,
output(o) {
out.push(o);
}
});
assert.deepEqual(out, ['a', 'b', 'c']);
});
it('supports requires inside functions', async function() {
let b = await bundle(
__dirname +
'/integration/scope-hoisting/commonjs/require-in-function/a.js'
);
let out = [];
await run(b, {
b: false,
output(o) {
out.push(o);
}
});
assert.deepEqual(out, ['a', 'c', 'b']);
});
it('can bundle the node stream module', async function() {
let b = await bundle(
__dirname + '/integration/scope-hoisting/commonjs/stream-module/a.js'
);
let res = await run(b);
assert.equal(typeof res.Readable, 'function');
assert.equal(typeof res.Writable, 'function');
assert.equal(typeof res.Duplex, 'function');
});
it('missing exports should be replaced with an empty object', async function() {
let b = await bundle(
__dirname + '/integration/scope-hoisting/commonjs/empty-module/a.js'
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册