From 41ec9e6d4d682a0eb41ee0e71b661f27a24f61d6 Mon Sep 17 00:00:00 2001 From: Jean Lauliac Date: Mon, 6 Nov 2017 03:30:10 -0800 Subject: [PATCH] react-native: BundleSegments Reviewed By: davidaurelio Differential Revision: D6231309 fbshipit-source-id: 565cbadedc5fd8ab25025b5846c098f24fb15a82 --- .../src/JSTransformer/worker/index.js | 12 +++- .../__tests__/collect-dependencies-test.js | 26 ++++--- .../worker/collect-dependencies.js | 72 +++++++++++++------ .../src/Resolver/polyfills/require.js | 14 +++- .../__snapshots__/basic_bundle-test.js.snap | 30 ++++++-- 5 files changed, 110 insertions(+), 44 deletions(-) diff --git a/packages/metro-bundler/src/JSTransformer/worker/index.js b/packages/metro-bundler/src/JSTransformer/worker/index.js index 505628aa..f995a7d8 100644 --- a/packages/metro-bundler/src/JSTransformer/worker/index.js +++ b/packages/metro-bundler/src/JSTransformer/worker/index.js @@ -36,13 +36,21 @@ export type TransformedCode = { map?: ?CompactRawMappings, }; -export type Transform = ({| +export type TransformArgs = {| filename: string, localPath: string, options: ExtraOptions & TransformOptions, plugins?: BabelPlugins, src: string, -|}) => {ast: ?Ast, code: string, map: ?MappingsMap | RawMappings}; +|}; +export type TransformResults = { + ast: ?Ast, + code: string, + map: ?MappingsMap | RawMappings, +}; +export type Transform = ( + TransformArgs, +) => TransformResults; export type Transformer = { transform: Transform, diff --git a/packages/metro-bundler/src/ModuleGraph/worker/__tests__/collect-dependencies-test.js b/packages/metro-bundler/src/ModuleGraph/worker/__tests__/collect-dependencies-test.js index b0bf1c50..74e66fa5 100644 --- a/packages/metro-bundler/src/ModuleGraph/worker/__tests__/collect-dependencies-test.js +++ b/packages/metro-bundler/src/ModuleGraph/worker/__tests__/collect-dependencies-test.js @@ -13,15 +13,16 @@ 'use strict'; +const babylon = require('babylon'); const collectDependencies = require('../collect-dependencies'); -const astFromCode = require('babylon').parse; + const {codeFromAst, comparableCode} = require('../../test-helpers'); const {any} = expect; const {InvalidRequireCallError} = collectDependencies; -describe('dependency collection from ASTs:', () => { +describe('dependency collection from ASTs', () => { it('collects dependency identifiers from the code', () => { const ast = astFromCode(` const a = require('b/lib/a'); @@ -42,13 +43,14 @@ describe('dependency collection from ASTs:', () => { const ast = astFromCode(` const a = require('b/lib/a'); if (!something) { - require.async("some/async/module").then(foo => {}); + import("some/async/module").then(foo => {}); } `); expect(collectDependencies(ast).dependencies).toEqual([ 'b/lib/a', 'some/async/module', + 'BundleSegments', ]); }); @@ -88,6 +90,7 @@ describe('dependency collection from ASTs:', () => { const ast = astFromCode(` const a = require('b/lib/a'); exports.do = () => require("do"); + import("some/async/module").then(foo => {}); if (!something) { require("setup/something"); } @@ -99,15 +102,16 @@ describe('dependency collection from ASTs:', () => { comparableCode(` const a = require(${dependencyMapName}[0], 'b/lib/a'); exports.do = () => require(${dependencyMapName}[1], "do"); + require(_dependencyMap[3], "BundleSegments").loadForModule(_dependencyMap[2]).then(function () { return require(_dependencyMap[2], "some/async/module"); }).then(foo => {}); if (!something) { - require(${dependencyMapName}[2], "setup/something"); + require(${dependencyMapName}[4], "setup/something"); } `), ); }); }); -describe('Dependency collection from optimized ASTs:', () => { +describe('Dependency collection from optimized ASTs', () => { const dependencyMapName = 'arbitrary'; const {forOptimization} = collectDependencies; let ast, names; @@ -116,12 +120,11 @@ describe('Dependency collection from optimized ASTs:', () => { ast = astFromCode(` const a = require(${dependencyMapName}[0], 'b/lib/a'); exports.do = () => require(${dependencyMapName}[1], "do"); - require.async(${dependencyMapName}[2], 'some/async/module').then(foo => {}); 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', () => { @@ -146,11 +149,14 @@ describe('Dependency collection from optimized ASTs:', () => { comparableCode(` const a = require(${dependencyMapName}[0]); exports.do = () => require(${dependencyMapName}[1]); - require.async(${dependencyMapName}[2]).then(foo => {}); if (!something) { - require(${dependencyMapName}[3]); + require(${dependencyMapName}[2]); } `), ); }); }); + +function astFromCode(code) { + return babylon.parse(code, {plugins: ['dynamicImport']}); +} diff --git a/packages/metro-bundler/src/ModuleGraph/worker/collect-dependencies.js b/packages/metro-bundler/src/ModuleGraph/worker/collect-dependencies.js index 30bd5796..730882d7 100644 --- a/packages/metro-bundler/src/ModuleGraph/worker/collect-dependencies.js +++ b/packages/metro-bundler/src/ModuleGraph/worker/collect-dependencies.js @@ -12,6 +12,7 @@ 'use strict'; +const babelTemplate = require('babel-template'); const nullthrows = require('fbjs/lib/nullthrows'); const {traverse, types} = require('babel-core'); @@ -20,6 +21,7 @@ const prettyPrint = require('babel-generator').default; class Replacement { nameToIndex: Map; nextIndex: number; + replaceImports = true; constructor() { this.nameToIndex = new Map(); @@ -72,6 +74,7 @@ function getInvalidProdRequireMessage(node) { class ProdReplacement { replacement: Replacement; names: Array; + replaceImports = false; constructor(names) { this.replacement = new Replacement(); @@ -130,16 +133,6 @@ function createMapLookup(dependencyMapIdentifier, propertyIdentifier) { function collectDependencies(ast, replacement, dependencyMapIdentifier) { const visited = new WeakSet(); 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( ast, { @@ -150,16 +143,27 @@ function collectDependencies(ast, replacement, dependencyMapIdentifier) { ); } }, + CallExpression(path, state) { const node = path.node; + if (replacement.replaceImports && node.callee.type === 'Import') { + processImportCall(path, node, replacement, state); + return; + } if (visited.has(node)) { return; } - if (isRequireCall(node.callee)) { - processRequireCall(node, state, false); - } else if (isAsyncRequireCall(node.callee)) { - processRequireCall(node, state, true); + if (!isRequireCall(node.callee)) { + return; } + const arg = replacement.getRequireCallArg(node); + const index = replacement.getIndex(arg); + node.arguments = replacement.makeArgs( + types.numericLiteral(index), + arg, + state.dependencyMapIdentifier, + ); + visited.add(node); }, }, null, @@ -172,6 +176,37 @@ function collectDependencies(ast, replacement, dependencyMapIdentifier) { }; } +const makeAsyncRequire = babelTemplate( + `require(BUNDLE_SEGMENTS_PATH).loadForModule(MODULE_ID).then( + function() { return require(MODULE_PATH); } + )`, +); + +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) { return ( node.type === 'StringLiteral' || @@ -183,15 +218,6 @@ function isRequireCall(callee) { 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 { constructor(message) { super(message); diff --git a/packages/metro-bundler/src/Resolver/polyfills/require.js b/packages/metro-bundler/src/Resolver/polyfills/require.js index b3b8ca1d..b6b2e8b1 100644 --- a/packages/metro-bundler/src/Resolver/polyfills/require.js +++ b/packages/metro-bundler/src/Resolver/polyfills/require.js @@ -138,12 +138,20 @@ function guardedLoadModule(moduleId: ModuleID, module) { const ID_MASK_SHIFT = 16; 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) { const nativeRequire = global.nativeRequire; if (!module && nativeRequire) { - const bundleId = moduleId >>> ID_MASK_SHIFT; - const localId = moduleId & LOCAL_ID_MASK; - nativeRequire(localId, bundleId); + const {segmentId, localId} = unpackModuleId(moduleId); + nativeRequire(localId, segmentId); module = modules[moduleId]; } diff --git a/packages/metro-bundler/src/integration_tests/__tests__/__snapshots__/basic_bundle-test.js.snap b/packages/metro-bundler/src/integration_tests/__tests__/__snapshots__/basic_bundle-test.js.snap index 82d46f26..9eadbbb5 100644 --- a/packages/metro-bundler/src/integration_tests/__tests__/__snapshots__/basic_bundle-test.js.snap +++ b/packages/metro-bundler/src/integration_tests/__tests__/__snapshots__/basic_bundle-test.js.snap @@ -56,12 +56,21 @@ function guardedLoadModule(moduleId, module) { var ID_MASK_SHIFT = 16; 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) { var nativeRequire = global.nativeRequire; if (!module && nativeRequire) { - var bundleId = moduleId >>> ID_MASK_SHIFT; - var localId = moduleId & LOCAL_ID_MASK; - nativeRequire(localId, bundleId); + var _unpackModuleId = unpackModuleId(moduleId), + segmentId = _unpackModuleId.segmentId, + localId = _unpackModuleId.localId; + + nativeRequire(localId, segmentId); module = modules[moduleId]; } @@ -217,12 +226,21 @@ function guardedLoadModule(moduleId, module) { var ID_MASK_SHIFT = 16; 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) { var nativeRequire = global.nativeRequire; if (!module && nativeRequire) { - var bundleId = moduleId >>> ID_MASK_SHIFT; - var localId = moduleId & LOCAL_ID_MASK; - nativeRequire(localId, bundleId); + var _unpackModuleId = unpackModuleId(moduleId), + segmentId = _unpackModuleId.segmentId, + localId = _unpackModuleId.localId; + + nativeRequire(localId, segmentId); module = modules[moduleId]; }