Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
224e953
test(e2e): use unique fixture directory per prepareFixtures call on m…
stormslowly May 15, 2026
7dc5f11
perf(core): enable rspack experiments.nativeWatcher
stormslowly May 15, 2026
3c20c88
test(e2e): bump watch fixture aggregateTimeout 10 -> 100 for nativeWa…
stormslowly May 17, 2026
2fda197
test(e2e): bump watch fixture aggregateTimeout 100 -> 500 for CI margin
stormslowly May 17, 2026
8400f59
debug(core): log final watchOptions on child rstest in watch mode
stormslowly May 17, 2026
0244813
Revert "debug(core): log final watchOptions on child rstest in watch …
stormslowly May 17, 2026
f49b588
test(e2e): bump watch fixture aggregateTimeout 500 -> 3000 for CI macOS
stormslowly May 17, 2026
d02a0cc
debug(e2e): dump fresh fixture rstest.config.mts content on prepareFi…
stormslowly May 17, 2026
1ede01c
debug(ci): tmate ssh into macos-14 node24 e2e job on failure
stormslowly May 17, 2026
3df5e41
debug(ci): override @rspack/core to traced canary + dump trace on fai…
stormslowly May 18, 2026
f1226f2
test(e2e): set watch fixture aggregateTimeout to 100ms
stormslowly May 18, 2026
35bec58
feat(core): bump default watch aggregateTimeout to 100ms; drop fixtur…
stormslowly May 18, 2026
0d20a19
chore: remove RCA debug instrumentation from CI workflow and prepareF…
stormslowly May 18, 2026
c88911a
chore(deps): bump @rspack-canary/core to 96980782 (incremental mtime …
stormslowly May 18, 2026
1124257
Revert "chore: remove RCA debug instrumentation from CI workflow and …
stormslowly May 18, 2026
6898c0b
ci: re-trigger to validate watch-mode stability
stormslowly May 18, 2026
202ddda
Merge remote-tracking branch 'origin/main' into e2e/macos-fresh-fixtu…
stormslowly May 25, 2026
ff4bd5c
chore(deps): bump @rspack-canary/core to ef6e2123
stormslowly May 25, 2026
f5820b8
chore(deps): bump @rspack-canary/core to 9b248702
stormslowly May 26, 2026
055cc28
Merge branch 'main' into e2e/macos-fresh-fixture-dir-per-call
stormslowly May 27, 2026
7a3e3f3
chore(deps): bump @rspack-canary/core to d1fbdb50
stormslowly May 30, 2026
c2a4add
Merge remote-tracking branch 'origin/main' into e2e/macos-fresh-fixtu…
stormslowly May 30, 2026
408901a
chore: remove RCA debug instrumentation from CI workflow and prepareF…
stormslowly May 30, 2026
64972aa
chore(deps): drop @rspack-canary/core override and use stable @rspack…
stormslowly May 30, 2026
c66a716
chore: revert @rspack-canary related pnpm config
stormslowly May 30, 2026
c7c2de7
fix(core): await chokidar ready before initial test run in watch mode
stormslowly May 31, 2026
8c65203
ci: temporary stress test for watch/restart flake (20x macos node 22+24)
stormslowly May 31, 2026
2193a4b
fix(core): poll config files instead of fs.watch for macOS reliability
stormslowly May 31, 2026
5bf636b
test(e2e): revert unique fixture dir per call (verify if still needed)
stormslowly May 31, 2026
a7b0b31
test: disable polling on config watcher to verify if fix is still needed
stormslowly May 31, 2026
3ec8b8c
test(e2e): re-enable unique fixture dir to verify it alone fixes flake
stormslowly May 31, 2026
253ca44
test: drop unique fixture dir, keep polling; verify nativeWatcher sti…
stormslowly May 31, 2026
54005e0
test: stress matrix polling x unique-dir x macos+ubuntu
stormslowly May 31, 2026
ec5160d
test: add @rspack-canary/core 0b8e3443 override to stress-test native…
stormslowly Jun 1, 2026
d93d2a6
test: stress matrix with RSPACK_WATCHER_TRACE on traced canary
stormslowly Jun 1, 2026
ef26921
test: rerun stress without RSPACK_WATCHER_TRACE to isolate eprintln e…
stormslowly Jun 1, 2026
07b7d0b
Merge remote-tracking branch 'origin/main' into e2e/macos-fresh-fixtu…
stormslowly Jun 2, 2026
cb83b93
test: bump canary to 553a58a6 (SeqCst ordering fix for executor atomics)
stormslowly Jun 2, 2026
9b66bfe
test: bump canary to 41ec55b0 + RSPACK_WATCHER_TRACE_FILE for failure…
stormslowly Jun 2, 2026
65784cb
test: bump canary to 1e7e5d3f (atomic trace writes)
stormslowly Jun 2, 2026
4ca1be5
test: bump canary to 42bfc9c0 (symlink alias fix for nativeWatcher)
stormslowly Jun 2, 2026
cee8450
test: bump canary to 2f684596 (aggregator re-flush fix)
stormslowly Jun 2, 2026
c1f42fa
test: bump canary to 573f86c5 (watchpack-style safeTime backfill)
stormslowly Jun 2, 2026
001abb2
test: bump canary to a85437e7 (dir-event rescan) + broaden trace dump
stormslowly Jun 2, 2026
7186aaa
test: bump canary to 17ee60b7 (full dir-walk rescan + safeTime over a…
stormslowly Jun 3, 2026
dd0716a
test: revert canary back to a85437e7 (dir-event rescan only, no recur…
stormslowly Jun 3, 2026
cb13159
test: bump canary to 061673cd (apply ignored filter to fs events)
stormslowly Jun 3, 2026
906ff2b
experiment: switch nativeWatcher off → use watchpack for A/B baseline
stormslowly Jun 3, 2026
fb6a691
test: include rstest-temp events in trace dump for diagnostics
stormslowly Jun 3, 2026
0dc6439
experiment: switch nativeWatcher back ON for trace-dump diagnosis run
stormslowly Jun 3, 2026
d0f40f0
test: grep -a so trace dump prints (file has non-utf8 bytes)
stormslowly Jun 3, 2026
8caf9c9
test: broaden fixture path filter (symlink path has no -rN suffix)
stormslowly Jun 3, 2026
94685f1
test: bump canary to 30a80444 (ignored filter walks ancestors)
stormslowly Jun 3, 2026
9086b67
test: re-run stress matrix to verify 80/80 stability on 30a80444
stormslowly Jun 3, 2026
3ff60c4
test(e2e): point native-watcher stress at safeTime canary
stormslowly Jun 4, 2026
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
57 changes: 57 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,63 @@ jobs:
if: matrix.test_script == 'test'
run: pnpm run test:vscode

# ======== stress test with RSPACK_WATCHER_TRACE ========
# macOS polling=on × unique-dir on/off × 20 rounds × node 22/24 = 80 jobs.
# We focus on the residual nativeWatcher flake (polling=off failures are
# unrelated chokidar issues already explained). All jobs set
# RSPACK_WATCHER_TRACE=1 so failing jobs dump the full event pipeline log.
stress-restart:
needs: prepare
if: needs.prepare.outputs.changed == 'true'
runs-on: macos-14
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node_version: ['22', '24']
round:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
unique_dir: ['on', 'off']
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1

- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8

- name: Setup Node.js ${{ matrix.node_version }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ matrix.node_version }}
cache: 'pnpm'
cache-dependency-path: pnpm-lock.yaml

- name: Install Dependencies
run: pnpm install --frozen-lockfile --prefer-offline

- name: Stress all watch tests
env:
STRESS_NO_UNIQUE_DIR: ${{ matrix.unique_dir == 'off' && '1' || '' }}
RSPACK_WATCHER_TRACE_FILE: /tmp/rspack-watcher-trace.log
run: cd e2e && pnpm exec rstest run watch/

- name: Dump rspack-watcher trace on failure
if: failure()
run: |
if [ ! -f /tmp/rspack-watcher-trace.log ]; then
echo "(trace file not present)"
exit 0
fi
echo '===== trace stats ====='
wc -l /tmp/rspack-watcher-trace.log
echo '===== js_event_handle calls (Rust → JS dispatch boundary) ====='
grep -a "js_event_handle" /tmp/rspack-watcher-trace.log || true
echo '===== ALL fixture-scoped events (incl rstest-temp to inspect dispatch noise) ====='
grep -aE "fixtures-test-(0|dynamic)-module" /tmp/rspack-watcher-trace.log | grep -v "node_modules" || true
echo '===== END trace ====='

# ======== codspeed ========
# Temporarily disabled: CodSpeed runner quota exhausted.
# Re-enable once quota resets.
Expand Down
10 changes: 1 addition & 9 deletions e2e/filter/fixtures-related-dynamic/rstest.config.mts
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
import { defineConfig } from '@rstest/core';

export default defineConfig({
tools: {
rspack: {
watchOptions: {
aggregateTimeout: 10,
},
},
},
});
export default defineConfig({});
32 changes: 31 additions & 1 deletion e2e/scripts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,16 @@ export async function runRstestCli({
};
}

// macOS only (gated by STRESS_NO_UNIQUE_DIR for the stress matrix):
// give every prepareFixtures call a fresh sibling directory and expose it
// via an in-repo symlink. Reusing the same distPath across retries makes
// file watchers observe a Delete+Create batch on the same path, which the
// macOS FSEvents per-path ring buffer can replay onto later watcher streams
// as spurious change events. A fresh path has no FSEvents history.
let fixtureRunCounter = 0;
const uniqueSuffix = () =>
`-r${process.pid}-${++fixtureRunCounter}-${Date.now().toString(36)}`;

export async function prepareFixtures({
fixturesPath,
fixturesTargetPath,
Expand All @@ -249,7 +259,14 @@ export async function prepareFixtures({
fixturesTargetPath?: string;
}) {
const root = path.dirname(fixturesPath);
const distPath = fixturesTargetPath || path.resolve(`${fixturesPath}-test`);
const exposedPath =
fixturesTargetPath || path.resolve(`${fixturesPath}-test`);

const useUniqueDir =
process.platform === 'darwin' && process.env.STRESS_NO_UNIQUE_DIR !== '1';
const distPath = useUniqueDir
? `${exposedPath}${uniqueSuffix()}`
: exposedPath;

// Clean up any leftover fixtures from previous runs
// On Windows, file handles may not be fully released, causing EBUSY errors
Expand All @@ -261,6 +278,14 @@ export async function prepareFixtures({
maxRetries: 10,
retryDelay: 500,
});
if (distPath !== exposedPath) {
fs.rmSync(exposedPath, {
recursive: true,
force: true,
maxRetries: 10,
retryDelay: 500,
});
}
} catch (err) {
if (process.platform !== 'win32') {
throw err;
Expand All @@ -278,6 +303,11 @@ export async function prepareFixtures({
filter: (src) => !path.basename(src).startsWith('fixtures-test-'),
});

if (distPath !== exposedPath) {
await fs.promises.mkdir(path.dirname(exposedPath), { recursive: true });
await fs.promises.symlink(distPath, exposedPath, 'dir');
}

const update = (
relativePath: string,
content: string | ((raw: string) => string),
Expand Down
10 changes: 1 addition & 9 deletions e2e/watch/fixtures-dynamic/rstest.config.mts
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
import { defineConfig } from '@rstest/core';

export default defineConfig({
tools: {
rspack: {
watchOptions: {
aggregateTimeout: 10,
},
},
},
});
export default defineConfig({});
7 changes: 0 additions & 7 deletions e2e/watch/fixtures-setup/rstest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,4 @@ export default defineConfig({
passWithNoTests: true,
setupFiles: ['./rstest.setup.ts'],
exclude: ['**/node_modules/**', '**/dist/**'],
tools: {
rspack: {
watchOptions: {
aggregateTimeout: 10,
},
},
},
});
7 changes: 0 additions & 7 deletions e2e/watch/fixtures-shortcuts/rstest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,4 @@ process.stdin.setRawMode = () => process.stdin;
export default defineConfig({
reporters: ['default'],
disableConsoleIntercept: true,
tools: {
rspack: {
watchOptions: {
aggregateTimeout: 10,
},
},
},
});
10 changes: 1 addition & 9 deletions e2e/watch/fixtures/rstest.config.mts
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
import { defineConfig } from '@rstest/core';

export default defineConfig({
tools: {
rspack: {
watchOptions: {
aggregateTimeout: 10,
},
},
},
});
export default defineConfig({});
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,10 @@
"engines": {
"node": "^20.19.0 || >=22.12.0",
"pnpm": ">=10.34.1"
},
"pnpm": {
"overrides": {
"@rspack/core": "npm:@rspack-canary/core@2.0.7-canary-a310bbfa-20260603233709"
}
}
}
2 changes: 1 addition & 1 deletion packages/core/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,7 @@ export const runRest = async ({
process.off('unhandledRejection', unexpectedlyExitHandler);
});

watchFilesForRestart({
await watchFilesForRestart({
rstest,
options,
filters,
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/core/plugins/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,14 @@ export const pluginEntryWatch: (params: {
};

config.watchOptions ??= {};
// FIXME: Temporarily default to 5 to debounce rerun in watch mode.
config.watchOptions.aggregateTimeout = 5;
// Default aggregate window for watch-mode rerun debouncing. On macOS
// with the rspack native watcher this also needs to be long enough
// for the FSEvent → vnode cache invalidation cycle to complete
// before rspack stats the changed file (otherwise the rebuild reads
// stale content and rstest reports "No test files need re-run").
// 100 ms is the minimum value that survives macos-14 GHA runners.
// User configs can override via `tools.rspack.watchOptions`.
config.watchOptions.aggregateTimeout = 100;
// TODO: rspack should support `(string | RegExp)[]` type
// https://github.com/web-infra-dev/rspack/issues/10596
config.watchOptions.ignored = castArray(
Expand All @@ -79,6 +85,9 @@ export const pluginEntryWatch: (params: {
'**/*.snap',
);

config.experiments ??= {};
config.experiments.nativeWatcher = true;

const configFilePath = context.projects.find(
(project) => project.environmentName === environment.name,
)?.configFilePath;
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/core/restart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,19 @@ export async function watchFilesForRestart({
}

const root = rstest.context.rootPath;
// STRESS_NO_POLLING is a temporary stress-test gate; default is polling ON.
const usePolling = process.env.STRESS_NO_POLLING !== '1';
const watcher = await createChokidar(configFilePaths, root, {
// do not trigger add for initial files
ignoreInitial: true,
// If watching fails due to read permissions, the errors will be suppressed silently.
ignorePermissionErrors: true,
// chokidar v5 dropped fsevents and relies on Node's fs.watch(), which is
// unreliable for single-file watching on macOS (kqueue silently drops
// change events). Poll the small set of config files instead — 100ms is
// fast enough for restarts while adding negligible CPU.
usePolling,
interval: 100,
...watchOptions,
});

Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/utils/watchFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,10 @@ export async function createChokidar(
}
}

return chokidar.watch(Array.from(watchFiles), options);
const watcher = chokidar.watch(Array.from(watchFiles), options);
// Await the initial scan so callers can rely on events being emitted for
// subsequent modifications — chokidar may drop events before 'ready',
// especially on macOS where FSEvents setup is async.
await new Promise<void>((resolve) => watcher.once('ready', () => resolve()));
return watcher;
}
5 changes: 4 additions & 1 deletion packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1738,6 +1738,9 @@ exports[`prepareRsbuild > should generate rspack config correctly in watch mode
"context": "<ROOT>/packages/core",
"devtool": "nosources-source-map",
"entry": [Function],
"experiments": {
"nativeWatcher": true,
},
"externals": [
[Function],
{
Expand Down Expand Up @@ -2301,7 +2304,7 @@ exports[`prepareRsbuild > should generate rspack config correctly in watch mode
},
"target": "node",
"watchOptions": {
"aggregateTimeout": 5,
"aggregateTimeout": 100,
"ignored": [
"**/.git",
"**/node_modules",
Expand Down
Loading
Loading