unloader Node.js loader with a Rollup-like interface. Overview unloader is a Node.js loader framework. Similar to Rollup as a general bundler, unloader provides customization capabilities through a subset of Rollup plugin API. unloader is designed to be a general-purpose loader, which can be used to develop various loaders, such as Oxc loader, TypeScript loader, etc. Install Pre-requisites ESM: Node.js v18.19 or v20.6 and above CJS: Node.js v22.15 and above npm i unloader Usage CLI node --import unloader/register ... # For ESM only, support both sync and async hooks node --require unloader/register-sync ... # For both ESM and CJS, only support sync hooks Plugin Development Hooks Hook Description options Modify the options from userland. resolveId Resolve the module id. load Load the module. transform Transform the module. ESM and CJS unloader supports both ESM and CJS, however, async hooks are only supported in ESM. To support both ESM and CJS, please make sure all hooks are synchronous, or use quansync. Here is an example of using sync hooks and quansync: Show code import { readFileSync } from 'node:fs' import { readFile } from '@quansync/fs' import { quansync } from 'quansync' import type { Plugin } from 'unloader' // sync usage const pluginSync: Plugin<true> = { name: 'my-plugin', resolveId(source, importer, options) { const result = this.resolve(`${source}.js`, importer, options) if (result) { console.log(result) return result } }, load(id) { const contents = readFileSync(id, 'utf8') console.log(contents) return contents }, } // quansync usage const pluginQuansync: Plugin = { name: 'my-plugin', resolveId: quansync(function* (source, importer, options) { const result = yield this.resolve(`${source}.js`, importer, options) if (result) { console.log(result) return result } }), load: quansync(function* (id) { const contents = yield readFile(id, 'utf8') console.log(contents) return contents }), } Example demo.ts let context: PluginContext export function demoPlugin(): Plugin { return { name: 'demo-plugin', options(config) { config.sourcemap = true }, buildStart(_context) { context = _context context.log('hello world') }, async resolveId(source, importer, options) { if (source.startsWith('node:')) return // Feature: virtual module if (source === 'virtual-mod') { return '/virtual-mod' } // Feature: try resolve with different extensions const result = await this.resolve(`${source}.js`, importer, options) if (result) return result }, load(id) { if (id === '/virtual-mod') { return { code: 'export const count = 42' } } }, transform(code, id) { if (typeof code === 'string') { // Feature: source map const s = new MagicString(code) s.prepend('// header\n') const map = s.generateMap({ file: id, hires: 'boundary', includeContent: true, }) return { code: s.toString(), map, } } }, } } See demo plugin and unloader.config.ts for more details. unplugin unloader is supported as a framework in unplugin. // unloader.config.ts import Oxc from 'unplugin-oxc/unloader' export default { plugins: [ Oxc({ // options }), ], } Credits Thanks to tsx! Sponsors License MIT License © 2025 三咲智子 Kevin Deng