/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
'use strict';
/**
* This transform inlines top-level require(...) aliases with to enable lazy
* loading of dependencies. It is able to inline both single references and
* child property references.
*
* For instance:
* var Foo = require('foo');
* f(Foo);
*
* Will be transformed into:
* f(require('foo'));
*
* When the assigment expression has a property access, it will be inlined too,
* keeping the property. For instance:
* var Bar = require('foo').bar;
* g(Bar);
*
* Will be transformed into:
* g(require('foo').bar);
*
* Destructuring also works the same way. For instance:
* const {Baz} = require('foo');
* h(Baz);
*
* Is also successfully inlined into:
* g(require('foo').Baz);
*/
module.exports = babel => {
return {
name: 'inline-requires',
visitor: {
Program: {
exit(path, state) {
var ignoredRequires = {};
var inlineableCalls = {require: true};
if (state.opts) {
if (state.opts.ignoredRequires) {
state.opts.ignoredRequires.forEach(function(name) {
ignoredRequires[name] = true;
});
}
if (state.opts.inlineableCalls) {
state.opts.inlineableCalls.forEach(function(name) {
inlineableCalls[name] = true;
});
}
}
path.scope.crawl();
path.traverse(
{CallExpression: call.bind(null, babel)},
{
ignoredRequires: ignoredRequires,
inlineableCalls: inlineableCalls,
}
);
},
},
},
};
};
function call(babel, path, state) {
var declaratorPath =
inlineableAlias(path, state) || inlineableMemberAlias(path, state);
var declarator = declaratorPath && declaratorPath.node;
if (declarator) {
var init = declarator.init;
var name = declarator.id && declarator.id.name;
var binding = declaratorPath.scope.getBinding(name);
var constantViolations = binding.constantViolations;
var thrown = false;
if (!constantViolations.length) {
deleteLocation(init);
babel.traverse(init, {
noScope: true,
enter: path => deleteLocation(path.node),
});
binding.referencePaths.forEach(ref => {
try {
ref.replaceWith(init);
} catch (err) {
thrown = true;
}
});
// If an error was thrown, it's most likely due to an invalid replacement
// happening (e.g. trying to replace a type annotation). It would usually
// be OK to ignore it, but to be safe, we will avoid removing the initial
// require.
if (!thrown) {
declaratorPath.remove();
}
}
}
}
function deleteLocation(node) {
delete node.start;
delete node.end;
delete node.loc;
}
function inlineableAlias(path, state) {
const isValid =
isInlineableCall(path.node, state) &&
path.parent.type === 'VariableDeclarator' &&
path.parent.id.type === 'Identifier' &&
path.parentPath.parent.type === 'VariableDeclaration' &&
path.parentPath.parentPath.parent.type === 'Program';
return isValid ? path.parentPath : null;
}
function inlineableMemberAlias(path, state) {
const isValid =
isInlineableCall(path.node, state) &&
path.parent.type === 'MemberExpression' &&
path.parentPath.parent.type === 'VariableDeclarator' &&
path.parentPath.parent.id.type === 'Identifier' &&
path.parentPath.parentPath.parent.type === 'VariableDeclaration' &&
path.parentPath.parentPath.parentPath.parent.type === 'Program';
return isValid ? path.parentPath.parentPath : null;
}
function isInlineableCall(node, state) {
const isInlineable =
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
state.inlineableCalls.hasOwnProperty(node.callee.name) &&
node['arguments'].length >= 1;
// require('foo');
const isStandardCall =
isInlineable &&
node['arguments'][0].type === 'StringLiteral' &&
!state.ignoredRequires.hasOwnProperty(node['arguments'][0].value);
// require(require.resolve('foo'));
const isRequireResolveCall =
isInlineable &&
node['arguments'][0].type === 'CallExpression' &&
node['arguments'][0].callee.type === 'MemberExpression' &&
node['arguments'][0].callee.object.type === 'Identifier' &&
state.inlineableCalls.hasOwnProperty(node['arguments'][0].callee.object.name) &&
node['arguments'][0].callee.property.type === 'Identifier' &&
node['arguments'][0].callee.property.name === 'resolve' &&
node['arguments'][0]['arguments'].length >= 1 &&
node['arguments'][0]['arguments'][0].type === 'StringLiteral' &&
!state.ignoredRequires.hasOwnProperty(node['arguments'][0]['arguments'][0].value);
return isStandardCall || isRequireResolveCall;
}