Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 52 additions & 26 deletions packages/metro/src/DeltaBundler/DeltaCalculator.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,17 @@ export default class DeltaCalculator<T> extends EventEmitter {
// processed (since we haven't actually created any delta). If we do not
// do so, asking for a delta after an error will produce an empty Delta,
// which is not correct.
modifiedFiles.forEach((file: string) => this._modifiedFiles.add(file));
deletedFiles.forEach((file: string) => this._deletedFiles.add(file));
addedFiles.forEach((file: string) => this._addedFiles.add(file));
// Changes that arrived while this delta was being built happened after
// the ones it took, so they are applied on top of them.
const laterAddedFiles = this._addedFiles;
const laterModifiedFiles = this._modifiedFiles;
const laterDeletedFiles = this._deletedFiles;
this._addedFiles = addedFiles;
this._modifiedFiles = modifiedFiles;
this._deletedFiles = deletedFiles;
laterAddedFiles.forEach((file: string) => this.#markAdded(file));
laterModifiedFiles.forEach((file: string) => this.#markModified(file));
laterDeletedFiles.forEach((file: string) => this.#markDeleted(file));

throw error;
} finally {
Expand Down Expand Up @@ -199,51 +207,60 @@ export default class DeltaCalculator<T> extends EventEmitter {
return false;
}

// Deleted+added = modified, otherwise added.
#markAdded(absolutePath: string): void {
if (this._deletedFiles.has(absolutePath)) {
this._deletedFiles.delete(absolutePath);
this._modifiedFiles.add(absolutePath);
} else {
this._addedFiles.add(absolutePath);
this._modifiedFiles.delete(absolutePath);
}
}

// Added+modified stays added, otherwise modified.
#markModified(absolutePath: string): void {
if (!this._addedFiles.has(absolutePath)) {
this._modifiedFiles.add(absolutePath);
}
this._deletedFiles.delete(absolutePath);
}

// Added+deleted = no change, otherwise deleted.
#markDeleted(absolutePath: string): void {
if (this._addedFiles.has(absolutePath)) {
this._addedFiles.delete(absolutePath);
} else {
this._deletedFiles.add(absolutePath);
this._modifiedFiles.delete(absolutePath);
}
}

_handleMultipleFileChanges = (changeEvent: ChangeEvent) => {
const {changes, logger, rootDir} = changeEvent;

// Process added files: deleted+added = modified, otherwise added
for (const [canonicalPath, metadata] of changes.addedFiles) {
debug('Handling add: %s', canonicalPath);
if (this.#shouldReset(canonicalPath, metadata)) {
this._requiresReset = true;
}
const absolutePath = path.join(rootDir, canonicalPath);
if (this._deletedFiles.has(absolutePath)) {
this._deletedFiles.delete(absolutePath);
this._modifiedFiles.add(absolutePath);
} else {
this._addedFiles.add(absolutePath);
this._modifiedFiles.delete(absolutePath);
}
this.#markAdded(path.join(rootDir, canonicalPath));
}

// Process modified files: added+modified stays added, otherwise modified
for (const [canonicalPath, metadata] of changes.modifiedFiles) {
debug('Handling change: %s', canonicalPath);
if (this.#shouldReset(canonicalPath, metadata)) {
this._requiresReset = true;
}
const absolutePath = path.join(rootDir, canonicalPath);
if (!this._addedFiles.has(absolutePath)) {
this._modifiedFiles.add(absolutePath);
}
this._deletedFiles.delete(absolutePath);
this.#markModified(path.join(rootDir, canonicalPath));
}

// Process removed files: added+deleted = no change, otherwise deleted
for (const [canonicalPath, metadata] of changes.removedFiles) {
debug('Handling delete: %s', canonicalPath);
if (this.#shouldReset(canonicalPath, metadata)) {
this._requiresReset = true;
}
const absolutePath = path.resolve(rootDir, canonicalPath);
if (this._addedFiles.has(absolutePath)) {
this._addedFiles.delete(absolutePath);
} else {
this._deletedFiles.add(absolutePath);
this._modifiedFiles.delete(absolutePath);
}
this.#markDeleted(path.resolve(rootDir, canonicalPath));
}

let changeId = changeEventIds.get(changeEvent);
Expand Down Expand Up @@ -287,6 +304,15 @@ export default class DeltaCalculator<T> extends EventEmitter {
}
});

// An added file that already has a module in the graph was deleted and
// recreated while a build was in flight. The graph never saw the deletion,
// so the module it holds is stale and needs revisiting.
addedFiles.forEach((filePath: string) => {
if (this._graph.dependencies.has(filePath)) {
modifiedFiles.add(filePath);
}
});

// NOTE(EvanBacon): This check adds extra complexity so we feature gate it
// to enable users to opt out.
if (this._options.unstable_allowRequireContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ describe.each(['posix', 'win32'])('DeltaCalculator (%s)', osPlatform => {
test('should calculate a delta after a file addition', async () => {
await deltaCalculator.getDelta({reset: false, shallow: false});

emitChange({addedFiles: ['foo']});
emitChange({addedFiles: ['unrelated']});

traverseDependencies.mockResolvedValueOnce({
added: new Map([[p('/foo'), fooModule]]),
Expand Down Expand Up @@ -560,6 +560,103 @@ describe.each(['posix', 'win32'])('DeltaCalculator (%s)', osPlatform => {
expect(traverseDependencies.mock.calls[0][0]).toEqual([p('/foo')]);
});

test('should traverse a file re-added while a failed delta was being built', async () => {
await deltaCalculator.getDelta({reset: false, shallow: false});

emitChange({removedFiles: ['foo']});

let rejectTraversal: (error: Error) => void = () => {};
traverseDependencies.mockReturnValueOnce(
new Promise((resolve, reject) => {
rejectTraversal = reject;
}),
);
const failedDelta = deltaCalculator.getDelta({
reset: false,
shallow: false,
});

// The file is recreated while the delta that saw it deleted is still being built.
emitChange({addedFiles: ['foo']});
rejectTraversal(new Error('Unable to resolve module'));
await expect(failedDelta).rejects.toBeInstanceOf(Error);

traverseDependencies.mockResolvedValueOnce({
added: new Map(),
modified: new Map([[p('/foo'), fooModule]]),
deleted: new Set(),
});
await deltaCalculator.getDelta({reset: false, shallow: false});

expect(traverseDependencies).toHaveBeenCalledTimes(2);
expect(traverseDependencies.mock.calls[1][0]).toContain(p('/foo'));
});

test('should not traverse a file deleted while a failed delta was being built', async () => {
await deltaCalculator.getDelta({reset: false, shallow: false});

emitChange({modifiedFiles: ['foo']});

let rejectTraversal: (error: Error) => void = () => {};
traverseDependencies.mockReturnValueOnce(
new Promise((resolve, reject) => {
rejectTraversal = reject;
}),
);
const failedDelta = deltaCalculator.getDelta({
reset: false,
shallow: false,
});

emitChange({removedFiles: ['foo']});
rejectTraversal(new Error('Unable to resolve module'));
await expect(failedDelta).rejects.toBeInstanceOf(Error);

traverseDependencies.mockResolvedValueOnce({
added: new Map(),
modified: new Map([[p('/bundle'), entryModule]]),
deleted: new Set([p('/foo')]),
});
await deltaCalculator.getDelta({reset: false, shallow: false});

expect(traverseDependencies).toHaveBeenCalledTimes(2);
expect(traverseDependencies.mock.calls[1][0]).toEqual([p('/bundle')]);
});

test('should traverse a file re-added while a successful delta was being built', async () => {
await deltaCalculator.getDelta({reset: false, shallow: false});

emitChange({removedFiles: ['foo']});

let resolveTraversal: (result: Result<$FlowFixMe>) => void = () => {};
traverseDependencies.mockReturnValueOnce(
new Promise(resolve => {
resolveTraversal = resolve;
}),
);
const delta = deltaCalculator.getDelta({reset: false, shallow: false});

// The file is recreated before its importer re-resolves it, so the build
// succeeds without revisiting the module the graph already holds.
emitChange({addedFiles: ['foo']});
resolveTraversal({
added: new Map(),
modified: new Map([[p('/bundle'), entryModule]]),
deleted: new Set(),
});
await delta;

traverseDependencies.mockResolvedValueOnce({
added: new Map(),
modified: new Map([[p('/foo'), fooModule]]),
deleted: new Set(),
});
await deltaCalculator.getDelta({reset: false, shallow: false});

expect(traverseDependencies).toHaveBeenCalledTimes(2);
expect(traverseDependencies.mock.calls[1][0]).toEqual([p('/foo')]);
});

test.each(['add', 'delete'])(
"should re-traverse everything after a symlink '%s'",
async eventType => {
Expand Down
Loading