Skip to content

Commit 19d3284

Browse files
committed
Fix - removing an async dependency may incorrectly free its target
Summary: Metro's core `Graph` maintains a `CountingSet` of inverse "eager" dependencies for each module - zero inverse dependencies triggers a module's removal from the graph. Currently, this is not decremented with on dependency removals consistently with the way it is incremented. In particular, removing an async dependency (in combination with `options.lazy`) removes an inverse dependency, even though the reverse does not add one. This leads to incorrect deletion of module B from the graph when module A has both sync and async dependencies on B, and the async dependency is removed. ``` * **[Fix]**: Fix Fast Refresh edge case where removing an async dependency could incorrectly remove modules from the graph. ``` Reviewed By: motiz88 Differential Revision: D50309038 fbshipit-source-id: 4a44703ac31fc400736c5666a51ca26783a34d19
1 parent 8ef5a39 commit 19d3284

File tree

3 files changed

+75
-15
lines changed

3 files changed

+75
-15
lines changed

packages/metro/src/DeltaBundler/Graph.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,16 +485,19 @@ export class Graph<T = MixedOutput> {
485485
return;
486486
}
487487

488+
const module = this.dependencies.get(absolutePath);
489+
488490
if (options.lazy && dependency.data.data.asyncType != null) {
489491
this._decrementImportBundleReference(dependency, parentModule);
492+
} else if (module) {
493+
// Decrement inverseDependencies only if the dependency is not async,
494+
// mirroring the increment conditions in _addDependency.
495+
module.inverseDependencies.delete(parentModule.path);
490496
}
491497

492-
const module = this.dependencies.get(absolutePath);
493-
494498
if (!module) {
495499
return;
496500
}
497-
module.inverseDependencies.delete(parentModule.path);
498501
if (
499502
module.inverseDependencies.size > 0 ||
500503
this.entryPoints.has(absolutePath)

packages/metro/src/DeltaBundler/__tests__/Graph-test.js

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,10 @@ const Actions = {
103103
name?: string,
104104
data?: DependencyDataInput,
105105
} = {},
106-
) {
107-
Actions.addInferredDependency(path, dependencyPath, options);
106+
): string {
107+
const key = Actions.addInferredDependency(path, dependencyPath, options);
108108
files.add(path);
109+
return key;
109110
},
110111

111112
addInferredDependency(
@@ -120,15 +121,15 @@ const Actions = {
120121
name?: string,
121122
data?: DependencyDataInput,
122123
} = {},
123-
): void {
124+
): string {
124125
if (!mockedDependencies.has(path)) {
125126
Actions.createFile(path);
126127
}
127128
const deps = getMockDependency(path);
128129
const depName = name ?? dependencyPath.replace('/', '');
129130
const key = require('crypto')
130131
.createHash('sha1')
131-
.update(depName)
132+
.update([depName, data?.asyncType ?? '(null)'].join('\0'))
132133
.digest('base64');
133134
const dep = {
134135
name: depName,
@@ -149,11 +150,16 @@ const Actions = {
149150

150151
mockedDependencyTree.set(path, deps);
151152
mockedDependencies.add(dependencyPath);
153+
return key;
152154
},
153155

154156
removeDependency(path: string, dependencyPath: string) {
155157
Actions.removeInferredDependency(path, dependencyPath);
158+
files.add(path);
159+
},
156160

161+
removeDependencyByKey(path: string, key: string) {
162+
Actions.removeInferredDependencyByKey(path, key);
157163
files.add(path);
158164
},
159165

@@ -166,6 +172,15 @@ const Actions = {
166172
mockedDependencyTree.set(path, deps);
167173
}
168174
},
175+
176+
removeInferredDependencyByKey(path: string, key: string) {
177+
const deps = nullthrows(mockedDependencyTree.get(path));
178+
const index = deps.findIndex(({data}) => data.key === key);
179+
if (index !== -1) {
180+
deps.splice(index, 1);
181+
mockedDependencyTree.set(path, deps);
182+
}
183+
},
169184
};
170185

171186
function deferred(
@@ -1545,6 +1560,48 @@ describe('edge cases', () => {
15451560
});
15461561
});
15471562

1563+
it('removing an async dependency preserves existing sync dependency', async () => {
1564+
const asyncDependencyKey = Actions.addDependency('/bundle', '/foo', {
1565+
data: {
1566+
asyncType: 'async',
1567+
},
1568+
});
1569+
/*
1570+
┌─────────┐ ───▶ ┌──────┐ ┌──────┐
1571+
│ /bundle │ │ /foo │ ──▶ │ /bar │
1572+
└─────────┘ ···▶ └──────┘ └──────┘
1573+
1574+
1575+
1576+
┌──────┐
1577+
│ /baz │
1578+
└──────┘
1579+
*/
1580+
await graph.initialTraverseDependencies(localOptions);
1581+
files.clear();
1582+
Actions.removeDependencyByKey('/bundle', asyncDependencyKey);
1583+
1584+
// The synchronous dependency remains, /foo should not be removed.
1585+
/*
1586+
┌─────────┐ ┌──────┐ ┌──────┐
1587+
│ /bundle │ ──▶ │ /foo │ ──▶ │ /bar │
1588+
└─────────┘ └──────┘ └──────┘
1589+
1590+
1591+
1592+
┌──────┐
1593+
│ /baz │
1594+
└──────┘
1595+
*/
1596+
expect(
1597+
getPaths(await graph.traverseDependencies([...files], localOptions)),
1598+
).toEqual({
1599+
added: new Set([]),
1600+
deleted: new Set([]),
1601+
modified: new Set(['/bundle']),
1602+
});
1603+
});
1604+
15481605
it('changing a sync dependency to async is a deletion', async () => {
15491606
/*
15501607
┌─────────┐ ┌──────┐ ┌──────┐

packages/metro/src/DeltaBundler/__tests__/__snapshots__/Graph-test.js.snap

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ TestGraph {
55
"dependencies": Map {
66
"/bundle" => Object {
77
"dependencies": Map {
8-
"C+7Hteo/D9vJXQ3UfzxbwnXaijM=" => Object {
8+
"LB7P4TKrvfdUdViBXGaVopqz7Os=" => Object {
99
"absolutePath": "/foo",
1010
"data": Object {
1111
"data": Object {
1212
"asyncType": null,
13-
"key": "C+7Hteo/D9vJXQ3UfzxbwnXaijM=",
13+
"key": "LB7P4TKrvfdUdViBXGaVopqz7Os=",
1414
"locs": Array [],
1515
},
1616
"name": "foo",
@@ -34,23 +34,23 @@ TestGraph {
3434
},
3535
"/foo" => Object {
3636
"dependencies": Map {
37-
"Ys23Ag/5IOWqZCw9QGaVDdHwH00=" => Object {
37+
"W+de6an7x9bzpev84O0W/hS4K8U=" => Object {
3838
"absolutePath": "/bar",
3939
"data": Object {
4040
"data": Object {
4141
"asyncType": null,
42-
"key": "Ys23Ag/5IOWqZCw9QGaVDdHwH00=",
42+
"key": "W+de6an7x9bzpev84O0W/hS4K8U=",
4343
"locs": Array [],
4444
},
4545
"name": "bar",
4646
},
4747
},
48-
"u+lgol6jEdIdQGaek98gA7qbkKI=" => Object {
48+
"x6e9Oz1JO0QPfIBBjUad2qqGFjI=" => Object {
4949
"absolutePath": "/baz",
5050
"data": Object {
5151
"data": Object {
5252
"asyncType": null,
53-
"key": "u+lgol6jEdIdQGaek98gA7qbkKI=",
53+
"key": "x6e9Oz1JO0QPfIBBjUad2qqGFjI=",
5454
"locs": Array [],
5555
},
5656
"name": "baz",
@@ -132,12 +132,12 @@ TestGraph {
132132
"dependencies": Map {
133133
"/bundle" => Object {
134134
"dependencies": Map {
135-
"C+7Hteo/D9vJXQ3UfzxbwnXaijM=" => Object {
135+
"LB7P4TKrvfdUdViBXGaVopqz7Os=" => Object {
136136
"absolutePath": "/foo",
137137
"data": Object {
138138
"data": Object {
139139
"asyncType": null,
140-
"key": "C+7Hteo/D9vJXQ3UfzxbwnXaijM=",
140+
"key": "LB7P4TKrvfdUdViBXGaVopqz7Os=",
141141
"locs": Array [],
142142
},
143143
"name": "foo",

0 commit comments

Comments
 (0)