Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support bun #339

Closed
wants to merge 1 commit into from
Closed
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
74 changes: 48 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,46 @@ Currently supports:
- [Webpack](https://webpack.js.org/)
- [esbuild](https://esbuild.github.io/)
- [Rspack](https://www.rspack.dev/) (⚠️ experimental)
- [Bun](https://bun.sh/) (⚠️ experimental)

## Hooks

`unplugin` extends the excellent [Rollup plugin API](https://rollupjs.org/guide/en/#plugins-overview) as the unified plugin interface and provides a compatible layer base on the build tools used with.

###### Supported

| Hook | Rollup | Vite | Webpack 4 | Webpack 5 | esbuild | Rspack |
| ----------------------------------------------------------------------- | :-------------: | :--: | :-------: | :-------: | :-------------: | :----: |
| [`enforce`](https://rollupjs.org/guide/en/#enforce) | ❌ <sup>1</sup> | βœ… | βœ… | βœ… | ❌ <sup>1</sup> | βœ… |
| [`buildStart`](https://rollupjs.org/guide/en/#buildstart) | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`resolveId`](https://rollupjs.org/guide/en/#resolveid) | βœ… | βœ… | βœ… | βœ… | βœ… | ❌ |
| `loadInclude`<sup>2</sup> | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`load`](https://rollupjs.org/guide/en/#load) | βœ… | βœ… | βœ… | βœ… | βœ… <sup>3</sup> | βœ… |
| `transformInclude`<sup>2</sup> | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`transform`](https://rollupjs.org/guide/en/#transformers) | βœ… | βœ… | βœ… | βœ… | βœ… <sup>3</sup> | βœ… |
| [`watchChange`](https://rollupjs.org/guide/en/#watchchange) | βœ… | βœ… | βœ… | βœ… | ❌ | ❌ |
| [`buildEnd`](https://rollupjs.org/guide/en/#buildend) | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`writeBundle`](https://rollupjs.org/guide/en/#writebundle)<sup>4</sup> | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| Hook | Rollup | Vite | Webpack 4 | Webpack 5 | esbuild | Rspack | Bun |
| ----------------------------------------------------------------------- | :-------------: | :--: | :-------: | :-------: | :-------------: | :----: | :-------------: |
| [`enforce`](https://rollupjs.org/guide/en/#enforce) | ❌ <sup>1</sup> | βœ… | βœ… | βœ… | ❌ <sup>1</sup> | βœ… | ❌ <sup>1</sup> |
| [`buildStart`](https://rollupjs.org/guide/en/#buildstart) | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | ❌ |
| [`resolveId`](https://rollupjs.org/guide/en/#resolveid) | βœ… | βœ… | βœ… | βœ… | βœ… | ❌ | βœ… |
| `loadInclude`<sup>2</sup> | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`load`](https://rollupjs.org/guide/en/#load) | βœ… | βœ… | βœ… | βœ… | βœ… <sup>3</sup> | βœ… | βœ… <sup>3</sup> |
| `transformInclude`<sup>2</sup> | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`transform`](https://rollupjs.org/guide/en/#transformers) | βœ… | βœ… | βœ… | βœ… | βœ… <sup>3</sup> | βœ… | βœ… <sup>3</sup> |
| [`watchChange`](https://rollupjs.org/guide/en/#watchchange) | βœ… | βœ… | βœ… | βœ… | ❌ | ❌ | ❌ |
| [`buildEnd`](https://rollupjs.org/guide/en/#buildend) | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | ❌ |
| [`writeBundle`](https://rollupjs.org/guide/en/#writebundle)<sup>4</sup> | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | ❌ |

1. Rollup and esbuild do not support using `enforce` to control the order of plugins. Users need to maintain the order manually.
2. Webpack's id filter is outside of loader logic; an additional hook is needed for better perf on Webpack. In Rollup and Vite, this hook has been polyfilled to match the behaviors. See for the following usage examples.
3. Although esbuild can handle both JavaScript and CSS and many other file formats, you can only return JavaScript in `load` and `transform` results.
4. Currently, `writeBundle` is only serves as a hook for the timing. It doesn't pass any arguments.

> **Warning**: The [Rspack](https://www.rspack.dev/) support is experimental. Future changes to Rspack integrations might not follow semver, please pin `unplugin` in your dependency when using. It's not recommended to use in production.
> **Warning**: The [Rspack](https://www.rspack.dev/) and [Bun](https://bun.sh/) support is experimental. Future changes to Rspack and Bun integrations might not follow semver, please pin `unplugin` in your dependency when using. It's not recommended to use in production.

### Hook Context

###### Supported

| Hook | Rollup | Vite | Webpack 4 | Webpack 5 | esbuild | Rspack |
| -------------------------------------------------------------------------- | :----: | :--: | :-------: | :-------: | :-----: | :----: |
| [`this.parse`](https://rollupjs.org/guide/en/#thisparse) | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`this.addWatchFile`](https://rollupjs.org/guide/en/#thisaddwatchfile) | βœ… | βœ… | βœ… | βœ… | ❌ | ❌ |
| [`this.emitFile`](https://rollupjs.org/guide/en/#thisemitfile)<sup>5</sup> | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`this.getWatchFiles`](https://rollupjs.org/guide/en/#thisgetwatchfiles) | βœ… | βœ… | βœ… | βœ… | ❌ | ❌ |
| [`this.warn`](https://rollupjs.org/guide/en/#thiswarn) | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`this.error`](https://rollupjs.org/guide/en/#thiserror) | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| Hook | Rollup | Vite | Webpack 4 | Webpack 5 | esbuild | Rspack | Bun |
| -------------------------------------------------------------------------- | :----: | :--: | :-------: | :-------: | :-----: | :----: | :-: |
| [`this.parse`](https://rollupjs.org/guide/en/#thisparse) | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`this.addWatchFile`](https://rollupjs.org/guide/en/#thisaddwatchfile) | βœ… | βœ… | βœ… | βœ… | ❌ | ❌ | ❌ |
| [`this.emitFile`](https://rollupjs.org/guide/en/#thisemitfile)<sup>5</sup> | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`this.getWatchFiles`](https://rollupjs.org/guide/en/#thisgetwatchfiles) | βœ… | βœ… | βœ… | βœ… | ❌ | ❌ | ❌ |
| [`this.warn`](https://rollupjs.org/guide/en/#thiswarn) | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |
| [`this.error`](https://rollupjs.org/guide/en/#thiserror) | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | βœ… |

5. Currently, [`this.emitFile`](https://rollupjs.org/guide/en/#thisemitfile) only supports the `EmittedAsset` variant.

Expand Down Expand Up @@ -81,6 +82,7 @@ export const rollupPlugin = unplugin.rollup
export const webpackPlugin = unplugin.webpack
export const rspackPlugin = unplugin.rspack
export const esbuildPlugin = unplugin.esbuild
export const bunPlugin = unplugin.bun
```

## Nested Plugins
Expand All @@ -89,12 +91,13 @@ Since `v0.10.0`, unplugin supports constructing multiple nested plugins to behav

###### Supported

| Rollup | Vite | Webpack 4 | Webpack 5 | Rspack | esbuild |
| :--------------------: | :--: | :-------: | :-------: | :----: | :------------: |
| βœ… `>=3.1`<sup>6</sup> | βœ… | βœ… | βœ… | βœ… | ⚠️<sup>7</sup> |
| Rollup | Vite | Webpack 4 | Webpack 5 | Rspack | esbuild | bun |
| :--------------------: | :--: | :-------: | :-------: | :----: | :------------: | -------------- |
| βœ… `>=3.1`<sup>6</sup> | βœ… | βœ… | βœ… | βœ… | ⚠️<sup>7</sup> | ⚠️<sup>7</sup> |


6. Rollup supports nested plugins since [v3.1.0](https://github.com/rollup/rollup/releases/tag/v3.1.0). Plugin author should ask users to have a Rollup version of `>=3.1.0` when using nested plugins. For single plugin format, unplugin works for any version of Rollup.
7. Since esbuild does not have a built-in transform phase, the `transform` hook of the nested plugin will not work on esbuild yet. Other hooks like `load` or `resolveId` work fine. We will try to find a way to support it in the future.
7. Since esbuild and Bun does not have a built-in transform phase, the `transform` hook of the nested plugin will not work on esbuild yet. Other hooks like `load` or `resolveId` work fine. We will try to find a way to support it in the future.

###### Usage

Expand Down Expand Up @@ -171,6 +174,16 @@ build({
})
```

###### Bun

```ts
import UnpluginFeature from './unplugin-feature'

Bun.build({
// ...
plugins: [UnpluginFeature.esbuild({ /* options */ })],
})
```

###### Rspack

Expand All @@ -189,7 +202,7 @@ While `unplugin` provides compatible layers for some hooks, the functionality of

```ts
export const unplugin = createUnplugin((options: UserOptions, meta) => {
console.log(meta.framework) // 'vite' | 'rollup' | 'webpack' | 'rspack' | 'esbuild'
console.log(meta.framework) // 'vite' | 'rollup' | 'webpack' | 'rspack' | 'esbuild' | 'bun'

return {
// common unplugin hooks
Expand Down Expand Up @@ -221,6 +234,13 @@ export const unplugin = createUnplugin((options: UserOptions, meta) => {
// or you can completely replace the setup logic
// setup?: EsbuildPlugin.setup,
},
bun: {
// change the filter of onResolve and onLoad
// onResolveFilter?: RegExp,
// onLoadFilter?: RegExp,
// or you can completely replace the setup logic
// setup?: BunPlugin.setup,
},
}
})
```
Expand All @@ -232,6 +252,7 @@ Each of the function takes the same generic factory argument as `createUnplugin`

```ts
import {
createBunPlugin,
createEsbuildPlugin,
createRollupPlugin,
createRspackPlugin,
Expand All @@ -242,6 +263,7 @@ import {
const vitePlugin = createVitePlugin({ /* options */ })
const rollupPlugin = createRollupPlugin({ /* options */ })
const esbuildPlugin = createEsbuildPlugin({ /* options */ })
const bunPlugin = createBunPlugin({ /* options */ })
const webpackPlugin = createWebpackPlugin({ /* options */ })
const rspackPlugin = createRspackPlugin({ /* options */ })
```
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@types/node": "^20.6.0",
"@types/webpack-sources": "^3.2.0",
"bumpp": "^9.2.0",
"bun-types": "^1.0.1",
"conventional-changelog-cli": "^3.0.0",
"esbuild": "^0.19.2",
"eslint": "^8.49.0",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

138 changes: 138 additions & 0 deletions src/bun/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import fs from 'fs'
import path from 'path'
import type { SourceMap } from 'rollup'
import type { RawSourceMap } from '@ampproject/remapping'
import type { BunPlugin, UnpluginBuildContext, UnpluginContext, UnpluginContextMeta, UnpluginFactory, UnpluginInstance, UnpluginOptions } from '../types'
import { combineSourcemaps, createBunContext, guessLoader, processCodeWithSourceMap, toArray } from './utils'

let i = 0

export function getBunPlugin<UserOptions = Record<string, never>>(
factory: UnpluginFactory<UserOptions>,
): UnpluginInstance<UserOptions>['bun'] {
return (userOptions?: UserOptions): BunPlugin => {
const meta: UnpluginContextMeta = {
framework: 'bun',
}
const plugins = toArray(factory(userOptions!, meta))

const setup = (plugin: UnpluginOptions): BunPlugin['setup'] =>
plugin.bun?.setup
?? ((build) => {
meta.build = build
const { onResolve, onLoad, config } = build

const onResolveFilter = plugin.esbuild?.onResolveFilter ?? /.*/
const onLoadFilter = plugin.esbuild?.onLoadFilter ?? /.*/

const context: UnpluginBuildContext = createBunContext(config)

if (plugin.resolveId) {
onResolve({ filter: onResolveFilter }, async (args) => {
if (config.external?.includes(args.path)) {
// We don't want to call the `resolveId` hook for external modules, since rollup doesn't do
// that and we want to have consistent behaviour across bundlers
return undefined
}

const isEntry = args.kind === 'entry-point'
const result = await plugin.resolveId!(
args.path,
// We explicitly have this if statement here for consistency with the integration of other bundelers.
// Here, `args.importer` is just an empty string on entry files whereas the euqivalent on other bundlers is `undefined.`
isEntry ? undefined : args.importer,
{ isEntry },
)
if (typeof result === 'string')
return { path: result, namespace: plugin.name }
else if (typeof result === 'object' && result !== null)
return { path: result.id, external: result.external, namespace: plugin.name }
})
}

if (plugin.load || plugin.transform) {
onLoad({ filter: onLoadFilter }, async (args) => {
const id = args.path

const pluginContext: UnpluginContext = {
error(message) {
console.error(message)
},
warn(message) {
console.warn(message)
},
}

let code: string | undefined, map: SourceMap | null | undefined

if (plugin.load && (!plugin.loadInclude || plugin.loadInclude(id))) {
const result = await plugin.load.call(Object.assign(context, pluginContext), id)
if (typeof result === 'string') {
code = result
}
else if (typeof result === 'object' && result !== null) {
code = result.code
map = result.map as any
}
}

if (!plugin.transform) {
if (code === undefined)
return undefined as never

if (map)
code = processCodeWithSourceMap(map, code)

return { contents: code, loader: guessLoader(args.path) }
}

if (!plugin.transformInclude || plugin.transformInclude(id)) {
if (!code) {
// 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.
code = await fs.promises.readFile(args.path, 'utf8')
}

const result = await plugin.transform.call(Object.assign(context, pluginContext), code, id)
if (typeof result === 'string') {
code = result
}
else if (typeof result === 'object' && result !== null) {
code = result.code
// if we already got sourcemap from `load()`,
// combine the two sourcemaps
if (map && result.map) {
map = combineSourcemaps(args.path, [
result.map as RawSourceMap,
map as RawSourceMap,
]) as SourceMap
}
else {
// otherwise, we always keep the last one, even if it's empty
map = result.map as any
}
}
}

if (code) {
if (map)
code = processCodeWithSourceMap(map, code)
return { contents: code, loader: guessLoader(args.path) }
}
return undefined as never
})
}
})

const setupMultiplePlugins = (): BunPlugin['setup'] =>
(build) => {
for (const plugin of plugins)
setup(plugin)(build)
}

return plugins.length === 1
? { name: plugins[0].name, setup: setup(plugins[0]) }
: { name: meta.bunHostName ?? `unplugin-host-${i++}`, setup: setupMultiplePlugins() }
}
}
Loading