Skip to content

Commit a0b0bc4

Browse files
authored
fix(bun): respect loader returned from load hook + feat(bun): support plugin.bun.loader for per-plugin loader resolution (#601)
* fix(bun): respect loader returned from load hook Use the loader returned by object load results before falling back to Bun's provided loader or guessed loader. * feat(bun): support plugin.bun.loader for per-plugin loader resolution Add `loader` to the `bun` plugin option block, accepting either a `Loader` literal or `(code, id) => Loader` function, mirroring the existing `plugin.esbuild.loader` API. * fix(bun): remove LoadResult and use TransformResult instead
1 parent 0fc3092 commit a0b0bc4

4 files changed

Lines changed: 109 additions & 5 deletions

File tree

src/bun/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { isAbsolute } from 'node:path'
44
import { version as unpluginVersion } from '../../package.json'
55
import { normalizeObjectHook } from '../utils/filter'
66
import { toArray } from '../utils/general'
7-
import { createBuildContext, createPluginContext, guessLoader } from './utils'
7+
import { createBuildContext, createPluginContext, guessLoader, unwrapLoader } from './utils'
88

99
// Coerce plugin.name to satisfy Bun's namespace validator:
1010
// https://github.com/oven-sh/bun/blob/12d77d1ac561771e9fa1d0822e954273248e7f9a/src/js/builtins/BundlerPlugin.ts#L215-L217
@@ -141,6 +141,7 @@ export function getBunPlugin<UserOptions = Record<string, never>>(
141141
): Promise<{ contents: string, loader: Loader } | undefined> {
142142
let code: string | undefined
143143
let hasResult = false
144+
let activePlugin: typeof plugins[number] | undefined
144145

145146
const namespaceLoadHooks = namespace === 'file'
146147
? loadHooks
@@ -153,7 +154,7 @@ export function getBunPlugin<UserOptions = Record<string, never>>(
153154
continue
154155

155156
const { mixedContext, errors, warnings } = createPluginContext(context)
156-
const result = await handler.call(mixedContext, id)
157+
const result: TransformResult = await handler.call(mixedContext, id)
157158

158159
for (const warning of warnings) {
159160
console.warn('[unplugin]', typeof warning === 'string' ? warning : warning.message)
@@ -166,11 +167,13 @@ export function getBunPlugin<UserOptions = Record<string, never>>(
166167
if (typeof result === 'string') {
167168
code = result
168169
hasResult = true
170+
activePlugin = plugin
169171
break
170172
}
171173
else if (typeof result === 'object' && result !== null) {
172174
code = result.code
173175
hasResult = true
176+
activePlugin = plugin
174177
break
175178
}
176179
}
@@ -213,9 +216,12 @@ export function getBunPlugin<UserOptions = Record<string, never>>(
213216
}
214217

215218
if (hasResult && code !== undefined) {
219+
const pluginLoader = activePlugin?.bun?.loader
216220
return {
217221
contents: code,
218-
loader: loader ?? guessLoader(id),
222+
loader: (pluginLoader && unwrapLoader(pluginLoader, code, id))
223+
?? loader
224+
?? guessLoader(id),
219225
}
220226
}
221227
}

src/bun/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ export function guessLoader(id: string): Loader {
2626
return ExtToLoader[path.extname(id).toLowerCase()] || 'js'
2727
}
2828

29+
export function unwrapLoader(
30+
loader: Loader | ((code: string, id: string) => Loader),
31+
code: string,
32+
id: string,
33+
): Loader {
34+
if (typeof loader === 'function')
35+
return loader(code, id)
36+
37+
return loader
38+
}
39+
2940
export function createBuildContext(build: PluginBuilder): UnpluginBuildContext {
3041
const watchFiles: string[] = []
3142

src/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { CompilationContext as FarmCompilationContext, JsPlugin as FarmPlugin } from '@farmfe/core'
22
import type { Compilation as RspackCompilation, Compiler as RspackCompiler, LoaderContext as RspackLoaderContext, RspackPluginInstance } from '@rspack/core'
3-
import type { BunPlugin, PluginBuilder as BunPluginBuilder } from 'bun'
3+
import type { Loader as BunLoader, BunPlugin, PluginBuilder as BunPluginBuilder } from 'bun'
44
import type { BuildOptions, Plugin as EsbuildPlugin, Loader, PluginBuild } from 'esbuild'
55
import type { Plugin as RolldownPlugin } from 'rolldown'
66
import type { EmittedAsset, PluginContextMeta as RollupContextMeta, Plugin as RollupPlugin, SourceMapInput } from 'rollup'
@@ -145,7 +145,10 @@ export interface UnpluginOptions {
145145
config?: ((options: BuildOptions) => void) | undefined
146146
} | undefined
147147
farm?: Partial<FarmPlugin> | undefined
148-
bun?: Partial<BunPlugin> | undefined
148+
bun?: {
149+
loader?: BunLoader | ((code: string, id: string) => BunLoader) | undefined
150+
setup?: ((build: BunPluginBuilder) => void | Promise<void>) | undefined
151+
} | undefined
149152
}
150153

151154
export interface ResolvedUnpluginOptions extends UnpluginOptions {

test/unit-tests/bun/nested.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,90 @@ describe.skipIf(typeof Bun === 'undefined')('bun nested plugin support', () => {
132132
Bun.file = originalFile
133133
})
134134

135+
it('should respect a static plugin.bun.loader', async () => {
136+
const unplugin = createUnplugin(() => ({
137+
name: 'tsx-loader',
138+
resolveId(id: string) {
139+
return id === 'virtual:component' ? id : null
140+
},
141+
load(id: string) {
142+
if (id === 'virtual:component') {
143+
return 'export default () => <h1>hi</h1>'
144+
}
145+
return null
146+
},
147+
bun: { loader: 'tsx' as const },
148+
}))
149+
150+
const bunPlugin = unplugin.bun()
151+
const onLoadCallbacks: Array<{ namespace?: string, cb: Bun.OnLoadCallback }> = []
152+
const mockBuild = {
153+
onResolve: vi.fn(),
154+
onLoad: vi.fn((options, callback) => {
155+
onLoadCallbacks.push({ namespace: options.namespace, cb: callback })
156+
}),
157+
onStart: vi.fn(),
158+
config: { outdir: './dist' },
159+
} as never as Bun.PluginBuilder
160+
161+
await bunPlugin.setup(mockBuild)
162+
163+
const virtualHandler = onLoadCallbacks.find(c => c.namespace !== 'file')?.cb
164+
expect(virtualHandler).toBeDefined()
165+
166+
const result = await virtualHandler!({
167+
path: 'virtual:component',
168+
loader: 'js',
169+
} as Bun.OnLoadArgs)
170+
171+
expect(result).toEqual({
172+
contents: 'export default () => <h1>hi</h1>',
173+
loader: 'tsx',
174+
})
175+
})
176+
177+
it('should call plugin.bun.loader as a function with code and id', async () => {
178+
const loaderFn = vi.fn((_code: string, _id: string) => 'tsx' as const)
179+
const unplugin = createUnplugin(() => ({
180+
name: 'tsx-loader-fn',
181+
resolveId(id: string) {
182+
return id === 'virtual:component' ? id : null
183+
},
184+
load(id: string) {
185+
if (id === 'virtual:component') {
186+
return 'export default () => <h1>hi</h1>'
187+
}
188+
return null
189+
},
190+
bun: { loader: loaderFn },
191+
}))
192+
193+
const bunPlugin = unplugin.bun()
194+
const onLoadCallbacks: Array<{ namespace?: string, cb: Bun.OnLoadCallback }> = []
195+
const mockBuild = {
196+
onResolve: vi.fn(),
197+
onLoad: vi.fn((options, callback) => {
198+
onLoadCallbacks.push({ namespace: options.namespace, cb: callback })
199+
}),
200+
onStart: vi.fn(),
201+
config: { outdir: './dist' },
202+
} as never as Bun.PluginBuilder
203+
204+
await bunPlugin.setup(mockBuild)
205+
206+
const virtualHandler = onLoadCallbacks.find(c => c.namespace !== 'file')?.cb
207+
const result = await virtualHandler!({
208+
path: 'virtual:component',
209+
loader: 'js',
210+
} as Bun.OnLoadArgs)
211+
212+
expect(loaderFn).toHaveBeenCalledWith('export default () => <h1>hi</h1>', 'virtual:component')
213+
expect(result).toEqual({
214+
contents: 'export default () => <h1>hi</h1>',
215+
loader: 'tsx',
216+
})
217+
})
218+
135219
it('should call plugin.bun.setup with the build before standard hooks', async () => {
136220
const callOrder: string[] = []
137221
const bunSetup = vi.fn((_build: Bun.PluginBuilder) => {

0 commit comments

Comments
 (0)