Repository navigation
fix: skip non-regular files in default readFile - #551
Merged
2wce merged 4 commits intoOct 19, 2026
Merged
Conversation
🦋 Changeset detectedLatest commit: 6bea833 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
2wce
approved these changes
Oct 19, 2026
2wce
left a comment
Owner
There was a problem hiding this comment.
Thank you, @christowiz, for the contribution and the clear reproduction! The fix is focused, preserves custom readers and symlinked sources, and includes a regression test that catches the original behavior. I verified all 13 tests, lint, type checking, and the build with the locked dependencies, and confirmed that the regression fails without the fix. Approved.
2wce
enabled auto-merge (squash)
October 19, 2026 08:47
2wce
disabled auto-merge
October 19, 2026 08:47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Make the default
readFilecheck that a path is a regular file before reading it, so theloadhook no longer returns directory contents as module source or hands a directory to the bundler's file watcher.The problem
The
loadhook assumes that any id passing the include/exclude filter is a readable source file. With no options (sourcemaps()),createFilter(undefined, undefined)admits every id, and the defaultreadFilecallsfs.readFile(id, 'utf8')with no check thatidis actually a file.On APFS, ext4 and friends, reading a directory fails with
EISDIRand the hook falls through tothis.warn('Failed reading file')and returns null. macOS devfs is different: it is the one filesystem mounted at root whose driver permitsread()on a directory.fs.readFile('/dev')succeeds and returns roughly 6.7 KB of raw directory records. The hook then:this.addWatchFile('/dev'), handing devfs to the bundler's file watcher.How it surfaces.
In a Vite SPA with a client-side route named
/dev, a hard navigation (typed URL, bookmark, refresh) sendsGET /devto the dev server.Vite classifies an extensionless path with no trailing slash as a JS module request, and its transform middleware runs ahead of the SPA HTML fallback, so
/devreaches pluginloadhooks as the module id. The directory bytes fail import analysis (HTTP 500,content contains invalid JS syntax), and the watcher then walks/dev/fd/*, hitsEBADFonlstatof an fd that is not open in the Vite process, and because Vite registers noerrorlistener on its watcher Node raises it as an unhandled event and the dev server exits.Originally bisected on Vite 5.4.17, where bare Vite serves
/devcorrectly and enablingsourcemaps()alone reproduces the crash. The minimal repro confirms the crash from a browser navigation on Vite 5.4.21, 6.4.4 and 7.1.7. Vite 7.1.8 (vitejs/vite#20866) stopped routing browser navigations through the transform middleware, and 8.3.2 (vitejs/vite#23503) added a watchererrorlistener, so current 8.x only logsfile watcher error: EBADF. The plugin's behaviour is the same on every version; the 5.x and 6.x lines are still maintained and still crash. Client-side navigation to the same route is unaffected because no request for the route path is ever made.Reproduction
https://github.com/christowiz/rollup-plugin-sourcemaps2-devfs-repro
A three-file Vite project pinned to this plugin at 0.5.9 and Vite 7.1.7.
pnpm install && pnpm dev, then openhttp://localhost:5173/devin a browser on macOS.The fix.
defaultReadFilenow callsfs.statfirst and only proceeds tofs.readFilewhenstats.isFile()is true. A non-file path (directory, character device) fails the read withNot a regular file: <path>, soloadtakes its existing catch path: the query-suffix retry runs, thenthis.warn('Failed reading file')andreturn null. Neither the raw bytes nor theaddWatchFilecall ever happen.Behaviour for real files is unchanged,
fs.statfollows symlinks so symlinked sources keep working, and the guard lives inside the default reader only, so consumers who pass their ownreadFile(virtual filesystems, in-memory sources) see no change at all.Changes Made
src/index.ts: wrap the defaultreadFilein anfs.statcheck and reject anything that is not a regular file. One new error path, no new options, no public API change. A comment explains the devfs case since it is the only reason the guard exists.src/__tests__/index.test.ts: two tests plus a small helper,loadWithDefaultReadFile, that calls the plugin'sloadhook directly with a minimalPluginContextstand-in. Direct invocation is needed because every existingrollupBundletest injects a customreadFile, which replaces the code under test. The precedent is the existing "finds last source map file definition" test, which callsresolveSourceMapdirectly.skips directories even when the filesystem lets them be readis the regression test. A plain temp directory is not enough: on APFS/ext4fs.readFileon a directory already fails, so the old code was also green there. The test stubsfs.readFileto succeed (mimicking devfs) while leavingfs.statreal, then asserts the result is null,fs.readFilewas never called,addWatchFilewas never called, andwarnwas called with'Failed reading file'. Reverting the fix makes this test fail with'not a source file'instead ofnull.reads regular files with the default readFileis the happy path. The default reader had no coverage before; this proves thestatwrapper did not break it (content returned,addWatchFilecalled once with the path, no warning).osandviimports and aPluginContexttype import to support the above. No new dependencies, no new lint directives.biome.json: bump$schemafrom2.5.0to2.5.15. Dependabot bumped@biomejs/biometo 2.5.15 in chore(deps-dev): bump @biomejs/biome from 2.5.14 to 2.5.15 #540 but the schema URL was left at 2.5.0, so everybiome ci/biome checkrun (includingpnpm buildviaprebuild) printsThe configuration schema version does not match the CLI version 2.5.15and suggestsbiome migrate. This is exactly whatbiome migrateproduces: a one-line change to the$schemaURL, no rule or formatter changes. It is info-level, not a failure. It is in its own commit so it can be dropped from this PR if you would rather take it separately..changeset/skip-non-regular-files.md: patch changeset so the release workflow opens a "Version packages" PR for this fix.Related Issues
Screenshots (if applicable)
Checklist
vitest run: 13 passed.tsc: clean.biome ci .: clean.)main. Commits are deliberately kept as four logical units: fix, lint config, test, changeset. Squash at merge is fine.)Additional Notes
defaultReadFilerather than inload, so a customreadFileis never second-guessed by anfs.staton a path that may not exist on disk.fs.statrather thanfs.lstat: symlinked sources should resolve to their target and be read normally./dev/nulland other character devices are rejected by the sameisFile()check. No separate test; it exercises no additional line.fs.readFilerather than the real/devso the suite behaves identically on Linux CI and macOS.