react-native: BundleSegments

Reviewed By: davidaurelio

Differential Revision: D6210177

fbshipit-source-id: c39e4118389a9739e9e70ba34feb5d335a7f2546
This commit is contained in:
Jean Lauliac 2017-11-02 09:51:21 -07:00 committed by Facebook Github Bot
parent 1ec323bbd5
commit 3129532cd9
5 changed files with 108 additions and 44 deletions

View File

@ -36,13 +36,21 @@ export type TransformedCode = {
map?: ?CompactRawMappings, map?: ?CompactRawMappings,
}; };
export type Transform<ExtraOptions: {}> = ({| export type TransformArgs<ExtraOptions: {}> = {|
filename: string, filename: string,
localPath: string, localPath: string,
options: ExtraOptions & TransformOptions, options: ExtraOptions & TransformOptions,
plugins?: BabelPlugins, plugins?: BabelPlugins,
src: string, src: string,
|}) => {ast: ?Ast, code: string, map: ?MappingsMap | RawMappings}; |};
export type TransformResults = {
ast: ?Ast,
code: string,
map: ?MappingsMap | RawMappings,
};
export type Transform<ExtraOptions: {}> = (
TransformArgs<ExtraOptions>,
) => TransformResults;
export type Transformer<ExtraOptions: {} = {}> = { export type Transformer<ExtraOptions: {} = {}> = {
transform: Transform<ExtraOptions>, transform: Transform<ExtraOptions>,

View File

@ -13,15 +13,16 @@
'use strict'; 'use strict';
const babylon = require('babylon');
const collectDependencies = require('../collect-dependencies'); const collectDependencies = require('../collect-dependencies');
const astFromCode = require('babylon').parse;
const {codeFromAst, comparableCode} = require('../../test-helpers'); const {codeFromAst, comparableCode} = require('../../test-helpers');
const {any} = expect; const {any} = expect;
const {InvalidRequireCallError} = collectDependencies; const {InvalidRequireCallError} = collectDependencies;
describe('dependency collection from ASTs:', () => { describe('dependency collection from ASTs', () => {
it('collects dependency identifiers from the code', () => { it('collects dependency identifiers from the code', () => {
const ast = astFromCode(` const ast = astFromCode(`
const a = require('b/lib/a'); const a = require('b/lib/a');
@ -42,13 +43,14 @@ describe('dependency collection from ASTs:', () => {
const ast = astFromCode(` const ast = astFromCode(`
const a = require('b/lib/a'); const a = require('b/lib/a');
if (!something) { if (!something) {
require.async("some/async/module").then(foo => {}); import("some/async/module").then(foo => {});
} }
`); `);
expect(collectDependencies(ast).dependencies).toEqual([ expect(collectDependencies(ast).dependencies).toEqual([
'b/lib/a', 'b/lib/a',
'some/async/module', 'some/async/module',
'BundleSegments',
]); ]);
}); });
@ -107,7 +109,7 @@ describe('dependency collection from ASTs:', () => {
}); });
}); });
describe('Dependency collection from optimized ASTs:', () => { describe('Dependency collection from optimized ASTs', () => {
const dependencyMapName = 'arbitrary'; const dependencyMapName = 'arbitrary';
const {forOptimization} = collectDependencies; const {forOptimization} = collectDependencies;
let ast, names; let ast, names;
@ -116,12 +118,11 @@ describe('Dependency collection from optimized ASTs:', () => {
ast = astFromCode(` ast = astFromCode(`
const a = require(${dependencyMapName}[0], 'b/lib/a'); const a = require(${dependencyMapName}[0], 'b/lib/a');
exports.do = () => require(${dependencyMapName}[1], "do"); exports.do = () => require(${dependencyMapName}[1], "do");
require.async(${dependencyMapName}[2], 'some/async/module').then(foo => {});
if (!something) { if (!something) {
require(${dependencyMapName}[3], "setup/something"); require(${dependencyMapName}[2], "setup/something");
} }
`); `);
names = ['b/lib/a', 'do', 'some/async/module', 'setup/something']; names = ['b/lib/a', 'do', 'setup/something'];
}); });
it('passes the `dependencyMapName` through', () => { it('passes the `dependencyMapName` through', () => {
@ -146,11 +147,14 @@ describe('Dependency collection from optimized ASTs:', () => {
comparableCode(` comparableCode(`
const a = require(${dependencyMapName}[0]); const a = require(${dependencyMapName}[0]);
exports.do = () => require(${dependencyMapName}[1]); exports.do = () => require(${dependencyMapName}[1]);
require.async(${dependencyMapName}[2]).then(foo => {});
if (!something) { if (!something) {
require(${dependencyMapName}[3]); require(${dependencyMapName}[2]);
} }
`), `),
); );
}); });
}); });
function astFromCode(code) {
return babylon.parse(code, {plugins: ['dynamicImport']});
}

View File

@ -12,6 +12,7 @@
'use strict'; 'use strict';
const babelTemplate = require('babel-template');
const nullthrows = require('fbjs/lib/nullthrows'); const nullthrows = require('fbjs/lib/nullthrows');
const {traverse, types} = require('babel-core'); const {traverse, types} = require('babel-core');
@ -22,6 +23,7 @@ type AST = Object;
class Replacement { class Replacement {
nameToIndex: Map<string, number>; nameToIndex: Map<string, number>;
nextIndex: number; nextIndex: number;
replaceImports = true;
constructor() { constructor() {
this.nameToIndex = new Map(); this.nameToIndex = new Map();
@ -74,6 +76,7 @@ function getInvalidProdRequireMessage(node) {
class ProdReplacement { class ProdReplacement {
replacement: Replacement; replacement: Replacement;
names: Array<string>; names: Array<string>;
replaceImports = false;
constructor(names) { constructor(names) {
this.replacement = new Replacement(); this.replacement = new Replacement();
@ -129,19 +132,15 @@ function createMapLookup(dependencyMapIdentifier, propertyIdentifier) {
); );
} }
const makeAsyncRequire = babelTemplate(
`require(BUNDLE_SEGMENTS_PATH).loadForModule(MODULE_ID).then(
() => require(MODULE_PATH),
)`,
);
function collectDependencies(ast, replacement, dependencyMapIdentifier) { function collectDependencies(ast, replacement, dependencyMapIdentifier) {
const visited = new WeakSet(); const visited = new WeakSet();
const traversalState = {dependencyMapIdentifier}; const traversalState = {dependencyMapIdentifier};
function processRequireCall(node, state, isAsync) {
const arg = replacement.getRequireCallArg(node);
const index = replacement.getIndex(arg);
node.arguments = replacement.makeArgs(
types.numericLiteral(index),
arg,
state.dependencyMapIdentifier,
);
visited.add(node);
}
traverse( traverse(
ast, ast,
{ {
@ -152,16 +151,27 @@ function collectDependencies(ast, replacement, dependencyMapIdentifier) {
); );
} }
}, },
CallExpression(path, state) { CallExpression(path, state) {
const node = path.node; const node = path.node;
if (replacement.replaceImports && node.callee.type === 'Import') {
processImportCall(path, node, replacement, state);
return;
}
if (visited.has(node)) { if (visited.has(node)) {
return; return;
} }
if (isRequireCall(node.callee)) { if (!isRequireCall(node.callee)) {
processRequireCall(node, state, false); return;
} else if (isAsyncRequireCall(node.callee)) {
processRequireCall(node, state, true);
} }
const arg = replacement.getRequireCallArg(node);
const index = replacement.getIndex(arg);
node.arguments = replacement.makeArgs(
types.numericLiteral(index),
arg,
state.dependencyMapIdentifier,
);
visited.add(node);
}, },
}, },
null, null,
@ -174,6 +184,31 @@ function collectDependencies(ast, replacement, dependencyMapIdentifier) {
}; };
} }
function processImportCall(path, node, replacement, state) {
const args = node.arguments;
if (args.length !== 1 || !isLiteralString(args[0])) {
throw new InvalidRequireCallError(
'Calls to import() expect exactly 1 string literal argument, ' +
'but this was found: ' +
prettyPrint(node).code,
);
}
const modulePath = args[0];
const index = replacement.getIndex(modulePath);
const newImport = makeAsyncRequire({
MODULE_PATH: modulePath,
MODULE_ID: createMapLookup(
state.dependencyMapIdentifier,
types.numericLiteral(index),
),
BUNDLE_SEGMENTS_PATH: {
type: 'StringLiteral',
value: 'BundleSegments',
},
});
path.replaceWith(newImport);
}
function isLiteralString(node) { function isLiteralString(node) {
return ( return (
node.type === 'StringLiteral' || node.type === 'StringLiteral' ||
@ -185,15 +220,6 @@ function isRequireCall(callee) {
return callee.type === 'Identifier' && callee.name === 'require'; return callee.type === 'Identifier' && callee.name === 'require';
} }
function isAsyncRequireCall(callee) {
return (
callee.type === 'MemberExpression' &&
!callee.computed &&
callee.property.name === 'async' &&
isRequireCall(callee.object)
);
}
class InvalidRequireCallError extends Error { class InvalidRequireCallError extends Error {
constructor(message) { constructor(message) {
super(message); super(message);
@ -201,7 +227,7 @@ class InvalidRequireCallError extends Error {
} }
const xp = (module.exports = (ast: AST) => const xp = (module.exports = (ast: AST) =>
collectDependencies(ast, new Replacement())); collectDependencies(ast, new Replacement(), undefined));
xp.forOptimization = ( xp.forOptimization = (
ast: AST, ast: AST,

View File

@ -138,12 +138,20 @@ function guardedLoadModule(moduleId: ModuleID, module) {
const ID_MASK_SHIFT = 16; const ID_MASK_SHIFT = 16;
const LOCAL_ID_MASK = ~0 >>> ID_MASK_SHIFT; const LOCAL_ID_MASK = ~0 >>> ID_MASK_SHIFT;
function unpackModuleId(
moduleId: ModuleID,
): {segmentId: number, localId: number} {
const segmentId = moduleId >>> ID_MASK_SHIFT;
const localId = moduleId & LOCAL_ID_MASK;
return {segmentId, localId};
}
require.unpackModuleId = unpackModuleId;
function loadModuleImplementation(moduleId, module) { function loadModuleImplementation(moduleId, module) {
const nativeRequire = global.nativeRequire; const nativeRequire = global.nativeRequire;
if (!module && nativeRequire) { if (!module && nativeRequire) {
const bundleId = moduleId >>> ID_MASK_SHIFT; const {segmentId, localId} = unpackModuleId(moduleId);
const localId = moduleId & LOCAL_ID_MASK; nativeRequire(localId, segmentId);
nativeRequire(localId, bundleId);
module = modules[moduleId]; module = modules[moduleId];
} }

View File

@ -56,12 +56,21 @@ function guardedLoadModule(moduleId, module) {
var ID_MASK_SHIFT = 16; var ID_MASK_SHIFT = 16;
var LOCAL_ID_MASK = ~0 >>> ID_MASK_SHIFT; var LOCAL_ID_MASK = ~0 >>> ID_MASK_SHIFT;
function unpackModuleId(moduleId) {
var segmentId = moduleId >>> ID_MASK_SHIFT;
var localId = moduleId & LOCAL_ID_MASK;
return { segmentId: segmentId, localId: localId };
}
_require.unpackModuleId = unpackModuleId;
function loadModuleImplementation(moduleId, module) { function loadModuleImplementation(moduleId, module) {
var nativeRequire = global.nativeRequire; var nativeRequire = global.nativeRequire;
if (!module && nativeRequire) { if (!module && nativeRequire) {
var bundleId = moduleId >>> ID_MASK_SHIFT; var _unpackModuleId = unpackModuleId(moduleId),
var localId = moduleId & LOCAL_ID_MASK; segmentId = _unpackModuleId.segmentId,
nativeRequire(localId, bundleId); localId = _unpackModuleId.localId;
nativeRequire(localId, segmentId);
module = modules[moduleId]; module = modules[moduleId];
} }
@ -217,12 +226,21 @@ function guardedLoadModule(moduleId, module) {
var ID_MASK_SHIFT = 16; var ID_MASK_SHIFT = 16;
var LOCAL_ID_MASK = ~0 >>> ID_MASK_SHIFT; var LOCAL_ID_MASK = ~0 >>> ID_MASK_SHIFT;
function unpackModuleId(moduleId) {
var segmentId = moduleId >>> ID_MASK_SHIFT;
var localId = moduleId & LOCAL_ID_MASK;
return { segmentId: segmentId, localId: localId };
}
_require.unpackModuleId = unpackModuleId;
function loadModuleImplementation(moduleId, module) { function loadModuleImplementation(moduleId, module) {
var nativeRequire = global.nativeRequire; var nativeRequire = global.nativeRequire;
if (!module && nativeRequire) { if (!module && nativeRequire) {
var bundleId = moduleId >>> ID_MASK_SHIFT; var _unpackModuleId = unpackModuleId(moduleId),
var localId = moduleId & LOCAL_ID_MASK; segmentId = _unpackModuleId.segmentId,
nativeRequire(localId, bundleId); localId = _unpackModuleId.localId;
nativeRequire(localId, segmentId);
module = modules[moduleId]; module = modules[moduleId];
} }