Adding ETag handling to packager improves local development

Summary: Closes https://github.com/facebook/react-native/pull/2473

Reviewed By: svcscm

Differential Revision: D2669385

Pulled By: mkonicek

fb-gh-sync-id: bb9ee4a309a05e0da0ccc7663a373f4a53b9a0a2
This commit is contained in:
tfallon@mail.depaul.edu 2016-01-21 12:59:41 -08:00 committed by facebook-github-bot-4
parent d1ae909bbb
commit 0dbc239685
5 changed files with 66 additions and 15 deletions

View File

@ -185,7 +185,7 @@ describe('AssetServer', () => {
});
});
describe('assetSerer.getAssetData', () => {
describe('assetServer.getAssetData', () => {
pit('should get assetData', () => {
const hash = {
update: jest.genMockFn(),

View File

@ -14,6 +14,7 @@ const BundleBase = require('./BundleBase');
const UglifyJS = require('uglify-js');
const ModuleTransport = require('../lib/ModuleTransport');
const Activity = require('../Activity');
const crypto = require('crypto');
const SOURCEMAPPING_URL = '\n\/\/# sourceMappingURL=';
@ -256,6 +257,11 @@ class Bundle extends BundleBase {
return map;
}
getEtag() {
var eTag = crypto.createHash('md5').update(this.getSource()).digest('hex');
return eTag;
}
_getMappings() {
const modules = super.getModules();

View File

@ -15,6 +15,7 @@ const ModuleTransport = require('../../lib/ModuleTransport');
const Promise = require('Promise');
const SourceMapGenerator = require('source-map').SourceMapGenerator;
const UglifyJS = require('uglify-js');
const crypto = require('crypto');
describe('Bundle', () => {
var bundle;
@ -301,6 +302,15 @@ describe('Bundle', () => {
});
});
});
describe('getEtag()', function() {
it('should return an etag', function() {
var bundle = new Bundle('test_url');
bundle.finalize({});
var eTag = crypto.createHash('md5').update(bundle.getSource()).digest('hex');
expect(bundle.getEtag()).toEqual(eTag);
});
});
});

View File

@ -14,7 +14,8 @@ jest.setMock('worker-farm', function() { return () => {}; })
.dontMock('url')
.setMock('timers', { setImmediate: (fn) => setTimeout(fn, 0) })
.setMock('uglify-js')
.dontMock('../');
.dontMock('../')
.setMock('crypto');
const Promise = require('promise');
@ -34,12 +35,17 @@ describe('processRequest', () => {
polyfillModuleNames: null
};
const makeRequest = (reqHandler, requrl) => new Promise(resolve =>
const makeRequest = (reqHandler, requrl, reqOptions) => new Promise(resolve =>
reqHandler(
{ url: requrl },
{ url: requrl, headers:{}, ...reqOptions },
{
setHeader: jest.genMockFunction(),
end: res => resolve(res),
headers: {},
getHeader(header) { return this.headers[header]; },
setHeader(header, value) { this.headers[header] = value; },
end(body) {
this.body = body;
resolve(this);
},
},
{ next: () => {} },
)
@ -55,6 +61,7 @@ describe('processRequest', () => {
Promise.resolve({
getSource: () => 'this is the source',
getSourceMap: () => 'this is the source map',
getEtag: () => 'this is an etag',
})
);
@ -76,9 +83,10 @@ describe('processRequest', () => {
pit('returns JS bundle source on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
'mybundle.bundle?runModule=true',
null
).then(response =>
expect(response).toEqual('this is the source')
expect(response.body).toEqual('this is the source')
);
});
@ -87,16 +95,35 @@ describe('processRequest', () => {
requestHandler,
'mybundle.runModule.bundle'
).then(response =>
expect(response).toEqual('this is the source')
expect(response.body).toEqual('this is the source')
);
});
pit('returns ETag header on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
).then(response => {
expect(response.getHeader('ETag')).toBeDefined()
});
});
pit('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 => {
expect(response.statusCode).toEqual(304)
});
});
pit('returns sourcemap on request of *.map', () => {
return makeRequest(
requestHandler,
'mybundle.map?runModule=true'
).then(response =>
expect(response).toEqual('this is the source map')
expect(response.body).toEqual('this is the source map')
);
});
@ -105,7 +132,7 @@ describe('processRequest', () => {
requestHandler,
'index.ios.includeRequire.bundle'
).then(response => {
expect(response).toEqual('this is the source');
expect(response.body).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'index.ios.js',
inlineSourceMap: false,
@ -126,7 +153,7 @@ describe('processRequest', () => {
requestHandler,
'index.bundle?platform=ios'
).then(function(response) {
expect(response).toEqual('this is the source');
expect(response.body).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'index.js',
inlineSourceMap: false,
@ -183,12 +210,14 @@ describe('processRequest', () => {
Promise.resolve({
getSource: () => 'this is the first source',
getSourceMap: () => {},
getEtag: () => () => 'this is an etag',
})
)
.mockReturnValue(
Promise.resolve({
getSource: () => 'this is the rebuilt source',
getSourceMap: () => {},
getEtag: () => () => 'this is an etag',
})
);
@ -200,7 +229,7 @@ describe('processRequest', () => {
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response => {
expect(response).toEqual('this is the first source');
expect(response.body).toEqual('this is the first source');
expect(bundleFunc.mock.calls.length).toBe(1);
});
@ -214,7 +243,7 @@ describe('processRequest', () => {
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response =>
expect(response).toEqual('this is the rebuilt source')
expect(response.body).toEqual('this is the rebuilt source')
);
jest.runAllTicks();
}

View File

@ -430,7 +430,13 @@ class Server {
dev: options.dev,
});
res.setHeader('Content-Type', 'application/javascript');
res.end(bundleSource);
res.setHeader('ETag', p.getEtag());
if (req.headers['if-none-match'] === res.getHeader('ETag')){
res.statusCode = 304;
res.end();
} else {
res.end(bundleSource);
}
Activity.endEvent(startReqEventId);
} else if (requestType === 'map') {
var sourceMap = p.getSourceMap({