mirror of https://github.com/status-im/metro.git
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:
parent
a3a60c912a
commit
7c69e832b2
|
@ -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()),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}
|
||||
`;
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`;
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue