From 7468864f5ef8404309f4c681a2aa104d400cf5fc Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Thu, 30 Sep 2021 19:19:58 -0700 Subject: [PATCH] HMR fixes for URL dependencies (#6986) --- packages/core/core/src/applyRuntimes.js | 22 +++++----- .../core/src/public/MutableBundleGraph.js | 4 +- .../core/src/requests/BundleGraphRequest.js | 34 ++++++++++++--- .../core/src/requests/ParcelBuildRequest.js | 7 ++- .../optimizers/image/src/ImageOptimizer.js | 4 ++ .../reporters/dev-server/src/HMRServer.js | 43 ++++++++++++++++--- packages/runtimes/hmr/src/HMRRuntime.js | 3 +- .../runtimes/hmr/src/loaders/hmr-runtime.js | 28 +++++++----- packages/runtimes/js/src/JSRuntime.js | 7 ++- 9 files changed, 116 insertions(+), 36 deletions(-) diff --git a/packages/core/core/src/applyRuntimes.js b/packages/core/core/src/applyRuntimes.js index fcd2a9a6707..c567ea10247 100644 --- a/packages/core/core/src/applyRuntimes.js +++ b/packages/core/core/src/applyRuntimes.js @@ -4,6 +4,7 @@ import type {ContentKey} from '@parcel/graph'; import type {Dependency, NamedBundle as INamedBundle} from '@parcel/types'; import type {SharedReference} from '@parcel/workers'; import type { + Asset, AssetGroup, Bundle as InternalBundle, Config, @@ -18,7 +19,7 @@ import path from 'path'; import assert from 'assert'; import invariant from 'assert'; import nullthrows from 'nullthrows'; -import AssetGraph, {nodeFromAssetGroup} from './AssetGraph'; +import {nodeFromAssetGroup} from './AssetGraph'; import BundleGraph from './public/BundleGraph'; import InternalBundleGraph, {bundleGraphEdgeTypes} from './BundleGraph'; import {NamedBundle} from './public/Bundle'; @@ -58,7 +59,7 @@ export default async function applyRuntimes({ previousDevDeps: Map, devDepRequests: Map, configs: Map, -|}): Promise { +|}): Promise> { let runtimes = await config.getRuntimes(); let connections: Array = []; @@ -137,11 +138,10 @@ export default async function applyRuntimes({ await runDevDepRequest(api, devDepRequest); } - let runtimesAssetGraph = await reconcileNewRuntimes( - api, - connections, - optionsRef, - ); + let { + assetGraph: runtimesAssetGraph, + changedAssets, + } = await reconcileNewRuntimes(api, connections, optionsRef); let runtimesGraph = InternalBundleGraph.fromAssetGraph( runtimesAssetGraph, @@ -245,13 +245,15 @@ export default async function applyRuntimes({ bundleGraph._graph.addEdge(dependencyNodeId, bundleGraphRuntimeNodeId); } } + + return changedAssets; } -async function reconcileNewRuntimes( +function reconcileNewRuntimes( api: RunAPI, connections: Array, optionsRef: SharedReference, -): Promise { +) { let assetGroups = connections.map(t => t.assetGroup); let request = createAssetGraphRequest({ name: 'Runtimes', @@ -260,5 +262,5 @@ async function reconcileNewRuntimes( }); // rebuild the graph - return (await api.runRequest(request, {force: true})).assetGraph; + return api.runRequest(request, {force: true}); } diff --git a/packages/core/core/src/public/MutableBundleGraph.js b/packages/core/core/src/public/MutableBundleGraph.js index e59a2d8ac9c..59a8028bd5d 100644 --- a/packages/core/core/src/public/MutableBundleGraph.js +++ b/packages/core/core/src/public/MutableBundleGraph.js @@ -191,7 +191,9 @@ export default class MutableBundleGraph extends BundleGraph id: bundleId, value: { id: bundleId, - hashReference: HASH_REF_PREFIX + bundleId, + hashReference: this.#options.shouldContentHash + ? HASH_REF_PREFIX + bundleId + : bundleId.slice(-8), type: opts.entryAsset ? opts.entryAsset.type : opts.type, env: opts.env ? environmentToInternalEnvironment(opts.env) diff --git a/packages/core/core/src/requests/BundleGraphRequest.js b/packages/core/core/src/requests/BundleGraphRequest.js index 13da2f1dbac..36952dc9e24 100644 --- a/packages/core/core/src/requests/BundleGraphRequest.js +++ b/packages/core/core/src/requests/BundleGraphRequest.js @@ -5,6 +5,7 @@ import type {SharedReference} from '@parcel/workers'; import type ParcelConfig, {LoadedPlugin} from '../ParcelConfig'; import type {StaticRunOpts, RunAPI} from '../RequestTracker'; import type { + Asset, Bundle as InternalBundle, Config, DevDepRequest, @@ -63,10 +64,15 @@ type RunInput = {| ...StaticRunOpts, |}; +type BundleGraphResult = {| + bundleGraph: InternalBundleGraph, + changedAssets: Map, +|}; + type BundleGraphRequest = {| id: string, +type: 'bundle_graph_request', - run: RunInput => Async, + run: RunInput => Async, input: BundleGraphRequestInput, |}; @@ -164,7 +170,7 @@ class BundlerRunner { await runDevDepRequest(this.api, devDepRequest); } - async bundle(graph: AssetGraph): Promise { + async bundle(graph: AssetGraph): Promise { report({ type: 'buildProgress', phase: 'bundling', @@ -194,7 +200,13 @@ class BundlerRunner { // Deserialize, and store the original buffer in an in memory cache so we avoid // re-serializing it when sending to workers, and in build mode, when writing to cache on shutdown. let graph = deserializeToCache(cached); - this.api.storeResult(graph, cacheKey); + this.api.storeResult( + { + bundleGraph: graph, + changedAssets: new Map(), + }, + cacheKey, + ); return graph; } } @@ -273,7 +285,7 @@ class BundlerRunner { await this.nameBundles(internalBundleGraph); - await applyRuntimes({ + let changedAssets = await applyRuntimes({ bundleGraph: internalBundleGraph, api: this.api, config: this.config, @@ -300,8 +312,18 @@ class BundlerRunner { // Recompute the cache key to account for new dev dependencies and invalidations. cacheKey = await this.getCacheKey(graph); - this.api.storeResult(internalBundleGraph, cacheKey); - return internalBundleGraph; + this.api.storeResult( + { + bundleGraph: internalBundleGraph, + changedAssets: new Map(), + }, + cacheKey, + ); + + return { + bundleGraph: internalBundleGraph, + changedAssets, + }; } async getCacheKey(assetGraph: AssetGraph): Promise { diff --git a/packages/core/core/src/requests/ParcelBuildRequest.js b/packages/core/core/src/requests/ParcelBuildRequest.js index ea5a2a00215..72a51011338 100644 --- a/packages/core/core/src/requests/ParcelBuildRequest.js +++ b/packages/core/core/src/requests/ParcelBuildRequest.js @@ -73,7 +73,12 @@ async function run({input, api, options}: RunInput) { optionsRef, }); - let bundleGraph = await api.runRequest(bundleGraphRequest); + let {bundleGraph, changedAssets: changedRuntimeAssets} = await api.runRequest( + bundleGraphRequest, + ); + for (let [id, asset] of changedRuntimeAssets) { + changedAssets.set(id, asset); + } // $FlowFixMe Added in Flow 0.121.0 upgrade in #4381 (Windows only) dumpGraphToGraphViz(bundleGraph._graph, 'BundleGraph', bundleGraphEdgeTypes); diff --git a/packages/optimizers/image/src/ImageOptimizer.js b/packages/optimizers/image/src/ImageOptimizer.js index 98be1160f89..e12cb53d3fc 100644 --- a/packages/optimizers/image/src/ImageOptimizer.js +++ b/packages/optimizers/image/src/ImageOptimizer.js @@ -5,6 +5,10 @@ import {optimize} from '../native'; export default (new Optimizer({ async optimize({bundle, contents}) { + if (!bundle.env.shouldOptimize) { + return {contents}; + } + let buffer = await blobToBuffer(contents); let optimized = optimize(bundle.type, buffer); return { diff --git a/packages/reporters/dev-server/src/HMRServer.js b/packages/reporters/dev-server/src/HMRServer.js index c245fb92fdc..15632b2e637 100644 --- a/packages/reporters/dev-server/src/HMRServer.js +++ b/packages/reporters/dev-server/src/HMRServer.js @@ -1,6 +1,6 @@ // @flow -import type {BuildSuccessEvent, PluginOptions} from '@parcel/types'; +import type {BuildSuccessEvent, Dependency, PluginOptions} from '@parcel/types'; import type {Diagnostic} from '@parcel/diagnostic'; import type {AnsiDiagnosticResult} from '@parcel/utils'; import type {ServerError, HMRServerOptions} from './types.js.flow'; @@ -95,11 +95,35 @@ export default class HMRServer { async emitUpdate(event: BuildSuccessEvent) { this.unresolvedError = null; - let changedAssets = Array.from(event.changedAssets.values()); - if (changedAssets.length === 0) return; + let changedAssets = new Set(event.changedAssets.values()); + if (changedAssets.size === 0) return; let queue = new PromiseQueue({maxConcurrent: FS_CONCURRENCY}); for (let asset of changedAssets) { + if (asset.type !== 'js') { + // If all of the incoming dependencies of the asset actually resolve to a JS asset + // rather than the original, we can mark the runtimes as changed instead. URL runtimes + // have a cache busting query param added with HMR enabled which will trigger a reload. + let runtimes = new Set(); + let incomingDeps = event.bundleGraph.getIncomingDependencies(asset); + let isOnlyReferencedByRuntimes = incomingDeps.every(dep => { + let resolved = event.bundleGraph.getResolvedAsset(dep); + let isRuntime = resolved?.type === 'js' && resolved !== asset; + if (resolved && isRuntime) { + runtimes.add(resolved); + } + return isRuntime; + }); + + if (isOnlyReferencedByRuntimes) { + for (let runtime of runtimes) { + changedAssets.add(runtime); + } + + continue; + } + } + queue.add(async () => { let dependencies = event.bundleGraph.getDependencies(asset); let depsByBundle = {}; @@ -108,7 +132,7 @@ export default class HMRServer { for (let dep of dependencies) { let resolved = event.bundleGraph.getResolvedAsset(dep, bundle); if (resolved) { - deps[dep.specifier] = event.bundleGraph.getAssetPublicId( + deps[getSpecifier(dep)] = event.bundleGraph.getAssetPublicId( resolved, ); } @@ -119,7 +143,8 @@ export default class HMRServer { return { id: event.bundleGraph.getAssetPublicId(asset), type: asset.type, - output: await asset.getCode(), + // No need to send the contents of non-JS assets to the client. + output: asset.type === 'js' ? await asset.getCode() : '', envHash: asset.env.id, depsByBundle, }; @@ -153,3 +178,11 @@ export default class HMRServer { } } } + +function getSpecifier(dep: Dependency): string { + if (typeof dep.meta.placeholder === 'string') { + return dep.meta.placeholder; + } + + return dep.specifier; +} diff --git a/packages/runtimes/hmr/src/HMRRuntime.js b/packages/runtimes/hmr/src/HMRRuntime.js index 5b953d1e680..5215b5c7e1d 100644 --- a/packages/runtimes/hmr/src/HMRRuntime.js +++ b/packages/runtimes/hmr/src/HMRRuntime.js @@ -15,7 +15,8 @@ export default (new Runtime({ bundle.type !== 'js' || !options.hmrOptions || bundle.env.isLibrary || - bundle.env.isWorklet() + bundle.env.isWorklet() || + bundle.env.sourceType === 'script' ) { return; } diff --git a/packages/runtimes/hmr/src/loaders/hmr-runtime.js b/packages/runtimes/hmr/src/loaders/hmr-runtime.js index 4b8da0d14d1..a5c0a9bb167 100644 --- a/packages/runtimes/hmr/src/loaders/hmr-runtime.js +++ b/packages/runtimes/hmr/src/loaders/hmr-runtime.js @@ -289,15 +289,14 @@ function hmrApply(bundle /*: ParcelRequire */, asset /*: HMRAsset */) { if (asset.type === 'css') { reloadCSS(); - return; - } - - let deps = asset.depsByBundle[bundle.HMR_BUNDLE_ID]; - if (deps) { - var fn = new Function('require', 'module', 'exports', asset.output); - modules[asset.id] = [fn, deps]; - } else if (bundle.parent) { - hmrApply(bundle.parent, asset); + } else if (asset.type === 'js') { + let deps = asset.depsByBundle[bundle.HMR_BUNDLE_ID]; + if (deps) { + var fn = new Function('require', 'module', 'exports', asset.output); + modules[asset.id] = [fn, deps]; + } else if (bundle.parent) { + hmrApply(bundle.parent, asset); + } } } @@ -322,7 +321,7 @@ function hmrAcceptCheck( } if (checkedAssets[id]) { - return; + return true; } checkedAssets[id] = true; @@ -335,7 +334,14 @@ function hmrAcceptCheck( return true; } - return getParents(module.bundle.root, id).some(function(v) { + let parents = getParents(module.bundle.root, id); + + // If no parents, the asset is new. Prevent reloading the page. + if (!parents.length) { + return true; + } + + return parents.some(function(v) { return hmrAcceptCheck(v[0], v[1], null); }); } diff --git a/packages/runtimes/js/src/JSRuntime.js b/packages/runtimes/js/src/JSRuntime.js index 21e19ef59bc..1bb52c84042 100644 --- a/packages/runtimes/js/src/JSRuntime.js +++ b/packages/runtimes/js/src/JSRuntime.js @@ -589,7 +589,12 @@ function getRelativePathExpr( ); } - return JSON.stringify(relativePath); + let res = JSON.stringify(relativePath); + if (options.hmrOptions) { + res += ' + "?" + Date.now()'; + } + + return res; } function getAbsoluteUrlExpr(relativePathExpr: string, bundle: NamedBundle) {