Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,34 @@ describe('inline-requires', function() {
]);
});

it('should inline multiple usage', function() {
compare([
'var foo = require("foo");',
'foo.bar()',
'foo.baz()',
], [
'require("foo").bar();',
'require("foo").baz();',
]);
});

it('should not matter the variable declaration length', function() {
compare([
'var foo = require("foo"), bar = require("bar"), baz = 4;',
'foo.method()',
], [
'var baz = 4;',
'require("foo").method();',
]);

compare([
'var foo = require("foo"), bar = require("bar");',
'foo.method()',
], [
'require("foo").method();',
]);
});

it('should inline requires that are not assigned', function() {
compare([
'require("foo");',
Expand All @@ -43,7 +71,21 @@ describe('inline-requires', function() {
'var foo = require("foo");',
'foo = "bar";',
]);
}).toThrow();
}).toThrow(ReferenceError);

expect(function() {
transform([
'var foo = require("foo");',
'foo = "bar";',
]);
}).toThrow(/\bline: 2\b/);

expect(function() {
transform([
'var foo = require("foo");',
'foo = "bar";',
]);
}).toThrow(/\bname: foo\b/);
});

it('should properly handle identifiers declared before their corresponding require statement', function() {
Expand All @@ -63,27 +105,43 @@ describe('inline-requires', function() {
]);
});

it('should be compatible with destructuring', function() {
compare([
'var tmp = require("./a");',
'var a = tmp.a',
'var D = {',
' b: function(c) { c ? a(c.toString()) : a("No c!"); },',
'};',
], [
'var D = {',
' b: function (c) {',
' c ? require("./a").a(c.toString()) : require("./a").a("No c!");',
' }',
'};',
]);
});

it('should be compatible with other transforms like transform-es2015-modules-commonjs', function() {
compare([
'import Imported from "foo";',
'console.log(Imported);',
], [
'var _foo2 = _interopRequireDefault(require(\"foo\"));',
'var _foo2 = _interopRequireDefault(require("foo"));',
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }',
'console.log(_foo2.default);',
]);
});

it('should be compatible with `transform-es2015-modules-commonjs` when using named imports', function() {
compare(`
import { a } from './a';

var D = {
b: function(c) { c ? a(c.toString()) : a('No c!'); },
};`, [
compare([
'import {a} from "./a";',
'var D = {',
' b: function(c) { c ? a(c.toString()) : a("No c!"); },',
'};',
], [
'var D = {',
' b: function (c) {',
` c ? (0, require('./a').a)(c.toString()) : (0, require('./a').a)('No c!');`,
' c ? (0, require("./a").a)(c.toString()) : (0, require("./a").a)("No c!");',
' }',
'};',
]);
Expand Down
156 changes: 47 additions & 109 deletions packages/babel-preset-fbjs/plugins/inline-requires.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,144 +9,82 @@

/**
* This transform inlines top-level require(...) aliases with to enable lazy
* loading of dependencies.
* loading of dependencies. It is able to inline both single references and
* child property references.
*
* Continuing with the example above, this replaces all references to `Foo` in
* the module to `require('ModuleFoo')`.
* 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);
*/
module.exports = function fbjsInlineRequiresTransform(babel) {
var t = babel.types;

function buildRequireCall(name, inlineRequiredDependencyMap) {
var call = t.callExpression(
t.identifier('require'),
[t.stringLiteral(inlineRequiredDependencyMap.get(name))]
);
call.new = true;
return call;
}

function inlineRequire(path, inlineRequiredDependencyMap, identifierToPathsMap) {
var node = path.node;

if (path.isReferenced()) {
path.replaceWith(buildRequireCall(node.name, inlineRequiredDependencyMap));
var paths = identifierToPathsMap.get(node.name);
if (paths) {
paths.delete(path);
}
}
}

module.exports = function fbjsInlineRequiresTransform() {
return {
visitor: {
Program(_, state) {
/**
* Map of require(...) aliases to module names.
*
* `Foo` is an alias for `require('ModuleFoo')` in the following example:
* var Foo = require('ModuleFoo');
*/
state.inlineRequiredDependencyMap = new Map();

/**
* Map of variable names that have not yet been inlined.
* We track them in case we later remove their require()s,
* In which case we have to come back and update them.
*/
state.identifierToPathsMap = new Map();
},
CallExpression: function(path) {
var declaratorPath = requireAlias(path) || requireMemberAlias(path);
var declarator = declaratorPath && declaratorPath.node;

/**
* Collect top-level require(...) aliases.
*/
CallExpression: function(path, state) {
var node = path.node;
if (declarator) {
declaratorPath.scope.crawl();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing that I don’t understand is why you have to crawl here, and not …

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to crawl because another plugin might have changed the scope already; for instance, the import / export Babel plugin. We need to have accurate variable binding, counting and referencing for the plugin to work.


if (isTopLevelRequireAlias(path)) {
var varName = path.parent.id.name;
var moduleName = node.arguments[0].value;
var inlineRequiredDependencyMap = state.inlineRequiredDependencyMap;
var identifierToPathsMap = state.identifierToPathsMap;
var init = declarator.init;
var name = declarator.id && declarator.id.name;
var binding = declaratorPath.scope.getBinding(name);
var constantViolations = binding.constantViolations;

inlineRequiredDependencyMap.set(varName, moduleName);
if (constantViolations.length) {
var line = constantViolations[0].node.loc.start.line;

// If we removed require() statements for variables we've already seen,
// We need to do a second pass on this program to replace them with require().
var maybePaths = identifierToPathsMap.get(varName);
if (maybePaths) {
maybePaths.forEach(path =>
inlineRequire(path, inlineRequiredDependencyMap, identifierToPathsMap));
maybePaths.delete(varName);
throw new ReferenceError(
'Cannot re-assign a require; name: ' + name + ', line: ' + line
);
}

// Remove the declaration.
path.parentPath.parentPath.remove();
// And the associated binding in the scope.
path.scope.removeBinding(varName);
binding.referencePaths.forEach(ref => ref.replaceWith(init));
declaratorPath.remove();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

… and not here.

}
},

/**
* Inline require(...) aliases.
*/
Identifier: function(path, state) {
var node = path.node;
var parent = path.parent;
var scope = path.scope;
var identifierToPathsMap = state.identifierToPathsMap;

if (!shouldInlineRequire(node, scope, state.inlineRequiredDependencyMap)) {
// Monitor this name in case we later remove its require().
// This won't happen often but if it does we need to come back and update here.
var paths = identifierToPathsMap.get(node.name);
if (paths) {
paths.add(path);
} else {
identifierToPathsMap.set(node.name, new Set([path]));
}

return;
}

if (
parent.type === 'AssignmentExpression' &&
path.isBindingIdentifier() &&
!scope.bindingIdentifierEquals(node.name, node)
) {
throw new Error(
'Cannot assign to a require(...) alias, ' + node.name +
'. Line: ' + node.loc.start.line + '.'
);
}

inlineRequire(path, state.inlineRequiredDependencyMap, identifierToPathsMap);
},
},
};
};

function isTopLevelRequireAlias(path) {
return (
function requireAlias(path) {
const isValid = (
isRequireCall(path.node) &&
path.parent.type === 'VariableDeclarator' &&
path.parent.id.type === 'Identifier' &&
path.parentPath.parent.type === 'VariableDeclaration' &&
path.parentPath.parent.declarations.length === 1 &&
path.parentPath.parentPath.parent.type === 'Program'
);

return isValid ? path.parentPath : null;
}

function shouldInlineRequire(node, scope, inlineRequiredDependencyMap) {
return (
inlineRequiredDependencyMap.has(node.name) &&
!scope.hasBinding(node.name, true /* noGlobals */)
function requireMemberAlias(path) {
const isValid = (
isRequireCall(path.node) &&
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 isRequireCall(node) {
return (
!node.new &&
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
Expand Down
49 changes: 24 additions & 25 deletions packages/fbjs/src/__forks__/warning.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,33 @@ var emptyFunction = require('emptyFunction');
* same logic and follow the same code paths.
*/

var warning = emptyFunction;

if (__DEV__) {
function printWarning(format, ...args) {
var argIndex = 0;
var message = 'Warning: ' + format.replace(/%s/g, () => args[argIndex++]);
if (typeof console !== 'undefined') {
console.error(message);
}
try {
// --- Welcome to debugging React ---
// This error was thrown as a convenience so that you can use this stack
// to find the callsite that caused this warning to fire.
throw new Error(message);
} catch (x) {}
function printWarning(format, ...args) {
var argIndex = 0;
var message = 'Warning: ' + format.replace(/%s/g, () => args[argIndex++]);
if (typeof console !== 'undefined') {
console.error(message);
}
try {
// --- Welcome to debugging React ---
// This error was thrown as a convenience so that you can use this stack
// to find the callsite that caused this warning to fire.
throw new Error(message);
} catch (x) {}
}

warning = function(condition, format, ...args) {
if (format === undefined) {
throw new Error(
'`warning(condition, format, ...args)` requires a warning ' +
'message argument'
);
var warning = __DEV__
? function(condition, format, ...args) {
if (format === undefined) {
throw new Error(
'`warning(condition, format, ...args)` requires a warning ' +
'message argument'
);
}
if (!condition) {
printWarning(format, ...args);
}
}
if (!condition) {
printWarning(format, ...args);
}
};
}
: emptyFunction;

module.exports = warning;