From 68ab9c93217eb601356c723ab9e0a6b81f1ba175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 14 Sep 2023 20:52:41 +0800 Subject: [PATCH] feat: support bun --- README.md | 74 +++++++++++++++--------- package.json | 1 + pnpm-lock.yaml | 7 +++ src/bun/index.ts | 138 +++++++++++++++++++++++++++++++++++++++++++ src/bun/utils.ts | 148 +++++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 15 +++++ 6 files changed, 357 insertions(+), 26 deletions(-) create mode 100644 src/bun/index.ts create mode 100644 src/bun/utils.ts diff --git a/README.md b/README.md index 47800697..0008b859 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Currently supports: - [Webpack](https://webpack.js.org/) - [esbuild](https://esbuild.github.io/) - [Rspack](https://www.rspack.dev/) (⚠️ experimental) +- [Bun](https://bun.sh/) (⚠️ experimental) ## Hooks @@ -20,38 +21,38 @@ Currently supports: ###### Supported -| Hook | Rollup | Vite | Webpack 4 | Webpack 5 | esbuild | Rspack | -| ----------------------------------------------------------------------- | :-------------: | :--: | :-------: | :-------: | :-------------: | :----: | -| [`enforce`](https://rollupjs.org/guide/en/#enforce) | ❌ 1 | ✅ | ✅ | ✅ | ❌ 1 | ✅ | -| [`buildStart`](https://rollupjs.org/guide/en/#buildstart) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| [`resolveId`](https://rollupjs.org/guide/en/#resolveid) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | -| `loadInclude`2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| [`load`](https://rollupjs.org/guide/en/#load) | ✅ | ✅ | ✅ | ✅ | ✅ 3 | ✅ | -| `transformInclude`2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| [`transform`](https://rollupjs.org/guide/en/#transformers) | ✅ | ✅ | ✅ | ✅ | ✅ 3 | ✅ | -| [`watchChange`](https://rollupjs.org/guide/en/#watchchange) | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | -| [`buildEnd`](https://rollupjs.org/guide/en/#buildend) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| [`writeBundle`](https://rollupjs.org/guide/en/#writebundle)4 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Hook | Rollup | Vite | Webpack 4 | Webpack 5 | esbuild | Rspack | Bun | +| ----------------------------------------------------------------------- | :-------------: | :--: | :-------: | :-------: | :-------------: | :----: | :-------------: | +| [`enforce`](https://rollupjs.org/guide/en/#enforce) | ❌ 1 | ✅ | ✅ | ✅ | ❌ 1 | ✅ | ❌ 1 | +| [`buildStart`](https://rollupjs.org/guide/en/#buildstart) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | +| [`resolveId`](https://rollupjs.org/guide/en/#resolveid) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | +| `loadInclude`2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [`load`](https://rollupjs.org/guide/en/#load) | ✅ | ✅ | ✅ | ✅ | ✅ 3 | ✅ | ✅ 3 | +| `transformInclude`2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [`transform`](https://rollupjs.org/guide/en/#transformers) | ✅ | ✅ | ✅ | ✅ | ✅ 3 | ✅ | ✅ 3 | +| [`watchChange`](https://rollupjs.org/guide/en/#watchchange) | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| [`buildEnd`](https://rollupjs.org/guide/en/#buildend) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | +| [`writeBundle`](https://rollupjs.org/guide/en/#writebundle)4 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | 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)5 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| [`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)5 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [`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. @@ -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 @@ -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`6 | ✅ | ✅ | ✅ | ✅ | ⚠️7 | +| Rollup | Vite | Webpack 4 | Webpack 5 | Rspack | esbuild | bun | +| :--------------------: | :--: | :-------: | :-------: | :----: | :------------: | -------------- | +| ✅ `>=3.1`6 | ✅ | ✅ | ✅ | ✅ | ⚠️7 | ⚠️7 | + 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 @@ -171,6 +174,16 @@ build({ }) ``` +###### Bun + +```ts +import UnpluginFeature from './unplugin-feature' + +Bun.build({ + // ... + plugins: [UnpluginFeature.esbuild({ /* options */ })], +}) +``` ###### Rspack @@ -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 @@ -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, + }, } }) ``` @@ -232,6 +252,7 @@ Each of the function takes the same generic factory argument as `createUnplugin` ```ts import { + createBunPlugin, createEsbuildPlugin, createRollupPlugin, createRspackPlugin, @@ -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 */ }) ``` diff --git a/package.json b/package.json index d35a9ca9..38b97261 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 281daca2..3dc093dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,9 @@ devDependencies: bumpp: specifier: ^9.2.0 version: 9.2.0 + bun-types: + specifier: ^1.0.1 + version: 1.0.1 conventional-changelog-cli: specifier: ^3.0.0 version: 3.0.0 @@ -2066,6 +2069,10 @@ packages: - supports-color dev: true + /bun-types@1.0.1: + resolution: {integrity: sha512-7NrXqhMIaNKmWn2dSWEQ50znMZqrN/5Z0NBMXvQTRu/+Y1CvoXRznFy0pnqLe024CeZgVdXoEpARNO1JZLAPGw==} + dev: true + /bundle-require@4.0.1(esbuild@0.18.11): resolution: {integrity: sha512-9NQkRHlNdNpDBGmLpngF3EFDcwodhMUuLz9PaWYciVcQF9SE4LFjM2DB/xV1Li5JiuDMv7ZUWuC3rGbqR0MAXQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} diff --git a/src/bun/index.ts b/src/bun/index.ts new file mode 100644 index 00000000..5c841ad3 --- /dev/null +++ b/src/bun/index.ts @@ -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>( + factory: UnpluginFactory, +): UnpluginInstance['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() } + } +} diff --git a/src/bun/utils.ts b/src/bun/utils.ts new file mode 100644 index 00000000..a61ae6af --- /dev/null +++ b/src/bun/utils.ts @@ -0,0 +1,148 @@ +/// + +import fs from 'fs' +import path from 'path' +import { Buffer } from 'buffer' +import remapping from '@ampproject/remapping' +import { Parser } from 'acorn' +import type { DecodedSourceMap, EncodedSourceMap } from '@ampproject/remapping' +import type { BuildConfig, Loader } from 'bun' +import type { SourceMap } from 'rollup' +import type { UnpluginBuildContext } from '../types' + +export * from '../utils' + +const ExtToLoader: Record = { + '.js': 'js', + '.mjs': 'js', + '.cjs': 'js', + '.jsx': 'jsx', + '.ts': 'ts', + '.cts': 'ts', + '.mts': 'ts', + '.tsx': 'tsx', + '.json': 'json', + '.txt': 'text', + '.toml': 'toml', + '.wasm': 'wasm', + '.napi': 'napi', + '.node': 'napi', +} + +export function guessLoader(id: string): Loader { + return ExtToLoader[path.extname(id).toLowerCase()] || 'js' +} + +// `load` and `transform` may return a sourcemap without toString and toUrl, +// but bun needs them, we fix the two methods +export function fixSourceMap(map: EncodedSourceMap): SourceMap { + if (!('toString' in map)) { + Object.defineProperty(map, 'toString', { + enumerable: false, + value: function toString() { + return JSON.stringify(this) + }, + }) + } + if (!('toUrl' in map)) { + Object.defineProperty(map, 'toUrl', { + enumerable: false, + value: function toUrl() { + return `data:application/json;charset=utf-8;base64,${Buffer.from( + this.toString(), + ).toString('base64')}` + }, + }) + } + return map as SourceMap +} + +// taken from https://github.com/vitejs/vite/blob/71868579058512b51991718655e089a78b99d39c/packages/vite/src/node/utils.ts#L525 +const nullSourceMap: EncodedSourceMap = { + names: [], + sources: [], + mappings: '', + version: 3, +} +export function combineSourcemaps( + filename: string, + sourcemapList: Array, +): EncodedSourceMap { + sourcemapList = sourcemapList.filter(m => m.sources) + + if ( + sourcemapList.length === 0 + || sourcemapList.every(m => m.sources.length === 0) + ) + return { ...nullSourceMap } + + // We don't declare type here so we can convert/fake/map as EncodedSourceMap + let map // : SourceMap + let mapIndex = 1 + const useArrayInterface + = sourcemapList.slice(0, -1).find(m => m.sources.length !== 1) === undefined + if (useArrayInterface) { + map = remapping(sourcemapList, () => null, true) + } + else { + map = remapping( + sourcemapList[0], + (sourcefile) => { + if (sourcefile === filename && sourcemapList[mapIndex]) + return sourcemapList[mapIndex++] + else return { ...nullSourceMap } + }, + true, + ) + } + if (!map.file) + delete map.file + + return map as EncodedSourceMap +} + +export function createBunContext( + initialOptions: BuildConfig, +): UnpluginBuildContext { + return { + parse(code: string, opts: any = {}) { + return Parser.parse(code, { + sourceType: 'module', + ecmaVersion: 'latest', + locations: true, + ...opts, + }) + }, + addWatchFile() {}, + emitFile(emittedFile) { + // Ensure output directory exists for this.emitFile + if (initialOptions.outdir && !fs.existsSync(initialOptions.outdir)) + fs.mkdirSync(initialOptions.outdir, { recursive: true }) + + const outFileName = emittedFile.fileName || emittedFile.name + if (initialOptions.outdir && emittedFile.source && outFileName) { + fs.writeFileSync( + path.resolve(initialOptions.outdir, outFileName), + emittedFile.source, + ) + } + }, + getWatchFiles() { + return [] + }, + } +} + +export function processCodeWithSourceMap( + map: SourceMap | null | undefined, + code: string, +) { + if (map) { + if (!map.sourcesContent || map.sourcesContent.length === 0) + map.sourcesContent = [code] + + map = fixSourceMap(map as EncodedSourceMap) + code += `\n//# sourceMappingURL=${map.toUrl()}` + } + return code +} diff --git a/src/types.ts b/src/types.ts index 8f7e5d5b..db8e19ed 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,13 +2,16 @@ import type { AcornNode, EmittedAsset, PluginContextMeta as RollupContextMeta, P import type { Compiler as WebpackCompiler, WebpackPluginInstance } from 'webpack' import type { Plugin as VitePlugin } from 'vite' import type { Plugin as EsbuildPlugin, PluginBuild } from 'esbuild' +import type { BunPlugin, PluginBuilder } from 'bun' import type { Compiler as RspackCompiler, RspackPluginInstance } from '@rspack/core' import type VirtualModulesPlugin from 'webpack-virtual-modules' export { EsbuildPlugin, + BunPlugin, RollupPlugin, VitePlugin, + WebpackPluginInstance, RspackPluginInstance, RspackCompiler, @@ -76,6 +79,12 @@ export interface UnpluginOptions { onLoadFilter?: RegExp setup?: EsbuildPlugin['setup'] } + bun?: { + // using regexp in bun improves performance + onResolveFilter?: RegExp + onLoadFilter?: RegExp + setup?: BunPlugin['setup'] + } } export interface ResolvedUnpluginOptions extends UnpluginOptions { @@ -99,6 +108,7 @@ export interface UnpluginInstance webpack: UnpluginFactoryOutput rspack: UnpluginFactoryOutput esbuild: UnpluginFactoryOutput + bun: UnpluginFactoryOutput raw: UnpluginFactory } @@ -114,6 +124,11 @@ export type UnpluginContextMeta = Partial & ({ build?: PluginBuild /** Set the host plugin name of esbuild when returning multiple plugins */ esbuildHostName?: string +} | { + framework: 'bun' + build?: PluginBuilder + /** Set the host plugin name of esbuild when returning multiple plugins */ + bunHostName?: string } | { framework: 'rspack' rspack: { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy