Skip to content

Commit

Permalink
Improve HMR (#10017)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett authored Nov 24, 2024
1 parent a53f8f3 commit 79c8d98
Show file tree
Hide file tree
Showing 17 changed files with 160 additions and 31 deletions.
8 changes: 8 additions & 0 deletions packages/core/core/src/AssetGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,14 @@ export default class AssetGraph extends ContentGraph<AssetGraphNode> {
...depNode.value.meta,
...existing.value.resolverMeta,
};
depNode.value.resolverMeta = existing.value.resolverMeta;
}
if (
existing?.type === 'dependency' &&
existing.value.resolverPriority != null
) {
depNode.value.priority = existing.value.resolverPriority;
depNode.value.resolverPriority = existing.value.resolverPriority;
}
let dependentAsset = dependentAssets.find(
a => a.uniqueKey === dep.specifier,
Expand Down
3 changes: 2 additions & 1 deletion packages/core/core/src/requests/PathRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,8 @@ export class ResolverRunner {
}

if (result.priority != null) {
dependency.priority = Priority[result.priority];
dependency.priority = dependency.resolverPriority =
Priority[result.priority];
}

if (result.invalidateOnEnvChange) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/core/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export type Dependency = {|
customPackageConditions?: Array<string>,
meta: Meta,
resolverMeta?: ?Meta,
resolverPriority?: $Values<typeof Priority>,
target: ?Target,
sourceAssetId: ?string,
sourcePath: ?ProjectPath,
Expand Down
40 changes: 37 additions & 3 deletions packages/core/integration-tests/test/hmr.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,15 +516,49 @@ module.hot.dispose((data) => {
});

it('should work across bundles', async function () {
let {reloaded} = await testHMRClient('hmr-dynamic', outputs => {
let {reloaded, outputs} = await testHMRClient('hmr-dynamic', outputs => {
assert.deepEqual(outputs, [3]);
return {
'local.js': 'exports.a = 5; exports.b = 5;',
};
});

// assert.deepEqual(outputs, [3, 10]);
assert(reloaded); // TODO: this should eventually not reload...
assert.deepEqual(outputs, [3, 10]);
assert(!reloaded);
});

it('should bubble to parents if child returns additional parents', async function () {
let {reloaded, outputs} = await testHMRClient('hmr-parents', outputs => {
assert.deepEqual(outputs, ['child 2', 'root']);
return {
'updated.js': 'exports.a = 3;',
};
});

assert.deepEqual(outputs, [
'child 2',
'root',
'child 3',
'accept child',
'root',
'accept root',
]);
assert(!reloaded);
});

it('should bubble to parents and reload if they do not accept', async function () {
let {reloaded, outputs} = await testHMRClient(
'hmr-parents-reload',
outputs => {
assert.deepEqual(outputs, ['child 2', 'root']);
return {
'updated.js': 'exports.a = 3;',
};
},
);

assert.deepEqual(outputs, []);
assert(reloaded);
});

it('should work with urls', async function () {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const updated = require('./updated');

output('child ' + updated.a);
module.hot.accept(getParents => {
output('accept child');
return getParents();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require('./middle');

output('root');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('./child');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports.a = 2;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const updated = require('./updated');

output('child ' + updated.a);
module.hot.accept(getParents => {
output('accept child');
return getParents();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require('./middle');

output('root');
module.hot.accept(() => {
output('accept root');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('./child');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports.a = 2;
10 changes: 10 additions & 0 deletions packages/packagers/js/src/DevPackager.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ export class DevPackager {
}
}

// Add dependencies for parcelRequire calls added by runtimes
// so that the HMR runtime can correctly traverse parents.
let hmrDeps = asset.meta.hmrDeps;
if (this.options.hmrOptions && Array.isArray(hmrDeps)) {
for (let id of hmrDeps) {
invariant(typeof id === 'string');
deps[id] = id;
}
}

let {code, mapBuffer} = results[i];
let output = code || '';
wrapped +=
Expand Down
53 changes: 34 additions & 19 deletions packages/runtimes/hmr/src/loaders/hmr-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ module.bundle.Module = Module;
module.bundle.hotData = {};

var checkedAssets /*: {|[string]: boolean|} */,
disposedAssets /*: {|[string]: boolean|} */,
assetsToDispose /*: Array<[ParcelRequire, string]> */,
assetsToAccept /*: Array<[ParcelRequire, string]> */;

Expand Down Expand Up @@ -134,6 +135,7 @@ if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') {
// $FlowFixMe
ws.onmessage = async function (event /*: {data: string, ...} */) {
checkedAssets = ({} /*: {|[string]: boolean|} */);
disposedAssets = ({} /*: {|[string]: boolean|} */);
assetsToAccept = [];
assetsToDispose = [];

Expand Down Expand Up @@ -171,19 +173,10 @@ if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') {

await hmrApplyUpdates(assets);

// Dispose all old assets.
let processedAssets = ({} /*: {|[string]: boolean|} */);
for (let i = 0; i < assetsToDispose.length; i++) {
let id = assetsToDispose[i][1];

if (!processedAssets[id]) {
hmrDispose(assetsToDispose[i][0], id);
processedAssets[id] = true;
}
}
hmrDisposeQueue();

// Run accept callbacks. This will also re-execute other disposed assets in topological order.
processedAssets = {};
let processedAssets = {};
for (let i = 0; i < assetsToAccept.length; i++) {
let id = assetsToAccept[i][1];

Expand Down Expand Up @@ -593,6 +586,20 @@ function hmrAcceptCheckOne(
}
}

function hmrDisposeQueue() {
// Dispose all old assets.
for (let i = 0; i < assetsToDispose.length; i++) {
let id = assetsToDispose[i][1];

if (!disposedAssets[id]) {
hmrDispose(assetsToDispose[i][0], id);
disposedAssets[id] = true;
}
}

assetsToDispose = [];
}

function hmrDispose(bundle /*: ParcelRequire */, id /*: string */) {
var cached = bundle.cache[id];
bundle.hotData[id] = {};
Expand All @@ -616,18 +623,26 @@ function hmrAccept(bundle /*: ParcelRequire */, id /*: string */) {
// Run the accept callbacks in the new version of the module.
var cached = bundle.cache[id];
if (cached && cached.hot && cached.hot._acceptCallbacks.length) {
let assetsToAlsoAccept = [];
cached.hot._acceptCallbacks.forEach(function (cb) {
var assetsToAlsoAccept = cb(function () {
let additionalAssets = cb(function () {
return getParents(module.bundle.root, id);
});
if (assetsToAlsoAccept && assetsToAccept.length) {
assetsToAlsoAccept.forEach(function (a) {
hmrDispose(a[0], a[1]);
});

// $FlowFixMe[method-unbinding]
assetsToAccept.push.apply(assetsToAccept, assetsToAlsoAccept);
if (Array.isArray(additionalAssets) && additionalAssets.length) {
assetsToAlsoAccept.push(...additionalAssets);
}
});

if (assetsToAccept.length) {
let handled = assetsToAlsoAccept.every(function (a) {
return hmrAcceptCheck(a[0], a[1]);
});

if (!handled) {
return fullReload();
}

hmrDisposeQueue();
}
}
}
12 changes: 5 additions & 7 deletions packages/runtimes/js/src/JSRuntime.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default (new Runtime({
// The linker handles this for scope-hoisting.
assets.push({
filePath: __filename,
code: `module.exports = Promise.resolve(module.bundle.root(${JSON.stringify(
code: `module.exports = Promise.resolve(parcelRequire(${JSON.stringify(
bundleGraph.getAssetPublicId(resolved.value),
)}))`,
dependency,
Expand Down Expand Up @@ -422,10 +422,7 @@ function getLoaderRuntime({
}

if (mainBundle.type === 'js') {
let parcelRequire = bundle.env.shouldScopeHoist
? 'parcelRequire'
: 'module.bundle.root';
loaderCode += `.then(() => ${parcelRequire}('${bundleGraph.getAssetPublicId(
loaderCode += `.then(() => parcelRequire('${bundleGraph.getAssetPublicId(
bundleGraph.getAssetById(bundleGroup.entryAssetId),
)}'))`;
}
Expand Down Expand Up @@ -552,7 +549,7 @@ function getURLRuntime(
to: NamedBundle,
options: PluginOptions,
): RuntimeAsset {
let relativePathExpr = getRelativePathExpr(from, to, options);
let relativePathExpr = getRelativePathExpr(from, to, options, true);
let code;

if (dependency.meta.webworker === true && !from.env.isLibrary) {
Expand Down Expand Up @@ -633,10 +630,11 @@ function getRelativePathExpr(
from: NamedBundle,
to: NamedBundle,
options: PluginOptions,
isURL = to.type !== 'js',
): string {
let relativePath = relativeBundlePath(from, to, {leadingDotSlash: false});
let res = JSON.stringify(relativePath);
if (options.hmrOptions) {
if (isURL && options.hmrOptions) {
res += ' + "?" + Date.now()';
}

Expand Down
29 changes: 28 additions & 1 deletion packages/transformers/js/core/src/dependency_collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use swc_core::{
ecma::{
ast::{self, Callee, IdentName, MemberProp},
atoms::{js_word, JsWord},
utils::stack_size::maybe_grow_default,
utils::{member_expr, stack_size::maybe_grow_default},
visit::{Fold, FoldWith},
},
};
Expand Down Expand Up @@ -93,6 +93,8 @@ pub enum DependencyKind {
///
/// * https://parceljs.org/features/node-emulation/#inlining-fs.readfilesync
File,
/// `parcelRequire` call.
Id,
}

impl fmt::Display for DependencyKind {
Expand Down Expand Up @@ -516,6 +518,31 @@ impl<'a> Fold for DependencyCollector<'a> {
))));
return call;
}
"parcelRequire" => {
if let Some(ast::ExprOrSpread { expr, .. }) = node.args.first() {
if let Some((id, span)) = match_str(expr) {
self.items.push(DependencyDescriptor {
kind: DependencyKind::Id,
loc: SourceLocation::from(&self.source_map, span),
specifier: id,
attributes: None,
is_optional: false,
is_helper: false,
source_type: None,
placeholder: None,
});
}
}
let mut call = node.fold_children_with(self);
if !self.config.scope_hoist {
call.callee = ast::Callee::Expr(Box::new(ast::Expr::Member(member_expr!(
Default::default(),
call.span,
module.bundle.root
))));
}
return call;
}
_ => return node.fold_children_with(self),
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/transformers/js/src/JSTransformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {transform, transformAsync} from '@parcel/rust';
import browserslist from 'browserslist';
import semver from 'semver';
import nullthrows from 'nullthrows';
import invariant from 'assert';
import ThrowableDiagnostic, {
encodeJSONKeyComponent,
convertSourceLocationToHighlight,
Expand Down Expand Up @@ -756,6 +757,13 @@ export default (new Transformer({
});
} else if (dep.kind === 'File') {
asset.invalidateOnFileChange(dep.specifier);
} else if (dep.kind === 'Id') {
// Record parcelRequire calls so that the dev packager can add them as dependencies.
// This allows the HMR runtime to collect parents across async boundaries (through runtimes).
// TODO: ideally this would result as an actual dep in the graph rather than asset.meta.
asset.meta.hmrDeps ??= [];
invariant(Array.isArray(asset.meta.hmrDeps));
asset.meta.hmrDeps.push(dep.specifier);
} else {
let meta: JSONObject = {kind: dep.kind};
if (dep.attributes) {
Expand Down

0 comments on commit 79c8d98

Please sign in to comment.