diff --git a/local-cli/server/util/debugger-ui/DeltaPatcher.js b/local-cli/server/util/debugger-ui/DeltaPatcher.js new file mode 100644 index 000000000..19f5cac99 --- /dev/null +++ b/local-cli/server/util/debugger-ui/DeltaPatcher.js @@ -0,0 +1,124 @@ +/** + * 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. + * + * @format + */ + +/** + * This file is a copy of the reference `DeltaPatcher`, located in + * metro-bundler. The reason to not reuse that file is that in this context + * we cannot have flow annotations or CJS syntax (since this file is directly) + * injected into a static HTML page. + * + * TODO: Find a simple and lightweight way to compile `DeltaPatcher` to avoid + * having this duplicated file. + */ +(function(global) { + 'use strict'; + + /** + * 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. + */ + class DeltaPatcher { + constructor() { + this._lastBundle = { + pre: new Map(), + post: new Map(), + modules: new Map(), + }; + this._initialized = false; + this._lastNumModifiedFiles = 0; + this._lastModifiedDate = new Date(); + } + + static get(id) { + 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. + */ + applyDelta(deltaBundle) { + // Make sure that the first received delta is a fresh one. + if (!this._initialized && !deltaBundle.reset) { + throw new Error( + 'DeltaPatcher should receive a fresh Delta when being initialized', + ); + } + + this._initialized = true; + + // Reset the current delta when we receive a fresh delta. + if (deltaBundle.reset) { + this._lastBundle = { + pre: new Map(), + post: new Map(), + modules: new Map(), + }; + } + + this._lastNumModifiedFiles = + deltaBundle.pre.size + deltaBundle.post.size + deltaBundle.delta.size; + + if (this._lastNumModifiedFiles > 0) { + this._lastModifiedDate = new Date(); + } + + this._patchMap(this._lastBundle.pre, deltaBundle.pre); + this._patchMap(this._lastBundle.post, deltaBundle.post); + this._patchMap(this._lastBundle.modules, deltaBundle.delta); + + 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() { + return this._lastNumModifiedFiles; + } + + getLastModifiedDate() { + return this._lastModifiedDate; + } + + getAllModules() { + return [].concat( + Array.from(this._lastBundle.pre.values()), + Array.from(this._lastBundle.modules.values()), + Array.from(this._lastBundle.post.values()), + ); + } + + _patchMap(original, patch) { + for (const [key, value] of patch.entries()) { + if (value == null) { + original.delete(key); + } else { + original.set(key, value); + } + } + } + } + + DeltaPatcher._deltaPatchers = new Map(); + + global.DeltaPatcher = DeltaPatcher; +})(window); diff --git a/local-cli/server/util/debugger-ui/deltaUrlToBlobUrl.js b/local-cli/server/util/debugger-ui/deltaUrlToBlobUrl.js new file mode 100644 index 000000000..e57b7e42f --- /dev/null +++ b/local-cli/server/util/debugger-ui/deltaUrlToBlobUrl.js @@ -0,0 +1,69 @@ +/** + * 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 + */ + +/* global Blob, URL: true */ + +(function(global) { + 'use strict'; + + let cachedBundleUrls = new Map(); + + /** + * Converts the passed delta URL into an URL object containing already the + * whole JS bundle Blob. + */ + async function deltaUrlToBlobUrl(deltaUrl) { + let cachedBundle = cachedBundleUrls.get(deltaUrl); + + const deltaBundleId = cachedBundle + ? `&deltaBundleId=${cachedBundle.id}` + : ''; + + const data = await fetch(deltaUrl + deltaBundleId); + const bundle = await data.json(); + + const deltaPatcher = global.DeltaPatcher.get(bundle.id).applyDelta({ + pre: new Map(bundle.pre), + post: new Map(bundle.post), + delta: new Map(bundle.delta), + reset: bundle.reset, + }); + + // If nothing changed, avoid recreating a bundle blob by reusing the + // previous one. + if (deltaPatcher.getLastNumModifiedFiles() === 0 && cachedBundle) { + return cachedBundle.url; + } + + // Clean up the previous bundle URL to not leak memory. + if (cachedBundle) { + URL.revokeObjectURL(cachedBundle.url); + } + + const blobContent = deltaPatcher.getAllModules(); + + // Build the blob with the whole JS bundle. + const blob = new Blob(blobContent, { + type: 'application/javascript', + }); + + const bundleUrl = URL.createObjectURL(blob); + cachedBundleUrls.set(deltaUrl, { + id: bundle.id, + url: bundleUrl, + }); + + return bundleUrl; + } + + global.deltaUrlToBlobUrl = deltaUrlToBlobUrl; +})(window || {}); diff --git a/local-cli/server/util/debugger-ui/index.html b/local-cli/server/util/debugger-ui/index.html index dba6d2a61..c21a21892 100644 --- a/local-cli/server/util/debugger-ui/index.html +++ b/local-cli/server/util/debugger-ui/index.html @@ -12,6 +12,8 @@