diff --git a/packages/metro-bundler/src/DeltaBundler/DeltaPatcher.js b/packages/metro-bundler/src/DeltaBundler/DeltaPatcher.js index aafeec84..9416a5b6 100644 --- a/packages/metro-bundler/src/DeltaBundler/DeltaPatcher.js +++ b/packages/metro-bundler/src/DeltaBundler/DeltaPatcher.js @@ -28,6 +28,7 @@ class DeltaPatcher { modules: new Map(), }; _initialized = false; + _lastNumModifiedFiles = 0; /** * Applies a Delta Bundle to the current bundle. @@ -51,6 +52,9 @@ class DeltaPatcher { }; } + this._lastNumModifiedFiles = + deltaBundle.pre.size + deltaBundle.post.size + deltaBundle.delta.size; + this._patchMap(this._lastBundle.pre, deltaBundle.pre); this._patchMap(this._lastBundle.post, deltaBundle.post); this._patchMap(this._lastBundle.modules, deltaBundle.delta); @@ -58,17 +62,27 @@ class DeltaPatcher { return this; } + /** + * Returns the number of modified files in the last received Delta. This is + * currently used to populate the `X-Metro-Files-Changed-Count` HTTP header + * when metro serves the whole JS bundle, and can potentially be removed once + * we only send the actual deltas to clients. + */ + getLastNumModifiedFiles(): number { + return this._lastNumModifiedFiles; + } + /** * Converts the current delta bundle to a standard string bundle, ready to * be interpreted by any JS VM. */ - stringifyCode() { + stringifyCode(): string { const code = this._getAllModules().map(m => m.code); return code.join('\n;'); } - stringifyMap({excludeSource}: {excludeSource?: boolean}) { + stringifyMap({excludeSource}: {excludeSource?: boolean}): string { const mappings = fromRawMappings(this._getAllModules()); return mappings.toString(undefined, {excludeSource}); diff --git a/packages/metro-bundler/src/DeltaBundler/__tests__/DeltaPatcher-test.js b/packages/metro-bundler/src/DeltaBundler/__tests__/DeltaPatcher-test.js index 1d9a3963..8dfbdd54 100644 --- a/packages/metro-bundler/src/DeltaBundler/__tests__/DeltaPatcher-test.js +++ b/packages/metro-bundler/src/DeltaBundler/__tests__/DeltaPatcher-test.js @@ -92,4 +92,29 @@ describe('DeltaPatcher', () => { .stringifyCode(), ).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(); + + expect(deltaPatcher.getLastNumModifiedFiles()).toEqual(3); + + deltaPatcher + .applyDelta({ + reset: 1, + pre: new Map([[1, null]]), + post: new Map(), + delta: new Map([[3, {code: 'different'}]]), + }) + .stringifyCode(); + + // A deleted module counts as a modified file. + expect(deltaPatcher.getLastNumModifiedFiles()).toEqual(2); + }); }); diff --git a/packages/metro-bundler/src/DeltaBundler/index.js b/packages/metro-bundler/src/DeltaBundler/index.js index 80e09df1..ea9c2b19 100644 --- a/packages/metro-bundler/src/DeltaBundler/index.js +++ b/packages/metro-bundler/src/DeltaBundler/index.js @@ -105,14 +105,20 @@ class DeltaBundler { }; } - async buildFullBundle(options: FullBuildOptions): Promise { - let output = (await this._getDeltaPatcher(options)).stringifyCode(); + async buildFullBundle( + options: FullBuildOptions, + ): Promise<{bundle: string, numModifiedFiles: number}> { + const deltaPatcher = await this._getDeltaPatcher(options); + let bundle = deltaPatcher.stringifyCode(); if (options.sourceMapUrl) { - output += '//# sourceMappingURL=' + options.sourceMapUrl; + bundle += '//# sourceMappingURL=' + options.sourceMapUrl; } - return output; + return { + bundle, + numModifiedFiles: deltaPatcher.getLastNumModifiedFiles(), + }; } async buildFullSourceMap(options: FullBuildOptions): Promise { diff --git a/packages/metro-bundler/src/Server/index.js b/packages/metro-bundler/src/Server/index.js index bbd4899c..d746fcf6 100644 --- a/packages/metro-bundler/src/Server/index.js +++ b/packages/metro-bundler/src/Server/index.js @@ -924,12 +924,13 @@ class Server { ); let bundle; + let numModifiedFiles; try { - bundle = await this._deltaBundler.buildFullBundle({ + ({bundle, numModifiedFiles} = await this._deltaBundler.buildFullBundle({ ...options, deltaBundleId: this.optionsHash(options), - }); + })); } catch (error) { this._handleError(res, this.optionsHash(options), error); @@ -947,22 +948,24 @@ class Server { debug('Responding with 304'); res.writeHead(304); res.end(); - - return; + } else { + res.setHeader(FILES_CHANGED_COUNT_HEADER, String(numModifiedFiles)); + res.setHeader('Content-Type', 'application/javascript'); + res.setHeader('ETag', etag); + res.setHeader('Content-Length', String(Buffer.byteLength(bundle))); + res.end(bundle); } - res.setHeader('Content-Type', 'application/javascript'); - res.setHeader('ETag', etag); - res.setHeader('Content-Length', String(Buffer.byteLength(bundle))); - res.end(bundle); - this._reporter.update({ buildID, type: 'bundle_build_done', }); debug('Finished response'); - log(createActionEndEntry(requestingBundleLogEntry)); + log({ + ...createActionEndEntry(requestingBundleLogEntry), + outdated_modules: numModifiedFiles, + }); } async _processSourceMapUsingDeltaBundler(