fix(esbuild): handle browser:false stubs in getContents#611
Conversation
…modules esbuild resolves package.json#browser entries mapped to false to a path that does not exist on disk. transform.getContents() called fs.readFile on that path unconditionally and threw an unhandled ENOENT, crashing the whole build. Treat a missing file the same way esbuild treats these stubs: return empty contents instead of propagating the fs error. Fixes unjs#546
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe esbuild plugin now caches empty filesystem reads for browser-disabled files, and the test suite adds a browser:false fixture plus assertions for build success and repeated-load cache reuse. ChangesENOENT handling for browser-disabled files
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant esbuild as esbuild
participant plugin as unplugin esbuild hook
participant getContents as getContents()
participant fs as fs.promises
esbuild->>plugin: onLoad(args.path)
plugin->>getContents: getContents(args.path)
getContents->>fs: readFile(args.path, 'utf8')
alt file exists
fs-->>getContents: contents
getContents-->>plugin: return contents
else ENOENT
fs-->>getContents: throw ENOENT
getContents-->>plugin: cache and return ''
end
plugin-->>esbuild: loaded result
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/esbuild/index.ts (1)
102-133: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick winCache check breaks for empty-content (stubbed) files.
The cache guard at Line 116 (
if (fsContentsCache) return fsContentsCache) is falsy for an empty string, so oncegetContents()caches''for an ENOENT/stubbed path (Line 131), every subsequent call within the sameonTransformCbloop re-enters the try/catch and re-attemptsfs.promises.readFile, re-triggering the ENOENT path each time instead of returning the cached value. This defeats the caching intent specifically for the scenario this PR targets.🐛 Proposed fix using a defined-check instead of truthiness
- let fsContentsCache: string | undefined + let fsContentsCache: string | undefined + let fsContentsCached = false for (const { options, onTransformCb } of loaders) { if (!checkFilter(options)) continue if (onTransformCb) { const newArgs: OnTransformArgs = { ...result, ...args, async getContents() { if (result?.contents) return result.contents as string - if (fsContentsCache) + if (fsContentsCached) return fsContentsCache // caution: 'utf8' assumes the input file is not in binary. // if you want your plugin handle binary files, make sure to // `plugin.load()` them first. try { - return (fsContentsCache = await fs.promises.readFile(args.path, 'utf8')) + fsContentsCache = await fs.promises.readFile(args.path, 'utf8') + fsContentsCached = true + return fsContentsCache } catch (error) { if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') - return (fsContentsCache = '') + { fsContentsCache = ''; fsContentsCached = true; return fsContentsCache } throw error } }, }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/esbuild/index.ts` around lines 102 - 133, The getContents cache in the esbuild transform path treats an empty string as uncached, so stubbed or ENOENT-backed files keep re-reading instead of reusing the cached empty result. Update the cache guard inside the onTransformCb/getContents flow to check whether fsContentsCache has been defined rather than relying on truthiness, so the cached '' value is returned correctly on later calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/esbuild/index.ts`:
- Around line 102-133: The getContents cache in the esbuild transform path
treats an empty string as uncached, so stubbed or ENOENT-backed files keep
re-reading instead of reusing the cached empty result. Update the cache guard
inside the onTransformCb/getContents flow to check whether fsContentsCache has
been defined rather than relying on truthiness, so the cached '' value is
returned correctly on later calls.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 75a57b95-668b-4634-9c82-a1604251bf62
📒 Files selected for processing (5)
src/esbuild/index.tstest/unit-tests/esbuild/fixtures/browser-false-stub/entry.jstest/unit-tests/esbuild/fixtures/browser-false-stub/pkg/lib/index.jstest/unit-tests/esbuild/fixtures/browser-false-stub/pkg/package.jsontest/unit-tests/esbuild/index.test.ts
There was a problem hiding this comment.
🧹 Nitpick comments (2)
test/unit-tests/esbuild/index.test.ts (2)
57-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicated build() configuration across tests.
The
build({...})options here mirror the earlier test's configuration (entryPoints, bundle, platform, write, plugins). Consider extracting a small shared helper (e.g.,buildFixture(plugin)) to avoid drift between the two tests.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/unit-tests/esbuild/index.test.ts` around lines 57 - 64, The esbuild test repeats the same build() options as the earlier test, so the configuration can drift. Extract the shared build setup into a small helper (for example, a fixture-specific helper used by plugin.esbuild()) and have both tests call it, keeping only the differing expectations in each test.
32-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRestore spy in a
finallyto avoid leaking across tests.If the assertion at Line 67 fails,
readFileSpy.mockRestore()on Line 69 never runs, leaving the spy active for subsequent tests in the suite.🧪 Proposed fix
it('caches the empty contents from an ENOENT stub so a second transform does not re-read the file', async () => { const readFileSpy = vi.spyOn(fs.promises, 'readFile') - ... - - await expect(build({ - ... - })).resolves.toBeDefined() - - const stubReads = readFileSpy.mock.calls.filter(([path]) => String(path).includes('terminal-highlight')) - expect(stubReads).toHaveLength(1) - - readFileSpy.mockRestore() + try { + await expect(build({ + ... + })).resolves.toBeDefined() + + const stubReads = readFileSpy.mock.calls.filter(([path]) => String(path).includes('terminal-highlight')) + expect(stubReads).toHaveLength(1) + } + finally { + readFileSpy.mockRestore() + } })Also applies to: 57-70
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/unit-tests/esbuild/index.test.ts` at line 32, The fs.promises.readFile spy in the esbuild unit test can leak into later tests if an assertion throws before cleanup. Update the test around readFileSpy in the test suite so the assertion and any related setup run inside a try block and move readFileSpy.mockRestore() into a finally block, ensuring the spy is always restored even when the test fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@test/unit-tests/esbuild/index.test.ts`:
- Around line 57-64: The esbuild test repeats the same build() options as the
earlier test, so the configuration can drift. Extract the shared build setup
into a small helper (for example, a fixture-specific helper used by
plugin.esbuild()) and have both tests call it, keeping only the differing
expectations in each test.
- Line 32: The fs.promises.readFile spy in the esbuild unit test can leak into
later tests if an assertion throws before cleanup. Update the test around
readFileSpy in the test suite so the assertion and any related setup run inside
a try block and move readFileSpy.mockRestore() into a finally block, ensuring
the spy is always restored even when the test fails.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ce7a6cac-9927-4de6-bd00-8fed12ea0ce0
📒 Files selected for processing (2)
src/esbuild/index.tstest/unit-tests/esbuild/index.test.ts
The esbuild adapter's
getContents()readsargs.pathwithfs.readFileunconditionally. When esbuild resolves apackage.json#browserentry mapped tofalse(a common way to stub a module out for the browser), unplugin gets a path that does not exist on disk andreadFilethrows an uncaughtENOENT, crashing the build (#546).This wraps the read in a try/catch: on
ENOENTit returns empty contents (matching how esbuild itself treats these stubs) and re-throws any other error unchanged.Added an in-process esbuild build test with a fixture whose
browserfield maps a module tofalse. It reproduces the ENOENT without the fix and passes with it.Closes #546
Summary by CodeRabbit
package.jsonbrowsermappings no longer fail when source files are absent; missing contents are treated as empty.ENOENTerrors.browser:falsestubs, including coverage for graceful resolution and cached empty-content behavior.