diff --git a/react-packager/src/AssetServer/__tests__/AssetServer-test.js b/react-packager/src/AssetServer/__tests__/AssetServer-test.js index 7a544a27..1cb90b33 100644 --- a/react-packager/src/AssetServer/__tests__/AssetServer-test.js +++ b/react-packager/src/AssetServer/__tests__/AssetServer-test.js @@ -185,7 +185,7 @@ describe('AssetServer', () => { }); }); - describe('assetSerer.getAssetData', () => { + describe('assetServer.getAssetData', () => { pit('should get assetData', () => { const hash = { update: jest.genMockFn(), diff --git a/react-packager/src/Bundler/Bundle.js b/react-packager/src/Bundler/Bundle.js index e457e203..4f54e3be 100644 --- a/react-packager/src/Bundler/Bundle.js +++ b/react-packager/src/Bundler/Bundle.js @@ -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(); diff --git a/react-packager/src/Bundler/__tests__/Bundle-test.js b/react-packager/src/Bundler/__tests__/Bundle-test.js index 1180f0a2..b82f4a6c 100644 --- a/react-packager/src/Bundler/__tests__/Bundle-test.js +++ b/react-packager/src/Bundler/__tests__/Bundle-test.js @@ -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); + }); + }); }); diff --git a/react-packager/src/Server/__tests__/Server-test.js b/react-packager/src/Server/__tests__/Server-test.js index a21462ca..123134f8 100644 --- a/react-packager/src/Server/__tests__/Server-test.js +++ b/react-packager/src/Server/__tests__/Server-test.js @@ -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(); } diff --git a/react-packager/src/Server/index.js b/react-packager/src/Server/index.js index 63432090..3bb8a9c0 100644 --- a/react-packager/src/Server/index.js +++ b/react-packager/src/Server/index.js @@ -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({