Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
0065632
rush-lib: allow daemon builds with plugins scoped to other commands; …
TheLarkInn Sep 23, 2026
eafab7e
Add change files
TheLarkInn Sep 23, 2026
0da26ea
docs: record Windows validation and daemon process check in dogfoodin…
TheLarkInn Sep 23, 2026
ef9a3fc
rush-daemon: hide console windows of tools the detached Windows daemo…
TheLarkInn Sep 23, 2026
278a3d2
docs: describe hidden Windows tool consoles and silent up-to-date builds
TheLarkInn Sep 23, 2026
8122fae
rush-cli-client: start without loading the Rush engine; cheaper runti…
TheLarkInn Sep 23, 2026
667af71
rush-client-core: let the startup helper finish a slow daemon start
TheLarkInn Sep 23, 2026
a277622
rush-lib: resolve rig profiles asynchronously in the daemon project l…
TheLarkInn Sep 23, 2026
3b948f2
docs: describe the first daemon start from a refreshed Windows snapshot
TheLarkInn Sep 23, 2026
531fd51
Merge remote-tracking branch 'origin/main' into thelarkinn-dogfood-ru…
TheLarkInn Sep 24, 2026
915e134
Merge remote-tracking branch 'origin/main' into thelarkinn-dogfood-ru…
TheLarkInn Sep 24, 2026
8c902ee
[rush] rushd: fix the bugs found by dogfooding the Rush daemon (stack…
TheLarkInn Sep 30, 2026
ef3c471
Squash change files: one per project
TheLarkInn Sep 30, 2026
85f4440
Change files: orphaned operations are stopped only on Linux
TheLarkInn Sep 30, 2026
bf50aef
Record origin/main 982b33b16c as merged
TheLarkInn Sep 30, 2026
fc76063
Merge main into the Rush daemon dogfood branch
TheLarkInn Sep 30, 2026
46dcd64
Give the deferred cache write tests a 30-second timeout
TheLarkInn Sep 30, 2026
3b08762
[docs] Restore the edited file in the daemon example instead of disca…
TheLarkInn Sep 30, 2026
0ffbc34
[rush-lib] use Rush-owned telemetry tag (review item M5)
TheLarkInn Sep 30, 2026
782bcbf
[rush-daemon-transport] clarify Linux orphan reaping (review item M7)
TheLarkInn Sep 30, 2026
655661e
[rush-lib] treat hashed assets as outputs (review item M14)
TheLarkInn Sep 30, 2026
d6a6ffd
[rush-lib] clear incremental guard records (review item M13)
TheLarkInn Sep 30, 2026
3f112e5
[rush-lib] share input change verification (review item M15)
TheLarkInn Sep 30, 2026
b3aa7b2
[rush-daemon] share output folder walking (review item M6)
TheLarkInn Sep 30, 2026
f4c80e4
[heft] Share watchpack pending-event wait (review item M8)
TheLarkInn Sep 30, 2026
18ddde6
[heft-webpack5-plugin] Match emitted asset fragment handling (review …
TheLarkInn Sep 30, 2026
b17f1ad
[rush-cli-client] clarify force stop owner note (CodeQL alert in daem…
TheLarkInn Sep 30, 2026
54fd89a
[rush-cli-client] preserve cancelled transport exits (review item M20)
TheLarkInn Sep 30, 2026
402e9b6
[rush-cli-client] share restart cause wording (review item M21)
TheLarkInn Sep 30, 2026
dc6411e
[rush-cli-client] make daemon tests portable (review item M23)
TheLarkInn Sep 30, 2026
c7b198d
[rush-lib] honor disabled build cache in daemon requests (review item…
TheLarkInn Sep 30, 2026
51d234b
[rush-lib] close exited warm workers with live streams (review item H2)
TheLarkInn Sep 30, 2026
6518316
[rush-lib] include initialize taps in engine sharing checks (review i…
TheLarkInn Sep 30, 2026
bbd8a93
[rush-daemon-transport] retain group records until close (review item…
TheLarkInn Sep 30, 2026
98c1b45
[rush-client-core] expire inactive startup helpers (review items M1, …
TheLarkInn Sep 30, 2026
b52317b
[rush-client-core] reap reused daemon PID operations (review item M18)
TheLarkInn Sep 30, 2026
0bea0f2
[rush-client-core] pass startup test paths as args (CodeQL alerts in …
TheLarkInn Sep 30, 2026
bbc0963
[rush-daemon] Preserve child process promisify results (review item M2)
TheLarkInn Sep 30, 2026
3dadc20
[rush-daemon] Stop registry lookup process groups (review item M3)
TheLarkInn Sep 30, 2026
6b4ae57
[rush-daemon] Wait for operation completion events (review item M10)
TheLarkInn Sep 30, 2026
83d6207
[rush-daemon] Escape fixture deadline script text (CodeQL alert in Da…
TheLarkInn Sep 30, 2026
0eaa69d
[rush-daemon] Respect cancellation while joining (review item M11)
TheLarkInn Sep 30, 2026
0cc7110
[rush-daemon] Notify joined requests before scheduling (review item M12)
TheLarkInn Sep 30, 2026
574d7df
[rush-daemon] Document RUSHD_TELEMETRY_TAG (review item M5)
TheLarkInn Sep 30, 2026
85851cd
[rush-daemon] Escape the early-failure fixture script text (CodeQL al…
TheLarkInn Sep 30, 2026
9ea065b
[rush-daemon] Share restart preparation helpers (review item M4)
TheLarkInn Sep 30, 2026
4e69a64
[rush-daemon] Yield custom incremental early failures (review item M9)
TheLarkInn Sep 30, 2026
27a2e3c
[rush-daemon] Clear reverted restart tickets (review item M4)
TheLarkInn Sep 30, 2026
2aa2815
[rush-client-core] Reclaim a reused daemon PID only when its endpoint…
TheLarkInn Sep 30, 2026
db039e2
[rush-lib] Limit the disabled build cache's full runs to daemon engin…
TheLarkInn Sep 30, 2026
80eea31
[rush-lib] Close a warm worker that a signal killed (review item H2)
TheLarkInn Sep 30, 2026
157bce9
[rush-lib] Declare the built-in build cache plugins command-agnostic …
TheLarkInn Sep 30, 2026
6f182d3
[heft] Wait the full second when a directory watcher's internals are …
TheLarkInn Sep 30, 2026
3456330
[rush-daemon] Report why the daemon installer failed (review item M3)
TheLarkInn Sep 30, 2026
016f87a
[rush-daemon] Tell a joining request that it started only once the it…
TheLarkInn Sep 30, 2026
ed5bc24
[rush-daemon] Say that only Linux stops a crashed daemon's operations…
TheLarkInn Sep 30, 2026
30d98e6
Give the native lock tests a 30-second timeout
TheLarkInn Sep 30, 2026
8bc591c
[rush-daemon] Keep the routing test fixture independent of the number…
TheLarkInn Sep 30, 2026
66207a1
[rush-daemon] Run a full idle collection on Node.js 20
TheLarkInn Sep 30, 2026
6d9bf1c
[rush-lib] Unlink the project configuration test junction
TheLarkInn Sep 30, 2026
97dd882
[rush-lib] Build the project shrinkwrap path natively
TheLarkInn Sep 30, 2026
169a3b8
[rush-lib] Create engine parser fixtures under the real temp path
TheLarkInn Sep 30, 2026
b1c62d5
[rush-lib] Normalize the event sink output assertion
TheLarkInn Sep 30, 2026
04ae55a
[package-deps-hash] Pin line endings in repo state tests
TheLarkInn Sep 30, 2026
f670eb3
[package-deps-hash] Don't use the untracked cache on Windows
TheLarkInn Sep 30, 2026
cda5138
[rush-lib] Clear cache output folders sequentially
TheLarkInn Sep 30, 2026
83d8a07
[rush-lib] Make the deferred cache write tests portable to Windows
TheLarkInn Sep 30, 2026
e5098d4
[rush-lib] Make the watch tests portable to Windows
TheLarkInn Sep 30, 2026
1a4dcf3
[rush-daemon] Use the long temp folder path in Windows fixtures
TheLarkInn Sep 30, 2026
9c1a5cf
[rush-daemon-transport] Read an invalid name in the file change test
TheLarkInn Sep 30, 2026
5f315f2
[rush-daemon] Hold the stop in the cancel-while-stopping test
TheLarkInn Sep 30, 2026
0b39e1e
[rush-daemon] Expect the Windows lock behavior in the lock tests
TheLarkInn Sep 30, 2026
77a73e0
[rush-client-core] Wait until sleep waits in the live owner test
TheLarkInn Sep 30, 2026
43d3c65
[rush-cli-client] Make the client tests portable to CI runners
TheLarkInn Sep 30, 2026
71dd2b4
[rush-client-core] Make the resend and restart tests portable to CI r…
TheLarkInn Sep 30, 2026
058214b
[rush-cli-client] Drop internal references from the tests
TheLarkInn Sep 30, 2026
bcd35dd
[rush-client-core] Drop the internal task number from a test name
TheLarkInn Sep 30, 2026
76ce872
[rush-daemon] Drop internal references from the tests
TheLarkInn Sep 30, 2026
57d3bff
[rush-lib] Drop internal references from the engine tests
TheLarkInn Sep 30, 2026
1dcc3e5
[rush-daemon-protocol] Name the wait timeout check after what it checks
TheLarkInn Sep 30, 2026
f2b337b
[rush-daemon] Wait for the stopped npm to exit in the registry deadli…
TheLarkInn Sep 30, 2026
351b10b
[rush-lib] Build the project dependency file path as before
TheLarkInn Sep 30, 2026
14d1019
[rush-daemon] Expect the project dependency file path as Rush builds it
TheLarkInn Sep 30, 2026
94af32d
[package-deps-hash] Turn off autocrlf in the remaining test repositories
TheLarkInn Sep 30, 2026
6845618
[package-deps-hash] Make the attributes test independent of when the …
TheLarkInn Sep 30, 2026
7a01c58
[rush-lib] Wait for the watcher's snapshots before removing the watch…
TheLarkInn Sep 30, 2026
e7c6963
[rush-daemon] Make the fingerprint tests independent of the resolutio…
TheLarkInn Sep 30, 2026
7ec8620
[rush-lib] Wait for the watcher's snapshots before the watch session …
TheLarkInn Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ for large scale TypeScript monorepos.
- [API Documenter](https://api-extractor.com/pages/setup/generating_docs/) - use TSDoc comments to publish an API documentation website
- [Lockfile Explorer](https://lfx.rushstack.io/) - investigate and solve version conflicts for PNPM lockfiles
- [TSDoc](https://tsdoc.org/) - the standard for doc comments in TypeScript code
- [Dogfooding the Rush daemon](./docs/rush/dogfooding-rush-daemon.md) - contributor guide for building this repo
with the opt-in `rush-client` daemon, built from source

## Related Repos

Expand Down
140 changes: 140 additions & 0 deletions apps/heft/src/cli/HeftCommandLineParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as childProcess from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';

jest.setTimeout(30_000);

const HEFT_START_PATH: string = path.resolve(__dirname, '../start.js');

// A project with one task. The task sets process.exitCode to FIXTURE_EXIT_CODE in the Heft process, the
// way a test that Jest runs in band can, and then fails if FIXTURE_FAIL is set.
const FIXTURE_FILES: Record<string, string> = {
'package.json': JSON.stringify({ name: 'heft-exit-code-fixture', version: '1.0.0', private: true }),
'heft-plugin.json': JSON.stringify({
taskPlugins: [{ pluginName: 'exit-code-fixture-plugin', entryPoint: './plugin.js' }]
}),
'config/heft.json': JSON.stringify({
phasesByName: {
test: { tasksByName: { fixture: { taskPlugin: { pluginPackage: 'heft-exit-code-fixture' } } } }
}
}),
'plugin.js': `module.exports = class {
apply(taskSession) {
taskSession.hooks.run.tapPromise('exit-code-fixture-plugin', async () => {
process.exitCode = Number(process.env.FIXTURE_EXIT_CODE);
if (process.env.FIXTURE_FAIL) {
throw new Error('The fixture task failed on purpose');
}
});
}
};
`
};

// The same project, but its heft.json declares an alias with the name of the "test" phase's action, so Heft fails
// before any task runs, with an error that nothing has reported yet.
const ALIAS_CLASH_FIXTURE_FILES: Record<string, string> = {
...FIXTURE_FILES,
'config/heft.json': JSON.stringify({
...JSON.parse(FIXTURE_FILES['config/heft.json']),
aliasesByName: { test: { actionName: 'test' } }
})
};

function createFixtureFolder(files: Record<string, string>): string {
const fixtureFolderPath: string = fs.mkdtempSync(path.join(os.tmpdir(), 'heft-exit-code-'));
for (const [relativePath, contents] of Object.entries(files)) {
const filePath: string = path.join(fixtureFolderPath, relativePath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, contents);
}
return fixtureFolderPath;
}

interface IHeftResult {
exitCode: number | undefined;
output: string;
}

describe('HeftCommandLineParser', () => {
let fixtureFolderPath: string;
let aliasClashFixtureFolderPath: string;

beforeAll(() => {
fixtureFolderPath = createFixtureFolder(FIXTURE_FILES);
aliasClashFixtureFolderPath = createFixtureFolder(ALIAS_CLASH_FIXTURE_FILES);
});

afterAll(() => {
fs.rmSync(fixtureFolderPath, { recursive: true, force: true });
fs.rmSync(aliasClashFixtureFolderPath, { recursive: true, force: true });
});

async function runHeftTestAsync(
taskExitCode: number,
taskFails: boolean,
folderPath: string = fixtureFolderPath
): Promise<IHeftResult> {
const env: NodeJS.ProcessEnv = { ...process.env, FIXTURE_EXIT_CODE: String(taskExitCode) };
delete env.FIXTURE_FAIL;
delete env._RUSH_REPORTER_CHILD_FD;
delete env._RUSH_REPORTER_CHILD_ACK_FD;
if (taskFails) {
env.FIXTURE_FAIL = '1';
}

const child: childProcess.ChildProcess = childProcess.spawn(process.execPath, [HEFT_START_PATH, 'test'], {
cwd: folderPath,
env,
stdio: ['ignore', 'pipe', 'pipe']
});
let output: string = '';
const appendOutput: (chunk: string) => void = (chunk: string) => {
output += chunk;
};
child.stdout?.setEncoding('utf8').on('data', appendOutput);
child.stderr?.setEncoding('utf8').on('data', appendOutput);
const exitCode: number | null = await new Promise((resolve, reject) => {
child.once('error', reject);
child.once('close', resolve);
});

return { exitCode: exitCode ?? undefined, output };
}

it('exits with 1 when a task fails after code in the Heft process set process.exitCode to 0', async () => {
const { exitCode, output }: IHeftResult = await runHeftTestAsync(0, true);
expect(output).toContain('The fixture task failed on purpose');
expect(exitCode).toBe(1);
});

it('exits with 1 when a task fails after code in the Heft process set a negative process.exitCode', async () => {
const { exitCode, output }: IHeftResult = await runHeftTestAsync(-1, true);
expect(output).toContain('The fixture task failed on purpose');
expect(exitCode).toBe(1);
});

it('keeps a positive process.exitCode when a task fails', async () => {
const { exitCode, output }: IHeftResult = await runHeftTestAsync(3, true);
expect(output).toContain('The fixture task failed on purpose');
expect(exitCode).toBe(3);
});

it('exits with 0 when the task succeeds after code in the Heft process set process.exitCode to 0', async () => {
const { exitCode, output }: IHeftResult = await runHeftTestAsync(0, false);
expect(output).not.toContain('The fixture task failed on purpose');
expect(exitCode).toBe(0);
});

it('writes an error that nothing reported before it exits, and exits with 1', async () => {
const { exitCode, output }: IHeftResult = await runHeftTestAsync(0, false, aliasClashFixtureFolderPath);
expect(output).toContain(
'The alias "test" specified in heft.json cannot be used because an action with that name already exists.'
);
expect(exitCode).toBe(1);
});
});
9 changes: 2 additions & 7 deletions apps/heft/src/cli/HeftCommandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { PhaseAction } from './actions/PhaseAction';
import { RunAction } from './actions/RunAction';
import type { IHeftActionOptions } from './actions/IHeftAction';
import { AliasAction } from './actions/AliasAction';
import { getToolParameterNamesFromArgs } from '../utilities/CliUtilities';
import { getErrorExitCode, getToolParameterNamesFromArgs } from '../utilities/CliUtilities';
import { Constants } from '../utilities/Constants';
import { HeftChildReporter } from '../pluginFramework/logging/HeftChildReporter';

Expand Down Expand Up @@ -271,11 +271,6 @@ export class HeftCommandLineParser extends CommandLineParser {
this.globalTerminal.writeErrorLine(error.stack!);
}

const exitCode: string | number | undefined = process.exitCode;
if (!exitCode || typeof exitCode !== 'number' || exitCode > 0) {
process.exit(exitCode);
} else {
process.exit(1);
}
process.exit(getErrorExitCode(process.exitCode));
}
}
7 changes: 7 additions & 0 deletions apps/heft/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ export type {
IReaddirOptions
} from './utilities/WatchFileSystemAdapter';

export {
type IWatchpackPendingEventState as _IWatchpackPendingEventState,
type IWatchpackPendingFileEvent as _IWatchpackPendingFileEvent,
_tryGetWatchpackPendingEventState,
_waitForWatchpackPendingEventsAsync
} from './utilities/WatchpackUtilities';

export {
type IHeftRecordMetricsHookOptions,
type IMetricsData,
Expand Down
12 changes: 12 additions & 0 deletions apps/heft/src/utilities/CliUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,15 @@ export function getToolParameterNamesFromArgs(argv: string[] = process.argv): Se
}
return toolParameters;
}

/**
* Get the exit code for a Heft process that is exiting because of an error. This is `exitCode` if it
* is a positive integer, and 1 otherwise, so it is never 0.
*
* @param exitCode - The current value of `process.exitCode`. Code that ran in the Heft process, such as
* a test that Jest ran in band, may have set it to 0.
*/
export function getErrorExitCode(exitCode: string | number | undefined): number {
const numericExitCode: number = Number(exitCode);
return Number.isInteger(numericExitCode) && numericExitCode > 0 ? numericExitCode : 1;
}
143 changes: 136 additions & 7 deletions apps/heft/src/utilities/WatchFileSystemAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import * as path from 'node:path';

import Watchpack from 'watchpack';

import { _tryGetWatchpackPendingEventState } from './WatchpackUtilities';

/**
* Options for `fs.readdir`
* @public
Expand Down Expand Up @@ -129,6 +131,96 @@ interface ITimeEntry {
safeTime: number;
}

/**
* What a run read. The run's watcher checks the changes that it reports against this.
*/
interface IRunInputs {
/**
* When the run started. The watcher reports files whose times are close to this time, or later.
*/
baseline: number | undefined;
/**
* The time of each file that the run read, as the run recorded it
*/
files: ReadonlyMap<string, number>;
/**
* The folders that the run read
*/
contexts: ReadonlyMap<string, number>;
/**
* The times that the previous watcher had when the run started
*/
times: ReadonlyMap<string, ITimeEntry> | undefined;
}

const OUTDATED_ON_ATTACH_EXPLANATION: string = 'watch (outdated on attach)';
// A new watcher's first scan reports each file whose mtime is later than the watcher's start time, less the file
// system's accuracy. A watcher that attaches to the watcher of a folder that has already scanned reports the
// folder in the same way. Watchpack's other explanations are for events from the file system.
const SCAN_EXPLANATIONS: ReadonlySet<string> = new Set(['scan (file)', OUTDATED_ON_ATTACH_EXPLANATION]);

function tryLstatSync(filePath: string): fs.Stats | undefined {
try {
return fs.lstatSync(filePath, { throwIfNoEntry: false });
} catch {
return undefined;
}
}

function getTimestamp(stats: fs.Stats): number {
return stats.mtime.getTime() || stats.ctime.getTime() || Date.now();
}

/**
* Watchpack records a file's new time only when its `fs.lstat()` after the file system's event finishes. If a
* run starts before then, the previous watcher's time for the file is from before the change. Read the time
* again, so that the run sees the change, and so that the run's watcher knows that the run saw it.
*/
function refreshPendingTimes(watcher: Watchpack, times: Map<string, ITimeEntry>): void {
const pendingEventState: ReturnType<typeof _tryGetWatchpackPendingEventState> =
_tryGetWatchpackPendingEventState(watcher);
if (!pendingEventState) {
return;
}

for (const { filePath } of pendingEventState.pendingFileEvents) {
const stats: fs.Stats | undefined = tryLstatSync(filePath);
if (!stats) {
times.delete(filePath);
} else if (!stats.isDirectory()) {
const timestamp: number = getTimestamp(stats);
times.set(filePath, { timestamp, safeTime: timestamp });
}
}
}

/**
* Returns false if the watcher reported the path only because the path's time is close to the run's start, and
* the run has already read the path as it is now.
*/
function isReportedChangeNew(filePath: string, explanation: string, inputs: IRunInputs): boolean {
if (!SCAN_EXPLANATIONS.has(explanation) || inputs.baseline === undefined) {
return true;
}

const stats: fs.Stats | undefined = tryLstatSync(filePath);
if (!stats) {
return true;
}

if (stats.isDirectory()) {
// The watcher of a folder that the run read reports the folder's files one by one
return explanation !== OUTDATED_ON_ATTACH_EXPLANATION || !inputs.contexts.has(filePath);
}

if (stats.ctimeMs >= inputs.baseline) {
return true;
}

const readTime: number | undefined = inputs.files.get(filePath) ?? inputs.times?.get(filePath)?.timestamp;
return readTime !== getTimestamp(stats);
}

/**
* A filesystem adapter for use with the "fast-glob" package. This adapter tracks file system accesses
* to initialize `watchpack`.
Expand Down Expand Up @@ -261,11 +353,21 @@ export class WatchFileSystemAdapter implements IWatchFileSystemAdapter {
public setBaseline(): void {
this.#lastQueryTime = Date.now();

if (this.#watcher) {
const times: Map<string, { timestamp: number; safeTime: number }> = new Map();
this.#watcher.pause();
this.#watcher.collectTimeInfoEntries(times, times);
const watcher: Watchpack | undefined = this.#watcher;
if (watcher) {
this.#watcher = undefined;
const times: Map<string, ITimeEntry> = new Map();
watcher.collectTimeInfoEntries(times, times);
refreshPendingTimes(watcher, times);
// Close the previous watcher instead of only pausing it. A paused watcher keeps its directory watchers,
// so every run would leak another set of them. A kept watcher also keeps its OS watch on a folder that
// was deleted and recreated, and later watchers on that folder would share the dead watch, so edits in
// the recreated folder would be reported one run late.
watcher.close();
this.#times = times;
} else {
// Nothing is watching, so times collected for an earlier run may be out of date.
this.#times = undefined;
}
}

Expand All @@ -277,11 +379,39 @@ export class WatchFileSystemAdapter implements IWatchFileSystemAdapter {
return;
}

const inputs: IRunInputs = {
baseline: this.#lastQueryTime,
files: this.#files,
contexts: this.#contexts,
times: this.#times
};

const watcher: Watchpack = new Watchpack({
aggregateTimeout: 0,
followSymlinks: false
});

// A file that changed just before the run started is reported again when the watcher starts, although the
// run has read the change. Call onChange only for a change that the run may not have seen.
let hasNewChange: boolean = false;
const onReportedChange = (filePath: string, modifiedTime: number, explanation: string): void => {
hasNewChange ||= isReportedChangeNew(filePath, explanation, inputs);
};
const onReportedRemove = (): void => {
hasNewChange = true;
};
const onAggregated = (): void => {
if (hasNewChange) {
watcher.off('change', onReportedChange);
watcher.off('remove', onReportedRemove);
watcher.off('aggregated', onAggregated);
onChange();
}
};
watcher.on('change', onReportedChange);
watcher.on('remove', onReportedRemove);
watcher.on('aggregated', onAggregated);

this.#watcher = watcher;
watcher.watch({
files: this.#files.keys(),
Expand All @@ -290,12 +420,11 @@ export class WatchFileSystemAdapter implements IWatchFileSystemAdapter {
startTime: this.#lastQueryTime
});

// The watcher checks its reports against `inputs`, so the next run records into new maps
this.#lastFiles = this.#files;
this.#files = new Map();
this.#contexts.clear();
this.#contexts = new Map();
this.#missing.clear();

watcher.once('aggregated', onChange);
}

/**
Expand Down
Loading
Loading