Add support to metro server to send Deltas to devices

Reviewed By: jeanlauliac

Differential Revision: D5890498

fbshipit-source-id: 3ce5c3edb69598adffd2224a418647f3b8897945
This commit is contained in:
Rafael Oleza 2017-09-26 17:59:39 -07:00 committed by Facebook Github Bot
parent a3a60c912a
commit 7c69e832b2
12 changed files with 708 additions and 393 deletions

View File

@ -12,16 +12,16 @@
'use strict';
const {fromRawMappings} = require('../Bundler/source-map');
import type {DeltaBundle} from './';
import type {DeltaTransformResponse as DeltaBundle} from './DeltaTransformer';
/**
* This is a reference client for the Delta Bundler: it maintains cached the
* last patched bundle delta and it's capable of applying new Deltas received
* from the Bundler and stringify them to convert them into a full bundle.
* from the Bundler.
*/
class DeltaPatcher {
static _deltaPatchers: Map<string, DeltaPatcher> = new Map();
_lastBundle = {
pre: new Map(),
post: new Map(),
@ -31,6 +31,17 @@ class DeltaPatcher {
_lastNumModifiedFiles = 0;
_lastModifiedDate = new Date();
static get(id: string): DeltaPatcher {
let deltaPatcher = this._deltaPatchers.get(id);
if (!deltaPatcher) {
deltaPatcher = new DeltaPatcher();
this._deltaPatchers.set(id, deltaPatcher);
}
return deltaPatcher;
}
/**
* Applies a Delta Bundle to the current bundle.
*/
@ -81,23 +92,7 @@ class DeltaPatcher {
return this._lastModifiedDate;
}
/**
* Converts the current delta bundle to a standard string bundle, ready to
* be interpreted by any JS VM.
*/
stringifyCode(): string {
const code = this._getAllModules().map(m => m.code);
return code.join('\n;');
}
stringifyMap({excludeSource}: {excludeSource?: boolean}): string {
const mappings = fromRawMappings(this._getAllModules());
return mappings.toString(undefined, {excludeSource});
}
_getAllModules() {
getAllModules() {
return [].concat(
Array.from(this._lastBundle.pre.values()),
Array.from(this._lastBundle.modules.values()),

View File

@ -258,13 +258,13 @@ class DeltaTransformer extends EventEmitter {
// First, get the modules correspondant to all the module names defined in
// the `runBeforeMainModule` config variable. Then, append the entry point
// module so the last thing that gets required is the entry point.
return new Map(
const append = new Map(
this._bundleOptions.runBeforeMainModule
.map(path => this._resolver.getModuleForPath(path))
.concat(entryPointModule)
.map(this._getModuleId)
.map(moduleId => {
const code = `;require(${JSON.stringify(moduleId)});`;
const code = `;require(${JSON.stringify(moduleId)})`;
const name = 'require-' + String(moduleId);
const path = name + '.js';
@ -280,6 +280,20 @@ class DeltaTransformer extends EventEmitter {
];
}),
);
if (this._bundleOptions.sourceMapUrl) {
const code = '//# sourceMappingURL=' + this._bundleOptions.sourceMapUrl;
append.set(this._getModuleId({path: '/sourcemap.js'}), {
code,
map: null,
name: 'sourcemap.js',
path: '/sourcemap.js',
source: code,
});
}
return append;
}
/**
@ -355,7 +369,7 @@ class DeltaTransformer extends EventEmitter {
return [
this._getModuleId(module),
{
code: wrapped.code,
code: ';' + wrapped.code,
map,
name,
source: metadata.source,

View File

@ -0,0 +1,116 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
'use strict';
const DeltaPatcher = require('./DeltaPatcher');
const {fromRawMappings} = require('../Bundler/source-map');
import type {BundleOptions} from '../Server';
import type DeltaBundler, {Options as BuildOptions} from './';
import type {DeltaTransformResponse} from './DeltaTransformer';
export type Options = BundleOptions & {
deltaBundleId: ?string,
};
/**
* This module contains many serializers for the Delta Bundler. Each serializer
* returns a string representation for any specific type of bundle, which can
* be directly sent to the devices.
*/
async function deltaBundle(
deltaBundler: DeltaBundler,
options: Options,
): Promise<{bundle: string, numModifiedFiles: number}> {
const {id, delta} = await _build(deltaBundler, {
...options,
wrapModules: true,
});
function stringifyModule([id, module]) {
return [id, module ? module.code : undefined];
}
const bundle = JSON.stringify({
id,
pre: Array.from(delta.pre).map(stringifyModule),
post: Array.from(delta.post).map(stringifyModule),
delta: Array.from(delta.delta).map(stringifyModule),
reset: delta.reset,
});
return {
bundle,
numModifiedFiles: delta.pre.size + delta.post.size + delta.delta.size,
};
}
async function fullSourceMap(
deltaBundler: DeltaBundler,
options: Options,
): Promise<string> {
const {id, delta} = await _build(deltaBundler, {
...options,
wrapModules: true,
});
const deltaPatcher = DeltaPatcher.get(id).applyDelta(delta);
return fromRawMappings(deltaPatcher.getAllModules()).toString(undefined, {
excludeSource: options.excludeSource,
});
}
/**
* Returns the full JS bundle, which can be directly parsed by a JS interpreter
*/
async function fullBundle(
deltaBundler: DeltaBundler,
options: Options,
): Promise<{bundle: string, numModifiedFiles: number, lastModified: Date}> {
const {id, delta} = await _build(deltaBundler, {
...options,
wrapModules: true,
});
const deltaPatcher = DeltaPatcher.get(id).applyDelta(delta);
const code = deltaPatcher.getAllModules().map(m => m.code);
return {
bundle: code.join('\n'),
lastModified: deltaPatcher.getLastModifiedDate(),
numModifiedFiles: deltaPatcher.getLastNumModifiedFiles(),
};
}
async function _build(
deltaBundler: DeltaBundler,
options: BuildOptions,
): Promise<{id: string, delta: DeltaTransformResponse}> {
const {deltaTransformer, id} = await deltaBundler.getDeltaTransformer(
options,
);
return {
id,
delta: await deltaTransformer.getDelta(),
};
}
module.exports = {
deltaBundle,
fullBundle,
fullSourceMap,
};

View File

@ -21,7 +21,6 @@ const DeltaTransformer = require('../DeltaTransformer');
const DeltaBundler = require('../');
describe('DeltaBundler', () => {
const OriginalDate = global.Date;
let deltaBundler;
let bundler;
const initialTransformerResponse = {
@ -32,10 +31,6 @@ describe('DeltaBundler', () => {
reset: true,
};
function setCurrentTime(time: number) {
global.Date = jest.fn(() => new OriginalDate(time));
}
beforeEach(() => {
DeltaTransformer.prototype.getDelta = jest
.fn()
@ -47,67 +42,35 @@ describe('DeltaBundler', () => {
bundler = new Bundler();
deltaBundler = new DeltaBundler(bundler, {});
setCurrentTime(1482363367000);
});
it('should create a new transformer to build the initial bundle', async () => {
expect(await deltaBundler.build({deltaBundleId: 10})).toEqual({
...initialTransformerResponse,
id: 10,
});
it('should create a new transformer the first time it gets called', async () => {
await deltaBundler.getDeltaTransformer({deltaBundleId: 10});
expect(DeltaTransformer.create.mock.calls.length).toBe(1);
});
it('should reuse the same transformer after a second call', async () => {
const secondResponse = {
delta: new Map([[3, {code: 'a different module'}]]),
pre: new Map(),
post: new Map(),
inverseDependencies: [],
};
DeltaTransformer.prototype.getDelta.mockReturnValueOnce(
Promise.resolve(secondResponse),
);
await deltaBundler.build({deltaBundleId: 10});
expect(await deltaBundler.build({deltaBundleId: 10})).toEqual({
...secondResponse,
id: 10,
});
await deltaBundler.getDeltaTransformer({deltaBundleId: 10});
await deltaBundler.getDeltaTransformer({deltaBundleId: 10});
expect(DeltaTransformer.create.mock.calls.length).toBe(1);
});
it('should reset everything after calling end()', async () => {
await deltaBundler.build({deltaBundleId: 10});
deltaBundler.end();
await deltaBundler.build({deltaBundleId: 10});
it('should create different transformers when there is no delta bundle id', async () => {
await deltaBundler.getDeltaTransformer({});
await deltaBundler.getDeltaTransformer({});
expect(DeltaTransformer.create.mock.calls.length).toBe(2);
});
it('should build the whole stringified bundle', async () => {
expect(
await deltaBundler.buildFullBundle({deltaBundleId: 10}),
).toMatchSnapshot();
it('should reset everything after calling end()', async () => {
await deltaBundler.getDeltaTransformer({deltaBundleId: 10});
DeltaTransformer.prototype.getDelta.mockReturnValueOnce(
Promise.resolve({
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: [],
}),
);
deltaBundler.end();
expect(
await deltaBundler.buildFullBundle({deltaBundleId: 10}),
).toMatchSnapshot();
await deltaBundler.getDeltaTransformer({deltaBundleId: 10});
expect(DeltaTransformer.create.mock.calls.length).toBe(2);
});
});

View File

@ -41,16 +41,16 @@ describe('DeltaPatcher', () => {
});
it('should apply an initial delta correctly', () => {
const result = deltaPatcher
.applyDelta({
reset: 1,
pre: new Map([[1, {code: 'pre'}]]),
post: new Map([[2, {code: 'post'}]]),
delta: new Map([[3, {code: 'middle'}]]),
})
.stringifyCode();
expect(result).toMatchSnapshot();
expect(
deltaPatcher
.applyDelta({
reset: 1,
pre: new Map([[1, {code: 'pre'}]]),
post: new Map([[2, {code: 'post'}]]),
delta: new Map([[3, {code: 'middle'}]]),
})
.getAllModules(),
).toMatchSnapshot();
});
it('should apply many different patches correctly', () => {
@ -71,7 +71,7 @@ describe('DeltaPatcher', () => {
post: new Map(),
delta: new Map([[2, {code: 'another'}], [87, {code: 'third'}]]),
})
.stringifyCode();
.getAllModules();
expect(result).toMatchSnapshot();
@ -86,7 +86,7 @@ describe('DeltaPatcher', () => {
post: new Map(),
delta: new Map([[2, null], [12, {code: 'twelve'}]]),
})
.stringifyCode();
.getAllModules();
expect(anotherResult).toMatchSnapshot();
@ -98,69 +98,59 @@ describe('DeltaPatcher', () => {
delta: new Map([[12, {code: 'ten'}]]),
reset: true,
})
.stringifyCode(),
.getAllModules(),
).toMatchSnapshot();
});
it('should return the number of modified files in the last Delta', () => {
deltaPatcher
.applyDelta({
reset: 1,
pre: new Map([[1, {code: 'pre'}]]),
post: new Map([[2, {code: 'post'}]]),
delta: new Map([[3, {code: 'middle'}]]),
})
.stringifyCode();
deltaPatcher.applyDelta({
reset: 1,
pre: new Map([[1, {code: 'pre'}]]),
post: new Map([[2, {code: 'post'}]]),
delta: new Map([[3, {code: 'middle'}]]),
});
expect(deltaPatcher.getLastNumModifiedFiles()).toEqual(3);
deltaPatcher
.applyDelta({
reset: 1,
pre: new Map([[1, null]]),
post: new Map(),
delta: new Map([[3, {code: 'different'}]]),
})
.stringifyCode();
deltaPatcher.applyDelta({
reset: 1,
pre: new Map([[1, null]]),
post: new Map(),
delta: new Map([[3, {code: 'different'}]]),
});
// A deleted module counts as a modified file.
expect(deltaPatcher.getLastNumModifiedFiles()).toEqual(2);
});
it('should return the time it was last modified', () => {
deltaPatcher
.applyDelta({
reset: 1,
pre: new Map([[1, {code: 'pre'}]]),
post: new Map([[2, {code: 'post'}]]),
delta: new Map([[3, {code: 'middle'}]]),
})
.stringifyCode();
deltaPatcher.applyDelta({
reset: 1,
pre: new Map([[1, {code: 'pre'}]]),
post: new Map([[2, {code: 'post'}]]),
delta: new Map([[3, {code: 'middle'}]]),
});
expect(deltaPatcher.getLastModifiedDate().getTime()).toEqual(INITIAL_TIME);
setCurrentTime(INITIAL_TIME + 1000);
// Apply empty delta
deltaPatcher
.applyDelta({
reset: 1,
pre: new Map(),
post: new Map(),
delta: new Map(),
})
.stringifyCode();
deltaPatcher.applyDelta({
reset: 1,
pre: new Map(),
post: new Map(),
delta: new Map(),
});
expect(deltaPatcher.getLastModifiedDate().getTime()).toEqual(INITIAL_TIME);
setCurrentTime(INITIAL_TIME + 2000);
deltaPatcher
.applyDelta({
reset: 1,
pre: new Map(),
post: new Map([[2, {code: 'newpost'}]]),
delta: new Map(),
})
.stringifyCode();
deltaPatcher.applyDelta({
reset: 1,
pre: new Map(),
post: new Map([[2, {code: 'newpost'}]]),
delta: new Map(),
});
expect(deltaPatcher.getLastModifiedDate().getTime()).toEqual(
INITIAL_TIME + 2000,

View File

@ -0,0 +1,117 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails oncall+javascript_foundation
* @format
*/
'use strict';
const Serializers = require('../Serializers');
const CURRENT_TIME = 1482363367000;
describe('Serializers', () => {
const OriginalDate = global.Date;
const getDelta = jest.fn();
let deltaBundler;
const deltaResponse = {
pre: new Map([[1, {code: 'pre;'}]]),
post: new Map([[2, {code: 'post;'}]]),
delta: new Map([[3, {code: 'module3;'}], [4, {code: 'another;'}]]),
inverseDependencies: [],
reset: true,
};
function setCurrentTime(time: number) {
global.Date = jest.fn(() => new OriginalDate(time));
}
beforeEach(() => {
getDelta.mockReturnValueOnce(Promise.resolve(deltaResponse));
deltaBundler = {
async getDeltaTransformer() {
return {
id: '1234',
deltaTransformer: {
getDelta,
},
};
},
};
setCurrentTime(CURRENT_TIME);
});
it('should return the stringified delta bundle', async () => {
expect(
await Serializers.deltaBundle(deltaBundler, {deltaBundleId: 10}),
).toMatchSnapshot();
// Simulate a delta with some changes now
getDelta.mockReturnValueOnce(
Promise.resolve({
delta: new Map([[3, {code: 'modified module;'}], [4, null]]),
pre: new Map(),
post: new Map(),
inverseDependencies: [],
}),
);
expect(
await Serializers.deltaBundle(deltaBundler, {deltaBundleId: 10}),
).toMatchSnapshot();
});
it('should build the full JS bundle', async () => {
expect(
await Serializers.fullBundle(deltaBundler, {deltaBundleId: 10}),
).toMatchSnapshot();
getDelta.mockReturnValueOnce(
Promise.resolve({
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, {
deltaBundleId: 10,
sourceMapUrl: 'http://localhost:8081/myBundle.js',
}),
).toMatchSnapshot();
});
// 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, {deltaBundleId: 10}),
).toMatchSnapshot();
getDelta.mockReturnValueOnce(
Promise.resolve({
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, {deltaBundleId: 10}),
).toMatchSnapshot();
});
});

View File

@ -1,25 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DeltaBundler should build the whole stringified bundle 1`] = `
Object {
"bundle": "pre
;module3
;another
;post",
"lastModified": 2016-12-21T23:36:07.000Z,
"numModifiedFiles": 4,
}
`;
exports[`DeltaBundler should build the whole stringified bundle 2`] = `
Object {
"bundle": "pre
;more pre
;modified module
;post
;bananas
;apples",
"lastModified": 2016-12-21T23:36:07.000Z,
"numModifiedFiles": 5,
}
`;

View File

@ -1,28 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DeltaPatcher should apply an initial delta correctly 1`] = `
"pre
;middle
;post"
Array [
Object {
"code": "pre",
},
Object {
"code": "middle",
},
Object {
"code": "post",
},
]
`;
exports[`DeltaPatcher should apply many different patches correctly 1`] = `
"pre
;middle
;another
;third
;post"
Array [
Object {
"code": "pre",
},
Object {
"code": "middle",
},
Object {
"code": "another",
},
Object {
"code": "third",
},
Object {
"code": "post",
},
]
`;
exports[`DeltaPatcher should apply many different patches correctly 2`] = `
"new pre
;third
;twelve
;post"
Array [
Object {
"code": "new pre",
},
Object {
"code": "third",
},
Object {
"code": "twelve",
},
Object {
"code": "post",
},
]
`;
exports[`DeltaPatcher should apply many different patches correctly 3`] = `
"1
;ten
;1"
Array [
Object {
"code": "1",
},
Object {
"code": "ten",
},
Object {
"code": "1",
},
]
`;

View File

@ -0,0 +1,43 @@
// 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 stringified delta bundle 1`] = `
Object {
"bundle": "{\\"id\\":\\"1234\\",\\"pre\\":[[1,\\"pre;\\"]],\\"post\\":[[2,\\"post;\\"]],\\"delta\\":[[3,\\"module3;\\"],[4,\\"another;\\"]],\\"reset\\":true}",
"numModifiedFiles": 4,
}
`;
exports[`Serializers should return the stringified delta bundle 2`] = `
Object {
"bundle": "{\\"id\\":\\"1234\\",\\"pre\\":[],\\"post\\":[],\\"delta\\":[[3,\\"modified module;\\"],[4,null]]}",
"numModifiedFiles": 2,
}
`;

View File

@ -12,32 +12,18 @@
'use strict';
const DeltaPatcher = require('./DeltaPatcher');
const DeltaTransformer = require('./DeltaTransformer');
import type Bundler from '../Bundler';
import type {BundleOptions} from '../Server';
import type {DeltaEntries} from './DeltaTransformer';
export type DeltaBundle = {|
+id: string,
+pre: DeltaEntries,
+post: DeltaEntries,
+delta: DeltaEntries,
+inverseDependencies: {[key: string]: $ReadOnlyArray<string>},
+reset: boolean,
|};
type MainOptions = {|
getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>,
polyfillModuleNames: $ReadOnlyArray<string>,
|};
type FullBuildOptions = BundleOptions & {
export type Options = BundleOptions & {
+deltaBundleId: ?string,
};
export type Options = FullBuildOptions & {
+wrapModules: boolean,
};
@ -52,7 +38,6 @@ class DeltaBundler {
_bundler: Bundler;
_options: MainOptions;
_deltaTransformers: Map<string, DeltaTransformer> = new Map();
_deltaPatchers: Map<string, DeltaPatcher> = new Map();
_currentId: number = 0;
constructor(bundler: Bundler, options: MainOptions) {
@ -63,22 +48,6 @@ class DeltaBundler {
end() {
this._deltaTransformers.forEach(DeltaTransformer => DeltaTransformer.end());
this._deltaTransformers = new Map();
this._deltaPatchers = new Map();
}
async build(options: Options): Promise<DeltaBundle> {
const {deltaTransformer, id} = await this.getDeltaTransformer({
...options,
// The Delta Bundler does not support minifying due to issues generating
// the source maps (T21699790).
minify: false,
});
const response = await deltaTransformer.getDelta();
return {
...response,
id,
};
}
async getDeltaTransformer(
@ -99,7 +68,10 @@ class DeltaBundler {
deltaTransformer = await DeltaTransformer.create(
this._bundler,
this._options,
options,
{
...options, // The Delta Bundler does not support minifying due to
minify: false, // issues generating the source maps (T21699790).
},
);
this._deltaTransformers.set(bundleId, deltaTransformer);
@ -110,45 +82,6 @@ class DeltaBundler {
id: bundleId,
};
}
async buildFullBundle(
options: FullBuildOptions,
): Promise<{bundle: string, numModifiedFiles: number, lastModified: Date}> {
const deltaPatcher = await this._getDeltaPatcher(options);
let bundle = deltaPatcher.stringifyCode();
if (options.sourceMapUrl) {
bundle += '//# sourceMappingURL=' + options.sourceMapUrl;
}
return {
bundle,
lastModified: deltaPatcher.getLastModifiedDate(),
numModifiedFiles: deltaPatcher.getLastNumModifiedFiles(),
};
}
async buildFullSourceMap(options: FullBuildOptions): Promise<string> {
return (await this._getDeltaPatcher(options)).stringifyMap({
excludeSource: options.excludeSource,
});
}
async _getDeltaPatcher(options: FullBuildOptions): Promise<DeltaPatcher> {
const deltaBundle = await this.build({
...options,
wrapModules: true,
});
let deltaPatcher = this._deltaPatchers.get(deltaBundle.id);
if (!deltaPatcher) {
deltaPatcher = new DeltaPatcher();
this._deltaPatchers.set(deltaBundle.id, deltaPatcher);
}
return deltaPatcher.applyDelta(deltaBundle);
}
}
module.exports = DeltaBundler;

View File

@ -7,26 +7,28 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails oncall+javascript_foundation
* @format
*/
'use strict';
jest.mock('../../worker-farm', () => () => () => {})
.mock('worker-farm', () => () => () => {})
.mock('../../JSTransformer/worker/minify')
.mock('crypto')
.mock(
'../symbolicate',
() => ({createWorker: jest.fn().mockReturnValue(jest.fn())}),
)
.mock('../../Bundler')
.mock('../../AssetServer')
.mock('../../node-haste/DependencyGraph')
.mock('../../Logger')
.mock('../../lib/GlobalTransformCache');
jest
.mock('../../worker-farm', () => () => () => {})
.mock('worker-farm', () => () => () => {})
.mock('../../JSTransformer/worker/minify')
.mock('crypto')
.mock('../symbolicate', () => ({
createWorker: jest.fn().mockReturnValue(jest.fn()),
}))
.mock('../../Bundler')
.mock('../../AssetServer')
.mock('../../node-haste/DependencyGraph')
.mock('../../Logger')
.mock('../../lib/GlobalTransformCache')
.mock('../../DeltaBundler/Serializers');
describe('processRequest', () => {
let Bundler, Server, AssetServer, symbolicate;
let Bundler, Server, AssetServer, symbolicate, Serializers;
beforeEach(() => {
jest.useFakeTimers();
jest.resetModules();
@ -34,6 +36,7 @@ describe('processRequest', () => {
Server = require('../');
AssetServer = require('../../AssetServer');
symbolicate = require('../symbolicate');
Serializers = require('../../DeltaBundler/Serializers');
});
let server;
@ -47,23 +50,30 @@ describe('processRequest', () => {
runBeforeMainModule: ['InitializeCore'],
};
const makeRequest = (reqHandler, requrl, reqOptions) => new Promise(resolve =>
reqHandler(
{url: requrl, headers:{}, ...reqOptions},
{
statusCode: 200,
headers: {},
getHeader(header) { return this.headers[header]; },
setHeader(header, value) { this.headers[header] = value; },
writeHead(statusCode) { this.statusCode = statusCode; },
end(body) {
this.body = body;
resolve(this);
const makeRequest = (reqHandler, requrl, reqOptions) =>
new Promise(resolve =>
reqHandler(
{url: requrl, headers: {}, ...reqOptions},
{
statusCode: 200,
headers: {},
getHeader(header) {
return this.headers[header];
},
setHeader(header, value) {
this.headers[header] = value;
},
writeHead(statusCode) {
this.statusCode = statusCode;
},
end(body) {
this.body = body;
resolve(this);
},
},
},
{next: () => {}},
)
);
{next: () => {}},
),
);
const invalidatorFunc = jest.fn();
let requestHandler;
@ -76,16 +86,18 @@ describe('processRequest', () => {
getSourceMap: () => ({version: 3}),
getSourceMapString: () => 'this is the source map',
getEtag: () => 'this is an etag',
}));
}),
);
Bundler.prototype.invalidateFile = invalidatorFunc;
Bundler.prototype.getResolver =
jest.fn().mockReturnValue(Promise.resolve({
Bundler.prototype.getResolver = jest.fn().mockReturnValue(
Promise.resolve({
getDependencyGraph: jest.fn().mockReturnValue({
getHasteMap: jest.fn().mockReturnValue({on: jest.fn()}),
load: jest.fn(() => Promise.resolve()),
}),
}));
}),
);
server = new Server(options);
requestHandler = server.processRequest.bind(server);
@ -95,25 +107,21 @@ describe('processRequest', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
null
).then(response =>
expect(response.body).toEqual('this is the source')
);
null,
).then(response => expect(response.body).toEqual('this is the source'));
});
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')
);
'mybundle.runModule.bundle',
).then(response => expect(response.body).toEqual('this is the source'));
});
it('returns ETag header on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
'mybundle.bundle?runModule=true',
).then(response => {
expect(response.getHeader('ETag')).toBeDefined();
});
@ -122,7 +130,7 @@ describe('processRequest', () => {
it('returns build info headers on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
'mybundle.bundle?runModule=true',
).then(response => {
expect(response.getHeader('X-Metro-Files-Changed-Count')).toBeDefined();
});
@ -131,19 +139,18 @@ describe('processRequest', () => {
it('returns Content-Length header on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
'mybundle.bundle?runModule=true',
).then(response => {
expect(response.getHeader('Content-Length'))
.toBe(Buffer.byteLength(response.body));
expect(response.getHeader('Content-Length')).toBe(
Buffer.byteLength(response.body),
);
});
});
it('returns 304 on request of *.bundle when if-none-match equals the ETag', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
{headers : {'if-none-match' : 'this is an etag'}}
).then(response => {
return makeRequest(requestHandler, 'mybundle.bundle?runModule=true', {
headers: {'if-none-match': 'this is an etag'},
}).then(response => {
expect(response.statusCode).toEqual(304);
});
});
@ -151,16 +158,14 @@ describe('processRequest', () => {
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')
);
'mybundle.map?runModule=true',
).then(response => expect(response.body).toEqual('this is the source map'));
});
it('works with .ios.js extension', () => {
return makeRequest(
requestHandler,
'index.ios.includeRequire.bundle'
'index.ios.includeRequire.bundle',
).then(response => {
expect(response.body).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith({
@ -188,7 +193,7 @@ describe('processRequest', () => {
it('passes in the platform param', function() {
return makeRequest(
requestHandler,
'index.bundle?platform=ios'
'index.bundle?platform=ios',
).then(function(response) {
expect(response.body).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith({
@ -216,7 +221,7 @@ describe('processRequest', () => {
it('passes in the assetPlugin param', function() {
return makeRequest(
requestHandler,
'index.bundle?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2'
'index.bundle?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2',
).then(function(response) {
expect(response.body).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith({
@ -235,14 +240,14 @@ describe('processRequest', () => {
resolutionResponse: null,
runBeforeMainModule: ['InitializeCore'],
runModule: true,
sourceMapUrl: 'index.map?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2',
sourceMapUrl:
'index.map?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2',
unbundle: false,
});
});
});
describe('file changes', () => {
it('does not rebuild the bundles that contain a file when that file is changed', () => {
const bundleFunc = jest.fn();
bundleFunc
@ -253,7 +258,7 @@ describe('processRequest', () => {
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
}),
)
.mockReturnValue(
Promise.resolve({
@ -262,7 +267,7 @@ describe('processRequest', () => {
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
}),
);
Bundler.prototype.bundle = bundleFunc;
@ -271,11 +276,13 @@ describe('processRequest', () => {
requestHandler = server.processRequest.bind(server);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response => {
expect(response.body).toEqual('this is the first source');
expect(bundleFunc.mock.calls.length).toBe(1);
});
makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
).done(response => {
expect(response.body).toEqual('this is the first source');
expect(bundleFunc.mock.calls.length).toBe(1);
});
jest.runAllTicks();
@ -285,16 +292,18 @@ describe('processRequest', () => {
expect(bundleFunc.mock.calls.length).toBe(1);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response =>
expect(response.body).toEqual('this is the rebuilt source')
);
makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
).done(response =>
expect(response.body).toEqual('this is the rebuilt source'),
);
jest.runAllTicks();
});
it(
'does not rebuild the bundles that contain a file ' +
'when that file is changed, even when hot loading is enabled',
'when that file is changed, even when hot loading is enabled',
() => {
const bundleFunc = jest.fn();
bundleFunc
@ -305,7 +314,7 @@ describe('processRequest', () => {
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
}),
)
.mockReturnValue(
Promise.resolve({
@ -314,7 +323,7 @@ describe('processRequest', () => {
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
}),
);
Bundler.prototype.bundle = bundleFunc;
@ -324,11 +333,13 @@ describe('processRequest', () => {
requestHandler = server.processRequest.bind(server);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response => {
expect(response.body).toEqual('this is the first source');
expect(bundleFunc.mock.calls.length).toBe(1);
});
makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
).done(response => {
expect(response.body).toEqual('this is the first source');
expect(bundleFunc.mock.calls.length).toBe(1);
});
jest.runAllTicks();
@ -339,13 +350,56 @@ describe('processRequest', () => {
expect(bundleFunc.mock.calls.length).toBe(1);
server.setHMRFileChangeListener(null);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response => {
expect(response.body).toEqual('this is the rebuilt source');
expect(bundleFunc.mock.calls.length).toBe(2);
});
makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
).done(response => {
expect(response.body).toEqual('this is the rebuilt source');
expect(bundleFunc.mock.calls.length).toBe(2);
});
jest.runAllTicks();
},
);
});
describe('Generate delta bundle endpoint', () => {
Serializers;
it('should generate a new delta correctly', () => {
Serializers.deltaBundle.mockImplementation(async (_, options) => {
expect(options.deltaBundleId).toBe(undefined);
return {
bundle: '{"delta": "bundle"}',
numModifiedFiles: 3,
};
});
return makeRequest(
requestHandler,
'index.delta?platform=ios',
).then(function(response) {
expect(response.body).toEqual('{"delta": "bundle"}');
});
});
it('should send the correct deltaBundlerId to the bundler', () => {
Serializers.deltaBundle.mockImplementation(async (_, options) => {
expect(options.deltaBundleId).toBe('1234');
return {
bundle: '{"delta": "bundle"}',
numModifiedFiles: 3,
};
});
return makeRequest(
requestHandler,
'index.delta?platform=ios&deltaBundleId=1234',
).then(function(response) {
expect(response.body).toEqual('{"delta": "bundle"}');
});
});
});
describe('/onchange endpoint', () => {
@ -392,7 +446,9 @@ describe('processRequest', () => {
const req = scaffoldReq({url: '/assets/imgs/a.png'});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() => Promise.resolve('i am image'));
AssetServer.prototype.get.mockImplementation(() =>
Promise.resolve('i am image'),
);
server.processRequest(req, res);
res.end.mockImplementation(value => {
@ -405,7 +461,9 @@ describe('processRequest', () => {
const req = scaffoldReq({url: '/assets/imgs/a.png?platform=ios'});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() => Promise.resolve('i am image'));
AssetServer.prototype.get.mockImplementation(() =>
Promise.resolve('i am image'),
);
server.processRequest(req, res);
res.end.mockImplementation(value => {
@ -423,7 +481,9 @@ describe('processRequest', () => {
const res = {end: jest.fn(), writeHead: jest.fn(), setHeader: jest.fn()};
const mockData = 'i am image';
AssetServer.prototype.get.mockImplementation(() => Promise.resolve(mockData));
AssetServer.prototype.get.mockImplementation(() =>
Promise.resolve(mockData),
);
server.processRequest(req, res);
res.end.mockImplementation(value => {
@ -433,17 +493,21 @@ describe('processRequest', () => {
});
});
it('should serve assets files\'s name contain non-latin letter', done => {
const req = scaffoldReq({url: '/assets/imgs/%E4%B8%BB%E9%A1%B5/logo.png'});
it("should serve assets files's name contain non-latin letter", done => {
const req = scaffoldReq({
url: '/assets/imgs/%E4%B8%BB%E9%A1%B5/logo.png',
});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() => Promise.resolve('i am image'));
AssetServer.prototype.get.mockImplementation(() =>
Promise.resolve('i am image'),
);
server.processRequest(req, res);
res.end.mockImplementation(value => {
expect(AssetServer.prototype.get).toBeCalledWith(
'imgs/\u{4E3B}\u{9875}/logo.png',
undefined
undefined,
);
expect(value).toBe('i am image');
done();
@ -453,36 +517,41 @@ describe('processRequest', () => {
describe('buildbundle(options)', () => {
it('Calls the bundler with the correct args', () => {
return server.buildBundle({
...Server.DEFAULT_BUNDLE_OPTIONS,
entryFile: 'foo file',
}).then(() =>
expect(Bundler.prototype.bundle).toBeCalledWith({
assetPlugins: [],
dev: true,
return server
.buildBundle({
...Server.DEFAULT_BUNDLE_OPTIONS,
entryFile: 'foo file',
entryModuleOnly: false,
excludeSource: false,
generateSourceMaps: false,
hot: false,
inlineSourceMap: false,
isolateModuleIDs: false,
minify: false,
onProgress: null,
platform: undefined,
resolutionResponse: null,
runBeforeMainModule: [],
runModule: true,
sourceMapUrl: null,
unbundle: false,
})
);
.then(() =>
expect(Bundler.prototype.bundle).toBeCalledWith({
assetPlugins: [],
dev: true,
entryFile: 'foo file',
entryModuleOnly: false,
excludeSource: false,
generateSourceMaps: false,
hot: false,
inlineSourceMap: false,
isolateModuleIDs: false,
minify: false,
onProgress: null,
platform: undefined,
resolutionResponse: null,
runBeforeMainModule: [],
runModule: true,
sourceMapUrl: null,
unbundle: false,
}),
);
});
});
describe('buildBundleFromUrl(options)', () => {
it('Calls the bundler with the correct args', () => {
return server.buildBundleFromUrl('/path/to/foo.bundle?dev=false&runModule=false&excludeSource=true')
return server
.buildBundleFromUrl(
'/path/to/foo.bundle?dev=false&runModule=false&excludeSource=true',
)
.then(() =>
expect(Bundler.prototype.bundle).toBeCalledWith({
assetPlugins: [],
@ -500,14 +569,18 @@ describe('processRequest', () => {
resolutionResponse: null,
runBeforeMainModule: ['InitializeCore'],
runModule: false,
sourceMapUrl: '/path/to/foo.map?dev=false&runModule=false&excludeSource=true',
sourceMapUrl:
'/path/to/foo.map?dev=false&runModule=false&excludeSource=true',
unbundle: false,
})
}),
);
});
it('ignores the `hot` parameter (since it is not used anymore)', () => {
return server.buildBundleFromUrl('/path/to/foo.bundle?dev=false&hot=false&runModule=false')
return server
.buildBundleFromUrl(
'/path/to/foo.bundle?dev=false&hot=false&runModule=false',
)
.then(() =>
expect(Bundler.prototype.bundle).toBeCalledWith({
assetPlugins: [],
@ -525,9 +598,10 @@ describe('processRequest', () => {
resolutionResponse: null,
runBeforeMainModule: ['InitializeCore'],
runModule: false,
sourceMapUrl: '/path/to/foo.map?dev=false&hot=false&runModule=false',
sourceMapUrl:
'/path/to/foo.map?dev=false&hot=false&runModule=false',
unbundle: false,
})
}),
);
});
});
@ -540,17 +614,21 @@ describe('processRequest', () => {
});
it('should symbolicate given stack trace', () => {
const inputStack = [{
file: 'http://foo.bundle?platform=ios',
lineNumber: 2100,
column: 44,
customPropShouldBeLeftUnchanged: 'foo',
}];
const outputStack = [{
source: 'foo.js',
line: 21,
column: 4,
}];
const inputStack = [
{
file: 'http://foo.bundle?platform=ios',
lineNumber: 2100,
column: 44,
customPropShouldBeLeftUnchanged: 'foo',
},
];
const outputStack = [
{
source: 'foo.js',
line: 21,
column: 4,
},
];
const body = JSON.stringify({stack: inputStack});
expect.assertions(2);
@ -559,12 +637,11 @@ describe('processRequest', () => {
return outputStack;
});
return makeRequest(
requestHandler,
'/symbolicate',
{rawBody: body},
).then(response =>
expect(JSON.parse(response.body)).toEqual({stack: outputStack}));
return makeRequest(requestHandler, '/symbolicate', {
rawBody: body,
}).then(response =>
expect(JSON.parse(response.body)).toEqual({stack: outputStack}),
);
});
});
@ -573,11 +650,9 @@ describe('processRequest', () => {
const body = 'clearly-not-json';
console.error = jest.fn();
return makeRequest(
requestHandler,
'/symbolicate',
{rawBody: body}
).then(response => {
return makeRequest(requestHandler, '/symbolicate', {
rawBody: body,
}).then(response => {
expect(response.statusCode).toEqual(500);
expect(JSON.parse(response.body)).toEqual({
error: jasmine.any(String),
@ -589,10 +664,12 @@ describe('processRequest', () => {
describe('_getOptionsFromUrl', () => {
it('ignores protocol, host and port of the passed in URL', () => {
const short = '/path/to/entry-file.js??platform=ios&dev=true&minify=false';
const short =
'/path/to/entry-file.js??platform=ios&dev=true&minify=false';
const long = `http://localhost:8081${short}`;
expect(server._getOptionsFromUrl(long))
.toEqual(server._getOptionsFromUrl(short));
expect(server._getOptionsFromUrl(long)).toEqual(
server._getOptionsFromUrl(short),
);
});
});

View File

@ -16,8 +16,7 @@ const AssetServer = require('../AssetServer');
const Bundler = require('../Bundler');
const DeltaBundler = require('../DeltaBundler');
const MultipartResponse = require('./MultipartResponse');
const crypto = require('crypto');
const Serializers = require('../DeltaBundler/Serializers');
const debug = require('debug')('Metro:Server');
const defaults = require('../defaults');
const emptyFunction = require('fbjs/lib/emptyFunction');
@ -37,6 +36,7 @@ import type {BundlingOptions} from '../Bundler';
import type Bundle from '../Bundler/Bundle';
import type HMRBundle from '../Bundler/HMRBundle';
import type {Reporter} from '../lib/reporting';
import type {Options as DeltaBundlerOptions} from '../DeltaBundler/Serializers';
import type {
GetTransformOptions,
PostProcessModules,
@ -469,7 +469,7 @@ class Server {
this._changeWatchers = [];
}
_processDebugRequest(reqUrl: string, res: ServerResponse) {
_processdebugRequest(reqUrl: string, res: ServerResponse) {
let ret = '<!doctype html>';
const pathname = url.parse(reqUrl).pathname;
/* $FlowFixMe: pathname would be null for an invalid URL */
@ -768,8 +768,11 @@ class Server {
requestType = 'map';
} else if (pathname.match(/\.assets$/)) {
requestType = 'assets';
} else if (pathname.match(/\.delta$/)) {
this._processDeltaRequest(req, res);
return;
} else if (pathname.match(/^\/debug/)) {
this._processDebugRequest(req.url, res);
this._processdebugRequest(req.url, res);
return;
} else if (pathname.match(/^\/onchange\/?$/)) {
this._processOnChangeRequest(req, res);
@ -887,7 +890,7 @@ class Server {
_prepareDeltaBundler(
req: IncomingMessage,
mres: MultipartResponse,
): {options: BundleOptions, buildID: string} {
): {options: DeltaBundlerOptions, buildID: string} {
const options = this._getOptionsFromUrl(req.url);
const buildID = this.getNewBuildID();
@ -917,6 +920,52 @@ class Server {
return {options, buildID};
}
async _processDeltaRequest(req: IncomingMessage, res: ServerResponse) {
const mres = MultipartResponse.wrap(req, res);
const {options, buildID} = this._prepareDeltaBundler(req, mres);
const requestingBundleLogEntry = log(
createActionStartEntry({
action_name: 'Requesting delta',
bundle_url: req.url,
entry_point: options.entryFile,
}),
);
let output;
try {
output = await Serializers.deltaBundle(this._deltaBundler, {
...options,
deltaBundleId: options.deltaBundleId,
});
} catch (error) {
this._handleError(res, this.optionsHash(options), error);
this._reporter.update({
buildID,
type: 'bundle_build_failed',
});
return;
}
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Content-Length', String(Buffer.byteLength(output.bundle)));
res.end(output.bundle);
this._reporter.update({
buildID,
type: 'bundle_build_done',
});
debug('Finished response');
log({
...createActionEndEntry(requestingBundleLogEntry),
outdated_modules: output.numModifiedFiles,
});
}
async _processBundleUsingDeltaBundler(
req: IncomingMessage,
res: ServerResponse,
@ -936,7 +985,7 @@ class Server {
let result;
try {
result = await this._deltaBundler.buildFullBundle({
result = await Serializers.fullBundle(this._deltaBundler, {
...options,
deltaBundleId: this.optionsHash(options),
});
@ -1006,7 +1055,7 @@ class Server {
let sourceMap;
try {
sourceMap = await this._deltaBundler.buildFullSourceMap({
sourceMap = await Serializers.fullSourceMap(this._deltaBundler, {
...options,
deltaBundleId: this.optionsHash(options),
});
@ -1126,7 +1175,7 @@ class Server {
this._reporter.update({error, type: 'bundling_error'});
}
_getOptionsFromUrl(reqUrl: string): BundleOptions {
_getOptionsFromUrl(reqUrl: string): BundleOptions & DeltaBundlerOptions {
// `true` to parse the query param as an object.
const urlObj = url.parse(reqUrl, true);
@ -1145,6 +1194,7 @@ class Server {
part === 'runModule' ||
part === 'bundle' ||
part === 'map' ||
part === 'delta' ||
part === 'assets'
) {
return false;
@ -1159,6 +1209,9 @@ class Server {
urlObj.query.platform ||
parsePlatformFilePath(pathname, this._platforms).platform;
/* $FlowFixMe: `query` could be empty for an invalid URL */
const deltaBundleId = urlObj.query.deltaBundleId;
/* $FlowFixMe: `query` could be empty for an invalid URL */
const assetPlugin = urlObj.query.assetPlugin;
const assetPlugins = Array.isArray(assetPlugin)
@ -1176,11 +1229,12 @@ class Server {
return {
sourceMapUrl: url.format({
hash: urlObj.hash,
pathname: pathname.replace(/\.bundle$/, '.map'),
pathname: pathname.replace(/\.(bundle|delta)$/, '.map'),
query: urlObj.query,
search: urlObj.search,
}),
entryFile,
deltaBundleId,
dev,
minify,
excludeSource,