From e7deea19001d903a47868bb1fe6456bdc4b585e7 Mon Sep 17 00:00:00 2001 From: Miguel Jimenez Esun Date: Mon, 22 Oct 2018 16:55:57 -0700 Subject: [PATCH] Transform _dependencyMap[n] into the corresponding integer to save space Summary: Uses Babel to transform references to `_dependencyMap[n]` into the corresponding integer, thus saving space when generating the bundles. Reviewed By: davidaurelio Differential Revision: D10488230 fbshipit-source-id: 072a2273e0d67a6051f7a5ab0b2da68c24e59df3 --- .../__tests__/indexed-ram-bundle-test.js | 16 ++--- .../multiple-files-ram-bundle-test.js | 10 ++-- .../ModuleGraph/output/__tests__/util-test.js | 55 +++++++++++++---- .../reverse-dependency-map-references.js | 59 +++++++++++++++++++ packages/metro/src/ModuleGraph/output/util.js | 56 ++++++++++++------ .../src/ModuleGraph/worker/mergeSourceMaps.js | 7 ++- 6 files changed, 161 insertions(+), 42 deletions(-) create mode 100644 packages/metro/src/ModuleGraph/output/reverse-dependency-map-references.js diff --git a/packages/metro/src/ModuleGraph/output/__tests__/indexed-ram-bundle-test.js b/packages/metro/src/ModuleGraph/output/__tests__/indexed-ram-bundle-test.js index 20947db7a4..fee1576176 100644 --- a/packages/metro/src/ModuleGraph/output/__tests__/indexed-ram-bundle-test.js +++ b/packages/metro/src/ModuleGraph/output/__tests__/indexed-ram-bundle-test.js @@ -89,7 +89,7 @@ it('creates a source map', () => { return section; }), ); - expect(map.x_facebook_offsets).toEqual([1, 2, 3, 4, 5, 6]); + expect(map.x_facebook_offsets).toEqual([1, 4, 7, 10, 13, 16]); }); describe('Startup section optimization', () => { @@ -136,7 +136,7 @@ describe('Startup section optimization', () => { countLines(requireCall), ); - expect(map.x_facebook_offsets).toEqual([4, 5, , , 6]); // eslint-disable-line no-sparse-arrays + expect(map.x_facebook_offsets).toEqual([10, 13, undefined, undefined, 16]); expect(map.sections.slice(1)).toEqual( modules.filter(not(Set.prototype.has), new Set(preloaded)).map(m => { @@ -177,9 +177,9 @@ describe('RAM groups / common sections', () => { }); it('reflects section groups in the source map', () => { - expect(map.x_facebook_offsets).toEqual([1, 2, 2, 5, 5, 2]); + expect(map.x_facebook_offsets).toEqual([1, 4, 4, 13, 13, 4]); const maps = map.sections.slice(-2); - const toplevelOffsets = [2, 5]; + const toplevelOffsets = [4, 13]; maps .map((groupMap, i) => [groups[i], groupMap]) @@ -240,14 +240,14 @@ function makeModule( function makeModuleMap(name, path) { return { version: 3, - mappings: Array(parseInt(name, 36) + 1).join(','), - names: [name], + mappings: '', + names: [], sources: [path], }; } function makeModuleCode(moduleCode) { - return `__d(() => {${moduleCode}})`; + return `__d(() => {\n${moduleCode}\n})`; } function makeModulePath(name) { @@ -310,7 +310,7 @@ function countLines(module) { function lineByLineMap(file) { return { file, - mappings: 'AAAA;', + mappings: '', names: [], sources: [file], version: 3, diff --git a/packages/metro/src/ModuleGraph/output/__tests__/multiple-files-ram-bundle-test.js b/packages/metro/src/ModuleGraph/output/__tests__/multiple-files-ram-bundle-test.js index 6cd4d394f7..608ab6cba5 100644 --- a/packages/metro/src/ModuleGraph/output/__tests__/multiple-files-ram-bundle-test.js +++ b/packages/metro/src/ModuleGraph/output/__tests__/multiple-files-ram-bundle-test.js @@ -67,7 +67,7 @@ it('creates a source map', () => { return section; }), ); - expect(map.x_facebook_offsets).toEqual([1, 2, 3, 4, 5, 6]); + expect(map.x_facebook_offsets).toEqual([1, 4, 7, 10, 13, 16]); }); it('creates a magic file with the number', () => { @@ -128,14 +128,14 @@ function makeModule( function makeModuleMap(name, path) { return { version: 3, - mappings: Array(parseInt(name, 36) + 1).join(','), - names: [name], + mappings: '', + names: [], sources: [path], }; } function makeModuleCode(moduleCode) { - return `__d(() => {${moduleCode}})`; + return `__d(() => {\n${moduleCode}\n})`; } function makeModulePath(name) { @@ -170,7 +170,7 @@ function countLines(module) { function lineByLineMap(file) { return { file, - mappings: 'AAAA;', + mappings: '', names: [], sources: [file], version: 3, diff --git a/packages/metro/src/ModuleGraph/output/__tests__/util-test.js b/packages/metro/src/ModuleGraph/output/__tests__/util-test.js index 7bc1301a40..683814909a 100644 --- a/packages/metro/src/ModuleGraph/output/__tests__/util-test.js +++ b/packages/metro/src/ModuleGraph/output/__tests__/util-test.js @@ -10,22 +10,44 @@ 'use strict'; -const {addModuleIdsToModuleWrapper, createIdForPathFn} = require('../util'); +const {inlineModuleIds, createIdForPathFn} = require('../util'); const {any} = jasmine; -describe('`addModuleIdsToModuleWrapper`:', () => { +describe('`inlineModuleIds`:', () => { const path = 'path/to/file'; + + const basicCode = ` + __d(function(require, depMap) { + require(depMap[0]); + require(depMap[1]); + }); + `; + const createModule = (dependencies = []) => ({ dependencies, - file: {code: '__d(function(){});', isModule: true, path}, + file: {code: basicCode, isModule: true, path}, }); - it('completes the module wrapped with module ID, and an array of dependency IDs', () => { + const reUsedVariableCode = ` + __d(function(require, depMap) { + function anotherScope(depMap) { + return depMap++; + } + }); + `; + + const createReUsedVariableModule = (dependencies = []) => ({ + dependencies, + file: {code: reUsedVariableCode, isModule: true, path}, + }); + + it('inlines module ids', () => { const dependencies = [ {id: 'a', path: 'path/to/a.js'}, {id: 'b', path: 'location/of/b.js'}, ]; + const module = createModule(dependencies); const idForPath = jest.fn().mockImplementation(({path: inputPath}) => { @@ -41,15 +63,28 @@ describe('`addModuleIdsToModuleWrapper`:', () => { throw new Error(`Unexpected path: ${inputPath}`); }); - expect(addModuleIdsToModuleWrapper(module, idForPath)).toEqual( - '__d(function(){},12,[345,6]);', + expect(inlineModuleIds(module, idForPath).moduleCode).toEqual( + [ + '__d(function (require, depMap) {', + ' require(345);', + '', + ' require(6);', + '},12);', + ].join('\n'), ); }); - it('omits the array of dependency IDs if it is empty', () => { - const module = createModule(); - expect(addModuleIdsToModuleWrapper(module, () => 98)).toEqual( - `__d(function(){},${98});`, + it('avoids inlining if the variable is in a different scope', () => { + const module = createReUsedVariableModule(); + + expect(inlineModuleIds(module, () => 98).moduleCode).toEqual( + [ + '__d(function (require, depMap) {', + ' function anotherScope(depMap) {', + ' return depMap++;', + ' }', + '},98);', + ].join('\n'), ); }); }); diff --git a/packages/metro/src/ModuleGraph/output/reverse-dependency-map-references.js b/packages/metro/src/ModuleGraph/output/reverse-dependency-map-references.js new file mode 100644 index 0000000000..18b9f76ae6 --- /dev/null +++ b/packages/metro/src/ModuleGraph/output/reverse-dependency-map-references.js @@ -0,0 +1,59 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import typeof {types as BabelTypes} from '@babel/core'; +import type {Path} from '@babel/traverse'; + +type State = {| + opts: {| + +dependencyIds: $ReadOnlyArray, + |}, +|}; + +function reverseDependencyMapReferences({types: t}: {types: BabelTypes}) { + return { + visitor: { + CallExpression(path: Path, state: State) { + const {node} = path; + + if (node.callee.name === '__d') { + const lastArg = node.arguments[0].params.slice(-1)[0]; + const depMapName = lastArg && lastArg.name; + + if (!depMapName) { + return; + } + + const scope = path.get('arguments.0.body').scope; + const binding = scope.getBinding(depMapName); + + binding.referencePaths.forEach(({parentPath}) => { + const memberNode = parentPath.node; + + if ( + memberNode.type === 'MemberExpression' && + memberNode.property.type === 'NumericLiteral' + ) { + parentPath.replaceWith( + t.numericLiteral( + state.opts.dependencyIds[memberNode.property.value], + ), + ); + } + }); + } + }, + }, + }; +} + +module.exports = reverseDependencyMapReferences; diff --git a/packages/metro/src/ModuleGraph/output/util.js b/packages/metro/src/ModuleGraph/output/util.js index ce057103c6..3b6fd95fd1 100644 --- a/packages/metro/src/ModuleGraph/output/util.js +++ b/packages/metro/src/ModuleGraph/output/util.js @@ -11,9 +11,15 @@ 'use strict'; const addParamsToDefineCall = require('../../lib/addParamsToDefineCall'); +const generate = require('../worker/generate'); +const mergeSourceMaps = require('../worker/mergeSourceMaps'); +const reverseDependencyMapReferences = require('./reverse-dependency-map-references'); const virtualModule = require('../module').virtual; +const {transformSync} = require('@babel/core'); + import type {IdsForPathFn, Module} from '../types.flow'; +import type {MetroSourceMap} from 'metro-source-map'; // Transformed modules have the form // __d(function(require, module, global, exports, dependencyMap) { @@ -22,28 +28,45 @@ import type {IdsForPathFn, Module} from '../types.flow'; // // This function adds the numeric module ID, and an array with dependencies of // the dependencies of the module before the closing parenthesis. -function addModuleIdsToModuleWrapper( +function inlineModuleIds( module: Module, idForPath: ({path: string}) => number, -): string { +): { + moduleCode: string, + moduleMap: ?MetroSourceMap, +} { const {dependencies, file} = module; - const {code} = file; + const {code, map, path} = file; // calling `idForPath` on the module itself first gives us a lower module id // for the file itself than for its dependencies. That reflects their order // in the bundle. const fileId = idForPath(file); + const dependencyIds = dependencies.map(idForPath); + + const {ast} = transformSync(code, { + ast: true, + babelrc: false, + code: false, + configFile: false, + minified: false, + plugins: [[reverseDependencyMapReferences, {dependencyIds}]], + }); + + const {code: generatedCode, map: generatedMap} = generate( + ast, + path, + '', + false, + ); - const paramsToAdd = [fileId]; - - if (dependencies.length) { - paramsToAdd.push(dependencies.map(idForPath)); - } - - return addParamsToDefineCall(code, ...paramsToAdd); + return { + moduleCode: addParamsToDefineCall(generatedCode, fileId), + moduleMap: map && generatedMap && mergeSourceMaps(path, map, generatedMap), + }; } -exports.addModuleIdsToModuleWrapper = addModuleIdsToModuleWrapper; +exports.inlineModuleIds = inlineModuleIds; type IdForPathFn = ({path: string}) => number; @@ -52,14 +75,11 @@ type IdForPathFn = ({path: string}) => number; function getModuleCodeAndMap(module: Module, idForPath: IdForPathFn) { const {file} = module; - const moduleCode = - file.type === 'module' - ? addModuleIdsToModuleWrapper(module, idForPath) - : file.code; - - const moduleMap = file.map; + if (file.type !== 'module') { + return {moduleCode: file.code, moduleMap: file.map}; + } - return {moduleCode, moduleMap}; + return inlineModuleIds(module, idForPath); } exports.getModuleCodeAndMap = getModuleCodeAndMap; diff --git a/packages/metro/src/ModuleGraph/worker/mergeSourceMaps.js b/packages/metro/src/ModuleGraph/worker/mergeSourceMaps.js index 3f48d46639..9992a7937e 100644 --- a/packages/metro/src/ModuleGraph/worker/mergeSourceMaps.js +++ b/packages/metro/src/ModuleGraph/worker/mergeSourceMaps.js @@ -25,6 +25,7 @@ function mergeSourceMaps( ): BabelSourceMap { const merged = new sourceMap.SourceMapGenerator(); const inputMap = new sourceMap.SourceMapConsumer(originalMap); + new sourceMap.SourceMapConsumer(secondMap).eachMapping(mapping => { const original = inputMap.originalPositionFor({ line: mapping.originalLine, @@ -41,7 +42,11 @@ function mergeSourceMaps( name: original.name || mapping.name, }); }); - return merged.toJSON(); + + return { + ...merged.toJSON(), + sources: inputMap.sources, + }; } module.exports = mergeSourceMaps;