├── .eslintignore ├── src ├── index.ts ├── webpack │ ├── utils.ts │ ├── loaders │ │ ├── transform.ts │ │ └── load.ts │ ├── genContext.ts │ └── index.ts ├── vite │ └── index.ts ├── define.ts ├── rollup │ └── index.ts ├── esbuild │ ├── utils.ts │ └── index.ts └── types.ts ├── test └── fixtures │ ├── transform │ ├── src │ │ ├── target.js │ │ ├── nontarget.js │ │ └── main.js │ ├── rollup.config.js │ ├── esbuild.config.js │ ├── webpack.config.js │ ├── vite.config.js │ ├── unplugin.js │ └── __test__ │ │ └── build.test.ts │ └── virtual-module │ ├── src │ └── main.js │ ├── rollup.config.js │ ├── esbuild.config.js │ ├── webpack.config.js │ ├── vite.config.js │ ├── unplugin.js │ └── __test__ │ └── build.test.ts ├── .gitignore ├── renovate.json ├── .eslintrc ├── tsup.config.ts ├── tsconfig.json ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── scripts └── buildFixtures.ts ├── package.json ├── README.md ├── CHANGELOG.md └── pnpm-lock.yaml /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | temp 3 | dist-* 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './define' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /test/fixtures/transform/src/target.js: -------------------------------------------------------------------------------- 1 | export const msg2 = 'TARGET: __UNPLUGIN__' 2 | -------------------------------------------------------------------------------- /test/fixtures/transform/src/nontarget.js: -------------------------------------------------------------------------------- 1 | export const msg1 = 'NON-TARGET: __UNPLUGIN__' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | .profile 3 | .vscode 4 | *.log* 5 | coverage 6 | dist 7 | node_modules 8 | temp 9 | -------------------------------------------------------------------------------- /test/fixtures/transform/src/main.js: -------------------------------------------------------------------------------- 1 | import { msg1 } from './nontarget' 2 | import { msg2 } from './target' 3 | 4 | console.log(msg1, msg2) 5 | -------------------------------------------------------------------------------- /test/fixtures/virtual-module/src/main.js: -------------------------------------------------------------------------------- 1 | import msg1 from 'virtual/1' 2 | import msg2 from 'virtual/2' 3 | 4 | console.log(msg1, msg2) 5 | -------------------------------------------------------------------------------- /src/webpack/utils.ts: -------------------------------------------------------------------------------- 1 | import { sep } from 'path' 2 | 3 | export function slash (path: string) { 4 | return path.replace(/[\\/]/g, sep) 5 | } 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs" 4 | ], 5 | "packageRules": [ 6 | { 7 | "depTypeList": ["peerDependencies"], 8 | "enabled": false 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs/eslint-config-typescript" 4 | ], 5 | "rules": { 6 | "no-use-before-define": "off", 7 | "import/no-named-as-default-member": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/virtual-module/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { rollup } = require('./unplugin') 2 | 3 | export default { 4 | input: './src/main.js', 5 | output: { 6 | dir: './dist/rollup' 7 | }, 8 | plugins: [ 9 | rollup() 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/transform/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { rollup } = require('./unplugin') 2 | 3 | export default { 4 | input: './src/main.js', 5 | output: { 6 | dir: './dist/rollup', 7 | sourcemap: true 8 | }, 9 | plugins: [ 10 | rollup({ msg: 'Rollup' }) 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/virtual-module/esbuild.config.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild') 2 | const { esbuild } = require('./unplugin') 3 | 4 | build({ 5 | entryPoints: ['src/main.js'], 6 | bundle: true, 7 | outdir: 'dist/esbuild', 8 | sourcemap: true, 9 | plugins: [ 10 | esbuild() 11 | ] 12 | }) 13 | -------------------------------------------------------------------------------- /test/fixtures/transform/esbuild.config.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild') 2 | const { esbuild } = require('./unplugin') 3 | 4 | build({ 5 | entryPoints: ['src/main.js'], 6 | bundle: true, 7 | outdir: 'dist/esbuild', 8 | sourcemap: true, 9 | plugins: [ 10 | esbuild({ msg: 'Esbuild' }) 11 | ] 12 | }) 13 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | // tsup.config.ts 2 | import type { Options } from 'tsup' 3 | 4 | export const tsup: Options = { 5 | splitting: false, 6 | sourcemap: false, 7 | clean: true, 8 | format: ['cjs', 'esm'], 9 | dts: true, 10 | entryPoints: [ 11 | 'src/index.ts', 12 | 'src/webpack/loaders/load.ts', 13 | 'src/webpack/loaders/transform.ts' 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/virtual-module/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const { webpack } = require('./unplugin') 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: resolve(__dirname, 'src/main.js'), 7 | output: { 8 | path: resolve(__dirname, 'dist/webpack'), 9 | filename: 'main.js' 10 | }, 11 | plugins: [ 12 | webpack() 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "declaration": true, 10 | "resolveJsonModule": true, 11 | "types": [ 12 | "node" 13 | ] 14 | }, 15 | "include": [ 16 | "src" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/virtual-module/vite.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const { vite } = require('./unplugin') 3 | 4 | module.exports = { 5 | root: __dirname, 6 | plugins: [ 7 | vite() 8 | ], 9 | build: { 10 | lib: { 11 | entry: resolve(__dirname, 'src/main.js'), 12 | name: 'main', 13 | fileName: 'main.js' 14 | }, 15 | outDir: 'dist/vite' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/transform/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const { webpack } = require('./unplugin') 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: resolve(__dirname, 'src/main.js'), 7 | output: { 8 | path: resolve(__dirname, 'dist/webpack'), 9 | filename: 'main.js' 10 | }, 11 | plugins: [ 12 | webpack({ msg: 'Webpack' }) 13 | ], 14 | devtool: 'source-map' 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/transform/vite.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const { vite } = require('./unplugin') 3 | 4 | module.exports = { 5 | root: __dirname, 6 | plugins: [ 7 | vite({ msg: 'Vite' }) 8 | ], 9 | build: { 10 | lib: { 11 | entry: resolve(__dirname, 'src/main.js'), 12 | name: 'main', 13 | fileName: 'main.js' 14 | }, 15 | outDir: 'dist/vite', 16 | sourcemap: true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/virtual-module/unplugin.js: -------------------------------------------------------------------------------- 1 | const { createUnplugin } = require('unplugin') 2 | 3 | module.exports = createUnplugin(() => { 4 | return { 5 | name: 'virtual-module-fixture', 6 | resolveId (id) { 7 | return id.startsWith('virtual/') ? id : null 8 | }, 9 | load (id) { 10 | if (id === 'virtual/1') { 11 | return 'export default "VIRTUAL:ONE"' 12 | } 13 | if (id === 'virtual/2') { 14 | return 'export default "VIRTUAL:TWO"' 15 | } 16 | return null 17 | } 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /src/vite/index.ts: -------------------------------------------------------------------------------- 1 | import { toRollupPlugin } from '../rollup' 2 | import { UnpluginInstance, UnpluginFactory, VitePlugin, UnpluginContextMeta } from '../types' 3 | 4 | export function getVitePlugin ( 5 | factory: UnpluginFactory 6 | ): UnpluginInstance['vite'] { 7 | return (userOptions?: UserOptions) => { 8 | const meta: UnpluginContextMeta = { 9 | framework: 'vite' 10 | } 11 | const rawPlugin = factory(userOptions, meta) 12 | 13 | const plugin = toRollupPlugin(rawPlugin, false) as VitePlugin 14 | 15 | if (rawPlugin.vite) { 16 | Object.assign(plugin, rawPlugin.vite) 17 | } 18 | 19 | return plugin 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/define.ts: -------------------------------------------------------------------------------- 1 | import { getEsbuildPlugin } from './esbuild' 2 | import { getRollupPlugin } from './rollup' 3 | import { UnpluginInstance, UnpluginFactory } from './types' 4 | import { getVitePlugin } from './vite' 5 | import { getWebpackPlugin } from './webpack' 6 | 7 | export function createUnplugin ( 8 | factory: UnpluginFactory 9 | ): UnpluginInstance { 10 | return { 11 | get esbuild () { 12 | return getEsbuildPlugin(factory) 13 | }, 14 | get rollup () { 15 | return getRollupPlugin(factory) 16 | }, 17 | get vite () { 18 | return getVitePlugin(factory) 19 | }, 20 | get webpack () { 21 | return getWebpackPlugin(factory) 22 | }, 23 | get raw () { 24 | return factory 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/transform/unplugin.js: -------------------------------------------------------------------------------- 1 | const { createUnplugin } = require('unplugin') 2 | const MagicString = require('magic-string') 3 | 4 | module.exports = createUnplugin((options) => { 5 | return { 6 | name: 'transform-fixture', 7 | transformInclude (id) { 8 | return id.match(/[/\\]target\.js$/) 9 | }, 10 | transform (code, id) { 11 | const s = new MagicString(code) 12 | const index = code.indexOf('__UNPLUGIN__') 13 | if (index === -1) { 14 | return null 15 | } 16 | 17 | s.overwrite(index, index + '__UNPLUGIN__'.length, `[Injected ${options.msg}]`) 18 | return { 19 | code: s.toString(), 20 | map: s.generateMap({ 21 | source: id, 22 | includeContent: true 23 | }) 24 | } 25 | } 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | ci: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | node: [14, 16] 19 | 20 | steps: 21 | - name: checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Install pnpm 25 | uses: pnpm/action-setup@v2 26 | 27 | - name: Set node version to ${{ matrix.node }} 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: ${{ matrix.node }} 31 | cache: "pnpm" 32 | 33 | - name: Install 34 | run: pnpm i 35 | 36 | - name: Build 37 | run: pnpm run build 38 | 39 | - name: Test 40 | run: pnpm run test 41 | -------------------------------------------------------------------------------- /src/rollup/index.ts: -------------------------------------------------------------------------------- 1 | import { UnpluginInstance, UnpluginFactory, UnpluginOptions, RollupPlugin, UnpluginContextMeta } from '../types' 2 | 3 | export function getRollupPlugin ( 4 | factory: UnpluginFactory 5 | ): UnpluginInstance['rollup'] { 6 | return (userOptions?: UserOptions) => { 7 | const meta: UnpluginContextMeta = { 8 | framework: 'rollup' 9 | } 10 | const rawPlugin = factory(userOptions, meta) 11 | return toRollupPlugin(rawPlugin) 12 | } 13 | } 14 | 15 | export function toRollupPlugin (plugin: UnpluginOptions, containRollupOptions = true): RollupPlugin { 16 | if (plugin.transform && plugin.transformInclude) { 17 | const _transform = plugin.transform 18 | plugin.transform = function (code, id) { 19 | if (plugin.transformInclude && !plugin.transformInclude(id)) { 20 | return null 21 | } 22 | return _transform.call(this, code, id) 23 | } 24 | } 25 | 26 | if (plugin.rollup && containRollupOptions) { 27 | Object.assign(plugin, plugin.rollup) 28 | } 29 | 30 | return plugin 31 | } 32 | -------------------------------------------------------------------------------- /src/webpack/loaders/transform.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderContext } from 'webpack' 2 | import { UnpluginContext } from '../../types' 3 | import genContext from '../genContext' 4 | 5 | export default async function transform (this: LoaderContext, source: string, map: any) { 6 | const callback = this.async() 7 | const { unpluginName } = this.query 8 | const plugin = this._compiler?.$unpluginContext[unpluginName] 9 | 10 | if (!plugin?.transform) { 11 | return callback(null, source, map) 12 | } 13 | 14 | const context: UnpluginContext = { 15 | error: error => this.emitError(typeof error === 'string' ? new Error(error) : error), 16 | warn: error => this.emitWarning(typeof error === 'string' ? new Error(error) : error) 17 | } 18 | const res = await plugin.transform.call(Object.assign(this._compilation && genContext(this._compilation), context), source, this.resource) 19 | 20 | if (res == null) { 21 | callback(null, source, map) 22 | } else if (typeof res !== 'string') { 23 | callback(null, res.code, map == null ? map : (res.map || map)) 24 | } else { 25 | callback(null, res, map) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-PRESENT Nuxt Contrib 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/fixtures/virtual-module/__test__/build.test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import fs from 'fs-extra' 3 | import { describe, it, expect } from 'vitest' 4 | 5 | const r = (...args: string[]) => resolve(__dirname, '../dist', ...args) 6 | 7 | describe('virtual-module build', () => { 8 | it('vite', async () => { 9 | const content = await fs.readFile(r('vite/main.js.es.js'), 'utf-8') 10 | 11 | expect(content).toContain('VIRTUAL:ONE') 12 | expect(content).toContain('VIRTUAL:TWO') 13 | }) 14 | 15 | it('rollup', async () => { 16 | const content = await fs.readFile(r('rollup/main.js'), 'utf-8') 17 | 18 | expect(content).toContain('VIRTUAL:ONE') 19 | expect(content).toContain('VIRTUAL:TWO') 20 | }) 21 | 22 | it('webpack', async () => { 23 | const content = await fs.readFile(r('webpack/main.js'), 'utf-8') 24 | 25 | expect(content).toContain('VIRTUAL:ONE') 26 | expect(content).toContain('VIRTUAL:TWO') 27 | }) 28 | 29 | it('esbuild', async () => { 30 | const content = await fs.readFile(r('esbuild/main.js'), 'utf-8') 31 | 32 | expect(content).toContain('VIRTUAL:ONE') 33 | expect(content).toContain('VIRTUAL:TWO') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /scripts/buildFixtures.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { resolve, join } from 'path' 3 | import { execSync } from 'child_process' 4 | import fs from 'fs-extra' 5 | import c from 'picocolors' 6 | 7 | async function run () { 8 | const dir = resolve(__dirname, '../test/fixtures') 9 | let fixtures = await fs.readdir(dir) 10 | 11 | if (process.argv[2]) { 12 | fixtures = fixtures.filter(i => i.includes(process.argv[2])) 13 | } 14 | 15 | for (const name of fixtures) { 16 | const path = join(dir, name) 17 | if (fs.existsSync(join(path, 'dist'))) { 18 | await fs.remove(join(path, 'dist')) 19 | } 20 | console.log(c.yellow(c.inverse(c.bold('\n Vite '))), name, '\n') 21 | execSync('npx vite build', { cwd: path, stdio: 'inherit' }) 22 | console.log(c.red(c.inverse(c.bold('\n Rollup '))), name, '\n') 23 | execSync('npx rollup -c', { cwd: path, stdio: 'inherit' }) 24 | console.log(c.blue(c.inverse(c.bold('\n Webpack '))), name, '\n') 25 | execSync('npx webpack', { cwd: path, stdio: 'inherit' }) 26 | console.log(c.yellow(c.inverse(c.bold('\n Esbuild '))), name, '\n') 27 | execSync('node esbuild.config.js', { cwd: path, stdio: 'inherit' }) 28 | } 29 | } 30 | 31 | run() 32 | -------------------------------------------------------------------------------- /test/fixtures/transform/__test__/build.test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import fs from 'fs-extra' 3 | import { describe, it, expect } from 'vitest' 4 | 5 | const r = (...args: string[]) => resolve(__dirname, '../dist', ...args) 6 | 7 | describe('transform build', () => { 8 | it('vite', async () => { 9 | const content = await fs.readFile(r('vite/main.js.es.js'), 'utf-8') 10 | 11 | expect(content).toContain('NON-TARGET: __UNPLUGIN__') 12 | expect(content).toContain('TARGET: [Injected Vite]') 13 | }) 14 | 15 | it('rollup', async () => { 16 | const content = await fs.readFile(r('rollup/main.js'), 'utf-8') 17 | 18 | expect(content).toContain('NON-TARGET: __UNPLUGIN__') 19 | expect(content).toContain('TARGET: [Injected Rollup]') 20 | }) 21 | 22 | it('webpack', async () => { 23 | const content = await fs.readFile(r('webpack/main.js'), 'utf-8') 24 | 25 | expect(content).toContain('NON-TARGET: __UNPLUGIN__') 26 | expect(content).toContain('TARGET: [Injected Webpack]') 27 | }) 28 | 29 | it('esbuild', async () => { 30 | const content = await fs.readFile(r('esbuild/main.js'), 'utf-8') 31 | 32 | expect(content).toContain('NON-TARGET: __UNPLUGIN__') 33 | expect(content).toContain('TARGET: [Injected Esbuild]') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/webpack/loaders/load.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderContext } from 'webpack' 2 | import { UnpluginContext } from '../../types' 3 | import genContext from '../genContext' 4 | import { slash } from '../utils' 5 | 6 | export default async function load (this: LoaderContext, source: string, map: any) { 7 | const callback = this.async() 8 | const { unpluginName } = this.query 9 | const plugin = this._compiler?.$unpluginContext[unpluginName] 10 | let id = this.resource 11 | 12 | if (!plugin?.load || !id) { 13 | return callback(null, source, map) 14 | } 15 | 16 | const context: UnpluginContext = { 17 | error: error => this.emitError(typeof error === 'string' ? new Error(error) : error), 18 | warn: error => this.emitWarning(typeof error === 'string' ? new Error(error) : error) 19 | } 20 | 21 | if (id.startsWith(plugin.__virtualModulePrefix)) { 22 | id = id.slice(plugin.__virtualModulePrefix.length) 23 | } 24 | 25 | const res = await plugin.load.call(Object.assign(this._compilation && genContext(this._compilation), context), slash(id)) 26 | 27 | if (res == null) { 28 | callback(null, source, map) 29 | } else if (typeof res !== 'string') { 30 | callback(null, res.code, res.map ?? map) 31 | } else { 32 | callback(null, res, map) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/webpack/genContext.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | // eslint-disable-next-line import/default 3 | import sources from 'webpack-sources' 4 | import type { Compilation } from 'webpack' 5 | import type { UnpluginBuildContext } from 'src' 6 | 7 | export default function genContext (compilation: Compilation):UnpluginBuildContext { 8 | return { 9 | addWatchFile (id) { 10 | (compilation.fileDependencies ?? compilation.compilationDependencies).add( 11 | resolve(process.cwd(), id) 12 | ) 13 | }, 14 | emitFile (emittedFile) { 15 | const outFileName = emittedFile.fileName || emittedFile.name 16 | if (emittedFile.source && outFileName) { 17 | compilation.emitAsset( 18 | outFileName, 19 | // @ts-ignore 20 | sources 21 | ? new sources.RawSource( 22 | // @ts-expect-error types mismatch 23 | typeof emittedFile.source === 'string' 24 | ? emittedFile.source 25 | : Buffer.from(emittedFile.source) 26 | ) 27 | : { 28 | source: () => emittedFile.source, 29 | size: () => emittedFile.source!.length 30 | } 31 | ) 32 | } 33 | }, 34 | getWatchFiles () { 35 | return Array.from( 36 | compilation.fileDependencies ?? compilation.compilationDependencies 37 | ) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unplugin", 3 | "version": "0.6.2", 4 | "packageManager": "pnpm@7.0.0", 5 | "description": "Unified plugin system for build tools", 6 | "repository": "unjs/unplugin", 7 | "license": "MIT", 8 | "sideEffects": false, 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.mjs", 12 | "require": "./dist/index.js" 13 | }, 14 | "./dist/webpack/loaders/load": "./dist/webpack/loaders/load.js", 15 | "./dist/webpack/loaders/transform": "./dist/webpack/loaders/transform.js" 16 | }, 17 | "main": "dist/index.js", 18 | "module": "dist/index.mjs", 19 | "types": "dist/index.d.ts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "scripts": { 24 | "build": "tsup", 25 | "dev": "tsup --watch src", 26 | "lint": "eslint --ext ts .", 27 | "prepublishOnly": "nr build", 28 | "release": "bumpp --commit --push --tag --all -x 'npx conventional-changelog -p angular -i CHANGELOG.md -s' && npm publish", 29 | "test": "nr lint && nr test:build && vitest run", 30 | "test:build": "nr build && jiti scripts/buildFixtures.ts" 31 | }, 32 | "dependencies": { 33 | "chokidar": "^3.5.3", 34 | "webpack-sources": "^3.2.3", 35 | "webpack-virtual-modules": "^0.4.3" 36 | }, 37 | "devDependencies": { 38 | "@ampproject/remapping": "^2.2.0", 39 | "@antfu/ni": "^0.16.2", 40 | "@nuxtjs/eslint-config-typescript": "^10.0.0", 41 | "@types/express": "^4.17.13", 42 | "@types/fs-extra": "^9.0.13", 43 | "@types/node": "^17.0.31", 44 | "@types/webpack-sources": "^3.2.0", 45 | "bumpp": "^7.1.1", 46 | "conventional-changelog-cli": "^2.2.2", 47 | "enhanced-resolve": "^5.9.3", 48 | "esbuild": "^0.14.38", 49 | "eslint": "^8.14.0", 50 | "fast-glob": "^3.2.11", 51 | "fs-extra": "^10.1.0", 52 | "jiti": "^1.13.0", 53 | "magic-string": "^0.26.1", 54 | "rollup": "^2.72.0", 55 | "tsup": "^5.12.7", 56 | "typescript": "^4.6.4", 57 | "vite": "^2.9.8", 58 | "vitest": "^0.10.5", 59 | "webpack": "^5.72.0", 60 | "webpack-cli": "^4.9.2" 61 | }, 62 | "peerDependencies": { 63 | "esbuild": ">=0.13", 64 | "rollup": "^2.50.0", 65 | "vite": "^2.3.0", 66 | "webpack": "4 || 5" 67 | }, 68 | "peerDependenciesMeta": { 69 | "esbuild": { 70 | "optional": true 71 | }, 72 | "rollup": { 73 | "optional": true 74 | }, 75 | "vite": { 76 | "optional": true 77 | }, 78 | "webpack": { 79 | "optional": true 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/esbuild/utils.ts: -------------------------------------------------------------------------------- 1 | import { extname } from 'path' 2 | import remapping from '@ampproject/remapping' 3 | import type { 4 | DecodedSourceMap, 5 | RawSourceMap 6 | } from '@ampproject/remapping/dist/types/types' 7 | import type { Loader } from 'esbuild' 8 | import type { SourceMap } from 'rollup' 9 | 10 | const ExtToLoader: Record = { 11 | '.js': 'js', 12 | '.mjs': 'js', 13 | '.cjs': 'js', 14 | '.jsx': 'jsx', 15 | '.ts': 'ts', 16 | '.cts': 'ts', 17 | '.mts': 'ts', 18 | '.tsx': 'tsx', 19 | '.css': 'css', 20 | '.less': 'css', 21 | '.stylus': 'css', 22 | '.scss': 'css', 23 | '.sass': 'css', 24 | '.json': 'json', 25 | '.txt': 'text' 26 | } 27 | 28 | export function guessLoader (id: string): Loader { 29 | return ExtToLoader[extname(id).toLowerCase()] || 'js' 30 | } 31 | 32 | // `load` and `transform` may return a sourcemap without toString and toUrl, 33 | // but esbuild needs them, we fix the two methods 34 | export function fixSourceMap (map: RawSourceMap): SourceMap { 35 | if (!('toString' in map)) { 36 | Object.defineProperty(map, 'toString', { 37 | enumerable: false, 38 | value: function toString () { 39 | return JSON.stringify(this) 40 | } 41 | }) 42 | } 43 | if (!('toUrl' in map)) { 44 | Object.defineProperty(map, 'toUrl', { 45 | enumerable: false, 46 | value: function toUrl () { 47 | return 'data:application/json;charset=utf-8;base64,' + Buffer.from(this.toString()).toString('base64') 48 | } 49 | }) 50 | } 51 | return map as SourceMap 52 | } 53 | 54 | // taken from https://github.com/vitejs/vite/blob/71868579058512b51991718655e089a78b99d39c/packages/vite/src/node/utils.ts#L525 55 | const nullSourceMap: RawSourceMap = { 56 | names: [], 57 | sources: [], 58 | mappings: '', 59 | version: 3 60 | } 61 | export function combineSourcemaps ( 62 | filename: string, 63 | sourcemapList: Array 64 | ): RawSourceMap { 65 | sourcemapList = sourcemapList.filter(m => m.sources) 66 | 67 | if ( 68 | sourcemapList.length === 0 || 69 | sourcemapList.every(m => m.sources.length === 0) 70 | ) { 71 | return { ...nullSourceMap } 72 | } 73 | 74 | // We don't declare type here so we can convert/fake/map as RawSourceMap 75 | let map // : SourceMap 76 | let mapIndex = 1 77 | const useArrayInterface = 78 | sourcemapList.slice(0, -1).find(m => m.sources.length !== 1) === undefined 79 | if (useArrayInterface) { 80 | map = remapping(sourcemapList, () => null, true) 81 | } else { 82 | map = remapping( 83 | sourcemapList[0], 84 | function loader (sourcefile) { 85 | if (sourcefile === filename && sourcemapList[mapIndex]) { 86 | return sourcemapList[mapIndex++] 87 | } else { 88 | return { ...nullSourceMap } 89 | } 90 | }, 91 | true 92 | ) 93 | } 94 | if (!map.file) { 95 | delete map.file 96 | } 97 | 98 | return map as RawSourceMap 99 | } 100 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { EmittedAsset, Plugin as RollupPlugin, PluginContextMeta as RollupContextMeta, SourceMap } from 'rollup' 2 | import type { Compiler as WebpackCompiler, WebpackPluginInstance } from 'webpack' 3 | import type { Plugin as VitePlugin } from 'vite' 4 | import type { Plugin as EsbuildPlugin } from 'esbuild' 5 | import type VirtualModulesPlugin from 'webpack-virtual-modules' 6 | 7 | export { 8 | EsbuildPlugin, 9 | RollupPlugin, 10 | VitePlugin, 11 | WebpackCompiler 12 | } 13 | 14 | export type Thenable = T | Promise 15 | 16 | export type TransformResult = string | { code: string; map?: SourceMap | null; } | null | undefined 17 | 18 | export type ExternalIdResult = { id: string, external?: boolean } 19 | 20 | export interface UnpluginBuildContext { 21 | addWatchFile: (id: string) => void; 22 | emitFile: (emittedFile: EmittedAsset) => void; 23 | getWatchFiles: () => string[]; 24 | } 25 | 26 | export interface UnpluginOptions { 27 | name: string; 28 | enforce?: 'post' | 'pre' | undefined; 29 | buildStart?: (this: UnpluginBuildContext) => Promise | void; 30 | buildEnd?: (this: UnpluginBuildContext) => Promise | void; 31 | transformInclude?: (id: string) => boolean; 32 | transform?: (this: UnpluginBuildContext & UnpluginContext, code: string, id: string) => Thenable; 33 | load?: (this: UnpluginBuildContext & UnpluginContext, id: string) => Thenable 34 | resolveId?: (id: string, importer?: string) => Thenable 35 | watchChange?: (this: UnpluginBuildContext, id: string, change: {event: 'create' | 'update' | 'delete'}) => void 36 | 37 | // framework specify extends 38 | rollup?: Partial 39 | webpack?: (compiler: WebpackCompiler) => void 40 | vite?: Partial 41 | esbuild?: { 42 | // using regexp in esbuild improves performance 43 | onResolveFilter?: RegExp 44 | onLoadFilter?: RegExp 45 | setup?: EsbuildPlugin['setup'] 46 | } 47 | } 48 | 49 | export interface ResolvedUnpluginOptions extends UnpluginOptions { 50 | // injected internal objects 51 | __vfs?: VirtualModulesPlugin 52 | __vfsModules?: Set 53 | __virtualModulePrefix: string 54 | } 55 | 56 | export type UnpluginFactory = (options: UserOptions | undefined, meta: UnpluginContextMeta) => UnpluginOptions 57 | 58 | export interface UnpluginInstance { 59 | rollup: (options?: UserOptions) => RollupPlugin; 60 | webpack: (options?: UserOptions) => WebpackPluginInstance; 61 | vite: (options?: UserOptions) => VitePlugin; 62 | esbuild: (options?: UserOptions) => EsbuildPlugin; 63 | raw: UnpluginFactory 64 | } 65 | 66 | export interface UnpluginContextMeta extends Partial { 67 | framework: 'rollup' | 'vite' | 'webpack' | 'esbuild' 68 | webpack?: { 69 | compiler: WebpackCompiler 70 | } 71 | } 72 | 73 | export interface UnpluginContext { 74 | error(message: any): void 75 | warn(message: any): void 76 | } 77 | 78 | declare module 'webpack' { 79 | interface Compiler { 80 | $unpluginContext: Record 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unplugin 2 | 3 | [![NPM version](https://img.shields.io/npm/v/unplugin?color=a1b858&label=)](https://www.npmjs.com/package/unplugin) 4 | 5 | Unified plugin system for build tools. 6 | 7 | Currently supports: 8 | - [Vite](https://vitejs.dev/) 9 | - [Rollup](https://rollupjs.org/) 10 | - [Webpack](https://webpack.js.org/) 11 | - [esbuild](https://esbuild.github.io/) 12 | 13 | ## Hooks 14 | 15 | `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. 16 | 17 | ###### Supported 18 | 19 | | Hook | Rollup | Vite | Webpack 4 | Webpack 5 | esbuild | 20 | | ---- | :----: | :--: | :-------: | :-------: | :-----: | 21 | | [`buildStart`](https://rollupjs.org/guide/en/#buildstart) | ✅ | ✅ | ✅ | ✅ | ✅ | 22 | | [`buildEnd`](https://rollupjs.org/guide/en/#buildend) | ✅ | ✅ | ✅ | ✅ | ✅ | 23 | | `transformInclude`1 | ✅ | ✅ | ✅ | ✅ | ✅ | 24 | | [`transform`](https://rollupjs.org/guide/en/#transformers) | ✅ | ✅ | ✅ | ✅ | ✅ 3 | 25 | | [`enforce`](https://rollupjs.org/guide/en/#enforce) | ❌ 2 | ✅ | ✅ | ✅ | ❌ 2 | 26 | | [`resolveId`](https://rollupjs.org/guide/en/#resolveid) | ✅ | ✅ | ✅ | ✅ | ✅ | 27 | | [`load`](https://rollupjs.org/guide/en/#load) | ✅ | ✅ | ✅ | ✅ | ✅ 3 | 28 | | [`watchChange`](https://rollupjs.org/guide/en/#watchchange) | ✅ | ✅ | ✅ | ✅ | ✅ | 29 | 30 | 1. 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 following usage examples. 31 | 2. Rollup and esbuild do not support using `enforce` to control the order of plugins. Users need to maintain the order manually. 32 | 3. Although esbuild can handle both JavaScript and CSS and many other file formats, you can only return JavaScript in `load` and `transform` results. 33 | 34 | ### Hook Context 35 | 36 | ###### Supported 37 | 38 | | Hook | Rollup | Vite | Webpack 4 | Webpack 5 | esbuild | 39 | | ---- | :----: | :--: | :-------: | :-------: | :-----: | 40 | | [`this.addWatchFile`](https://rollupjs.org/guide/en/#thisaddwatchfile) | ✅ | ✅ | ✅ | ✅ | ✅ | 41 | | [`this.emitFile`](https://rollupjs.org/guide/en/#thisemitfile)4 | ✅ | ✅ | ✅ | ✅ | ✅ | 42 | | [`this.getWatchFiles`](https://rollupjs.org/guide/en/#thisgetwatchfiles) | ✅ | ✅ | ✅ | ✅ | ✅ | 43 | 44 | 4. Currently, [`this.emitFile`](https://rollupjs.org/guide/en/#thisemitfile) only supports the `EmittedAsset` variant. 45 | 46 | ## Usage 47 | 48 | ```ts 49 | import { createUnplugin } from 'unplugin' 50 | 51 | export const unplugin = createUnplugin((options: UserOptions) => { 52 | return { 53 | name: 'my-first-unplugin', 54 | // webpack's id filter is outside of loader logic, 55 | // an additional hook is needed for better perf on webpack 56 | transformInclude (id) { 57 | return id.endsWith('.vue') 58 | }, 59 | // just like rollup transform 60 | transform (code) { 61 | return code.replace(/