Use the new Graph object for generating dev bundle/sourcemaps

Reviewed By: mjesun

Differential Revision: D7275599

fbshipit-source-id: 4889d259005b3df19977925a6729805b9df68113
This commit is contained in:
Rafael Oleza 2018-03-20 06:53:19 -07:00 committed by Facebook Github Bot
parent 12fe345e1b
commit b9b541542b
8 changed files with 331 additions and 230 deletions

View File

@ -71,17 +71,6 @@ async function deltaBundle(
};
}
async function fullSourceMap(
deltaBundler: DeltaBundler,
options: BundleOptions,
): Promise<string> {
const {modules} = await _getAllModules(deltaBundler, options);
return fromRawMappings(modules).toString(undefined, {
excludeSource: options.excludeSource,
});
}
async function fullSourceMapObject(
deltaBundler: DeltaBundler,
options: BundleOptions,
@ -93,27 +82,6 @@ async function fullSourceMapObject(
});
}
/**
* Returns the full JS bundle, which can be directly parsed by a JS interpreter
*/
async function fullBundle(
deltaBundler: DeltaBundler,
options: BundleOptions,
): Promise<{bundle: string, numModifiedFiles: number, lastModified: Date}> {
const {modules, numModifiedFiles, lastModified} = await _getAllModules(
deltaBundler,
options,
);
const code = modules.map(m => m.code);
return {
bundle: code.join('\n'),
lastModified,
numModifiedFiles,
};
}
async function _getAllModules(
deltaBundler: DeltaBundler,
options: BundleOptions,
@ -252,8 +220,6 @@ async function _build(
module.exports = {
deltaBundle,
fullBundle,
fullSourceMap,
fullSourceMapObject,
getRamBundleInfo,
};

View File

@ -86,61 +86,6 @@ describe('Serializers', () => {
).toMatchSnapshot();
});
it('should build the full JS bundle', async () => {
expect(
await Serializers.fullBundle(deltaBundler, {
sourceMapUrl: 'http://localhost:8081/myBundle.js',
}),
).toMatchSnapshot();
getDelta.mockReturnValueOnce(
Promise.resolve({
id: '1234',
delta: new Map([[3, {code: 'modified module;'}], [4, null]]),
pre: new Map([[5, {code: 'more pre;'}]]),
post: new Map([[6, {code: 'bananas;'}], [7, {code: 'apples;'}]]),
inverseDependencies: [],
}),
);
setCurrentTime(CURRENT_TIME + 5000);
expect(
await Serializers.fullBundle(deltaBundler, {
sourceMapUrl: 'http://localhost:8081/myBundle.js',
}),
).toMatchSnapshot();
});
it('should pass the sourcemapURL param to the transformer', async () => {
await Serializers.fullBundle(deltaBundler, {
sourceMapUrl: 'http://localhost:8081/myBundle.js',
});
expect(deltaBundler.getDeltaTransformer.mock.calls[0][1]).toEqual({
deltaBundleId: '1234',
sourceMapUrl: 'http://localhost:8081/myBundle.js',
});
});
// This test actually does not test the sourcemaps generation logic, which
// is already tested in the source-map file.
it('should build the full Source Maps', async () => {
expect(await Serializers.fullSourceMap(deltaBundler, {})).toMatchSnapshot();
getDelta.mockReturnValueOnce(
Promise.resolve({
id: '1234',
delta: new Map([[3, {code: 'modified module;'}], [4, null]]),
pre: new Map([[5, {code: 'more pre;'}]]),
post: new Map([[6, {code: 'bananas;'}], [7, {code: 'apples;'}]]),
inverseDependencies: [],
}),
);
setCurrentTime(CURRENT_TIME + 5000);
expect(await Serializers.fullSourceMap(deltaBundler, {})).toMatchSnapshot();
});
it('should return the RAM bundle info', async () => {
expect(
await Serializers.getRamBundleInfo(deltaBundler, {}),

View File

@ -1,33 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Serializers should build the full JS bundle 1`] = `
Object {
"bundle": "pre;
module3;
another;
post;",
"lastModified": 2016-12-21T23:36:07.000Z,
"numModifiedFiles": 4,
}
`;
exports[`Serializers should build the full JS bundle 2`] = `
Object {
"bundle": "pre;
more pre;
modified module;
post;
bananas;
apples;",
"lastModified": 2016-12-21T23:36:12.000Z,
"numModifiedFiles": 5,
}
`;
exports[`Serializers should build the full Source Maps 1`] = `"{\\"version\\":3,\\"sources\\":[],\\"sourcesContent\\":[],\\"names\\":[],\\"mappings\\":\\"\\"}"`;
exports[`Serializers should build the full Source Maps 2`] = `"{\\"version\\":3,\\"sources\\":[],\\"sourcesContent\\":[],\\"names\\":[],\\"mappings\\":\\"\\"}"`;
exports[`Serializers should return the RAM bundle info 1`] = `
Object {
"getDependencies": [Function],

View File

@ -0,0 +1,64 @@
/**
* 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 sourceMapString = require('../sourceMapString');
const polyfill = {
path: '/root/pre.js',
output: {
type: 'script',
code: '__d(function() {/* code for polyfill */});',
map: [],
source: 'source pre',
},
};
const fooModule = {
path: '/root/foo.js',
dependencies: new Map([['./bar', 'bar']]),
output: {
code: '__d(function() {/* code for foo */});',
map: [],
source: 'source foo',
},
};
const barModule = {
path: '/root/bar.js',
dependencies: new Map(),
output: {
code: '__d(function() {/* code for bar */});',
map: [],
source: 'source bar',
},
};
it('should serialize a very simple bundle', () => {
expect(
JSON.parse(
sourceMapString(
[polyfill],
{
dependencies: new Map([['foo', fooModule], ['bar', barModule]]),
entryPoints: ['foo'],
},
{excludesSource: false},
),
),
).toEqual({
version: 3,
sources: ['/root/pre.js', '/root/foo.js', '/root/bar.js'],
sourcesContent: ['source pre', 'source foo', 'source bar'],
names: [],
mappings: '',
});
});

View File

@ -20,16 +20,14 @@ function fullSourceMap(
graph: Graph,
options: {|+excludeSource: boolean|},
): string {
const modules = pre.concat(...graph.dependencies.values());
const modulesWithMaps = modules.map(module => {
const modules = [...pre, ...graph.dependencies.values()].map(module => {
return {
...module.output,
path: module.path,
};
});
return fromRawMappings(modulesWithMaps).toString(undefined, {
return fromRawMappings(modules).toString(undefined, {
excludeSource: options.excludeSource,
});
}

View File

@ -18,6 +18,7 @@ jest
createWorker: jest.fn().mockReturnValue(jest.fn()),
}))
.mock('../../Bundler')
.mock('../../DeltaBundler')
.mock('../../Assets')
.mock('../../lib/getPrependedScripts')
.mock('../../node-haste/DependencyGraph')
@ -34,7 +35,8 @@ describe('processRequest', () => {
let symbolicate;
let Serializers;
let DeltaBundler;
const lastModified = new Date();
const NativeDate = global.Date;
beforeEach(() => {
jest.useFakeTimers();
@ -84,42 +86,58 @@ describe('processRequest', () => {
),
);
const invalidatorFunc = jest.fn();
let requestHandler;
beforeEach(() => {
DeltaBundler.prototype.buildGraph = jest.fn().mockReturnValue(
global.Date = NativeDate;
DeltaBundler.prototype.buildGraph.mockReturnValue(
Promise.resolve({
entryPoints: [''],
dependencies: new Map(),
entryPoints: ['/root/mybundle.js'],
dependencies: new Map([
[
'/root/mybundle.js',
{
path: '/root/mybundle.js',
dependencies: new Map([['foo', '/root/foo.js']]),
output: {
code: '__d(function() {entry();});',
map: [],
source: 'code-mybundle',
},
},
],
[
'/root/foo.js',
{
path: '/root/foo.js',
dependencies: new Map(),
output: {
code: '__d(function() {foo();});',
map: [],
source: 'code-foo',
},
},
],
]),
}),
);
getPrependedScripts.mockReturnValue(Promise.resolve([]));
Serializers.fullBundle.mockReturnValue(
Promise.resolve({
bundle: 'this is the source',
numModifiedFiles: 38,
lastModified,
}),
getPrependedScripts.mockReturnValue(
Promise.resolve([
{
path: 'require-js',
dependencies: new Map(),
output: {
code: 'function () {require();}',
map: [],
type: 'script',
source: 'code-require',
},
},
]),
);
Serializers.fullSourceMap.mockReturnValue(
Promise.resolve('this is the source map'),
);
Bundler.prototype.bundle = jest.fn(() =>
Promise.resolve({
getModules: () => [],
getSource: () => 'this is the source',
getSourceMap: () => ({version: 3}),
getSourceMapString: () => 'this is the source map',
getEtag: () => 'this is an etag',
}),
);
Bundler.prototype.invalidateFile = invalidatorFunc;
Bundler.prototype.getDependencyGraph = jest.fn().mockReturnValue(
Promise.resolve({
getHasteMap: jest.fn().mockReturnValue({on: jest.fn()}),
@ -131,17 +149,38 @@ describe('processRequest', () => {
requestHandler = server.processRequest.bind(server);
});
it('returns JS bundle source on request of *.bundle', () => {
return makeRequest(
it('returns JS bundle source on request of *.bundle', async () => {
const response = await makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
null,
).then(response => expect(response.body).toEqual('this is the source'));
);
expect(response.body).toEqual(
[
'function () {require();}',
'__d(function() {entry();},0,[1],"mybundle.js");',
'__d(function() {foo();},1,[],"foo.js");',
'require(0);',
'//# sourceMappingURL=http://localhost:8081/mybundle.map?runModule=true',
].join('\n'),
);
});
it('returns JS bundle source on request of *.bundle (compat)', () => {
return makeRequest(requestHandler, 'mybundle.runModule.bundle').then(
response => expect(response.body).toEqual('this is the source'),
it('returns JS bundle without the initial require() call', async () => {
const response = await makeRequest(
requestHandler,
'mybundle.bundle?runModule=false',
null,
);
expect(response.body).toEqual(
[
'function () {require();}',
'__d(function() {entry();},0,[1],"mybundle.js");',
'__d(function() {foo();},1,[],"foo.js");',
'//# sourceMappingURL=http://localhost:8081/mybundle.map?runModule=false',
].join('\n'),
);
});
@ -153,12 +192,13 @@ describe('processRequest', () => {
);
});
it('returns build info headers on request of *.bundle', () => {
return makeRequest(requestHandler, 'mybundle.bundle?runModule=true').then(
response => {
expect(response.getHeader('X-Metro-Files-Changed-Count')).toEqual('38');
},
it('returns build info headers on request of *.bundle', async () => {
const response = await makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
);
expect(response.getHeader('X-Metro-Files-Changed-Count')).toEqual('3');
});
it('returns Content-Length header on request of *.bundle', () => {
@ -171,47 +211,133 @@ describe('processRequest', () => {
);
});
it('returns 304 on request of *.bundle when if-modified-since equals Last-Modified', () => {
it('returns 304 on request of *.bundle when if-modified-since equals Last-Modified', async () => {
const response = await makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
);
const lastModified = response.headers['Last-Modified'];
DeltaBundler.prototype.getDelta.mockReturnValue(
Promise.resolve({
modified: new Map(),
deleted: new Set(),
reset: false,
}),
);
global.Date = class {
constructor() {
return new NativeDate('2017-07-07T00:10:20.000Z');
}
now() {
return NativeDate.now();
}
};
return makeRequest(requestHandler, 'mybundle.bundle?runModule=true', {
headers: {'if-modified-since': lastModified.toUTCString()},
headers: {'if-modified-since': lastModified},
}).then(response => {
expect(response.statusCode).toEqual(304);
});
});
it('returns sourcemap on request of *.map', () => {
return makeRequest(requestHandler, 'mybundle.map?runModule=true').then(
response => expect(response.body).toEqual('this is the source map'),
it('returns 200 on request of *.bundle when something changes (ignoring if-modified-since headers)', async () => {
const response = await makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
);
const lastModified = response.headers['Last-Modified'];
DeltaBundler.prototype.getDelta.mockReturnValue(
Promise.resolve({
modified: new Map([
[0, '__d(function() {entry();},0,[1],"mybundle.js");'],
]),
deleted: new Set(),
reset: false,
}),
);
global.Date = class {
constructor() {
return new NativeDate('2017-07-07T00:10:20.000Z');
}
now() {
return NativeDate.now();
}
};
return makeRequest(requestHandler, 'mybundle.bundle?runModule=true', {
headers: {'if-modified-since': lastModified},
}).then(response => {
expect(response.statusCode).toEqual(200);
expect(response.getHeader('X-Metro-Files-Changed-Count')).toEqual('1');
});
});
it('returns sourcemap on request of *.map', async () => {
const response = await makeRequest(requestHandler, 'mybundle.map');
expect(JSON.parse(response.body)).toEqual({
version: 3,
sources: ['require-js', '/root/mybundle.js', '/root/foo.js'],
sourcesContent: ['code-require', 'code-mybundle', 'code-foo'],
names: [],
mappings: '',
});
});
it('does not rebuild the graph when requesting the sourcemaps after having requested the same bundle', async () => {
expect(
(await makeRequest(requestHandler, 'mybundle.bundle?platform=ios'))
.statusCode,
).toBe(200);
DeltaBundler.prototype.buildGraph.mockClear();
DeltaBundler.prototype.getDelta.mockClear();
expect(
(await makeRequest(requestHandler, 'mybundle.map?platform=ios'))
.statusCode,
).toBe(200);
expect(DeltaBundler.prototype.buildGraph.mock.calls.length).toBe(0);
expect(DeltaBundler.prototype.getDelta.mock.calls.length).toBe(0);
});
it('does rebuild the graph when requesting the sourcemaps if the bundle has not been built yet', async () => {
expect(
(await makeRequest(requestHandler, 'mybundle.bundle?platform=ios'))
.statusCode,
).toBe(200);
DeltaBundler.prototype.buildGraph.mockClear();
DeltaBundler.prototype.getDelta.mockClear();
// request the map of a different bundle
expect(
(await makeRequest(requestHandler, 'mybundle.map?platform=android'))
.statusCode,
).toBe(200);
expect(DeltaBundler.prototype.buildGraph.mock.calls.length).toBe(1);
});
it('works with .ios.js extension', () => {
return makeRequest(requestHandler, 'index.ios.includeRequire.bundle').then(
response => {
expect(response.body).toEqual('this is the source');
expect(Serializers.fullBundle).toBeCalledWith(
expect.any(DeltaBundler),
{
assetPlugins: [],
bundleType: 'bundle',
customTransformOptions: {},
dev: true,
entryFile: '/root/index.ios.js',
entryModuleOnly: false,
excludeSource: false,
hot: true,
inlineSourceMap: false,
isolateModuleIDs: false,
minify: false,
onProgress: jasmine.any(Function),
platform: null,
resolutionResponse: null,
runBeforeMainModule: ['InitializeCore'],
runModule: true,
sourceMapUrl: 'http://localhost:8081/index.ios.includeRequire.map',
unbundle: false,
},
);
expect(DeltaBundler.prototype.buildGraph).toBeCalledWith({
assetPlugins: [],
customTransformOptions: {},
dev: true,
entryPoints: ['/root/index.ios.js'],
hot: true,
minify: false,
onProgress: jasmine.any(Function),
platform: null,
type: 'module',
});
},
);
});
@ -219,30 +345,17 @@ describe('processRequest', () => {
it('passes in the platform param', function() {
return makeRequest(requestHandler, 'index.bundle?platform=ios').then(
function(response) {
expect(response.body).toEqual('this is the source');
expect(Serializers.fullBundle).toBeCalledWith(
expect.any(DeltaBundler),
{
assetPlugins: [],
bundleType: 'bundle',
customTransformOptions: {},
dev: true,
entryFile: '/root/index.js',
entryModuleOnly: false,
excludeSource: false,
hot: true,
inlineSourceMap: false,
isolateModuleIDs: false,
minify: false,
onProgress: jasmine.any(Function),
platform: 'ios',
resolutionResponse: null,
runBeforeMainModule: ['InitializeCore'],
runModule: true,
sourceMapUrl: 'http://localhost:8081/index.map?platform=ios',
unbundle: false,
},
);
expect(DeltaBundler.prototype.buildGraph).toBeCalledWith({
assetPlugins: [],
customTransformOptions: {},
dev: true,
entryPoints: ['/root/index.js'],
hot: true,
minify: false,
onProgress: jasmine.any(Function),
platform: 'ios',
type: 'module',
});
},
);
});
@ -252,27 +365,16 @@ describe('processRequest', () => {
requestHandler,
'index.bundle?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2',
).then(function(response) {
expect(response.body).toEqual('this is the source');
expect(Serializers.fullBundle).toBeCalledWith(expect.any(DeltaBundler), {
expect(DeltaBundler.prototype.buildGraph).toBeCalledWith({
assetPlugins: ['assetPlugin1', 'assetPlugin2'],
bundleType: 'bundle',
customTransformOptions: {},
dev: true,
entryFile: '/root/index.js',
entryModuleOnly: false,
excludeSource: false,
entryPoints: ['/root/index.js'],
hot: true,
inlineSourceMap: false,
isolateModuleIDs: false,
minify: false,
onProgress: jasmine.any(Function),
platform: null,
resolutionResponse: null,
runBeforeMainModule: ['InitializeCore'],
runModule: true,
sourceMapUrl:
'http://localhost:8081/index.map?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2',
unbundle: false,
type: 'module',
});
});
});

View File

@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`processRequest Generate delta bundle endpoint should send the correct deltaBundlerId to the bundler 1`] = `"{\\"sourceMapUrl\\":null,\\"bundleType\\":\\"delta\\",\\"customTransformOptions\\":{},\\"entryFile\\":\\"/root/index.js\\",\\"deltaBundleId\\":null,\\"dev\\":true,\\"minify\\":false,\\"excludeSource\\":null,\\"hot\\":true,\\"runBeforeMainModule\\":[\\"InitializeCore\\"],\\"runModule\\":true,\\"inlineSourceMap\\":false,\\"isolateModuleIDs\\":false,\\"platform\\":\\"ios\\",\\"resolutionResponse\\":null,\\"entryModuleOnly\\":false,\\"assetPlugins\\":[],\\"onProgress\\":null,\\"unbundle\\":false}"`;
exports[`processRequest Generate delta bundle endpoint should send the correct deltaBundlerId to the bundler 1`] = `"{\\"sourceMapUrl\\":null,\\"bundleType\\":null,\\"customTransformOptions\\":{},\\"entryFile\\":\\"/root/index.js\\",\\"deltaBundleId\\":null,\\"dev\\":true,\\"minify\\":false,\\"excludeSource\\":null,\\"hot\\":true,\\"runBeforeMainModule\\":[\\"InitializeCore\\"],\\"runModule\\":true,\\"inlineSourceMap\\":false,\\"isolateModuleIDs\\":false,\\"platform\\":\\"ios\\",\\"resolutionResponse\\":null,\\"entryModuleOnly\\":false,\\"assetPlugins\\":[],\\"onProgress\\":null,\\"unbundle\\":false}"`;

View File

@ -69,6 +69,7 @@ type ResolveSync = (path: string, opts: ?{baseDir?: string}) => string;
type GraphInfo = {|
graph: Graph,
prepend: $ReadOnlyArray<DependencyEdge>,
lastModified: Date,
|};
function debounceAndBatch(fn, delay) {
@ -125,6 +126,7 @@ class Server {
_platforms: Set<string>;
_nextBundleBuildID: number;
_deltaBundler: DeltaBundler;
_graphs: Map<string, GraphInfo> = new Map();
constructor(options: Options) {
const reporter =
@ -353,9 +355,37 @@ class Server {
return {
prepend,
graph,
lastModified: new Date(),
};
}
async _getGraphInfo(
options: BundleOptions,
{rebuild}: {rebuild: boolean},
): Promise<{...GraphInfo, numModifiedFiles: number}> {
const id = this._optionsHash(options);
let graphInfo = this._graphs.get(id);
let numModifiedFiles = 0;
if (!graphInfo) {
graphInfo = await this._buildGraph(options);
this._graphs.set(id, graphInfo);
numModifiedFiles =
graphInfo.prepend.length + graphInfo.graph.dependencies.size;
} else if (rebuild) {
const delta = await this._deltaBundler.getDelta(graphInfo.graph, {
reset: false,
});
numModifiedFiles = delta.modified.size;
if (numModifiedFiles > 0) {
graphInfo.lastModified = new Date();
}
}
return {...graphInfo, numModifiedFiles};
}
async _minifyModule(module: DependencyEdge): Promise<DependencyEdge> {
const {code, map} = await this._bundler.minifyModule(
module.path,
@ -487,6 +517,7 @@ class Server {
// List of option parameters that won't affect the build result, so they
// can be ignored to calculate the options hash.
const ignoredParams = {
bundleType: null,
onProgress: null,
deltaBundleId: null,
excludeSource: null,
@ -649,7 +680,24 @@ class Server {
let result;
try {
result = await Serializers.fullBundle(this._deltaBundler, options);
const {
graph,
prepend,
lastModified,
numModifiedFiles,
} = await this._getGraphInfo(options, {rebuild: true});
result = {
bundle: plainJSBundle(options.entryFile, prepend, graph, {
createModuleId: this._opts.createModuleId,
dev: options.dev,
runBeforeMainModule: options.runBeforeMainModule,
runModule: options.runModule,
sourceMapUrl: options.sourceMapUrl,
}),
numModifiedFiles,
lastModified,
};
} catch (error) {
this._handleError(mres, this._optionsHash(options), error);
@ -713,7 +761,13 @@ class Server {
let sourceMap;
try {
sourceMap = await Serializers.fullSourceMap(this._deltaBundler, options);
const {graph, prepend} = await this._getGraphInfo(options, {
rebuild: false,
});
sourceMap = sourceMapString(prepend, graph, {
excludeSource: options.excludeSource,
});
} catch (error) {
this._handleError(mres, this._optionsHash(options), error);