diff --git a/packages/metro/src/DeltaBundler/DeltaTransformer.js b/packages/metro/src/DeltaBundler/DeltaTransformer.js index 2627ab68..e796c38d 100644 --- a/packages/metro/src/DeltaBundler/DeltaTransformer.js +++ b/packages/metro/src/DeltaBundler/DeltaTransformer.js @@ -12,13 +12,12 @@ const DeltaCalculator = require('./DeltaCalculator'); -const addParamsToDefineCall = require('../lib/addParamsToDefineCall'); const createModuleIdFactory = require('../lib/createModuleIdFactory'); const crypto = require('crypto'); const defaults = require('../defaults'); const getPreludeCode = require('../lib/getPreludeCode'); -const nullthrows = require('fbjs/lib/nullthrows'); +const {wrapModule} = require('./Serializers/helpers/js'); const {EventEmitter} = require('events'); import type Bundler from '../Bundler'; @@ -444,27 +443,10 @@ class DeltaTransformer extends EventEmitter { ): Promise<[number, ?DeltaEntry]> { const name = this._dependencyGraph.getHasteName(edge.path); - const dependencyPairs = edge ? edge.dependencies : new Map(); - - let wrappedCode; - - // Get the absolute path of each of the module dependencies from the - // dependency edges. The module dependencies ensure correct order, while - // the dependency edges do not ensure the same order between rebuilds. - const dependencies = Array.from(edge.dependencies.keys()).map(dependency => - nullthrows(dependencyPairs.get(dependency)), - ); - - if (edge.output.type !== 'script') { - wrappedCode = this._addDependencyMap({ - code: edge.output.code, - dependencies, - name, - path: edge.path, - }); - } else { - wrappedCode = edge.output.code; - } + const wrappedCode = wrapModule(edge, { + createModuleIdFn: this._getModuleId, + dev: transformOptions.dev, + }); const {code, map} = transformOptions.minify ? await this._bundler.minifyModule( @@ -490,35 +472,6 @@ class DeltaTransformer extends EventEmitter { ]; } - /** - * Function to add the mapping object between local module ids and - * actual bundle module ids for dependencies. This way, we can do the path - * replacements on require() calls on transformers (since local ids do not - * change between bundles). - */ - _addDependencyMap({ - code, - dependencies, - name, - path, - }: { - code: string, - dependencies: $ReadOnlyArray, - name: string, - path: string, - }): string { - const moduleId = this._getModuleId(path); - const params = [moduleId, dependencies.map(this._getModuleId)]; - - // Add the module name as the last parameter (to make it easier to do - // requires by name when debugging). - if (this._bundleOptions.dev) { - params.push(name); - } - - return addParamsToDefineCall(code, ...params); - } - _onFileChange = () => { this.emit('change'); }; diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/plainJSBundle-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/plainJSBundle-test.js new file mode 100644 index 00000000..7088e9d2 --- /dev/null +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/plainJSBundle-test.js @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails oncall+javascript_foundation + * @format + */ + +'use strict'; + +const createModuleIdFactory = require('../../../lib/createModuleIdFactory'); +const plainJSBundle = require('../plainJSBundle'); + +const polyfill = { + output: { + type: 'script', + code: '__d(function() {/* code for polyfill */});', + }, +}; + +const fooModule = { + path: 'foo', + dependencies: new Map([['./bar', 'bar']]), + output: {code: '__d(function() {/* code for foo */});'}, +}; + +const barModule = { + path: 'bar', + dependencies: new Map(), + output: {code: '__d(function() {/* code for bar */});'}, +}; + +it('should serialize a very simple bundle', () => { + expect( + plainJSBundle( + 'foo', + [polyfill], + { + dependencies: new Map([['foo', fooModule], ['bar', barModule]]), + entryPoints: ['foo'], + }, + { + createModuleIdFn: path => path, + dev: true, + runBeforeMainModule: [], + runModule: true, + sourceMapUrl: 'http://localhost/bundle.map', + }, + ), + ).toEqual( + [ + '__d(function() {/* code for polyfill */});', + '__d(function() {/* code for foo */},"foo",["bar"],"foo");', + '__d(function() {/* code for bar */},"bar",[],"bar");', + 'require("foo");', + '//# sourceMappingURL=http://localhost/bundle.map', + ].join('\n'), + ); +}); + +it('should add runBeforeMainModule statements if found in the graph', () => { + expect( + plainJSBundle( + 'foo', + [polyfill], + { + dependencies: new Map([['foo', fooModule], ['bar', barModule]]), + entryPoints: ['foo'], + }, + { + createModuleIdFn: path => path, + dev: true, + runBeforeMainModule: ['bar', 'non-existant'], + runModule: true, + sourceMapUrl: 'http://localhost/bundle.map', + }, + ), + ).toEqual( + [ + '__d(function() {/* code for polyfill */});', + '__d(function() {/* code for foo */},"foo",["bar"],"foo");', + '__d(function() {/* code for bar */},"bar",[],"bar");', + 'require("bar");', + 'require("foo");', + '//# sourceMappingURL=http://localhost/bundle.map', + ].join('\n'), + ); +}); + +it('should handle numeric module ids', () => { + expect( + plainJSBundle( + 'foo', + [polyfill], + { + dependencies: new Map([['foo', fooModule], ['bar', barModule]]), + entryPoints: ['foo'], + }, + { + createModuleIdFn: createModuleIdFactory(), + dev: true, + runBeforeMainModule: ['bar', 'non-existant'], + runModule: true, + sourceMapUrl: 'http://localhost/bundle.map', + }, + ), + ).toEqual( + [ + '__d(function() {/* code for polyfill */});', + '__d(function() {/* code for foo */},0,[1],"foo");', + '__d(function() {/* code for bar */},1,[],"bar");', + 'require(1);', + 'require(0);', + '//# sourceMappingURL=http://localhost/bundle.map', + ].join('\n'), + ); +}); diff --git a/packages/metro/src/DeltaBundler/Serializers/helpers/__tests__/js-test.js b/packages/metro/src/DeltaBundler/Serializers/helpers/__tests__/js-test.js new file mode 100644 index 00000000..8fdfce65 --- /dev/null +++ b/packages/metro/src/DeltaBundler/Serializers/helpers/__tests__/js-test.js @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails oncall+javascript_foundation + * @flow + * @format + */ + +'use strict'; + +const createModuleIdFactory = require('../../../../lib/createModuleIdFactory'); + +const {wrapModule} = require('../js'); + +let myModule; + +beforeEach(() => { + myModule = { + path: '/root/foo.js', + dependencies: new Map([['bar', '/bar'], ['baz', '/baz']]), + inverseDependencies: new Set(), + output: { + code: '__d(function() { console.log("foo") });', + map: [], + source: '', + type: 'module', + }, + }; +}); + +describe('wrapModule()', () => { + it('Should wrap a module in nondev mode', () => { + expect( + wrapModule(myModule, { + createModuleIdFn: createModuleIdFactory(), + dev: false, + }), + ).toEqual('__d(function() { console.log("foo") },0,[1,2]);'); + }); + + it('Should wrap a module in dev mode', () => { + expect( + wrapModule(myModule, { + createModuleIdFn: createModuleIdFactory(), + dev: true, + }), + ).toEqual('__d(function() { console.log("foo") },0,[1,2],"foo.js");'); + }); + + it('should not wrap a script', () => { + myModule.output.type = 'script'; + + expect( + wrapModule(myModule, { + createModuleIdFn: createModuleIdFactory(), + dev: true, + }), + ).toEqual(myModule.output.code); + }); + + it('should use custom createModuleIdFn param', () => { + // Just use a createModuleIdFn that returns the same path. + expect( + wrapModule(myModule, { + createModuleIdFn: path => path, + dev: false, + }), + ).toEqual( + '__d(function() { console.log("foo") },"/root/foo.js",["/bar","/baz"]);', + ); + }); +}); diff --git a/packages/metro/src/DeltaBundler/Serializers/helpers/js.js b/packages/metro/src/DeltaBundler/Serializers/helpers/js.js new file mode 100644 index 00000000..067602e9 --- /dev/null +++ b/packages/metro/src/DeltaBundler/Serializers/helpers/js.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const addParamsToDefineCall = require('../../../lib/addParamsToDefineCall'); +const path = require('path'); + +import type {DependencyEdge} from '../../traverseDependencies'; + +export type Options = { + +createModuleIdFn: string => number | string, + +dev: boolean, +}; + +function wrapModule(module: DependencyEdge, options: Options) { + if (module.output.type === 'script') { + return module.output.code; + } + + const moduleId = options.createModuleIdFn(module.path); + const params = [ + moduleId, + Array.from(module.dependencies.values()).map(options.createModuleIdFn), + ]; + + // Add the module name as the last parameter (to make it easier to do + // requires by name when debugging). + // TODO (t26853986): Switch this to use the relative file path (once we have + // as single project root). + if (options.dev) { + params.push(path.basename(module.path)); + } + + return addParamsToDefineCall(module.output.code, ...params); +} + +module.exports = { + wrapModule, +}; diff --git a/packages/metro/src/DeltaBundler/Serializers/plainJSBundle.js b/packages/metro/src/DeltaBundler/Serializers/plainJSBundle.js new file mode 100644 index 00000000..a76aa425 --- /dev/null +++ b/packages/metro/src/DeltaBundler/Serializers/plainJSBundle.js @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const {wrapModule} = require('./helpers/js'); + +import type {Graph} from '../DeltaCalculator'; +import type {DependencyEdge} from '../traverseDependencies'; + +type Options = {| + createModuleIdFn: string => number | string, + +dev: boolean, + +runBeforeMainModule: $ReadOnlyArray, + +runModule: boolean, + +sourceMapUrl: ?string, +|}; + +function plainJSBundle( + entryPoint: string, + pre: $ReadOnlyArray, + graph: Graph, + options: Options, +): string { + const output = []; + + for (const module of pre) { + output.push(wrapModule(module, options)); + } + + for (const module of graph.dependencies.values()) { + output.push(wrapModule(module, options)); + } + + for (const path of options.runBeforeMainModule) { + if (graph.dependencies.has(path)) { + output.push( + `require(${JSON.stringify(options.createModuleIdFn(path))});`, + ); + } + } + + if (options.runModule && graph.dependencies.has(entryPoint)) { + output.push( + `require(${JSON.stringify(options.createModuleIdFn(entryPoint))});`, + ); + } + + if (options.sourceMapUrl) { + output.push(`//# sourceMappingURL=${options.sourceMapUrl}`); + } + + return output.join('\n'); +} + +module.exports = plainJSBundle; diff --git a/packages/metro/src/DeltaBundler/Serializers/sourceMapString.js b/packages/metro/src/DeltaBundler/Serializers/sourceMapString.js new file mode 100644 index 00000000..987a11ef --- /dev/null +++ b/packages/metro/src/DeltaBundler/Serializers/sourceMapString.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const {fromRawMappings} = require('metro-source-map'); + +import type {Graph} from '../DeltaCalculator'; +import type {DependencyEdge} from '../traverseDependencies'; + +function fullSourceMap( + pre: $ReadOnlyArray, + graph: Graph, + options: {|+excludeSource: boolean|}, +): string { + const modules = pre.concat(...graph.dependencies.values()); + + const modulesWithMaps = modules.map(module => { + return { + ...module.output, + path: module.path, + }; + }); + + return fromRawMappings(modulesWithMaps).toString(undefined, { + excludeSource: options.excludeSource, + }); +} + +module.exports = fullSourceMap;