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
2 changes: 2 additions & 0 deletions docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ More details can be found in the [Rolldown's documentation](https://rolldown.rs/
| Context | Rollup | Vite | webpack | esbuild | Rspack | Farm | Rolldown | Bun |
| ------------------------------------------------------------------------------------- | :----: | :--: | :-----: | :-----: | :----: | :--: | :------: | :-: |
| [`this.parse`](https://rollupjs.org/plugin-development/#this-parse)<sup>1</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [`this.fs`](https://rollupjs.org/plugin-development/#this-fs)<sup>3</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [`this.addWatchFile`](https://rollupjs.org/plugin-development/#this-addwatchfile) | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
| [`this.emitFile`](https://rollupjs.org/plugin-development/#this-emitfile)<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [`this.getWatchFiles`](https://rollupjs.org/plugin-development/#this-getwatchfiles) | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
Expand All @@ -320,6 +321,7 @@ More details can be found in the [Rolldown's documentation](https://rolldown.rs/

1. For bundlers other than Rollup, Rolldown, or Vite, `setParseImpl` must be called to manually provide a parser implementation. Parsers such as [Acorn](https://github.com/acornjs/acorn), [Babel](https://babeljs.io/), or [Oxc](https://oxc.rs/) can be used.
2. Currently, [`this.emitFile`](https://rollupjs.org/plugin-development/#thisemitfile) only supports the `EmittedAsset` variant.
3. For bundlers without native plugin context file-system APIs, `this.fs` falls back to a Node-compatible implementation.

:::

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"dependencies": {
"@jridgewell/remapping": "catalog:prod",
"picomatch": "catalog:prod",
"pify": "catalog:prod",
"webpack-virtual-modules": "catalog:prod"
},
"devDependencies": {
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

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

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ catalogs:
prod:
'@jridgewell/remapping': ^2.3.5
picomatch: ^4.0.4
pify: ^6.1.0
webpack-virtual-modules: ^0.6.2

test:
Expand Down
2 changes: 2 additions & 0 deletions src/bun/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Loader, PluginBuilder } from 'bun'
import type { UnpluginBuildContext, UnpluginContext, UnpluginMessage } from '../types'
import fs from 'node:fs'
import path from 'node:path'
import { createBuildContextFs } from '../utils/fs'
import { parse } from '../utils/parse'

const ExtToLoader: Record<string, Loader> = {
Expand Down Expand Up @@ -30,6 +31,7 @@ export function createBuildContext(build: PluginBuilder): UnpluginBuildContext {
const watchFiles: string[] = []

return {
fs: createBuildContextFs(),
addWatchFile(file) {
watchFiles.push(file)
},
Expand Down
2 changes: 2 additions & 0 deletions src/esbuild/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Buffer } from 'node:buffer'
import fs from 'node:fs'
import path from 'node:path'
import remapping from '@jridgewell/remapping'
import { createBuildContextFs } from '../utils/fs'
import { parse } from '../utils/parse'

const ExtToLoader: Record<string, Loader> = {
Expand Down Expand Up @@ -113,6 +114,7 @@ export function createBuildContext(build: PluginBuild): UnpluginBuildContext {
const watchFiles: string[] = []
const { initialOptions } = build
return {
fs: createBuildContextFs(),
parse,
addWatchFile() {
throw new Error('unplugin/esbuild: addWatchFile outside supported hooks (resolveId, load, transform)')
Expand Down
2 changes: 2 additions & 0 deletions src/farm/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import type { CompilationContext } from '@farmfe/core'
import type { UnpluginBuildContext, UnpluginContext } from '../types'
import { Buffer } from 'node:buffer'
import { extname } from 'node:path'
import { createBuildContextFs } from '../utils/fs'
import { parse } from '../utils/parse'

export function createFarmContext(
context: CompilationContext,
currentResolveId?: string,
): UnpluginBuildContext {
return {
fs: createBuildContextFs(),
parse,

addWatchFile(id: string) {
Expand Down
3 changes: 3 additions & 0 deletions src/rspack/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import type { Compilation, Compiler, LoaderContext } from '@rspack/core'
import type { UnpluginBuildContext, UnpluginContext, UnpluginMessage } from '../types'
import { Buffer } from 'node:buffer'
import { resolve } from 'node:path'
import { createBuildContextFs } from '../utils/fs'
import { parse } from '../utils/parse'

export function createBuildContext(compiler: Compiler, compilation: Compilation, loaderContext?: LoaderContext, inputSourceMap?: any): UnpluginBuildContext {
const inputFs = loaderContext?.fs ?? compiler.inputFileSystem
return {
fs: createBuildContextFs(inputFs),
getNativeBuildContext() {
return {
framework: 'rspack',
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CompilationContext as FarmCompilationContext, JsPlugin as FarmPlug
import type { Compilation as RspackCompilation, Compiler as RspackCompiler, LoaderContext as RspackLoaderContext, RspackPluginInstance } from '@rspack/core'
import type { BunPlugin, PluginBuilder as BunPluginBuilder } from 'bun'
import type { BuildOptions, Plugin as EsbuildPlugin, Loader, PluginBuild } from 'esbuild'
import type { PathLike, Stats } from 'node:fs'
import type { Plugin as RolldownPlugin } from 'rolldown'
import type { EmittedAsset, PluginContextMeta as RollupContextMeta, Plugin as RollupPlugin, SourceMapInput } from 'rollup'
import type { Plugin as UnloaderPlugin } from 'unloader'
Expand Down Expand Up @@ -57,13 +58,20 @@ export type NativeBuildContext
| { framework: 'bun', build: BunPluginBuilder }

export interface UnpluginBuildContext {
fs: UnpluginContextFs
addWatchFile: (id: string) => void
emitFile: (emittedFile: EmittedAsset) => void
getWatchFiles: () => string[]
parse: (input: string, options?: any) => any
getNativeBuildContext?: (() => NativeBuildContext) | undefined
}

export interface UnpluginContextFs {
readFile: (path: PathLike, options?: any) => Promise<string | Uint8Array>
stat: (path: PathLike, options?: any) => Promise<Stats>
lstat: (path: PathLike, options?: any) => Promise<Stats>
}
Comment on lines +69 to +73

export type StringOrRegExp = string | RegExp
export type FilterPattern = Arrayable<StringOrRegExp>
export type StringFilter
Expand Down
32 changes: 32 additions & 0 deletions src/utils/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { InputFileSystem } from 'webpack'
import type { UnpluginContextFs } from '../types'
import pify from 'pify'
Comment on lines +1 to +3

// Dynamic import node:fs module since it may not be available in some environments
function createNodeFs(): UnpluginContextFs {
const fsPromiseModule = import('node:fs/promises')
return {
readFile: async (path, options) => {
const fs = await fsPromiseModule
return fs.readFile(path, options)
},
stat: async (path, options) => {
const fs = await fsPromiseModule
return fs.stat(path, options)
},
lstat: async (path, options) => {
const fs = await fsPromiseModule
Comment on lines +8 to +18
return fs.lstat(path, options)
},
}
}

export function createBuildContextFs(inputFs?: InputFileSystem | null): UnpluginContextFs {
const fs = inputFs ? pify(inputFs) : createNodeFs()

return {
readFile: fs.readFile as UnpluginContextFs['readFile'],
stat: fs.stat as UnpluginContextFs['stat'],
lstat: fs.lstat as UnpluginContextFs['lstat'],
}
}
3 changes: 3 additions & 0 deletions src/webpack/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Buffer } from 'node:buffer'
import { createRequire } from 'node:module'
import { resolve } from 'node:path'
import process from 'node:process'
import { createBuildContextFs } from '../utils/fs'
import { parse } from '../utils/parse'

interface ContextOptions {
Expand Down Expand Up @@ -31,7 +32,9 @@ export function getSource(fileSource: string | Uint8Array): sources.RawSource {
}

export function createBuildContext(options: ContextOptions, compiler: Compiler, compilation?: Compilation, loaderContext?: LoaderContext<{ unpluginName: string }>, inputSourceMap?: any): UnpluginBuildContext {
const inputFs = loaderContext?.fs ?? compiler.inputFileSystem
return {
fs: createBuildContextFs(inputFs),
parse,
addWatchFile(id) {
options.addWatchFile(resolve(process.cwd(), id))
Expand Down
4 changes: 4 additions & 0 deletions test/unit-tests/bun/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ describe('bun utils', () => {
expect(context.addWatchFile).toBeInstanceOf(Function)
expect(context.getWatchFiles).toBeInstanceOf(Function)
expect(context.emitFile).toBeInstanceOf(Function)
expect(context.fs).toBeInstanceOf(Object)
expect(context.fs.readFile).toBeInstanceOf(Function)
expect(context.fs.stat).toBeInstanceOf(Function)
expect(context.fs.lstat).toBeInstanceOf(Function)
expect(context.parse).toBeInstanceOf(Function)
expect(context.getNativeBuildContext).toBeInstanceOf(Function)
})
Expand Down
4 changes: 4 additions & 0 deletions test/unit-tests/esbuild/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ describe('utils', () => {
expect(actual.parse).toBeInstanceOf(Function)
expect(actual.emitFile).toBeInstanceOf(Function)
expect(actual.addWatchFile).toBeInstanceOf(Function)
expect(actual.fs).toBeInstanceOf(Object)
expect(actual.fs.readFile).toBeInstanceOf(Function)
expect(actual.fs.stat).toBeInstanceOf(Function)
expect(actual.fs.lstat).toBeInstanceOf(Function)
expect(actual.getNativeBuildContext).toBeInstanceOf(Function)

expect(actual.getNativeBuildContext!()).toEqual({
Expand Down
4 changes: 4 additions & 0 deletions test/unit-tests/farm/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ describe('createFarmContext', () => {

const farmContext = createFarmContext(mockContext)

expect(farmContext.fs).toBeDefined()
expect(farmContext.fs.readFile).toBeInstanceOf(Function)
expect(farmContext.fs.stat).toBeInstanceOf(Function)
expect(farmContext.fs.lstat).toBeInstanceOf(Function)
expect(farmContext.parse).toBeDefined()
expect(farmContext.parse).toBeInstanceOf(Function)
})
Expand Down
18 changes: 15 additions & 3 deletions test/unit-tests/resolve-id/resolve-id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,32 @@ function createUnpluginWithCallback(resolveIdCallback: UnpluginOptions['resolveI
}

// We extract this check because all bundlers should behave the same
const propsToTest: (keyof (UnpluginContext & UnpluginBuildContext))[] = ['addWatchFile', 'emitFile', 'getWatchFiles', 'parse', 'error', 'warn']
const propsToTest: (keyof (UnpluginContext & UnpluginBuildContext))[] = ['addWatchFile', 'emitFile', 'getWatchFiles', 'parse', 'error', 'warn', 'fs']

function createResolveIdHook(): Mock {
const mockResolveIdHook = vi.fn(function (this: UnpluginContext & UnpluginBuildContext) {
for (const prop of propsToTest) {
expect(this).toHaveProperty(prop)
expect(this[prop]).toBeInstanceOf(Function)
if (prop === 'fs') {
expect(this.fs).toBeTruthy()
expect(typeof this.fs).toBe('object')
expect(this.fs.readFile).toBeInstanceOf(Function)
expect(this.fs.stat).toBeInstanceOf(Function)
expect(this.fs.lstat).toBeInstanceOf(Function)
}
else {
expect(this[prop]).toBeInstanceOf(Function)
}
}
})
return mockResolveIdHook
}

function checkResolveIdHook(resolveIdCallback: Mock): void {
expect.assertions(4 * (1 + propsToTest.length * 2))
const fsAssertionsPerHookCall = 6 // `toHaveProperty('fs')` + 5 assertions (`toBeTruthy`, `typeof`, `readFile`, `stat`, `lstat`)
const nonFsAssertionsPerHookCall = (propsToTest.length - 1) * 2
const calledWithAssertionPerHookCall = 1
expect.assertions(4 * (calledWithAssertionPerHookCall + nonFsAssertionsPerHookCall + fsAssertionsPerHookCall))

expect(resolveIdCallback).toHaveBeenCalledWith(
expect.stringMatching(/(?:\/|\\)entry\.js$/),
Expand Down
4 changes: 4 additions & 0 deletions test/unit-tests/rspack/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ describe('createBuildContext', () => {
const inputSourceMap = { name: 'inputSourceMap' }

const buildContext = createBuildContext(compiler as any, compilation as any, loaderContext as any, inputSourceMap as any)
expect(buildContext.fs).toBeInstanceOf(Object)
expect(buildContext.fs.readFile).toBeInstanceOf(Function)
expect(buildContext.fs.stat).toBeInstanceOf(Function)
expect(buildContext.fs.lstat).toBeInstanceOf(Function)

expect(buildContext.getNativeBuildContext!()).toEqual({
framework: 'rspack',
Expand Down
4 changes: 4 additions & 0 deletions test/unit-tests/webpack/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ describe('webpack - utils', () => {
} as unknown as Compilation

const buildContext = createBuildContext(mockOptions, mockCompiler, mockCompilation)
expect(buildContext.fs).toBeInstanceOf(Object)
expect(buildContext.fs.readFile).toBeInstanceOf(Function)
expect(buildContext.fs.stat).toBeInstanceOf(Function)
expect(buildContext.fs.lstat).toBeInstanceOf(Function)
buildContext.addWatchFile('file2.js')
expect(mockOptions.addWatchFile).toHaveBeenCalledWith(expect.stringContaining('file2.js'))

Expand Down
Loading