Skip to content
Open
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
23 changes: 20 additions & 3 deletions src/esbuild/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export function getEsbuildPlugin<UserOptions = Record<string, never>>(
}

let fsContentsCache: string | undefined
let fsContentsCached = false

for (const { options, onTransformCb } of loaders) {
if (!checkFilter(options))
Expand All @@ -113,13 +114,29 @@ export function getEsbuildPlugin<UserOptions = Record<string, never>>(
if (result?.contents)
return result.contents as string

if (fsContentsCache)
return fsContentsCache
if (fsContentsCached)
return fsContentsCache as string

// 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.
return (fsContentsCache = await fs.promises.readFile(args.path, 'utf8'))
try {
fsContentsCache = await fs.promises.readFile(args.path, 'utf8')
fsContentsCached = true
return fsContentsCache
}
catch (error) {
// esbuild resolves `package.json#browser` entries mapped to `false`
// to a path that does not exist on disk (the module is meant to be
// stubbed out as empty). Treat a missing file the same way esbuild
// itself treats these stubs instead of throwing.
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
fsContentsCache = ''
fsContentsCached = true
return fsContentsCache
}
throw error
}
},
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('./pkg')
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require('./terminal-highlight')

module.exports = 'pkg-index'
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "pkg",
"version": "1.0.0",
"main": "./lib/index.js",
"browser": {
"./lib/terminal-highlight": false
}
}
72 changes: 72 additions & 0 deletions test/unit-tests/esbuild/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { Plugin } from 'esbuild'
import fs from 'node:fs'
import { resolve } from 'node:path'
import { build } from 'esbuild'
import { describe, expect, it, vi } from 'vitest'
import { createUnplugin } from '../../../src/index'

const fixtureDir = resolve(__dirname, 'fixtures/browser-false-stub')

function buildFixture(esbuildPlugin: Plugin) {
return build({
absWorkingDir: fixtureDir,
entryPoints: ['./entry.js'],
bundle: true,
platform: 'browser',
write: false,
plugins: [esbuildPlugin],
})
}

describe('esbuild getContents with browser:false stubbed module', () => {
it('does not throw ENOENT when transform.getContents() is called for a file switched off via package.json/browser', async () => {
const plugin = createUnplugin(() => ({
name: 'passthru',
transform: {
filter: { id: /\.[cm]?js$/ },
async handler(code) {
return { code }
},
},
}))

await expect(buildFixture(plugin.esbuild())).resolves.toBeDefined()
})

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')

try {
// Two transform hooks on the same plugin instance both call getContents()
// for the same onLoad args, exercising the shared fsContentsCache.
const plugin = createUnplugin(() => [
{
name: 'passthru-1',
transform: {
filter: { id: /\.[cm]?js$/ },
async handler(code) {
return { code }
},
},
},
{
name: 'passthru-2',
transform: {
filter: { id: /\.[cm]?js$/ },
async handler(code) {
return { code }
},
},
},
])

await expect(buildFixture(plugin.esbuild())).resolves.toBeDefined()

const stubReads = readFileSpy.mock.calls.filter(([path]) => String(path).includes('terminal-highlight'))
expect(stubReads).toHaveLength(1)
}
finally {
readFileSpy.mockRestore()
}
})
})