parcel-bundler
Version:
Blazing fast, zero configuration web application bundler
598 lines (511 loc) • 18.6 kB
JavaScript
const path = require('path');
const mm = require('micromatch');
const t = require('@babel/types');
const template = require('@babel/template').default;
const traverse = require('@babel/traverse').default;
const rename = require('./renamer');
const {getName, getIdentifier, getExportIdentifier} = require('./utils');
const WRAPPER_TEMPLATE = template(`
var NAME = (function () {
var exports = this;
var module = {exports: this};
BODY;
return module.exports;
}).call({});
`);
const ESMODULE_TEMPLATE = template(`exports.__esModule = true;`);
const EXPORT_ASSIGN_TEMPLATE = template('EXPORTS.NAME = LOCAL');
const EXPORT_ALL_TEMPLATE = template(
'$parcel$exportWildcard(OLD_NAME, $parcel$require(ID, SOURCE))'
);
const REQUIRE_CALL_TEMPLATE = template('$parcel$require(ID, SOURCE)');
const REQUIRE_RESOLVE_CALL_TEMPLATE = template(
'$parcel$require$resolve(ID, SOURCE)'
);
const TYPEOF = {
module: 'object',
require: 'function'
};
function hasSideEffects(asset, {sideEffects} = asset._package) {
switch (typeof sideEffects) {
case 'undefined':
return true;
case 'boolean':
return sideEffects;
case 'string':
return mm.isMatch(
path.relative(asset._package.pkgdir, asset.name),
sideEffects,
{matchBase: true}
);
case 'object':
return sideEffects.some(sideEffects =>
hasSideEffects(asset, {sideEffects})
);
}
}
module.exports = {
Program: {
enter(path, asset) {
traverse.cache.clearScope();
path.scope.crawl();
asset.cacheData.imports = asset.cacheData.imports || Object.create(null);
asset.cacheData.exports = asset.cacheData.exports || Object.create(null);
asset.cacheData.wildcards = asset.cacheData.wildcards || [];
asset.cacheData.sideEffects = asset._package && hasSideEffects(asset);
let shouldWrap = false;
path.traverse({
CallExpression(path) {
// If we see an `eval` call, wrap the module in a function.
// Otherwise, local variables accessed inside the eval won't work.
let callee = path.node.callee;
if (
t.isIdentifier(callee) &&
callee.name === 'eval' &&
!path.scope.hasBinding('eval', true)
) {
asset.cacheData.isCommonJS = true;
shouldWrap = true;
path.stop();
}
},
ReturnStatement(path) {
// Wrap in a function if we see a top-level return statement.
if (!path.getFunctionParent()) {
shouldWrap = true;
asset.cacheData.isCommonJS = true;
path.replaceWith(
t.returnStatement(
t.memberExpression(
t.identifier('module'),
t.identifier('exports')
)
)
);
path.stop();
}
},
ReferencedIdentifier(path) {
// We must wrap if `module` is referenced as a free identifier rather
// than a statically resolvable member expression.
if (
path.node.name === 'module' &&
(!path.parentPath.isMemberExpression() || path.parent.computed) &&
!(
path.parentPath.isUnaryExpression() &&
path.parent.operator === 'typeof'
) &&
!path.scope.hasBinding('module') &&
!path.scope.getData('shouldWrap')
) {
asset.cacheData.isCommonJS = true;
shouldWrap = true;
path.stop();
}
}
});
path.scope.setData('shouldWrap', shouldWrap);
},
exit(path, asset) {
let scope = path.scope;
if (scope.getData('shouldWrap')) {
if (asset.cacheData.isES6Module) {
path.unshiftContainer('body', [ESMODULE_TEMPLATE()]);
}
path.replaceWith(
t.program([
WRAPPER_TEMPLATE({
NAME: getExportsIdentifier(asset),
BODY: path.node.body
})
])
);
asset.cacheData.exports = {};
asset.cacheData.isCommonJS = true;
asset.cacheData.isES6Module = false;
} else {
// Re-crawl scope so we are sure to have all bindings.
scope.crawl();
// Rename each binding in the top-level scope to something unique.
for (let name in scope.bindings) {
if (!name.startsWith('$' + t.toIdentifier(asset.id))) {
let newName = getName(asset, 'var', name);
rename(scope, name, newName);
}
}
let exportsIdentifier = getExportsIdentifier(asset);
// Add variable that represents module.exports if it is referenced and not declared.
if (
scope.hasGlobal(exportsIdentifier.name) &&
!scope.hasBinding(exportsIdentifier.name)
) {
scope.push({id: exportsIdentifier, init: t.objectExpression([])});
}
}
path.stop();
asset.isAstDirty = true;
}
},
DirectiveLiteral(path) {
// Remove 'use strict' directives, since modules are concatenated - one strict mode
// module should not apply to all other modules in the same scope.
if (path.node.value === 'use strict') {
path.parentPath.remove();
}
},
MemberExpression(path, asset) {
if (path.scope.hasBinding('module') || path.scope.getData('shouldWrap')) {
return;
}
if (t.matchesPattern(path.node, 'module.exports')) {
path.replaceWith(getExportsIdentifier(asset));
asset.cacheData.isCommonJS = true;
}
if (t.matchesPattern(path.node, 'module.id')) {
path.replaceWith(t.stringLiteral(asset.id));
}
if (t.matchesPattern(path.node, 'module.hot')) {
path.replaceWith(t.identifier('null'));
}
if (
t.matchesPattern(path.node, 'module.require') &&
asset.options.target !== 'node'
) {
path.replaceWith(t.identifier('null'));
}
if (t.matchesPattern(path.node, 'module.bundle')) {
path.replaceWith(t.identifier('require'));
}
},
ReferencedIdentifier(path, asset) {
if (
path.node.name === 'exports' &&
!path.scope.hasBinding('exports') &&
!path.scope.getData('shouldWrap')
) {
path.replaceWith(getExportsIdentifier(asset));
asset.cacheData.isCommonJS = true;
}
if (path.node.name === 'global' && !path.scope.hasBinding('global')) {
path.replaceWith(t.identifier('$parcel$global'));
asset.globals.delete('global');
}
let globalCode = asset.globals.get(path.node.name);
if (globalCode) {
path.scope
.getProgramParent()
.path.unshiftContainer('body', [template(globalCode)()]);
asset.globals.delete(path.node.name);
}
},
ThisExpression(path, asset) {
if (!path.scope.parent && !path.scope.getData('shouldWrap')) {
path.replaceWith(getExportsIdentifier(asset));
asset.cacheData.isCommonJS = true;
}
},
AssignmentExpression(path, asset) {
if (path.scope.hasBinding('exports') || path.scope.getData('shouldWrap')) {
return;
}
let {left, right} = path.node;
if (t.isIdentifier(left) && left.name === 'exports') {
path.get('left').replaceWith(getExportsIdentifier(asset));
asset.cacheData.isCommonJS = true;
}
// If we can statically evaluate the name of a CommonJS export, create an ES6-style export for it.
// This allows us to remove the CommonJS export object completely in many cases.
if (
t.isMemberExpression(left) &&
t.isIdentifier(left.object, {name: 'exports'}) &&
((t.isIdentifier(left.property) && !left.computed) ||
t.isStringLiteral(left.property))
) {
let name = t.isIdentifier(left.property)
? left.property.name
: left.property.value;
let identifier = getExportIdentifier(asset, name);
// Replace the CommonJS assignment with a reference to the ES6 identifier.
path.get('left.object').replaceWith(getExportsIdentifier(asset));
path.get('right').replaceWith(identifier);
// If this is the first assignment, create a binding for the ES6-style export identifier.
// Otherwise, assign to the existing export binding.
let scope = path.scope.getProgramParent();
if (!scope.hasBinding(identifier.name)) {
asset.cacheData.exports[name] = identifier.name;
// If in the program scope, create a variable declaration and initialize with the exported value.
// Otherwise, declare the variable in the program scope, and assign to it here.
if (path.scope === scope) {
let [decl] = path.insertBefore(
t.variableDeclaration('var', [
t.variableDeclarator(t.clone(identifier), right)
])
);
scope.registerDeclaration(decl);
} else {
scope.push({id: t.clone(identifier)});
path.insertBefore(
t.assignmentExpression('=', t.clone(identifier), right)
);
}
} else {
path.insertBefore(
t.assignmentExpression('=', t.clone(identifier), right)
);
}
asset.cacheData.isCommonJS = true;
}
},
UnaryExpression(path) {
// Replace `typeof module` with "object"
if (
path.node.operator === 'typeof' &&
t.isIdentifier(path.node.argument) &&
TYPEOF[path.node.argument.name] &&
!path.scope.hasBinding(path.node.argument.name) &&
!path.scope.getData('shouldWrap')
) {
path.replaceWith(t.stringLiteral(TYPEOF[path.node.argument.name]));
}
},
CallExpression(path, asset) {
let {callee, arguments: args} = path.node;
let ignore =
args.length !== 1 ||
!t.isStringLiteral(args[0]) ||
path.scope.hasBinding('require');
if (ignore) {
return;
}
if (t.isIdentifier(callee, {name: 'require'})) {
let source = args[0].value;
// Ignore require calls that were ignored earlier.
if (!asset.dependencies.has(source)) {
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(source).shouldWrap = true;
}
asset.cacheData.imports['$require$' + source] = [source, '*'];
// 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(
REQUIRE_CALL_TEMPLATE({
ID: t.stringLiteral(asset.id),
SOURCE: t.stringLiteral(args[0].value)
})
);
}
if (t.matchesPattern(callee, 'require.resolve')) {
path.replaceWith(
REQUIRE_RESOLVE_CALL_TEMPLATE({
ID: t.stringLiteral(asset.id),
SOURCE: args[0]
})
);
}
},
ImportDeclaration(path, asset) {
// For each specifier, rename the local variables to point to the imported name.
// This will be replaced by the final variable name of the resolved asset in the packager.
for (let specifier of path.node.specifiers) {
let id = getIdentifier(asset, 'import', specifier.local.name);
rename(path.scope, specifier.local.name, id.name);
if (t.isImportDefaultSpecifier(specifier)) {
asset.cacheData.imports[id.name] = [path.node.source.value, 'default'];
} else if (t.isImportSpecifier(specifier)) {
asset.cacheData.imports[id.name] = [
path.node.source.value,
specifier.imported.name
];
} else if (t.isImportNamespaceSpecifier(specifier)) {
asset.cacheData.imports[id.name] = [path.node.source.value, '*'];
}
}
addImport(asset, path);
path.remove();
},
ExportDefaultDeclaration(path, asset) {
let {declaration} = path.node;
let identifier = getExportIdentifier(asset, 'default');
let name = declaration.id ? declaration.id.name : declaration.name;
if (asset.cacheData.imports[name]) {
asset.cacheData.exports['default'] = asset.cacheData.imports[name];
identifier = t.identifier(name);
}
if (hasExport(asset, name)) {
identifier = t.identifier(name);
}
// Add assignment to exports object for namespace imports and commonjs.
path.insertAfter(
EXPORT_ASSIGN_TEMPLATE({
EXPORTS: getExportsIdentifier(asset, path.scope),
NAME: t.identifier('default'),
LOCAL: t.clone(identifier)
})
);
if (t.isIdentifier(declaration)) {
// Rename the variable being exported.
safeRename(path, asset, declaration.name, identifier.name);
path.remove();
} else if (t.isExpression(declaration) || !declaration.id) {
// Declare a variable to hold the exported value.
path.replaceWith(
t.variableDeclaration('var', [
t.variableDeclarator(identifier, t.toExpression(declaration))
])
);
path.scope.registerDeclaration(path);
} else {
// Rename the declaration to the exported name.
safeRename(path, asset, declaration.id.name, identifier.name);
path.replaceWith(declaration);
}
if (!asset.cacheData.exports['default']) {
asset.cacheData.exports['default'] = identifier.name;
}
// Mark the asset as an ES6 module, so we handle imports correctly in the packager.
asset.cacheData.isES6Module = true;
},
ExportNamedDeclaration(path, asset) {
let {declaration, source, specifiers} = path.node;
if (source) {
for (let specifier of specifiers) {
let exported = specifier.exported;
if (t.isExportDefaultSpecifier(specifier)) {
asset.cacheData.exports[exported.name] = [source.value, 'default'];
} else if (t.isExportNamespaceSpecifier(specifier)) {
asset.cacheData.exports[exported.name] = [source.value, '*'];
} else if (t.isExportSpecifier(specifier)) {
asset.cacheData.exports[exported.name] = [
source.value,
specifier.local.name
];
}
let id = getIdentifier(asset, 'import', exported.name);
asset.cacheData.imports[id.name] =
asset.cacheData.exports[exported.name];
path.insertAfter(
EXPORT_ASSIGN_TEMPLATE({
EXPORTS: getExportsIdentifier(asset, path.scope),
NAME: exported,
LOCAL: id
})
);
}
addImport(asset, path);
path.remove();
} else if (declaration) {
path.replaceWith(declaration);
let identifiers = t.isIdentifier(declaration.id)
? [declaration.id]
: t.getBindingIdentifiers(declaration);
for (let id in identifiers) {
addExport(asset, path, identifiers[id], identifiers[id]);
}
} else if (specifiers.length > 0) {
for (let specifier of specifiers) {
addExport(asset, path, specifier.local, specifier.exported);
}
path.remove();
}
// Mark the asset as an ES6 module, so we handle imports correctly in the packager.
asset.cacheData.isES6Module = true;
},
ExportAllDeclaration(path, asset) {
asset.cacheData.wildcards.push(path.node.source.value);
asset.cacheData.isES6Module = true;
path.replaceWith(
EXPORT_ALL_TEMPLATE({
OLD_NAME: getExportsIdentifier(asset),
SOURCE: t.stringLiteral(path.node.source.value),
ID: t.stringLiteral(asset.id)
})
);
}
};
function addImport(asset, path) {
// Replace with a $parcel$require call so we know where to insert side effects.
let requireCall = REQUIRE_CALL_TEMPLATE({
ID: t.stringLiteral(asset.id),
SOURCE: t.stringLiteral(path.node.source.value)
});
// Hoist the call to the top of the file.
let lastImport = path.scope.getData('hoistedImport');
if (lastImport) {
[lastImport] = lastImport.insertAfter(requireCall);
} else {
[lastImport] = path.parentPath.unshiftContainer('body', [requireCall]);
}
path.scope.setData('hoistedImport', lastImport);
}
function addExport(asset, path, local, exported) {
let scope = path.scope.getProgramParent();
let identifier = getExportIdentifier(asset, exported.name);
if (asset.cacheData.imports[local.name]) {
asset.cacheData.exports[exported.name] =
asset.cacheData.imports[local.name];
identifier = t.identifier(local.name);
}
if (hasExport(asset, local.name)) {
identifier = t.identifier(local.name);
}
let assignNode = EXPORT_ASSIGN_TEMPLATE({
EXPORTS: getExportsIdentifier(asset, scope),
NAME: t.identifier(exported.name),
LOCAL: identifier
});
let binding = scope.getBinding(local.name);
let constantViolations = binding
? binding.constantViolations.concat(path)
: [path];
if (!asset.cacheData.exports[exported.name]) {
asset.cacheData.exports[exported.name] = identifier.name;
}
try {
rename(scope, local.name, identifier.name);
} catch (e) {
throw new Error('export ' + e.message);
}
constantViolations.forEach(path => path.insertAfter(t.cloneDeep(assignNode)));
}
function hasExport(asset, name) {
let exports = asset.cacheData.exports;
return Object.keys(exports).some(k => exports[k] === name);
}
function safeRename(path, asset, from, to) {
if (from === to) {
return;
}
// If the binding that we're renaming is constant, it's safe to rename it.
// Otherwise, create a new binding that references the original.
let binding = path.scope.getBinding(from);
if (binding && binding.constant) {
rename(path.scope, from, to);
} else {
let [decl] = path.insertAfter(
t.variableDeclaration('var', [
t.variableDeclarator(t.identifier(to), t.identifier(from))
])
);
path.scope.getBinding(from).reference(decl.get('declarations.0.init'));
path.scope.registerDeclaration(decl);
}
}
function getExportsIdentifier(asset, scope) {
if (scope && scope.getData('shouldWrap')) {
return t.identifier('exports');
} else {
return getIdentifier(asset, 'exports');
}
}