├── .github ├── renovate.json5 └── workflows │ ├── ci.yml │ ├── gh-pages.yml │ ├── publish-commit.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .env.example ├── .vitepress │ ├── assets │ │ ├── farm.svg │ │ ├── rolldown.svg │ │ └── rspack.svg │ ├── components │ │ ├── RepoInfo.vue │ │ └── Repositories.vue │ ├── config.ts │ ├── constant.ts │ ├── data │ │ ├── gen-files.ts │ │ ├── meta.ts │ │ └── repository.data.ts │ ├── plugins │ │ └── markdownTransform.ts │ └── theme │ │ ├── CustomLayout.vue │ │ ├── index.ts │ │ └── style.css ├── README.md ├── guide │ ├── index.md │ ├── plugin-conventions.md │ └── why-unplugin.md ├── index.md ├── package.json ├── public │ ├── favicon.ico │ ├── features │ │ ├── astro.svg │ │ ├── esbuild.svg │ │ ├── farm.png │ │ ├── more.svg │ │ ├── nuxt.svg │ │ ├── rolldown.svg │ │ ├── rollup.svg │ │ ├── rspack.png │ │ ├── vitejs.svg │ │ └── webpack.svg │ ├── logo.svg │ ├── logo_dark.svg │ ├── logo_light.svg │ ├── og.png │ └── open_in_codeflow.svg ├── showcase │ └── index.md ├── tsconfig.json ├── uno.config.ts └── vite.config.ts ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── buildFixtures.ts ├── src ├── define.ts ├── esbuild │ ├── index.ts │ └── utils.ts ├── farm │ ├── context.ts │ ├── index.ts │ └── utils.ts ├── globals.d.ts ├── index.ts ├── rolldown │ └── index.ts ├── rollup │ └── index.ts ├── rspack │ ├── context.ts │ ├── index.ts │ ├── loaders │ │ ├── load.ts │ │ └── transform.ts │ └── utils.ts ├── types.ts ├── unloader │ └── index.ts ├── utils │ ├── context.ts │ ├── filter.ts │ ├── general.ts │ └── webpack-like.ts ├── vite │ └── index.ts └── webpack │ ├── context.ts │ ├── index.ts │ └── loaders │ ├── load.ts │ └── transform.ts ├── test ├── fixtures │ ├── load │ │ ├── __test__ │ │ │ └── build.test.ts │ │ ├── esbuild.config.js │ │ ├── farm.config.js │ │ ├── rollup.config.js │ │ ├── rspack.config.js │ │ ├── src │ │ │ ├── main.js │ │ │ └── msg.js │ │ ├── unplugin.js │ │ ├── vite.config.js │ │ └── webpack.config.js │ ├── transform │ │ ├── __test__ │ │ │ └── build.test.ts │ │ ├── esbuild.config.js │ │ ├── farm.config.js │ │ ├── rollup.config.js │ │ ├── rspack.config.js │ │ ├── src │ │ │ ├── main.js │ │ │ ├── nontarget.js │ │ │ ├── query.js │ │ │ └── target.js │ │ ├── unplugin.js │ │ ├── vite.config.js │ │ └── webpack.config.js │ └── virtual-module │ │ ├── __test__ │ │ └── build.test.ts │ │ ├── esbuild.config.js │ │ ├── farm.config.js │ │ ├── rollup.config.js │ │ ├── rspack.config.js │ │ ├── src │ │ └── main.js │ │ ├── unplugin.js │ │ ├── vite.config.js │ │ └── webpack.config.js ├── package.json └── unit-tests │ ├── esbuild │ └── utils.test.ts │ ├── farm │ ├── context.test.ts │ ├── index.test.ts │ └── utils.test.ts │ ├── filter │ ├── filter.test.ts │ └── test-src │ │ ├── entry.js │ │ ├── mod.js │ │ └── not-expect.js │ ├── id-consistency │ ├── id-consistency.test.ts │ └── test-src │ │ ├── default-export.js │ │ ├── entry.js │ │ ├── proxy-export.js │ │ └── sub-folder │ │ └── named-export.js │ ├── resolve-id-external │ ├── resolve-id-external.test.ts │ └── test-src │ │ ├── entry.js │ │ └── internal-module.js │ ├── resolve-id │ ├── resolve-id.test.ts │ └── test-src │ │ ├── default-export.js │ │ ├── entry.js │ │ ├── named-export.js │ │ └── proxy-export.js │ ├── rolldown │ └── index.test.ts │ ├── rspack │ ├── context.test.ts │ └── loaders │ │ ├── load.test.ts │ │ └── transform.test.ts │ ├── unloader │ └── index.test.ts │ ├── utils.ts │ ├── utils │ └── context.test.ts │ ├── virtual-id │ ├── test-src │ │ ├── entry.js │ │ └── imported.js │ └── virtual-id.test.ts │ ├── webpack │ ├── context.test.ts │ └── loaders │ │ ├── load.test.ts │ │ └── transform.test.ts │ └── write-bundle │ ├── .gitignore │ ├── test-src │ ├── entry.js │ └── import.js │ └── write-bundle.test.ts ├── tsconfig.json ├── tsdown.config.ts └── vitest.config.ts /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>unjs/renovate-config"], 3 | "packageRules": [ 4 | { 5 | "matchDepTypes": ["peerDependencies"], 6 | "enabled": false 7 | }, 8 | { 9 | "groupName": "farm", 10 | "matchPackageNames": ["@farmfe{/,}**"] 11 | }, 12 | { 13 | "groupName": "rspack", 14 | "matchPackageNames": ["@rspack{/,}**"] 15 | }, 16 | { 17 | "groupName": "webpack", 18 | "matchPackageNames": ["webpack", "webpack-cli"] 19 | } 20 | ], 21 | "ignoreDeps": [ 22 | "node", 23 | "typescript" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'v*' 8 | pull_request: 9 | branches: 10 | - main 11 | - 'v*' 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v4.1.0 22 | 23 | - name: Setup node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: lts/* 27 | cache: pnpm 28 | 29 | - run: pnpm install 30 | - run: pnpm run lint 31 | - run: pnpm run typecheck 32 | 33 | ci: 34 | runs-on: ${{ matrix.os }} 35 | 36 | strategy: 37 | matrix: 38 | os: [ubuntu-latest, macos-latest, windows-latest] 39 | node: [18, 20, 22] 40 | fail-fast: false 41 | 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | 46 | - name: Install pnpm 47 | uses: pnpm/action-setup@v4.1.0 48 | 49 | - name: Set node version to ${{ matrix.node }} 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: ${{ matrix.node }} 53 | cache: pnpm 54 | 55 | - name: Install 56 | run: pnpm install 57 | 58 | - name: Build 59 | run: pnpm run build 60 | 61 | - name: Test 62 | run: pnpm run test 63 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Github Page Docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: pages 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4.1.0 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | cache: pnpm 26 | 27 | - run: pnpm i 28 | 29 | - name: Build unplugin 30 | run: pnpm run build 31 | 32 | - name: Build docs 33 | run: pnpm run docs:build 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Upload Artifact 38 | uses: actions/upload-pages-artifact@v3 39 | with: 40 | path: ./docs/.vitepress/dist 41 | 42 | deploy: 43 | needs: build 44 | permissions: 45 | pages: write 46 | id-token: write 47 | environment: 48 | name: github-pages 49 | url: ${{ steps.deployment.outputs.page_url }} 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Deploy to GitHub Pages 🚀 53 | id: deployment 54 | uses: actions/deploy-pages@v4 55 | -------------------------------------------------------------------------------- /.github/workflows/publish-commit.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - name: Install pnpm 12 | uses: pnpm/action-setup@v4.1.0 13 | 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: lts/* 17 | cache: pnpm 18 | 19 | - name: Install dependencies 20 | run: pnpm install 21 | 22 | - name: Build 23 | run: pnpm build 24 | 25 | - run: pnpx pkg-pr-new publish 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | 24 | - run: npx changelogithub 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | .profile 3 | .idea 4 | *.log* 5 | coverage 6 | dist 7 | node_modules 8 | temp 9 | .eslintcache 10 | 11 | # docs 12 | docs/showcase/*.md 13 | !docs/showcase/index.md 14 | docs/.vitepress/cache 15 | docs/.vitepress/components.d.ts 16 | docs/.env 17 | docs/.vitepress/data/repository.json 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shell-emulator=true 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "antfu.pnpm-catalog-lens" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | 5 | // Auto fix 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": "explicit", 8 | "source.organizeImports": "never" 9 | }, 10 | 11 | // Silent the stylistic rules in you IDE, but still auto fix them 12 | "eslint.rules.customizations": [ 13 | { "rule": "style/*", "severity": "off" }, 14 | { "rule": "*-indent", "severity": "off" }, 15 | { "rule": "*-spacing", "severity": "off" }, 16 | { "rule": "*-spaces", "severity": "off" }, 17 | { "rule": "*-order", "severity": "off" }, 18 | { "rule": "*-dangle", "severity": "off" }, 19 | { "rule": "*-newline", "severity": "off" }, 20 | { "rule": "*quotes", "severity": "off" }, 21 | { "rule": "*semi", "severity": "off" } 22 | ], 23 | 24 | // Enable eslint for all supported languages 25 | "eslint.validate": [ 26 | "javascript", 27 | "javascriptreact", 28 | "typescript", 29 | "typescriptreact", 30 | "vue", 31 | "html", 32 | "markdown", 33 | "json", 34 | "jsonc", 35 | "yaml" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unplugin 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![License][license-src]][license-href] 6 | 7 | Unified plugin system for build tools. 8 | 9 | Currently supports: 10 | 11 | - [Vite](https://vite.dev/) 12 | - [Rollup](https://rollupjs.org/) 13 | - [Webpack](https://webpack.js.org/) 14 | - [esbuild](https://esbuild.github.io/) 15 | - [Rspack](https://www.rspack.dev/) 16 | - [Rolldown](https://rolldown.rs/) 17 | - [Farm](https://www.farmfe.org/) 18 | - And every framework built on top of them. 19 | 20 | ## Documentations 21 | 22 | Learn more on the [Documentation](https://unplugin.unjs.io/) 23 | 24 | ## License 25 | 26 | [MIT](./LICENSE) License © 2021-PRESENT Nuxt Contrib 27 | 28 | 29 | 30 | [npm-version-src]: https://img.shields.io/npm/v/unplugin?style=flat&colorA=18181B&colorB=F0DB4F 31 | [npm-version-href]: https://npmjs.com/package/unplugin 32 | [npm-downloads-src]: https://img.shields.io/npm/dm/unplugin?style=flat&colorA=18181B&colorB=F0DB4F 33 | [npm-downloads-href]: https://npmjs.com/package/unplugin 34 | [license-src]: https://img.shields.io/github/license/unjs/unplugin.svg?style=flat&colorA=18181B&colorB=F0DB4F 35 | [license-href]: https://github.com/unjs/unplugin/blob/main/LICENSE 36 | -------------------------------------------------------------------------------- /docs/.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN= -------------------------------------------------------------------------------- /docs/.vitepress/assets/rolldown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/.vitepress/components/RepoInfo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Repositories.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 54 | 55 | 61 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { transformerTwoslash } from '@shikijs/vitepress-twoslash' 3 | import MarkdownItGitHubAlerts from 'markdown-it-github-alerts' 4 | import { defineConfig } from 'vitepress' 5 | import { groupIconMdPlugin } from 'vitepress-plugin-group-icons' 6 | import { description, ogImage, title } from './constant' 7 | import { repositoryMeta } from './data/meta' 8 | 9 | // https://vitepress.dev/reference/site-config 10 | export default defineConfig({ 11 | title, 12 | description, 13 | lastUpdated: true, 14 | themeConfig: { 15 | // https://vitepress.dev/reference/default-theme-config 16 | nav: [ 17 | { text: 'Guide', link: '/guide/', activeMatch: '/guide/' }, 18 | { text: 'Showcase', link: '/showcase/', activeMatch: '/showcase/' }, 19 | ], 20 | search: { 21 | provider: 'local', 22 | }, 23 | logo: { 24 | light: '/logo_light.svg', 25 | dark: '/logo_dark.svg', 26 | }, 27 | 28 | sidebar: { 29 | '/': [ 30 | { 31 | text: 'Guide', 32 | items: [ 33 | { text: 'Getting Started', link: '/guide/' }, 34 | // { text: 'Why Unplugin', link: '/guide/why' }, 35 | { text: 'Plugin Conventions', link: '/guide/plugin-conventions' }, 36 | ], 37 | }, 38 | { 39 | text: 'Showcase', 40 | link: '/showcase/', 41 | items: [ 42 | { 43 | text: 'Overview', 44 | link: '/showcase/', 45 | }, 46 | ...repositoryMeta.map(repo => ( 47 | { 48 | text: repo.name, 49 | link: `/showcase/${repo.name}`, 50 | } 51 | )), 52 | ], 53 | }, 54 | ], 55 | }, 56 | 57 | socialLinks: [ 58 | { icon: 'github', link: 'https://github.com/unjs/unplugin' }, 59 | ], 60 | 61 | footer: { 62 | message: 'Released under the MIT License.', 63 | copyright: 'Copyright (c) 2021-PRESENT UnJS Team', 64 | }, 65 | }, 66 | head: [ 67 | ['meta', { name: 'theme-color', content: '#ffffff' }], 68 | ['link', { rel: 'icon', href: '/logo.svg', type: 'image/svg+xml' }], 69 | ['meta', { name: 'author', content: 'Nuxt Contrib' }], 70 | ['meta', { property: 'og:title', content: title }], 71 | ['meta', { property: 'og:image', content: ogImage }], 72 | ['meta', { property: 'og:description', content: description }], 73 | ['meta', { name: 'twitter:card', content: 'summary_large_image' }], 74 | ['meta', { name: 'twitter:image', content: ogImage }], 75 | ['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1.0, viewport-fit=cover' }], 76 | ], 77 | markdown: { 78 | config: (md) => { 79 | md.use(MarkdownItGitHubAlerts) 80 | md.use(groupIconMdPlugin) 81 | }, 82 | codeTransformers: [ 83 | transformerTwoslash({ 84 | twoslashOptions: { 85 | compilerOptions: { paths: { 86 | unplugin: [path.resolve(import.meta.dirname, '../../src/index.ts')], 87 | } }, 88 | }, 89 | }), 90 | ], 91 | }, 92 | ignoreDeadLinks: true, 93 | }) 94 | -------------------------------------------------------------------------------- /docs/.vitepress/constant.ts: -------------------------------------------------------------------------------- 1 | export const title = 'Unplugin' 2 | export const description = 'Unified plugin system. Support Vite, Rollup, webpack, esbuild, and every frameworks on top of them.' 3 | export const url = 'https://unplugin.unjs.io/' 4 | export const ogImage = `${url}/og.png` 5 | -------------------------------------------------------------------------------- /docs/.vitepress/data/gen-files.ts: -------------------------------------------------------------------------------- 1 | import type { Repository } from './repository.data' 2 | import { writeFileSync } from 'node:fs' 3 | import { dirname, join } from 'node:path' 4 | import { env } from 'node:process' 5 | import { fileURLToPath } from 'node:url' 6 | import { consola } from 'consola' 7 | import { $fetch } from 'ofetch' 8 | import { repositoryMeta } from './meta' 9 | import 'dotenv/config' 10 | 11 | const GITHUB_TOKEN = env.GITHUB_TOKEN 12 | 13 | const gql = `#graphql 14 | query repositoryQuery($owner: String!, $name: String!, $readme: String!) { 15 | repository(owner: $owner, name: $name) { 16 | name 17 | stargazers { 18 | totalCount 19 | } 20 | owner { 21 | avatarUrl 22 | login 23 | } 24 | description 25 | primaryLanguage { 26 | name 27 | color 28 | } 29 | forkCount 30 | object(expression: $readme) { 31 | ... on Blob { 32 | text 33 | } 34 | } 35 | } 36 | }` 37 | 38 | async function fetchRepo(meta: { 39 | owner: string 40 | name: string 41 | readme?: string 42 | }) { 43 | const { owner, name, readme } = meta 44 | 45 | const _readme = readme || 'main:README.md' 46 | try { 47 | const results = await $fetch('https://api.github.com/graphql', { 48 | method: 'POST', 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | 'Authorization': `Bearer ${GITHUB_TOKEN}`, 52 | }, 53 | body: JSON.stringify({ 54 | query: gql, 55 | variables: { 56 | owner, 57 | name, 58 | readme: _readme, 59 | }, 60 | }), 61 | }) 62 | 63 | const repositoryInfo = results.data.repository as Repository 64 | 65 | const markdownFrontmatter = `--- 66 | title: ${repositoryInfo.name} 67 | owner: ${repositoryInfo.owner.login} 68 | name: ${repositoryInfo.name} 69 | stars: ${repositoryInfo.stargazers.totalCount} 70 | forks: ${repositoryInfo.forkCount} 71 | outline: deep 72 | --- 73 | 74 | 75 | 76 | --- 77 | 78 | ` 79 | 80 | writeFileSync( 81 | join(dirname(fileURLToPath(import.meta.url)), `../../showcase/${name}.md`), 82 | markdownFrontmatter + repositoryInfo.object.text, 83 | ) 84 | consola.success(`[${name}.md]: generate success`) 85 | return repositoryInfo 86 | } 87 | catch (error) { 88 | consola.error(`[${name}.md]: generate failed: ${error}`) 89 | } 90 | } 91 | 92 | function main() { 93 | if (!GITHUB_TOKEN) { 94 | consola.error('GITHUB_TOKEN is missing, please refer to https://github.com/unjs/unplugin/blob/main/docs/README.md#development') 95 | return false 96 | } 97 | 98 | const fetchs = repositoryMeta.map((repository) => { 99 | return fetchRepo({ 100 | name: repository.name, 101 | owner: repository.owner, 102 | readme: repository.defaultBranch ? `${repository.defaultBranch}:README.md` : 'main:README.md', 103 | }) 104 | }) 105 | 106 | Promise.allSettled(fetchs).then((res) => { 107 | const repoMeta = res?.map((item) => { 108 | if (item.status === 'fulfilled') { 109 | return { 110 | name: item.value?.name, 111 | stargazers: item.value?.stargazers, 112 | owner: item.value?.owner, 113 | description: item.value?.description, 114 | url: item.value?.url, 115 | isTemplate: item.value?.isTemplate, 116 | primaryLanguage: item.value?.primaryLanguage, 117 | forkCount: item.value?.forkCount, 118 | } 119 | } 120 | 121 | return null 122 | })?.filter(item => item && item.name) 123 | 124 | writeFileSync( 125 | join(dirname(fileURLToPath(import.meta.url)), './repository.json'), 126 | JSON.stringify(repoMeta, null, 2), 127 | ) 128 | consola.success('[repository.json] generate success!') 129 | consola.success('All files generate done!') 130 | }).catch((error) => { 131 | consola.error(error) 132 | }) 133 | } 134 | 135 | main() 136 | -------------------------------------------------------------------------------- /docs/.vitepress/data/meta.ts: -------------------------------------------------------------------------------- 1 | export const repositoryMeta: { 2 | owner: string 3 | name: string 4 | defaultBranch?: string 5 | }[] = [ 6 | { owner: 'unplugin', name: 'unplugin-vue-components' }, 7 | { owner: 'unplugin', name: 'unplugin-icons' }, 8 | { owner: 'unplugin', name: 'unplugin-auto-import' }, 9 | { owner: 'unplugin', name: 'unplugin-vue2-script-setup' }, 10 | { owner: 'unplugin', name: 'unplugin-vue-markdown' }, 11 | { owner: 'unplugin', name: 'unplugin-swc' }, 12 | { owner: 'unplugin', name: 'unplugin-turbo-console' }, 13 | { owner: 'unplugin', name: 'unplugin-imagemin' }, 14 | { owner: 'unplugin', name: 'unplugin-vue-cssvars', defaultBranch: 'master' }, 15 | { owner: 'unplugin', name: 'unplugin-vue' }, 16 | { owner: 'unplugin', name: 'unplugin-macros' }, 17 | { owner: 'unplugin', name: 'unplugin-isolated-decl' }, 18 | { owner: 'unplugin', name: 'unplugin-unused' }, 19 | { owner: 'unplugin', name: 'unplugin-ast' }, 20 | { owner: 'unplugin', name: 'unplugin-replace' }, 21 | ] 22 | -------------------------------------------------------------------------------- /docs/.vitepress/data/repository.data.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | export interface Repository { 6 | name: string 7 | stargazers: { 8 | totalCount: number 9 | } 10 | owner: { 11 | avatarUrl: string 12 | login: string 13 | } 14 | description: string 15 | url: string 16 | isTemplate: boolean 17 | primaryLanguage: { 18 | name: string 19 | color: string 20 | } 21 | forkCount: number 22 | object: { 23 | text: string 24 | } 25 | } 26 | 27 | declare const data: Repository[] 28 | export { data } 29 | 30 | export default { 31 | watch: ['./repository.json'], 32 | load() { 33 | const fileContent = readFileSync(resolve(fileURLToPath(import.meta.url), '../repository.json'), 'utf-8') 34 | return JSON.parse(fileContent) 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /docs/.vitepress/plugins/markdownTransform.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOption } from 'vite' 2 | import { basename } from 'node:path' 3 | import { repositoryMeta } from '../data/meta' 4 | 5 | const repos = repositoryMeta.map(({ name }) => `${name}`) 6 | 7 | export function MarkdownTransform(): PluginOption { 8 | // eslint-disable-next-line regexp/no-super-linear-backtracking 9 | const MARKDOWN_LINK_RE = /(?\[.*?\]\((?.*?)\)|.*?)".*?>)/g 10 | const GH_RAW_URL = 'https://raw.githubusercontent.com' 11 | const GH_URL = 'https://github.com/unplugin' 12 | const images = ['png', 'jpg', 'jpeg', 'gif', 'svg'].map(ext => `.${ext}`) 13 | return { 14 | name: 'unplugin-md-transform', 15 | enforce: 'pre', 16 | async transform(code, id) { 17 | // only transform markdown on meta files 18 | if (!repos.includes(basename(id, '.md'))) 19 | return null 20 | 21 | // https://github.com/unplugin/unplugin-vue-components/blob/main/README.md?plain=1#L66 22 | // Manual add line break 23 | code = code.replaceAll('
', '
\n') 24 | 25 | // https://github.com/unplugin/unplugin-icons/blob/main/README.md?plain=1#L425 26 | code = code.replaceAll(' < ', ' < ').replaceAll(' > ', ' > ') 27 | 28 | // replace markdown img link 29 | // code reference: https://github.com/unjs/ungh/blob/main/utils/markdown.ts 30 | const { name, owner, defaultBranch } = repositoryMeta.find(({ name }) => name === basename(id, '.md'))! 31 | const _defaultBranch = defaultBranch || 'main' 32 | code = code.replaceAll(MARKDOWN_LINK_RE, (match, _, url: string | undefined, url2: string) => { 33 | const path = url || url2 34 | // If path is already a URL, return the match 35 | if (path.startsWith('http') || path.startsWith('https')) 36 | return match 37 | 38 | // handle images and links differently 39 | return match.includes(' match.includes(ext)) 40 | ? match.replace(path, `${GH_RAW_URL}/${owner}/${name}/${_defaultBranch}/${path.replace(/^\.\//, '')}`) 41 | : match.replace(path, `${GH_URL}/${name}/tree/${_defaultBranch}/${path.replace(/^\.\//, '')}`) 42 | }) 43 | 44 | let useCode = code 45 | let idx = 0 46 | 47 | while (true) { 48 | const detailIdx = useCode.indexOf('
', idx) 49 | if (detailIdx === -1) 50 | break 51 | 52 | const summaryIdx = useCode.indexOf('', idx + 10) 53 | if (summaryIdx === -1) 54 | break 55 | 56 | const endSummaryIdx = useCode.indexOf('', summaryIdx + 10) 57 | if (endSummaryIdx === -1) 58 | break 59 | 60 | const title = useCode.slice(summaryIdx + 9, endSummaryIdx) 61 | .trim() 62 | .replaceAll('
', '') 63 | .replaceAll('
', '') 64 | .replaceAll('
', '') 65 | 66 | const endDetailIdx = useCode.indexOf('
', endSummaryIdx + 11) 67 | if (endDetailIdx === -1) 68 | break 69 | 70 | const detailBody = useCode.slice(endSummaryIdx + 10, endDetailIdx) 71 | .trim() 72 | .replaceAll('
', '') 73 | .replaceAll('
', '') 74 | .replaceAll('
', '') 75 | 76 | let rest = useCode.slice(endDetailIdx + 11).trim() 77 | // additional
in some readme packages between details 78 | if (rest.startsWith('
')) 79 | rest = rest.slice(4) 80 | if (rest.startsWith('
')) 81 | rest = rest.slice(5) 82 | if (rest.startsWith('
')) 83 | rest = rest.slice(6) 84 | 85 | useCode = `${useCode.slice(0, detailIdx)}\n::: details ${title}\n\n${detailBody}\n:::\n` 86 | idx = useCode.length 87 | useCode += rest 88 | } 89 | 90 | return useCode 91 | }, 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/CustomLayout.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 46 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import type { EnhanceAppContext } from 'vitepress' 2 | import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' 3 | import DefaultTheme from 'vitepress/theme' 4 | // https://vitepress.dev/guide/custom-theme 5 | import { h } from 'vue' 6 | import CustomLayout from './CustomLayout.vue' 7 | 8 | import '@shikijs/vitepress-twoslash/style.css' 9 | import 'uno.css' 10 | import './style.css' 11 | import 'virtual:group-icons.css' 12 | 13 | export default { 14 | extends: DefaultTheme, 15 | Layout: () => { 16 | return h(CustomLayout) 17 | }, 18 | enhanceApp({ app }: EnhanceAppContext) { 19 | app.use(TwoslashFloatingVue) 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/style.css: -------------------------------------------------------------------------------- 1 | @import 'markdown-it-github-alerts/styles/github-colors-light.css'; 2 | @import 'markdown-it-github-alerts/styles/github-colors-dark-media.css'; 3 | @import 'markdown-it-github-alerts/styles/github-base.css'; 4 | 5 | :root { 6 | --vp-c-brand-1: #000; 7 | --vp-c-brand-2: #383838; 8 | --vp-c-brand-3: #09090b; 9 | --vp-button-brand-bg: #171717; 10 | --vp-c-text-2: #444; 11 | } 12 | 13 | .dark { 14 | --vp-c-brand-1: #fff; 15 | --vp-c-brand-2: #71717a; 16 | --vp-c-brand-3: #52525b; 17 | --vp-c-brand-soft: #09090b50; 18 | --vp-c-text-2: #999; 19 | --vp-button-brand-bg: #ededed; 20 | --vp-button-brand-text: #0a0a0a; 21 | --vp-button-brand-hover-bg:#dad9d9; 22 | --vp-button-brand-hover-text: #383838; 23 | --vp-button-brand-active-text: #09090b; 24 | --vp-home-hero-name-background: -webkit-linear-gradient( 25 | 0deg, 26 | #fff 30%, 27 | #d1d5db 0% 28 | )!important; 29 | } 30 | 31 | :root { 32 | --vp-c-default-1: var(--vp-c-gray-1); 33 | --vp-c-default-2: var(--vp-c-gray-2); 34 | --vp-c-default-3: var(--vp-c-gray-3); 35 | --vp-c-default-soft: var(--vp-c-gray-soft); 36 | 37 | --vp-c-tip-1: var(--vp-c-brand-1); 38 | --vp-c-tip-2: var(--vp-c-brand-2); 39 | --vp-c-tip-3: var(--vp-c-brand-3); 40 | --vp-c-tip-soft: var(--vp-c-brand-soft); 41 | 42 | --vp-c-warning-1: var(--vp-c-yellow-1); 43 | --vp-c-warning-2: var(--vp-c-yellow-2); 44 | --vp-c-warning-3: var(--vp-c-yellow-3); 45 | --vp-c-warning-soft: var(--vp-c-yellow-soft); 46 | 47 | --vp-c-danger-1: var(--vp-c-red-1); 48 | --vp-c-danger-2: var(--vp-c-red-2); 49 | --vp-c-danger-3: var(--vp-c-red-3); 50 | --vp-c-danger-soft: var(--vp-c-red-soft); 51 | } 52 | 53 | /** 54 | * Component: Button 55 | * -------------------------------------------------------------------------- */ 56 | 57 | :root { 58 | --vp-button-brand-border: transparent; 59 | --vp-button-brand-hover-border: transparent; 60 | --vp-button-brand-active-border: transparent; 61 | --vp-button-brand-active-bg: var(--vp-c-brand-1); 62 | } 63 | 64 | /** 65 | * Component: Home 66 | * -------------------------------------------------------------------------- */ 67 | 68 | :root { 69 | --vp-home-hero-name-color: transparent; 70 | --vp-home-hero-name-background: -webkit-linear-gradient( 71 | 0deg, 72 | #333 30%, 73 | #888 1% 74 | ); 75 | } 76 | 77 | /** 78 | * Component: Custom Block 79 | * -------------------------------------------------------------------------- */ 80 | 81 | :root { 82 | --vp-custom-block-tip-border: transparent; 83 | --vp-custom-block-tip-text: var(--vp-c-text-1); 84 | --vp-custom-block-tip-bg: var(--vp-c-brand-soft); 85 | --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); 86 | } 87 | 88 | /** 89 | * Component: Algolia 90 | * -------------------------------------------------------------------------- */ 91 | 92 | .DocSearch { 93 | --docsearch-primary-color: var(--vp-c-brand-1) !important; 94 | } 95 | 96 | a > img { 97 | display: inline; 98 | } 99 | 100 | img[src*="features"] { 101 | height: auto; 102 | width: 48px; 103 | } 104 | 105 | img[src*="/features/rspack"] { 106 | margin-bottom: 28px!important; 107 | } 108 | 109 | details > summary:hover { 110 | cursor: pointer; 111 | user-select: none; 112 | } 113 | 114 | ::view-transition-old(root), 115 | ::view-transition-new(root) { 116 | animation: none; 117 | mix-blend-mode: normal; 118 | } 119 | ::view-transition-old(root) { 120 | z-index: 1; 121 | } 122 | ::view-transition-new(root) { 123 | z-index: 9999; 124 | } 125 | .dark::view-transition-old(root) { 126 | z-index: 9999; 127 | } 128 | .dark::view-transition-new(root) { 129 | z-index: 1; 130 | } 131 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | Unplugin 7 |

8 |

9 | Unified plugin system, Support Vite, Rollup, webpack, esbuild, and more 10 |

11 | 12 |

13 | Documentation 14 |

15 | 16 | ## Development 17 | 18 | This project use [GitHub GraphQL API](https://docs.github.com/en/graphql) to generate the showcase data. So you need to create a [GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new) first. 19 | 20 | ```bash 21 | cp .env.example .env 22 | ``` 23 | 24 | ```ini 25 | # .env 26 | GITHUB_TOKEN= 27 | ``` 28 | 29 | ### Generate files 30 | 31 | ```bash 32 | pnpm gen-files 33 | ``` 34 | 35 | ## Contributing 36 | 37 | Please refer to https://github.com/antfu/contribute 38 | -------------------------------------------------------------------------------- /docs/guide/plugin-conventions.md: -------------------------------------------------------------------------------- 1 | --- 2 | aside: false 3 | lastUpdated: false 4 | --- 5 | 6 | # Plugin Conventions 7 | 8 | To have a better community and ecosystem, we encourage plugin authors to follow these conventions when creating unplugins. 9 | 10 | - Plugins powered by Unplugin should have a clear name with `unplugin-` prefix. 11 | - Include `unplugin` keyword in `package.json`. 12 | - To provide better DX, packages could export 2 kinds of entry points: 13 | - Default export: the returned value of `createUnplugin` function 14 | 15 | ```ts 16 | import UnpluginFeature from 'unplugin-feature' 17 | ``` 18 | 19 | - Subpath exports: properties of the returned value of `createUnplugin` function for each bundler users 20 | 21 | ```ts 22 | import VitePlugin from 'unplugin-feature/vite' 23 | ``` 24 | 25 | - Refer to [unplugin-starter](https://github.com/unplugin/unplugin-starter) for more details about this setup. 26 | -------------------------------------------------------------------------------- /docs/guide/why-unplugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | lastUpdated: false 3 | --- 4 | 5 | # Why Unplugin 6 | 7 | TODO 8 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | sidebar: false 4 | 5 | hero: 6 | name: Unplugin 7 | text: The Unified
Plugin System 8 | tagline: Supports Vite, Rollup, webpack, esbuild, and every framework built on top of them. 9 | image: 10 | light: /logo_light.svg 11 | dark: /logo_dark.svg 12 | alt: Unplugin 13 | actions: 14 | - theme: brand 15 | text: Getting Started 16 | link: /guide/ 17 | - theme: alt 18 | text: Showcase 19 | link: /showcase/ 20 | - theme: alt 21 | text: View on GitHub 22 | link: https://github.com/unjs/unplugin 23 | 24 | features: 25 | - title: Vite 26 | details: Next Generation Frontend Tooling. 27 | link: https://vite.dev/ 28 | icon: 29 | src: /features/vitejs.svg 30 | 31 | - title: Rollup 32 | details: Next Generation ES module bundler. 33 | link: https://rollupjs.org/ 34 | icon: 35 | src: /features/rollup.svg 36 | 37 | - title: webpack 38 | details: A static module bundler for modern JavaScript applications. 39 | link: https://webpack.js.org/ 40 | icon: 41 | src: /features/webpack.svg 42 | 43 | - title: esbuild 44 | details: An extremely fast bundler for the web. 45 | link: https://esbuild.github.io/ 46 | icon: 47 | src: /features/esbuild.svg 48 | 49 | - title: Rspack 50 | details: A fast Rust-based web bundler. 51 | link: https://www.rspack.dev/ 52 | icon: 53 | src: /features/rspack.png 54 | 55 | - title: Farm 56 | details: Extremely fast web build tool written in Rust 57 | link: https://www.farmfe.org/ 58 | icon: 59 | src: /features/farm.png 60 | 61 | - title: Rolldown 62 | details: Fast Rust bundler for JavaScript with Rollup-compatible API 63 | link: https://rolldown.rs/ 64 | icon: 65 | src: /features/rolldown.svg 66 | 67 | - title: More 68 | details: More supported bundlers... 69 | link: /guide/#supported-hooks 70 | icon: 71 | src: /features/more.svg 72 | --- 73 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unplugin-docs", 3 | "type": "module", 4 | "private": true, 5 | "engines": { 6 | "node": ">=18.19.0" 7 | }, 8 | "scripts": { 9 | "gen-files": "tsx ./.vitepress/data/gen-files.ts", 10 | "dev": "vitepress dev --open", 11 | "build": "pnpm gen-files && vitepress build", 12 | "lint": "case-police '**/*.md'", 13 | "typecheck": "vue-tsc --noEmit" 14 | }, 15 | "devDependencies": { 16 | "@iconify-json/ri": "^1.2.5", 17 | "@shikijs/vitepress-twoslash": "^3.4.2", 18 | "case-police": "^2.0.0", 19 | "consola": "^3.4.2", 20 | "dotenv": "^16.5.0", 21 | "markdown-it": "^14.1.0", 22 | "markdown-it-github-alerts": "^1.0.0", 23 | "ofetch": "^1.4.1", 24 | "tsx": "^4.19.4", 25 | "unocss": "^66.1.2", 26 | "unplugin": "workspace:*", 27 | "unplugin-icons": "^22.1.0", 28 | "unplugin-vue-components": "^28.7.0", 29 | "vitepress": "2.0.0-alpha.2", 30 | "vitepress-plugin-group-icons": "^1.5.5", 31 | "vue": "^3.5.15", 32 | "vue-tsc": "^2.2.10" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unjs/unplugin/8c291f725f3f76aa70bcce8f989a6a76b018649a/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/features/astro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /docs/public/features/esbuild.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/public/features/farm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unjs/unplugin/8c291f725f3f76aa70bcce8f989a6a76b018649a/docs/public/features/farm.png -------------------------------------------------------------------------------- /docs/public/features/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /docs/public/features/nuxt.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /docs/public/features/rolldown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/public/features/rollup.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 14 | 17 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/public/features/rspack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unjs/unplugin/8c291f725f3f76aa70bcce8f989a6a76b018649a/docs/public/features/rspack.png -------------------------------------------------------------------------------- /docs/public/features/vitejs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/public/features/webpack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 24 | 25 | 26 | 27 | 29 | 33 | 36 | 38 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/public/logo_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 14 | 15 | 16 | 17 | 19 | 23 | 26 | 28 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/public/logo_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 14 | 15 | 16 | 17 | 19 | 23 | 26 | 28 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unjs/unplugin/8c291f725f3f76aa70bcce8f989a6a76b018649a/docs/public/og.png -------------------------------------------------------------------------------- /docs/showcase/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | aside: false 3 | --- 4 | 5 | # Overview 6 | 7 | Here are a few unplugins maintained by the unplugin team, find more on [npm](https://www.npmjs.com/search?ranking=popularity&q=keywords%3Aunplugin). 8 | 9 | 10 | 11 | ::: info Join us! 12 | 13 | We have started a [GitHub organization](https://github.com/unplugin) to host and collaborate on popular unplugins. You can go there to find more plugins maintained by the unplugin team or even join us with your own plugins! 14 | 15 | ::: 16 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ESNext"], 5 | "isolatedDeclarations": false 6 | }, 7 | "include": ["**/*", ".**/*", "../src/globals.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /docs/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetAttributify, presetIcons, presetWind3, transformerDirectives } from 'unocss' 2 | 3 | export default defineConfig({ 4 | presets: [ 5 | presetWind3(), 6 | presetAttributify(), 7 | presetIcons({ 8 | scale: 1.2, 9 | }), 10 | ], 11 | transformers: [ 12 | transformerDirectives(), 13 | ], 14 | }) 15 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import Unocss from 'unocss/vite' 2 | import Icons from 'unplugin-icons/vite' 3 | import Components from 'unplugin-vue-components/vite' 4 | import { defineConfig } from 'vite' 5 | import { groupIconVitePlugin, localIconLoader } from 'vitepress-plugin-group-icons' 6 | import { MarkdownTransform } from './.vitepress/plugins/markdownTransform' 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | MarkdownTransform(), 11 | Components({ 12 | include: [/\.vue/, /\.md/], 13 | dirs: '.vitepress/components', 14 | dts: '.vitepress/components.d.ts', 15 | }), 16 | // @ts-expect-error mismatch vite version 17 | Unocss(), 18 | Icons(), 19 | groupIconVitePlugin({ 20 | customIcon: { 21 | farm: localIconLoader(import.meta.url, '.vitepress/assets/farm.svg'), 22 | rolldown: localIconLoader(import.meta.url, '.vitepress/assets/rolldown.svg'), 23 | rspack: localIconLoader(import.meta.url, '.vitepress/assets/rspack.svg'), 24 | }, 25 | }), 26 | ], 27 | }) 28 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | 4 | export default antfu( 5 | { 6 | vue: true, 7 | formatters: { 8 | markdown: 'dprint', 9 | }, 10 | }, 11 | { 12 | ignores: [ 13 | 'test-out/**', 14 | '**/output.js', 15 | 'docs/showcase/*.md', 16 | 'docs/.vitepress/data/repository.json', 17 | ], 18 | }, 19 | { 20 | files: ['**/fixtures/**/*.js'], 21 | rules: { 22 | 'no-console': 'off', 23 | }, 24 | }, 25 | { 26 | rules: { 27 | 'node/prefer-global/process': 'off', 28 | }, 29 | }, 30 | { 31 | files: ['**/src/**/*.ts'], 32 | rules: { 33 | 'node/no-unsupported-features/node-builtins': 'error', 34 | 'node/no-unsupported-features/es-builtins': 'error', 35 | }, 36 | }, 37 | ) 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unplugin", 3 | "type": "module", 4 | "version": "2.3.5", 5 | "packageManager": "pnpm@10.11.0", 6 | "description": "Unified plugin system for build tools", 7 | "license": "MIT", 8 | "homepage": "https://unplugin.unjs.io", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/unjs/unplugin.git" 12 | }, 13 | "sideEffects": false, 14 | "exports": { 15 | ".": { 16 | "import": "./dist/index.js", 17 | "require": "./dist/index.cjs" 18 | }, 19 | "./dist/webpack/loaders/*": "./dist/webpack/loaders/*.cjs", 20 | "./dist/rspack/loaders/*": "./dist/rspack/loaders/*.cjs" 21 | }, 22 | "main": "dist/index.js", 23 | "module": "dist/index.js", 24 | "types": "dist/index.d.ts", 25 | "files": [ 26 | "dist" 27 | ], 28 | "engines": { 29 | "node": ">=18.12.0" 30 | }, 31 | "scripts": { 32 | "build": "tsdown", 33 | "dev": "tsdown --watch src", 34 | "lint": "eslint --cache .", 35 | "lint:fix": "nr lint --fix", 36 | "typecheck": "tsc --noEmit", 37 | "docs:dev": "pnpm -C docs run dev", 38 | "docs:build": "pnpm -C docs run build", 39 | "docs:gen-files": "pnpm -C docs run gen-files", 40 | "prepublishOnly": "nr build", 41 | "release": "bumpp --all && npm publish", 42 | "test": "nr test:build && vitest run --pool=forks", 43 | "test:build": "jiti scripts/buildFixtures.ts" 44 | }, 45 | "dependencies": { 46 | "acorn": "^8.14.1", 47 | "picomatch": "^4.0.2", 48 | "webpack-virtual-modules": "^0.6.2" 49 | }, 50 | "devDependencies": { 51 | "@ampproject/remapping": "^2.3.0", 52 | "@antfu/eslint-config": "^4.13.2", 53 | "@antfu/ni": "^25.0.0", 54 | "@farmfe/cli": "^1.0.4", 55 | "@farmfe/core": "^1.7.5", 56 | "@rspack/cli": "^1.3.12", 57 | "@rspack/core": "^1.3.12", 58 | "@types/fs-extra": "^11.0.4", 59 | "@types/node": "^22.15.21", 60 | "@types/picomatch": "^4.0.0", 61 | "ansis": "^4.0.0", 62 | "bumpp": "^10.1.1", 63 | "esbuild": "^0.25.5", 64 | "esbuild-plugin-copy": "^2.1.1", 65 | "eslint": "^9.27.0", 66 | "eslint-plugin-format": "^1.0.1", 67 | "fast-glob": "^3.3.3", 68 | "fs-extra": "^11.3.0", 69 | "jiti": "^2.4.2", 70 | "lint-staged": "^16.0.0", 71 | "magic-string": "^0.30.17", 72 | "rolldown": "^1.0.0-beta.9", 73 | "rollup": "^4.41.1", 74 | "simple-git-hooks": "^2.13.0", 75 | "tsdown": "^0.12.3", 76 | "typescript": "~5.8.3", 77 | "unloader": "^0.4.5", 78 | "unplugin": "workspace:*", 79 | "unplugin-unused": "^0.5.0", 80 | "vite": "^6.3.5", 81 | "vitest": "^3.1.4", 82 | "webpack": "^5.99.9", 83 | "webpack-cli": "^6.0.1" 84 | }, 85 | "resolutions": { 86 | "esbuild": "^0.25.5" 87 | }, 88 | "simple-git-hooks": { 89 | "pre-commit": "pnpm i --frozen-lockfile --ignore-scripts --offline && npx lint-staged" 90 | }, 91 | "lint-staged": { 92 | "*": "eslint --fix" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - docs 3 | -------------------------------------------------------------------------------- /scripts/buildFixtures.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process' 2 | import { join, resolve } from 'node:path' 3 | import process from 'node:process' 4 | import c from 'ansis' 5 | import fs from 'fs-extra' 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 | for (const name of fixtures) { 15 | const path = join(dir, name) 16 | if (fs.existsSync(join(path, 'dist'))) 17 | await fs.remove(join(path, 'dist')) 18 | 19 | console.log(c.yellow.inverse.bold`\n Vite `, name, '\n') 20 | execSync('npx vite --version', { cwd: path, stdio: 'inherit' }) 21 | execSync('npx vite build', { cwd: path, stdio: 'inherit' }) 22 | 23 | console.log(c.red.inverse.bold`\n Rollup `, name, '\n') 24 | execSync('npx rollup --version', { cwd: path, stdio: 'inherit' }) 25 | execSync('npx rollup --bundleConfigAsCjs -c', { cwd: path, stdio: 'inherit' }) 26 | 27 | console.log(c.blue.inverse.bold`\n Webpack `, name, '\n') 28 | execSync('npx webpack --version', { cwd: path, stdio: 'inherit' }) 29 | execSync('npx webpack', { cwd: path, stdio: 'inherit' }) 30 | 31 | console.log(c.yellow.inverse.bold`\n Esbuild `, name, '\n') 32 | execSync('npx esbuild --version', { cwd: path, stdio: 'inherit' }) 33 | execSync('node esbuild.config.js', { cwd: path, stdio: 'inherit' }) 34 | 35 | console.log(c.cyan.inverse.bold`\n Rspack `, name, '\n') 36 | execSync('npx rspack --version', { cwd: path, stdio: 'inherit' }) 37 | execSync('npx rspack', { cwd: path, stdio: 'inherit' }) 38 | 39 | console.log(c.magenta.inverse.bold`\n Farm `, name, '\n') 40 | execSync('npx farm --version', { cwd: path, stdio: 'inherit' }) 41 | execSync('npx farm build', { cwd: path, stdio: 'inherit' }) 42 | } 43 | } 44 | 45 | run() 46 | -------------------------------------------------------------------------------- /src/define.ts: -------------------------------------------------------------------------------- 1 | import type { UnpluginFactory, UnpluginInstance } from './types' 2 | import { getEsbuildPlugin } from './esbuild' 3 | import { getFarmPlugin } from './farm' 4 | import { getRolldownPlugin } from './rolldown' 5 | import { getRollupPlugin } from './rollup' 6 | import { getRspackPlugin } from './rspack' 7 | import { getUnloaderPlugin } from './unloader' 8 | import { getVitePlugin } from './vite' 9 | import { getWebpackPlugin } from './webpack' 10 | 11 | export function createUnplugin( 12 | factory: UnpluginFactory, 13 | ): UnpluginInstance { 14 | return { 15 | get esbuild() { 16 | return getEsbuildPlugin(factory) 17 | }, 18 | get rollup() { 19 | return getRollupPlugin(factory) 20 | }, 21 | get vite() { 22 | return getVitePlugin(factory) 23 | }, 24 | get rolldown() { 25 | return getRolldownPlugin(factory) 26 | }, 27 | get webpack() { 28 | return getWebpackPlugin(factory) 29 | }, 30 | get rspack() { 31 | return getRspackPlugin(factory) 32 | }, 33 | get farm() { 34 | return getFarmPlugin(factory) 35 | }, 36 | get unloader() { 37 | return getUnloaderPlugin(factory) 38 | }, 39 | get raw() { 40 | return factory 41 | }, 42 | } 43 | } 44 | 45 | export function createEsbuildPlugin( 46 | factory: UnpluginFactory, 47 | ): UnpluginInstance['esbuild'] { 48 | return getEsbuildPlugin(factory) 49 | } 50 | 51 | export function createRollupPlugin( 52 | factory: UnpluginFactory, 53 | ): UnpluginInstance['rollup'] { 54 | return getRollupPlugin(factory) 55 | } 56 | 57 | export function createVitePlugin( 58 | factory: UnpluginFactory, 59 | ): UnpluginInstance['vite'] { 60 | return getVitePlugin(factory) 61 | } 62 | 63 | export function createRolldownPlugin( 64 | factory: UnpluginFactory, 65 | ): UnpluginInstance['rolldown'] { 66 | return getRolldownPlugin(factory) 67 | } 68 | 69 | export function createWebpackPlugin( 70 | factory: UnpluginFactory, 71 | ): UnpluginInstance['webpack'] { 72 | return getWebpackPlugin(factory) 73 | } 74 | 75 | export function createRspackPlugin( 76 | factory: UnpluginFactory, 77 | ): UnpluginInstance['rspack'] { 78 | return getRspackPlugin(factory) 79 | } 80 | 81 | export function createFarmPlugin( 82 | factory: UnpluginFactory, 83 | ): UnpluginInstance['farm'] { 84 | return getFarmPlugin(factory) 85 | } 86 | 87 | export function createUnloaderPlugin( 88 | factory: UnpluginFactory, 89 | ): UnpluginInstance['unloader'] { 90 | return getUnloaderPlugin(factory) 91 | } 92 | -------------------------------------------------------------------------------- /src/esbuild/utils.ts: -------------------------------------------------------------------------------- 1 | import type { DecodedSourceMap, EncodedSourceMap } from '@ampproject/remapping' 2 | import type { Loader, Location, Message, PartialMessage, PluginBuild } from 'esbuild' 3 | import type { SourceMap } from 'rollup' 4 | import type { UnpluginBuildContext, UnpluginContext, UnpluginMessage } from '../types' 5 | import { Buffer } from 'node:buffer' 6 | import fs from 'node:fs' 7 | import path from 'node:path' 8 | import remapping from '@ampproject/remapping' 9 | import { parse } from '../utils/context' 10 | 11 | const ExtToLoader: Record = { 12 | '.js': 'js', 13 | '.mjs': 'js', 14 | '.cjs': 'js', 15 | '.jsx': 'jsx', 16 | '.ts': 'ts', 17 | '.cts': 'ts', 18 | '.mts': 'ts', 19 | '.tsx': 'tsx', 20 | '.css': 'css', 21 | '.less': 'css', 22 | '.stylus': 'css', 23 | '.scss': 'css', 24 | '.sass': 'css', 25 | '.json': 'json', 26 | '.txt': 'text', 27 | } 28 | 29 | export function guessLoader(code: string, id: string): Loader { 30 | return ExtToLoader[path.extname(id).toLowerCase()] || 'js' 31 | } 32 | 33 | export function unwrapLoader( 34 | loader: Loader | ((code: string, id: string) => Loader), 35 | code: string, 36 | id: string, 37 | ): Loader { 38 | if (typeof loader === 'function') 39 | return loader(code, id) 40 | 41 | return loader 42 | } 43 | 44 | // `load` and `transform` may return a sourcemap without toString and toUrl, 45 | // but esbuild needs them, we fix the two methods 46 | export function fixSourceMap(map: EncodedSourceMap): SourceMap { 47 | if (!Object.prototype.hasOwnProperty.call(map, 'toString')) { 48 | Object.defineProperty(map, 'toString', { 49 | enumerable: false, 50 | value: function toString() { 51 | return JSON.stringify(this) 52 | }, 53 | }) 54 | } 55 | if (!Object.prototype.hasOwnProperty.call(map, 'toUrl')) { 56 | Object.defineProperty(map, 'toUrl', { 57 | enumerable: false, 58 | value: function toUrl() { 59 | return `data:application/json;charset=utf-8;base64,${Buffer.from(this.toString()).toString('base64')}` 60 | }, 61 | }) 62 | } 63 | return map as SourceMap 64 | } 65 | 66 | // taken from https://github.com/vitejs/vite/blob/71868579058512b51991718655e089a78b99d39c/packages/vite/src/node/utils.ts#L525 67 | const nullSourceMap: EncodedSourceMap = { 68 | names: [], 69 | sources: [], 70 | mappings: '', 71 | version: 3, 72 | } 73 | export function combineSourcemaps( 74 | filename: string, 75 | sourcemapList: Array, 76 | ): EncodedSourceMap { 77 | sourcemapList = sourcemapList.filter(m => m.sources) 78 | 79 | if ( 80 | sourcemapList.length === 0 81 | || sourcemapList.every(m => m.sources.length === 0) 82 | ) { 83 | return { ...nullSourceMap } 84 | } 85 | 86 | // We don't declare type here so we can convert/fake/map as EncodedSourceMap 87 | let map // : SourceMap 88 | let mapIndex = 1 89 | const useArrayInterface 90 | = sourcemapList.slice(0, -1).find(m => m.sources.length !== 1) === undefined 91 | if (useArrayInterface) { 92 | map = remapping(sourcemapList, () => null, true) 93 | } 94 | else { 95 | map = remapping( 96 | sourcemapList[0], 97 | (sourcefile) => { 98 | if (sourcefile === filename && sourcemapList[mapIndex]) 99 | return sourcemapList[mapIndex++] 100 | else 101 | return { ...nullSourceMap } 102 | }, 103 | true, 104 | ) 105 | } 106 | if (!map.file) 107 | delete map.file 108 | 109 | return map as EncodedSourceMap 110 | } 111 | 112 | export function createBuildContext(build: PluginBuild): UnpluginBuildContext { 113 | const watchFiles: string[] = [] 114 | const { initialOptions } = build 115 | return { 116 | parse, 117 | addWatchFile() { 118 | throw new Error('unplugin/esbuild: addWatchFile outside supported hooks (resolveId, load, transform)') 119 | }, 120 | emitFile(emittedFile) { 121 | const outFileName = emittedFile.fileName || emittedFile.name 122 | if (initialOptions.outdir && emittedFile.source && outFileName) { 123 | const outPath = path.resolve(initialOptions.outdir, outFileName) 124 | // Ensure output directory exists for this.emitFile 125 | const outDir = path.dirname(outPath) 126 | if (!fs.existsSync(outDir)) 127 | fs.mkdirSync(outDir, { recursive: true }) 128 | fs.writeFileSync(outPath, emittedFile.source) 129 | } 130 | }, 131 | getWatchFiles() { 132 | return watchFiles 133 | }, 134 | getNativeBuildContext() { 135 | return { framework: 'esbuild', build } 136 | }, 137 | } 138 | } 139 | 140 | export function createPluginContext(context: UnpluginBuildContext): { 141 | errors: PartialMessage[] 142 | warnings: PartialMessage[] 143 | mixedContext: UnpluginContext & UnpluginBuildContext 144 | } { 145 | const errors: PartialMessage[] = [] 146 | const warnings: PartialMessage[] = [] 147 | const pluginContext: UnpluginContext = { 148 | error(message) { errors.push(normalizeMessage(message)) }, 149 | warn(message) { warnings.push(normalizeMessage(message)) }, 150 | } 151 | 152 | const mixedContext: UnpluginContext & UnpluginBuildContext = { 153 | ...context, 154 | ...pluginContext, 155 | addWatchFile(id: string) { 156 | context.getWatchFiles().push(id) 157 | }, 158 | } 159 | 160 | return { 161 | errors, 162 | warnings, 163 | mixedContext, 164 | } 165 | } 166 | 167 | function normalizeMessage(message: string | UnpluginMessage): Message { 168 | if (typeof message === 'string') 169 | message = { message } 170 | 171 | return { 172 | id: message.id!, 173 | pluginName: message.plugin!, 174 | text: message.message!, 175 | 176 | location: message.loc 177 | ? { 178 | file: message.loc.file, 179 | line: message.loc.line, 180 | column: message.loc.column, 181 | } as Location 182 | : null, 183 | 184 | detail: message.meta, 185 | notes: [], 186 | } 187 | } 188 | 189 | export function processCodeWithSourceMap(map: SourceMap | null | undefined, code: string): string { 190 | if (map) { 191 | if (!map.sourcesContent || map.sourcesContent.length === 0) 192 | map.sourcesContent = [code] 193 | 194 | map = fixSourceMap(map as EncodedSourceMap) 195 | code += `\n//# sourceMappingURL=${map.toUrl()}` 196 | } 197 | return code 198 | } 199 | -------------------------------------------------------------------------------- /src/farm/context.ts: -------------------------------------------------------------------------------- 1 | import type { CompilationContext } from '@farmfe/core' 2 | import type { UnpluginBuildContext, UnpluginContext } from '../types' 3 | import { Buffer } from 'node:buffer' 4 | import { extname } from 'node:path' 5 | import { parse } from '../utils/context' 6 | 7 | export function createFarmContext( 8 | context: CompilationContext, 9 | currentResolveId?: string, 10 | ): UnpluginBuildContext { 11 | return { 12 | parse, 13 | 14 | addWatchFile(id: string) { 15 | context.addWatchFile(id, currentResolveId || id) 16 | }, 17 | emitFile(emittedFile) { 18 | const outFileName = emittedFile.fileName || emittedFile.name 19 | if (emittedFile.source && outFileName) { 20 | context.emitFile({ 21 | resolvedPath: outFileName, 22 | name: outFileName, 23 | content: [...Buffer.from(emittedFile.source as any)], 24 | resourceType: extname(outFileName), 25 | }) 26 | } 27 | }, 28 | getWatchFiles() { 29 | return context.getWatchFiles() 30 | }, 31 | getNativeBuildContext() { 32 | return { framework: 'farm', context } 33 | }, 34 | } 35 | } 36 | 37 | export function unpluginContext(context: CompilationContext): UnpluginContext { 38 | return { 39 | error: (error: any) => 40 | context!.error(typeof error === 'string' ? new Error(error) : error), 41 | warn: (error: any) => 42 | context!.warn(typeof error === 'string' ? new Error(error) : error), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/farm/utils.ts: -------------------------------------------------------------------------------- 1 | import type { JsPlugin } from '@farmfe/core' 2 | 3 | import path from 'node:path' 4 | import * as querystring from 'node:querystring' 5 | 6 | export type WatchChangeEvents = 'create' | 'update' | 'delete' 7 | 8 | const ExtToLoader: Record = { 9 | '.js': 'js', 10 | '.mjs': 'js', 11 | '.cjs': 'js', 12 | '.jsx': 'jsx', 13 | '.ts': 'ts', 14 | '.cts': 'ts', 15 | '.mts': 'ts', 16 | '.tsx': 'tsx', 17 | '.json': 'json', 18 | '.toml': 'toml', 19 | '.text': 'text', 20 | '.wasm': 'wasm', 21 | '.napi': 'napi', 22 | '.node': 'napi', 23 | } 24 | 25 | export const DEFAULT_PATTERN = '.*' 26 | 27 | export function guessIdLoader(id: string): string { 28 | return ExtToLoader[path.extname(id).toLowerCase()] || 'js' 29 | } 30 | 31 | export function transformQuery(context: any): void { 32 | const queryParamsObject: Record = {} 33 | context.query.forEach(([param, value]: string[]) => { 34 | queryParamsObject[param] = value 35 | }) 36 | const transformQuery = querystring.stringify(queryParamsObject) 37 | context.resolvedPath = `${context.resolvedPath}?${transformQuery}` 38 | } 39 | 40 | export function convertEnforceToPriority(value: 'pre' | 'post' | undefined): number { 41 | const defaultPriority = 100 42 | const enforceToPriority = { 43 | pre: 102, 44 | post: 98, 45 | } 46 | 47 | return enforceToPriority[value!] !== undefined 48 | ? enforceToPriority[value!] 49 | : defaultPriority 50 | } 51 | 52 | export function convertWatchEventChange( 53 | value: WatchChangeEvents, 54 | ): WatchChangeEvents { 55 | const watchEventChange = { 56 | Added: 'create', 57 | Updated: 'update', 58 | Removed: 'delete', 59 | } as unknown as { [key in WatchChangeEvents]: WatchChangeEvents } 60 | 61 | return watchEventChange[value] 62 | } 63 | 64 | export function isString(variable: unknown): variable is string { 65 | return typeof variable === 'string' 66 | } 67 | 68 | export function isObject(variable: unknown): variable is object { 69 | return typeof variable === 'object' && variable !== null 70 | } 71 | 72 | export function customParseQueryString(url: string | null): [string, string][] { 73 | if (!url) 74 | return [] 75 | 76 | const queryString = url.split('?')[1] 77 | 78 | const parsedParams = querystring.parse(queryString) 79 | const paramsArray: [string, string][] = [] 80 | 81 | for (const key in parsedParams) 82 | paramsArray.push([key, parsedParams[key] as string]) 83 | 84 | return paramsArray 85 | } 86 | 87 | export function encodeStr(str: string): string { 88 | const len = str.length 89 | if (len === 0) 90 | return str 91 | 92 | const firstNullIndex = str.indexOf('\0') 93 | if (firstNullIndex === -1) 94 | return str 95 | 96 | const result = Array.from({ length: len + countNulls(str, firstNullIndex) }) 97 | 98 | let pos = 0 99 | for (let i = 0; i < firstNullIndex; i++) { 100 | result[pos++] = str[i] 101 | } 102 | 103 | for (let i = firstNullIndex; i < len; i++) { 104 | const char = str[i] 105 | if (char === '\0') { 106 | result[pos++] = '\\' 107 | result[pos++] = '0' 108 | } 109 | else { 110 | result[pos++] = char 111 | } 112 | } 113 | 114 | return path.posix.normalize(result.join('')) 115 | } 116 | 117 | export function decodeStr(str: string): string { 118 | const len = str.length 119 | if (len === 0) 120 | return str 121 | 122 | const firstIndex = str.indexOf('\\0') 123 | if (firstIndex === -1) 124 | return str 125 | 126 | const result = Array.from({ length: len - countBackslashZeros(str, firstIndex) }) 127 | 128 | let pos = 0 129 | for (let i = 0; i < firstIndex; i++) { 130 | result[pos++] = str[i] 131 | } 132 | 133 | let i = firstIndex 134 | while (i < len) { 135 | if (str[i] === '\\' && str[i + 1] === '0') { 136 | result[pos++] = '\0' 137 | i += 2 138 | } 139 | else { 140 | result[pos++] = str[i++] 141 | } 142 | } 143 | 144 | return path.posix.normalize(result.join('')) 145 | } 146 | 147 | export function getContentValue(content: any): string { 148 | if (content === null || content === undefined) { 149 | throw new Error('Content cannot be null or undefined') 150 | } 151 | 152 | const strContent = typeof content === 'string' 153 | ? content 154 | : (content.code || '') 155 | 156 | return encodeStr(strContent) 157 | } 158 | 159 | function countNulls(str: string, startIndex: number): number { 160 | let count = 0 161 | const len = str.length 162 | for (let i = startIndex; i < len; i++) { 163 | if (str[i] === '\0') 164 | count++ 165 | } 166 | return count 167 | } 168 | 169 | function countBackslashZeros(str: string, startIndex: number): number { 170 | let count = 0 171 | const len = str.length 172 | for (let i = startIndex; i < len - 1; i++) { 173 | if (str[i] === '\\' && str[i + 1] === '0') { 174 | count++ 175 | i++ 176 | } 177 | } 178 | return count 179 | } 180 | 181 | export function removeQuery(pathe: string): string { 182 | const queryIndex = pathe.indexOf('?') 183 | if (queryIndex !== -1) { 184 | return path.posix.normalize(pathe.slice(0, queryIndex)) 185 | } 186 | return path.posix.normalize(pathe) 187 | } 188 | 189 | export function isStartsWithSlash(str: string): boolean { 190 | return str?.startsWith('/') 191 | } 192 | 193 | export function appendQuery(id: string, query: [string, string][]): string { 194 | if (!query.length) { 195 | return id 196 | } 197 | 198 | return `${id}?${stringifyQuery(query)}` 199 | } 200 | 201 | export function stringifyQuery(query: [string, string][]): string { 202 | if (!query.length) { 203 | return '' 204 | } 205 | 206 | let queryStr = '' 207 | 208 | for (const [key, value] of query) { 209 | queryStr += `${key}${value ? `=${value}` : ''}&` 210 | } 211 | 212 | return `${queryStr.slice(0, -1)}` 213 | } 214 | 215 | export interface JsPluginExtended extends JsPlugin { 216 | [key: string]: any 217 | } 218 | 219 | export const CSS_LANGS_RES: [RegExp, string][] = [ 220 | [/\.(less)(?:$|\?)/, 'less'], 221 | [/\.(scss|sass)(?:$|\?)/, 'sass'], 222 | [/\.(styl|stylus)(?:$|\?)/, 'stylus'], 223 | [/\.(css)(?:$|\?)/, 'css'], 224 | ] 225 | 226 | export const JS_LANGS_RES: [RegExp, string][] = [ 227 | [/\.(js|mjs|cjs)(?:$|\?)/, 'js'], 228 | // jsx 229 | [/\.(jsx)(?:$|\?)/, 'jsx'], 230 | // ts 231 | [/\.(ts|cts|mts)(?:$|\?)/, 'ts'], 232 | // tsx 233 | [/\.(tsx)(?:$|\?)/, 'tsx'], 234 | ] 235 | 236 | export function getCssModuleType(id: string): string | null { 237 | for (const [reg, lang] of CSS_LANGS_RES) { 238 | if (reg.test(id)) { 239 | return lang 240 | } 241 | } 242 | 243 | return null 244 | } 245 | 246 | export function getJsModuleType(id: string): string | null { 247 | for (const [reg, lang] of JS_LANGS_RES) { 248 | if (reg.test(id)) { 249 | return lang 250 | } 251 | } 252 | 253 | return null 254 | } 255 | 256 | export function formatLoadModuleType(id: string): string { 257 | const cssModuleType = getCssModuleType(id) 258 | 259 | if (cssModuleType) { 260 | return cssModuleType 261 | } 262 | 263 | const jsModuleType = getJsModuleType(id) 264 | 265 | if (jsModuleType) { 266 | return jsModuleType 267 | } 268 | 269 | return 'js' 270 | } 271 | 272 | export function formatTransformModuleType(id: string): string { 273 | return formatLoadModuleType(id) 274 | } 275 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Flag that is replaced with a boolean during build time. 3 | * __DEV__ is false in the final library output, and it is 4 | * true when the library is ad-hoc transpiled, ie. during tests. 5 | * 6 | * See "tsdown.config.ts" and "vitest.config.ts" for more info. 7 | */ 8 | declare const __DEV__: boolean 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './define' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /src/rolldown/index.ts: -------------------------------------------------------------------------------- 1 | import type { RolldownPlugin, UnpluginContextMeta, UnpluginFactory, UnpluginInstance } from '../types' 2 | import { toRollupPlugin } from '../rollup' 3 | import { toArray } from '../utils/general' 4 | 5 | export function getRolldownPlugin, Nested extends boolean = boolean>( 6 | factory: UnpluginFactory, 7 | ) { 8 | return ((userOptions?: UserOptions) => { 9 | const meta: UnpluginContextMeta = { 10 | framework: 'rolldown', 11 | } 12 | const rawPlugins = toArray(factory(userOptions!, meta)) 13 | 14 | const plugins = rawPlugins.map((rawPlugin) => { 15 | const plugin = toRollupPlugin(rawPlugin, 'rolldown') as RolldownPlugin 16 | return plugin 17 | }) 18 | 19 | return plugins.length === 1 ? plugins[0] : plugins 20 | }) as UnpluginInstance['rolldown'] 21 | } 22 | -------------------------------------------------------------------------------- /src/rollup/index.ts: -------------------------------------------------------------------------------- 1 | import type { Hook, HookFnMap, RollupPlugin, UnpluginContextMeta, UnpluginFactory, UnpluginInstance, UnpluginOptions } from '../types' 2 | import { normalizeObjectHook } from '../utils/filter' 3 | import { toArray } from '../utils/general' 4 | 5 | export function getRollupPlugin, Nested extends boolean = boolean>( 6 | factory: UnpluginFactory, 7 | ) { 8 | return ((userOptions?: UserOptions) => { 9 | const meta: UnpluginContextMeta = { 10 | framework: 'rollup', 11 | } 12 | const rawPlugins = toArray(factory(userOptions!, meta)) 13 | const plugins = rawPlugins.map(plugin => toRollupPlugin(plugin, 'rollup')) 14 | return plugins.length === 1 ? plugins[0] : plugins 15 | }) as UnpluginInstance['rollup'] 16 | } 17 | 18 | export function toRollupPlugin(plugin: UnpluginOptions, key: 'rollup' | 'rolldown' | 'vite' | 'unloader'): RollupPlugin { 19 | const nativeFilter = key === 'rolldown' 20 | 21 | if ( 22 | plugin.resolveId 23 | && (!nativeFilter && typeof plugin.resolveId === 'object' && plugin.resolveId.filter) 24 | ) { 25 | const resolveIdHook = plugin.resolveId 26 | const { handler, filter } = normalizeObjectHook('load', resolveIdHook) 27 | 28 | replaceHookHandler('resolveId', resolveIdHook, function (...args) { 29 | const [id] = args 30 | const supportFilter = supportNativeFilter(this) 31 | if (!supportFilter && !filter(id)) 32 | return 33 | 34 | return handler.apply(this, args) 35 | }) 36 | } 37 | 38 | if (plugin.load && ( 39 | plugin.loadInclude 40 | || (!nativeFilter && typeof plugin.load === 'object' && plugin.load.filter)) 41 | ) { 42 | const loadHook = plugin.load 43 | const { handler, filter } = normalizeObjectHook('load', loadHook) 44 | 45 | replaceHookHandler('load', loadHook, function (...args) { 46 | const [id] = args 47 | if (plugin.loadInclude && !plugin.loadInclude(id)) 48 | return 49 | 50 | const supportFilter = supportNativeFilter(this) 51 | if (!supportFilter && !filter(id)) 52 | return 53 | 54 | return handler.apply(this, args) 55 | }) 56 | } 57 | 58 | if (plugin.transform && ( 59 | plugin.transformInclude 60 | || (!nativeFilter && typeof plugin.transform === 'object' && plugin.transform.filter)) 61 | ) { 62 | const transformHook = plugin.transform 63 | const { handler, filter } = normalizeObjectHook('transform', transformHook) 64 | 65 | replaceHookHandler('transform', transformHook, function (...args) { 66 | const [code, id] = args 67 | if (plugin.transformInclude && !plugin.transformInclude(id)) 68 | return 69 | 70 | const supportFilter = supportNativeFilter(this) 71 | if (!supportFilter && !filter(id, code)) 72 | return 73 | 74 | return handler.apply(this, args) 75 | }) 76 | } 77 | 78 | if (plugin[key]) 79 | Object.assign(plugin, plugin[key]) 80 | 81 | return plugin as RollupPlugin 82 | 83 | function replaceHookHandler( 84 | name: K, 85 | hook: Hook, 86 | handler: HookFnMap[K], 87 | ) { 88 | if (typeof hook === 'function') { 89 | plugin[name] = handler as any 90 | } 91 | else { 92 | hook.handler = handler 93 | } 94 | } 95 | } 96 | 97 | function supportNativeFilter(context: any) { 98 | const rollupVersion: string | undefined = context?.meta?.rollupVersion 99 | if (!rollupVersion) 100 | return false 101 | 102 | const [major, minor] = rollupVersion.split('.') 103 | // https://github.com/rollup/rollup/pull/5909#issuecomment-2798739729 104 | return (Number(major) > 4 || (Number(major) === 4 && Number(minor) >= 40)) 105 | } 106 | -------------------------------------------------------------------------------- /src/rspack/context.ts: -------------------------------------------------------------------------------- 1 | import type { Compilation, Compiler, LoaderContext } from '@rspack/core' 2 | import type { UnpluginBuildContext, UnpluginContext, UnpluginMessage } from '../types' 3 | import { Buffer } from 'node:buffer' 4 | import { resolve } from 'node:path' 5 | import { parse } from '../utils/context' 6 | 7 | export function createBuildContext(compiler: Compiler, compilation: Compilation, loaderContext?: LoaderContext): UnpluginBuildContext { 8 | return { 9 | getNativeBuildContext() { 10 | return { 11 | framework: 'rspack', 12 | compiler, 13 | compilation, 14 | loaderContext, 15 | } 16 | }, 17 | addWatchFile(file) { 18 | const cwd = process.cwd() 19 | compilation.fileDependencies.add(resolve(cwd, file)) 20 | }, 21 | getWatchFiles() { 22 | return Array.from(compilation.fileDependencies) 23 | }, 24 | parse, 25 | emitFile(emittedFile) { 26 | const outFileName = emittedFile.fileName || emittedFile.name 27 | if (emittedFile.source && outFileName) { 28 | const { sources } = compilation.compiler.webpack 29 | compilation.emitAsset( 30 | outFileName, 31 | new sources.RawSource( 32 | typeof emittedFile.source === 'string' 33 | ? emittedFile.source 34 | : Buffer.from(emittedFile.source), 35 | ), 36 | ) 37 | } 38 | }, 39 | } 40 | } 41 | 42 | export function createContext(loader: LoaderContext): UnpluginContext { 43 | return { 44 | error: error => loader.emitError(normalizeMessage(error)), 45 | warn: message => loader.emitWarning(normalizeMessage(message)), 46 | } 47 | } 48 | 49 | export function normalizeMessage(error: string | UnpluginMessage): Error { 50 | const err = new Error(typeof error === 'string' ? error : error.message) 51 | if (typeof error === 'object') { 52 | err.stack = error.stack 53 | err.cause = error.meta 54 | } 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /src/rspack/loaders/load.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderContext } from '@rspack/core' 2 | import type { ResolvedUnpluginOptions } from '../../types' 3 | import { normalizeObjectHook } from '../../utils/filter' 4 | import { normalizeAbsolutePath } from '../../utils/webpack-like' 5 | import { createBuildContext, createContext } from '../context' 6 | import { decodeVirtualModuleId, isVirtualModuleId } from '../utils' 7 | 8 | export default async function load(this: LoaderContext, source: string, map: any): Promise { 9 | const callback = this.async() 10 | const { plugin } = this.query as { plugin: ResolvedUnpluginOptions } 11 | 12 | let id = this.resource 13 | if (!plugin?.load || !id) 14 | return callback(null, source, map) 15 | 16 | if (isVirtualModuleId(id, plugin)) 17 | id = decodeVirtualModuleId(id, plugin) 18 | 19 | const context = createContext(this) 20 | const { handler } = normalizeObjectHook('load', plugin.load) 21 | const res = await handler.call( 22 | Object.assign( 23 | {}, 24 | this._compilation && createBuildContext(this._compiler, this._compilation, this), 25 | context, 26 | ), 27 | normalizeAbsolutePath(id), 28 | ) 29 | 30 | if (res == null) 31 | callback(null, source, map) 32 | else if (typeof res !== 'string') 33 | callback(null, res.code, res.map ?? map) 34 | else 35 | callback(null, res, map) 36 | } 37 | -------------------------------------------------------------------------------- /src/rspack/loaders/transform.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderContext } from '@rspack/core' 2 | import type { ResolvedUnpluginOptions } from '../../types' 3 | import { normalizeObjectHook } from '../../utils/filter' 4 | import { createBuildContext, createContext } from '../context' 5 | 6 | export default async function transform( 7 | this: LoaderContext, 8 | source: string, 9 | map: any, 10 | ): Promise { 11 | const callback = this.async() 12 | const { plugin } = this.query as { plugin: ResolvedUnpluginOptions } 13 | if (!plugin?.transform) 14 | return callback(null, source, map) 15 | 16 | const id = this.resource 17 | const context = createContext(this) 18 | const { handler, filter } = normalizeObjectHook('transform', plugin.transform) 19 | if (!filter(this.resource, source)) 20 | return callback(null, source, map) 21 | 22 | try { 23 | const res = await handler.call( 24 | Object.assign( 25 | {}, 26 | this._compilation && createBuildContext(this._compiler, this._compilation, this), 27 | context, 28 | ), 29 | source, 30 | id, 31 | ) 32 | 33 | if (res == null) 34 | callback(null, source, map) 35 | else if (typeof res !== 'string') 36 | callback(null, res.code, map == null ? map : (res.map || map)) 37 | else callback(null, res, map) 38 | } 39 | catch (error) { 40 | if (error instanceof Error) { 41 | callback(error) 42 | } 43 | else { 44 | callback(new Error(String(error))) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/rspack/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Compiler } from '@rspack/core' 2 | import type { ResolvedUnpluginOptions } from '../types' 3 | import fs from 'node:fs' 4 | import { basename, dirname, resolve } from 'node:path' 5 | 6 | export function encodeVirtualModuleId(id: string, plugin: ResolvedUnpluginOptions): string { 7 | return resolve(plugin.__virtualModulePrefix, encodeURIComponent(id)) 8 | } 9 | 10 | export function decodeVirtualModuleId(encoded: string, _plugin: ResolvedUnpluginOptions): string { 11 | return decodeURIComponent(basename(encoded)) 12 | } 13 | 14 | export function isVirtualModuleId(encoded: string, plugin: ResolvedUnpluginOptions): boolean { 15 | return dirname(encoded) === plugin.__virtualModulePrefix 16 | } 17 | 18 | export class FakeVirtualModulesPlugin { 19 | name = 'FakeVirtualModulesPlugin' 20 | static counter = 0 21 | constructor(private plugin: ResolvedUnpluginOptions) {} 22 | 23 | apply(compiler: Compiler): void { 24 | FakeVirtualModulesPlugin.counter++ 25 | const dir = this.plugin.__virtualModulePrefix 26 | if (!fs.existsSync(dir)) { 27 | fs.mkdirSync(dir, { recursive: true }) 28 | } 29 | compiler.hooks.shutdown.tap(this.name, () => { 30 | if (--FakeVirtualModulesPlugin.counter === 0) { 31 | fs.rmSync(dir, { recursive: true, force: true }) 32 | } 33 | }) 34 | } 35 | 36 | async writeModule(file: string): Promise { 37 | const path = encodeVirtualModuleId(file, this.plugin) 38 | await fs.promises.writeFile(path, '') 39 | return path 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/unloader/index.ts: -------------------------------------------------------------------------------- 1 | import type { UnloaderPlugin, UnpluginContextMeta, UnpluginFactory, UnpluginInstance } from '../types' 2 | import { toRollupPlugin } from '../rollup' 3 | import { toArray } from '../utils/general' 4 | 5 | export function getUnloaderPlugin, Nested extends boolean = boolean>( 6 | factory: UnpluginFactory, 7 | ) { 8 | return ((userOptions?: UserOptions) => { 9 | const meta: UnpluginContextMeta = { 10 | framework: 'unloader', 11 | } 12 | const rawPlugins = toArray(factory(userOptions!, meta)) 13 | 14 | const plugins = rawPlugins.map((rawPlugin) => { 15 | const plugin = toRollupPlugin(rawPlugin, 'unloader') as UnloaderPlugin 16 | return plugin 17 | }) 18 | 19 | return plugins.length === 1 ? plugins[0] : plugins 20 | }) as UnpluginInstance['unloader'] 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/context.ts: -------------------------------------------------------------------------------- 1 | import type { Program } from 'acorn' 2 | import { Parser } from 'acorn' 3 | 4 | export function parse(code: string, opts: any = {}): Program { 5 | return Parser.parse(code, { 6 | sourceType: 'module', 7 | ecmaVersion: 'latest', 8 | locations: true, 9 | ...opts, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/filter.ts: -------------------------------------------------------------------------------- 1 | import type { Hook, HookFilter, StringFilter, StringOrRegExp } from '../types' 2 | import { resolve } from 'node:path' 3 | import picomatch from 'picomatch' 4 | import { toArray } from './general' 5 | 6 | const BACKSLASH_REGEX = /\\/g 7 | function normalize(path: string): string { 8 | return path.replace(BACKSLASH_REGEX, '/') 9 | } 10 | 11 | const ABSOLUTE_PATH_REGEX = /^(?:\/|(?:[A-Z]:)?[/\\|])/i 12 | function isAbsolute(path: string): boolean { 13 | return ABSOLUTE_PATH_REGEX.test(path) 14 | } 15 | 16 | export type PluginFilter = (input: string) => boolean 17 | export type TransformHookFilter = (id: string, code: string) => boolean 18 | 19 | interface NormalizedStringFilter { 20 | include?: StringOrRegExp[] 21 | exclude?: StringOrRegExp[] 22 | } 23 | 24 | function getMatcherString(glob: string, cwd: string) { 25 | if (glob.startsWith('**') || isAbsolute(glob)) { 26 | return normalize(glob) 27 | } 28 | 29 | const resolved = resolve(cwd, glob) 30 | return normalize(resolved) 31 | } 32 | 33 | function patternToIdFilter(pattern: StringOrRegExp): PluginFilter { 34 | if (pattern instanceof RegExp) { 35 | return (id: string) => { 36 | const normalizedId = normalize(id) 37 | const result = pattern.test(normalizedId) 38 | pattern.lastIndex = 0 39 | return result 40 | } 41 | } 42 | const cwd = process.cwd() 43 | const glob = getMatcherString(pattern, cwd) 44 | const matcher = picomatch(glob, { dot: true }) 45 | return (id: string) => { 46 | const normalizedId = normalize(id) 47 | return matcher(normalizedId) 48 | } 49 | } 50 | 51 | function patternToCodeFilter(pattern: StringOrRegExp): PluginFilter { 52 | if (pattern instanceof RegExp) { 53 | return (code: string) => { 54 | const result = pattern.test(code) 55 | pattern.lastIndex = 0 56 | return result 57 | } 58 | } 59 | return (code: string) => code.includes(pattern) 60 | } 61 | 62 | function createFilter( 63 | exclude: PluginFilter[] | undefined, 64 | include: PluginFilter[] | undefined, 65 | ): PluginFilter | undefined { 66 | if (!exclude && !include) { 67 | return 68 | } 69 | 70 | return (input) => { 71 | if (exclude?.some(filter => filter(input))) { 72 | return false 73 | } 74 | if (include?.some(filter => filter(input))) { 75 | return true 76 | } 77 | return !(include && include.length > 0) 78 | } 79 | } 80 | 81 | function normalizeFilter(filter: StringFilter): NormalizedStringFilter { 82 | if (typeof filter === 'string' || filter instanceof RegExp) { 83 | return { 84 | include: [filter], 85 | } 86 | } 87 | if (Array.isArray(filter)) { 88 | return { 89 | include: filter, 90 | } 91 | } 92 | return { 93 | exclude: filter.exclude ? toArray(filter.exclude) : undefined, 94 | include: filter.include ? toArray(filter.include) : undefined, 95 | } 96 | } 97 | 98 | function createIdFilter(filter: StringFilter | undefined): PluginFilter | undefined { 99 | if (!filter) 100 | return 101 | const { exclude, include } = normalizeFilter(filter) 102 | const excludeFilter = exclude?.map(patternToIdFilter) 103 | const includeFilter = include?.map(patternToIdFilter) 104 | return createFilter(excludeFilter, includeFilter) 105 | } 106 | 107 | function createCodeFilter(filter: StringFilter | undefined): PluginFilter | undefined { 108 | if (!filter) 109 | return 110 | const { exclude, include } = normalizeFilter(filter) 111 | const excludeFilter = exclude?.map(patternToCodeFilter) 112 | const includeFilter = include?.map(patternToCodeFilter) 113 | return createFilter(excludeFilter, includeFilter) 114 | } 115 | 116 | function createFilterForId(filter: StringFilter | undefined): PluginFilter | undefined { 117 | const filterFunction = createIdFilter(filter) 118 | return filterFunction ? id => !!filterFunction(id) : undefined 119 | } 120 | 121 | function createFilterForTransform( 122 | idFilter: StringFilter | undefined, 123 | codeFilter: StringFilter | undefined, 124 | ): TransformHookFilter | undefined { 125 | if (!idFilter && !codeFilter) 126 | return 127 | const idFilterFunction = createIdFilter(idFilter) 128 | const codeFilterFunction = createCodeFilter(codeFilter) 129 | return (id, code) => { 130 | let fallback = true 131 | if (idFilterFunction) { 132 | fallback &&= idFilterFunction(id) 133 | } 134 | if (!fallback) { 135 | return false 136 | } 137 | 138 | if (codeFilterFunction) { 139 | fallback &&= codeFilterFunction(code) 140 | } 141 | return fallback 142 | } 143 | } 144 | 145 | export function normalizeObjectHook any, F extends keyof HookFilter>( 146 | name: 'resolveId' | 'load', 147 | hook: Hook, 148 | ): { handler: T, filter: PluginFilter } 149 | export function normalizeObjectHook any, F extends keyof HookFilter>( 150 | name: 'transform', 151 | hook: Hook, 152 | ): { handler: T, filter: TransformHookFilter } 153 | export function normalizeObjectHook any, F extends keyof HookFilter>( 154 | name: 'resolveId' | 'load' | 'transform', 155 | hook: Hook, 156 | ): { 157 | handler: T 158 | filter: PluginFilter | TransformHookFilter 159 | } { 160 | let handler: T 161 | let filter: PluginFilter | TransformHookFilter | undefined 162 | 163 | if (typeof hook === 'function') { 164 | handler = hook 165 | } 166 | else { 167 | handler = hook.handler 168 | const hookFilter = hook.filter as HookFilter | undefined 169 | if (name === 'resolveId' || name === 'load') { 170 | filter = createFilterForId(hookFilter?.id) 171 | } 172 | else { 173 | filter = createFilterForTransform(hookFilter?.id, hookFilter?.code) 174 | } 175 | } 176 | 177 | return { 178 | handler, 179 | filter: filter || (() => true), 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/utils/general.ts: -------------------------------------------------------------------------------- 1 | import type { Arrayable, Nullable } from '../types' 2 | 3 | export function toArray(array?: Nullable>): Array { 4 | array = array || [] 5 | if (Array.isArray(array)) 6 | return array 7 | return [array] 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/webpack-like.ts: -------------------------------------------------------------------------------- 1 | import type { RuleSetUseItem } from '@rspack/core' 2 | import type { ResolvedUnpluginOptions } from '../types' 3 | import { isAbsolute, normalize } from 'node:path' 4 | import { normalizeObjectHook } from './filter' 5 | 6 | export function transformUse( 7 | data: { resource?: string, resourceQuery?: string }, 8 | plugin: ResolvedUnpluginOptions, 9 | transformLoader: string, 10 | ): RuleSetUseItem[] { 11 | if (data.resource == null) 12 | return [] 13 | 14 | const id = normalizeAbsolutePath(data.resource + (data.resourceQuery || '')) 15 | if (plugin.transformInclude && !plugin.transformInclude(id)) 16 | return [] 17 | 18 | const { filter } = normalizeObjectHook( 19 | // WARN: treat `transform` as `load` here, since cannot get `code` outside of `transform` 20 | // `code` should be checked in the loader 21 | 'load', 22 | plugin.transform!, 23 | ) 24 | if (!filter(id)) 25 | return [] 26 | 27 | return [ 28 | { 29 | loader: transformLoader, 30 | options: { plugin }, 31 | ident: plugin.name, 32 | }, 33 | ] 34 | } 35 | 36 | /** 37 | * Normalizes a given path when it's absolute. Normalizing means returning a new path by converting 38 | * the input path to the native os format. This is useful in cases where we want to normalize 39 | * the `id` argument of a hook. Any absolute ids should be in the default format 40 | * of the operating system. Any relative imports or node_module imports should remain 41 | * untouched. 42 | * 43 | * @param path - Path to normalize. 44 | * @returns a new normalized path. 45 | */ 46 | export function normalizeAbsolutePath(path: string): string { 47 | if (isAbsolute(path)) 48 | return normalize(path) 49 | else 50 | return path 51 | } 52 | -------------------------------------------------------------------------------- /src/vite/index.ts: -------------------------------------------------------------------------------- 1 | import type { UnpluginContextMeta, UnpluginFactory, UnpluginInstance, VitePlugin } from '../types' 2 | import { toRollupPlugin } from '../rollup' 3 | import { toArray } from '../utils/general' 4 | 5 | export function getVitePlugin, Nested extends boolean = boolean>( 6 | factory: UnpluginFactory, 7 | ) { 8 | return ((userOptions?: UserOptions) => { 9 | const meta: UnpluginContextMeta = { 10 | framework: 'vite', 11 | } 12 | const rawPlugins = toArray(factory(userOptions!, meta)) 13 | 14 | const plugins = rawPlugins.map((rawPlugin) => { 15 | const plugin = toRollupPlugin(rawPlugin, 'vite') as VitePlugin 16 | return plugin 17 | }) 18 | 19 | return plugins.length === 1 ? plugins[0] : plugins 20 | }) as UnpluginInstance['vite'] 21 | } 22 | -------------------------------------------------------------------------------- /src/webpack/context.ts: -------------------------------------------------------------------------------- 1 | import type { Compilation, Compiler, LoaderContext, sources } from 'webpack' 2 | import type { UnpluginBuildContext, UnpluginContext, UnpluginMessage } from '../types' 3 | import { Buffer } from 'node:buffer' 4 | import { createRequire } from 'node:module' 5 | import { resolve } from 'node:path' 6 | import process from 'node:process' 7 | import { parse } from '../utils/context' 8 | 9 | interface ContextOptions { 10 | addWatchFile: (file: string) => void 11 | getWatchFiles: () => string[] 12 | } 13 | 14 | export function contextOptionsFromCompilation(compilation: Compilation): ContextOptions { 15 | return { 16 | addWatchFile(file) { 17 | (compilation.fileDependencies ?? compilation.compilationDependencies).add(file) 18 | }, 19 | getWatchFiles() { 20 | return Array.from(compilation.fileDependencies ?? compilation.compilationDependencies) 21 | }, 22 | } 23 | } 24 | 25 | const require = createRequire(import.meta.url) 26 | export function getSource(fileSource: string | Uint8Array): sources.RawSource { 27 | const webpack = require('webpack') 28 | return new webpack.sources.RawSource( 29 | typeof fileSource === 'string' ? fileSource : Buffer.from(fileSource.buffer), 30 | ) 31 | } 32 | 33 | export function createBuildContext(options: ContextOptions, compiler: Compiler, compilation?: Compilation, loaderContext?: LoaderContext<{ unpluginName: string }>): UnpluginBuildContext { 34 | return { 35 | parse, 36 | addWatchFile(id) { 37 | options.addWatchFile(resolve(process.cwd(), id)) 38 | }, 39 | emitFile(emittedFile) { 40 | const outFileName = emittedFile.fileName || emittedFile.name 41 | if (emittedFile.source && outFileName) { 42 | if (!compilation) 43 | throw new Error('unplugin/webpack: emitFile outside supported hooks (buildStart, buildEnd, load, transform, watchChange)') 44 | compilation.emitAsset( 45 | outFileName, 46 | getSource(emittedFile.source), 47 | ) 48 | } 49 | }, 50 | getWatchFiles() { 51 | return options.getWatchFiles() 52 | }, 53 | getNativeBuildContext() { 54 | return { framework: 'webpack', compiler, compilation, loaderContext } 55 | }, 56 | } 57 | } 58 | 59 | export function createContext(loader: LoaderContext<{ unpluginName: string }>): UnpluginContext { 60 | return { 61 | error: error => loader.emitError(normalizeMessage(error)), 62 | warn: message => loader.emitWarning(normalizeMessage(message)), 63 | } 64 | } 65 | 66 | export function normalizeMessage(error: string | UnpluginMessage): Error { 67 | const err = new Error(typeof error === 'string' ? error : error.message) 68 | if (typeof error === 'object') { 69 | err.stack = error.stack 70 | err.cause = error.meta 71 | } 72 | return err 73 | } 74 | -------------------------------------------------------------------------------- /src/webpack/loaders/load.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderContext } from 'webpack' 2 | import type { ResolvedUnpluginOptions } from '../../types' 3 | import { normalizeObjectHook } from '../../utils/filter' 4 | import { normalizeAbsolutePath } from '../../utils/webpack-like' 5 | import { createBuildContext, createContext } from '../context' 6 | 7 | export default async function load(this: LoaderContext, source: string, map: any): Promise { 8 | const callback = this.async() 9 | const { plugin } = this.query as { plugin: ResolvedUnpluginOptions } 10 | let id = this.resource 11 | 12 | if (!plugin?.load || !id) 13 | return callback(null, source, map) 14 | 15 | if (id.startsWith(plugin.__virtualModulePrefix)) 16 | id = decodeURIComponent(id.slice(plugin.__virtualModulePrefix.length)) 17 | 18 | const context = createContext(this) 19 | const { handler } = normalizeObjectHook('load', plugin.load) 20 | const res = await handler.call( 21 | Object.assign({}, createBuildContext({ 22 | addWatchFile: (file) => { 23 | this.addDependency(file) 24 | }, 25 | getWatchFiles: () => { 26 | return this.getDependencies() 27 | }, 28 | }, this._compiler!, this._compilation, this), context), 29 | normalizeAbsolutePath(id), 30 | ) 31 | 32 | if (res == null) 33 | callback(null, source, map) 34 | else if (typeof res !== 'string') 35 | callback(null, res.code, res.map ?? map) 36 | else 37 | callback(null, res, map) 38 | } 39 | -------------------------------------------------------------------------------- /src/webpack/loaders/transform.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderContext } from 'webpack' 2 | import type { ResolvedUnpluginOptions } from '../../types' 3 | import { normalizeObjectHook } from '../../utils/filter' 4 | import { createBuildContext, createContext } from '../context' 5 | 6 | export default async function transform(this: LoaderContext, source: string, map: any): Promise { 7 | const callback = this.async() 8 | 9 | const { plugin } = this.query as { plugin: ResolvedUnpluginOptions } 10 | if (!plugin?.transform) 11 | return callback(null, source, map) 12 | 13 | const context = createContext(this) 14 | const { handler, filter } = normalizeObjectHook('transform', plugin.transform) 15 | if (!filter(this.resource, source)) 16 | return callback(null, source, map) 17 | 18 | try { 19 | const res = await handler.call( 20 | Object.assign({}, createBuildContext({ 21 | addWatchFile: (file) => { 22 | this.addDependency(file) 23 | }, 24 | getWatchFiles: () => { 25 | return this.getDependencies() 26 | }, 27 | }, this._compiler!, this._compilation, this), context), 28 | source, 29 | this.resource, 30 | ) 31 | 32 | if (res == null) 33 | callback(null, source, map) 34 | else if (typeof res !== 'string') 35 | callback(null, res.code, map == null ? map : (res.map || map)) 36 | else 37 | callback(null, res, map) 38 | } 39 | catch (error) { 40 | if (error instanceof Error) { 41 | callback(error) 42 | } 43 | else { 44 | callback(new Error(String(error))) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/fixtures/load/__test__/build.test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import fs from 'fs-extra' 3 | import { describe, expect, it } from 'vitest' 4 | 5 | const r = (...args: string[]) => resolve(__dirname, '../dist', ...args) 6 | 7 | describe('load-called-before-transform', () => { 8 | it('vite', async () => { 9 | const content = await fs.readFile(r('vite/main.js.mjs'), 'utf-8') 10 | expect(content).toContain('it is a msg -> through the load hook -> transform-[Injected Vite]') 11 | }) 12 | 13 | it('rollup', async () => { 14 | const content = await fs.readFile(r('rollup/main.js'), 'utf-8') 15 | 16 | expect(content).toContain('it is a msg -> through the load hook -> transform-[Injected Rollup]') 17 | }) 18 | 19 | it('webpack', async () => { 20 | const content = await fs.readFile(r('webpack/main.js'), 'utf-8') 21 | 22 | expect(content).toContain('it is a msg -> through the load hook -> transform-[Injected Webpack]') 23 | }) 24 | 25 | it('esbuild', async () => { 26 | const content = await fs.readFile(r('esbuild/main.js'), 'utf-8') 27 | expect(content).toContain('it is a msg -> through the load hook -> transform-[Injected Esbuild]') 28 | }) 29 | 30 | it('rspack', async () => { 31 | const content = await fs.readFile(r('rspack/main.js'), 'utf-8') 32 | 33 | expect(content).toContain('it is a msg -> through the load hook -> transform-[Injected Rspack]') 34 | }) 35 | 36 | it('farm', async () => { 37 | const content = await fs.readFile(r('farm/main.js'), 'utf-8') 38 | 39 | expect(content).toContain('it is a msg -> through the load hook -> transform-[Injected Farm]') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/fixtures/load/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 | -------------------------------------------------------------------------------- /test/fixtures/load/farm.config.js: -------------------------------------------------------------------------------- 1 | const { farm } = require('./unplugin') 2 | 3 | /** 4 | * @type {import('@farmfe/core').UserConfig} 5 | */ 6 | module.exports = { 7 | compilation: { 8 | persistentCache: false, 9 | input: { 10 | index: './src/main.js', 11 | }, 12 | presetEnv: false, 13 | output: { 14 | entryFilename: 'main.[ext]', 15 | path: './dist/farm', 16 | targetEnv: 'node', 17 | format: 'cjs', 18 | }, 19 | }, 20 | plugins: [ 21 | farm({ msg: 'Farm' }), 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/load/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/load/rspack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node:path') 2 | const { rspack } = require('./unplugin') 3 | 4 | /** @type {import('@rspack/core').Configuration} */ 5 | module.exports = { 6 | mode: 'development', 7 | entry: resolve(__dirname, 'src/main.js'), 8 | output: { 9 | path: resolve(__dirname, 'dist/rspack'), 10 | filename: 'main.js', 11 | }, 12 | plugins: [rspack({ msg: 'Rspack' })], 13 | devtool: 'source-map', 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/load/src/main.js: -------------------------------------------------------------------------------- 1 | import msg1 from './msg' 2 | 3 | console.log(msg1) 4 | -------------------------------------------------------------------------------- /test/fixtures/load/src/msg.js: -------------------------------------------------------------------------------- 1 | export default 'it is a msg' 2 | -------------------------------------------------------------------------------- /test/fixtures/load/unplugin.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs') 2 | const MagicString = require('magic-string') 3 | const { createUnplugin } = require('unplugin') 4 | 5 | const targetFileReg = /(?:\/|\\)msg\.js$/ 6 | module.exports = createUnplugin((options) => { 7 | return { 8 | name: 'load-called-before-transform', 9 | loadInclude(id) { 10 | return targetFileReg.test(id) 11 | }, 12 | load(id) { 13 | const code = fs.readFileSync(id, { encoding: 'utf-8' }) 14 | const str = new MagicString(code) 15 | const _index = code.indexOf('msg') 16 | const loadInjectedCode = 'msg -> through the load hook -> __unplugin__' 17 | 18 | str.overwrite(_index, _index + 'msg'.length, loadInjectedCode) 19 | return str.toString() 20 | }, 21 | transformInclude(id) { 22 | return targetFileReg.test(id) 23 | }, 24 | transform(code, id) { 25 | const s = new MagicString(code) 26 | const index = code.indexOf('__unplugin__') 27 | if (index === -1) 28 | return null 29 | 30 | const injectedCode = `transform-[Injected ${options.msg}]` 31 | 32 | if (code.includes(injectedCode)) 33 | throw new Error('File was already transformed') 34 | 35 | s.overwrite(index, index + '__unplugin__'.length, injectedCode) 36 | return { 37 | code: s.toString(), 38 | map: s.generateMap({ 39 | source: id, 40 | includeContent: true, 41 | }), 42 | } 43 | }, 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /test/fixtures/load/vite.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node: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/load/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node: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/__test__/build.test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import fs from 'fs-extra' 3 | import { describe, expect, it } 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.mjs'), 'utf-8') 10 | 11 | expect(content).toContain('NON-TARGET: __UNPLUGIN__') 12 | expect(content).toContain('TARGET: [Injected Post Vite]') 13 | expect(content).toContain('QUERY: [Injected Post Vite]') 14 | }) 15 | 16 | it('rollup', async () => { 17 | const content = await fs.readFile(r('rollup/main.js'), 'utf-8') 18 | 19 | expect(content).toContain('NON-TARGET: __UNPLUGIN__') 20 | expect(content).toContain('TARGET: [Injected Post Rollup]') 21 | }) 22 | 23 | it('webpack', async () => { 24 | const content = await fs.readFile(r('webpack/main.js'), 'utf-8') 25 | 26 | expect(content).toContain('NON-TARGET: __UNPLUGIN__') 27 | expect(content).toContain('TARGET: [Injected Post Webpack]') 28 | expect(content).toContain('QUERY: [Injected Post Webpack]') 29 | }) 30 | 31 | it('esbuild', async () => { 32 | const content = await fs.readFile(r('esbuild/main.js'), 'utf-8') 33 | 34 | expect(content).toContain('NON-TARGET: __UNPLUGIN__') 35 | expect(content).toContain('TARGET: [Injected Post Esbuild]') 36 | expect(content).toContain('QUERY: [Injected Post Esbuild]') 37 | }) 38 | 39 | it('rspack', async () => { 40 | const content = await fs.readFile(r('rspack/main.js'), 'utf-8') 41 | 42 | expect(content).toContain('NON-TARGET: __UNPLUGIN__') 43 | expect(content).toContain('TARGET: [Injected Post Rspack]') 44 | expect(content).toContain('QUERY: [Injected Post Rspack]') 45 | }) 46 | 47 | it('farm', async () => { 48 | const content = await fs.readFile(r('farm/main.js'), 'utf-8') 49 | 50 | expect(content).toContain('NON-TARGET: __UNPLUGIN__') 51 | expect(content).toContain('TARGET: [Injected Post Farm]') 52 | expect(content).toContain('QUERY: [Injected Post Farm]') 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/fixtures/transform/farm.config.js: -------------------------------------------------------------------------------- 1 | const { farm } = require('./unplugin') 2 | 3 | /** 4 | * @type {import('@farmfe/core').UserConfig} 5 | */ 6 | module.exports = { 7 | compilation: { 8 | persistentCache: false, 9 | input: { 10 | index: './src/main.js', 11 | }, 12 | output: { 13 | path: './dist/farm', 14 | entryFilename: 'main.[ext]', 15 | targetEnv: 'node', 16 | format: 'cjs', 17 | }, 18 | presetEnv: false, 19 | }, 20 | plugins: [ 21 | farm({ msg: 'Farm' }), 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /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/transform/rspack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node:path') 2 | const { rspack } = require('./unplugin') 3 | 4 | /** @type {import('@rspack/core').Configuration} */ 5 | module.exports = { 6 | mode: 'development', 7 | entry: resolve(__dirname, 'src/main.js'), 8 | output: { 9 | path: resolve(__dirname, 'dist/rspack'), 10 | filename: 'main.js', 11 | }, 12 | plugins: [rspack({ msg: 'Rspack' })], 13 | devtool: 'source-map', 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/transform/src/main.js: -------------------------------------------------------------------------------- 1 | import { msg1 } from './nontarget' 2 | import { msg3 } from './query?query-param=query-value' 3 | import { msg2 } from './target' 4 | 5 | console.log(msg1, msg2, msg3) 6 | -------------------------------------------------------------------------------- /test/fixtures/transform/src/nontarget.js: -------------------------------------------------------------------------------- 1 | export const msg1 = 'NON-TARGET: __UNPLUGIN__' 2 | -------------------------------------------------------------------------------- /test/fixtures/transform/src/query.js: -------------------------------------------------------------------------------- 1 | export const msg3 = 'QUERY: __UNPLUGIN__' 2 | -------------------------------------------------------------------------------- /test/fixtures/transform/src/target.js: -------------------------------------------------------------------------------- 1 | export const msg2 = 'TARGET: __UNPLUGIN__' 2 | -------------------------------------------------------------------------------- /test/fixtures/transform/unplugin.js: -------------------------------------------------------------------------------- 1 | const MagicString = require('magic-string') 2 | const { createUnplugin } = require('unplugin') 3 | 4 | module.exports = createUnplugin((options, meta) => { 5 | return [ 6 | { 7 | name: 'transform-fixture-pre', 8 | resolveId(id) { 9 | // Rollup doesn't know how to import module with query string so we ignore the module 10 | if (id.includes('?query-param=query-value') && meta.framework === 'rollup') { 11 | return { 12 | id, 13 | external: true, 14 | } 15 | } 16 | }, 17 | transformInclude(id) { 18 | return id.match(/[/\\]target\.js$/) || id.includes('?query-param=query-value') 19 | }, 20 | transform(code, id) { 21 | const s = new MagicString(code) 22 | const index = code.indexOf('__UNPLUGIN__') 23 | if (index === -1) 24 | return null 25 | 26 | const injectedCode = `[Injected ${options.msg}]` 27 | 28 | if (id.includes(injectedCode)) 29 | throw new Error('File was already transformed') 30 | 31 | s.overwrite(index, index + '__UNPLUGIN__'.length, injectedCode) 32 | 33 | return { 34 | code: s.toString(), 35 | map: s.generateMap({ 36 | source: id, 37 | includeContent: true, 38 | }), 39 | } 40 | }, 41 | }, 42 | { 43 | name: 'transform-fixture-post', 44 | transformInclude(id) { 45 | return id.match(/[/\\]target\.js$/) || id.includes('?query-param=query-value') 46 | }, 47 | transform(code, id) { 48 | if (!code.includes('Injected')) 49 | return null 50 | 51 | const s = new MagicString(code) 52 | s.replace( 53 | 'Injected', 54 | 'Injected Post', 55 | ) 56 | 57 | return { 58 | code: s.toString(), 59 | map: s.generateMap({ 60 | source: id, 61 | includeContent: true, 62 | }), 63 | } 64 | }, 65 | }, 66 | ] 67 | }) 68 | -------------------------------------------------------------------------------- /test/fixtures/transform/vite.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node: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/transform/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node: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/virtual-module/__test__/build.test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import fs from 'fs-extra' 3 | import { describe, expect, it } 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.mjs'), '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('rspack', async () => { 30 | const content = await fs.readFile(r('rspack/main.js'), 'utf-8') 31 | 32 | expect(content).toContain('VIRTUAL:ONE') 33 | expect(content).toContain('VIRTUAL:TWO') 34 | }) 35 | 36 | it('esbuild', async () => { 37 | const content = await fs.readFile(r('esbuild/main.js'), 'utf-8') 38 | 39 | expect(content).toContain('VIRTUAL:ONE') 40 | expect(content).toContain('VIRTUAL:TWO') 41 | }) 42 | 43 | it('farm', async () => { 44 | const content = await fs.readFile(r('farm/main.js'), 'utf-8') 45 | 46 | expect(content).toContain('VIRTUAL:ONE') 47 | expect(content).toContain('VIRTUAL:TWO') 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /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/virtual-module/farm.config.js: -------------------------------------------------------------------------------- 1 | const { farm } = require('./unplugin') 2 | 3 | /** 4 | * @type {import('@farmfe/core').UserConfig} 5 | */ 6 | module.exports = { 7 | compilation: { 8 | persistentCache: false, 9 | input: { 10 | index: './src/main.js', 11 | }, 12 | presetEnv: false, 13 | output: { 14 | entryFilename: 'main.[ext]', 15 | path: './dist/farm', 16 | targetEnv: 'node', 17 | format: 'cjs', 18 | }, 19 | }, 20 | plugins: [ 21 | farm(), 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /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/virtual-module/rspack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node:path') 2 | const { rspack } = require('./unplugin') 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: resolve(__dirname, 'src/main.js'), 7 | output: { 8 | path: resolve(__dirname, 'dist/rspack'), 9 | filename: 'main.js', 10 | }, 11 | plugins: [ 12 | rspack(), 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | loadInclude(id) { 10 | return id.startsWith('virtual/') 11 | }, 12 | load(id) { 13 | if (id === 'virtual/1') 14 | return 'export default "VIRTUAL:ONE"' 15 | 16 | else if (id === 'virtual/2') 17 | return 'export default "VIRTUAL:TWO"' 18 | 19 | else 20 | throw new Error(`Unexpected id: ${id}`) 21 | }, 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /test/fixtures/virtual-module/vite.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node: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/virtual-module/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node: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 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/unit-tests/esbuild/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer' 2 | import { describe, expect, it, vi } from 'vitest' 3 | import { 4 | combineSourcemaps, 5 | createPluginContext, 6 | fixSourceMap, 7 | guessLoader, 8 | processCodeWithSourceMap, 9 | unwrapLoader, 10 | } from '../../../src/esbuild/utils' 11 | 12 | describe('utils', () => { 13 | describe('guessLoader', () => { 14 | it('should return expected', () => { 15 | const actual = guessLoader('js', 'test.js') 16 | expect(actual).toEqual('js') 17 | }) 18 | }) 19 | 20 | describe('unwrapLoader', () => { 21 | it('when loader is Loader, should return expected', () => { 22 | const actual = unwrapLoader('base64', 'code', 'id') 23 | expect(actual).toEqual('base64') 24 | }) 25 | it('when loader is function, should return expected', () => { 26 | const loader = vi.fn().mockReturnValue('base64') 27 | const actual = unwrapLoader(loader, 'code', 'id') 28 | 29 | expect(loader).toHaveBeenCalledOnce() 30 | expect(loader).toHaveBeenCalledWith('code', 'id') 31 | expect(actual).toEqual('base64') 32 | }) 33 | }) 34 | 35 | describe('fixSourceMap', () => { 36 | it('when encodedSourceMap does not has toString() and toUrl(), should return expected', () => { 37 | const actual = fixSourceMap({ 38 | mappings: '', 39 | names: [], 40 | sources: [], 41 | version: 3, 42 | }) 43 | expect(actual.toString).toBeInstanceOf(Function) 44 | expect(actual.toUrl).toBeInstanceOf(Function) 45 | 46 | const actualString = actual.toString() 47 | expect(actualString).toEqual(JSON.stringify(actual)) 48 | 49 | const actualUrl = actual.toUrl() 50 | expect(actualUrl).toEqual( 51 | `data:application/json;charset=utf-8;base64,${Buffer.from(actualString).toString('base64')}`, 52 | ) 53 | }) 54 | }) 55 | 56 | describe('combineSourcemaps', () => { 57 | it('when combineSourcemaps is empty, should return expected', () => { 58 | const actual = combineSourcemaps('filename', []) 59 | expect(actual).toEqual({ 60 | names: [], 61 | sources: [], 62 | mappings: '', 63 | version: 3, 64 | }) 65 | }) 66 | 67 | it('when combineSourcemaps has sources, should return expected', () => { 68 | const actual = combineSourcemaps('filename', [ 69 | { 70 | names: [], 71 | sources: ['source1'], 72 | mappings: 'AAAA', 73 | version: 3, 74 | }, 75 | { 76 | names: [], 77 | sources: ['source2'], 78 | mappings: 'AAAA', 79 | version: 3, 80 | }, 81 | ]) 82 | expect(actual).toEqual({ 83 | names: [], 84 | ignoreList: [], 85 | sourceRoot: undefined, 86 | sources: ['source2'], 87 | mappings: 'AAAA', 88 | version: 3, 89 | }) 90 | }) 91 | 92 | it('when combineSourcemaps not use array interface, should return expected', () => { 93 | const actual = combineSourcemaps('filename', [ 94 | { 95 | names: [], 96 | sources: ['source1', 'source2'], 97 | mappings: 'AAAA', 98 | version: 3, 99 | }, 100 | { 101 | names: [], 102 | sources: [], 103 | mappings: '', 104 | version: 3, 105 | }, 106 | ]) 107 | expect(actual).toEqual({ 108 | ignoreList: [], 109 | sourceRoot: undefined, 110 | names: [], 111 | sources: [], 112 | mappings: '', 113 | version: 3, 114 | }) 115 | }) 116 | }) 117 | 118 | describe('createBuildContext', async () => { 119 | it('should return expected', async () => { 120 | const { createBuildContext } = await import('../../../src/esbuild/utils') 121 | const actual = createBuildContext({ initialOptions: { outdir: '/path/to' } } as any) 122 | expect(actual.parse).toBeInstanceOf(Function) 123 | expect(actual.emitFile).toBeInstanceOf(Function) 124 | expect(actual.addWatchFile).toBeInstanceOf(Function) 125 | expect(actual.getNativeBuildContext).toBeInstanceOf(Function) 126 | 127 | expect(actual.getNativeBuildContext!()).toEqual({ 128 | framework: 'esbuild', 129 | build: { initialOptions: { outdir: '/path/to' } }, 130 | }) 131 | expect(() => actual.addWatchFile('id')).toThrow( 132 | 'unplugin/esbuild: addWatchFile outside supported hooks (resolveId, load, transform)', 133 | ) 134 | }) 135 | }) 136 | 137 | describe('createPluginContext', () => { 138 | it('should return expected', () => { 139 | const watchFiles: any = [] 140 | const actual = createPluginContext({ getWatchFiles: () => watchFiles } as any) 141 | expect(actual.errors).toBeInstanceOf(Array) 142 | expect(actual.warnings).toBeInstanceOf(Array) 143 | expect(actual.mixedContext).toBeInstanceOf(Object) 144 | expect(actual.mixedContext.addWatchFile).toBeInstanceOf(Function) 145 | expect(actual.mixedContext.error).toBeInstanceOf(Function) 146 | expect(actual.mixedContext.warn).toBeInstanceOf(Function) 147 | 148 | actual.mixedContext.addWatchFile('id') 149 | expect(watchFiles).toContain('id') 150 | 151 | actual.mixedContext.error('error') 152 | expect(actual.errors).toHaveLength(1) 153 | expect(actual.errors[0].text).toEqual('error') 154 | actual.mixedContext.warn('warn') 155 | expect(actual.warnings).toHaveLength(1) 156 | expect(actual.warnings[0].text).toEqual('warn') 157 | 158 | actual.mixedContext.error({ 159 | id: '1', 160 | message: 'message', 161 | stack: 'stack', 162 | code: 'code', 163 | plugin: 'plugin', 164 | loc: { 165 | column: 2, 166 | file: 'file', 167 | line: 2, 168 | }, 169 | meta: 'meta', 170 | }) 171 | expect(actual.errors).toHaveLength(2) 172 | expect(actual.errors[1]).toEqual({ 173 | id: '1', 174 | pluginName: 'plugin', 175 | text: 'message', 176 | location: { 177 | file: 'file', 178 | line: 2, 179 | column: 2, 180 | }, 181 | detail: 'meta', 182 | notes: [], 183 | }) 184 | 185 | actual.mixedContext.warn({ 186 | id: '2', 187 | message: 'message', 188 | stack: 'stack', 189 | code: 'code', 190 | plugin: 'plugin', 191 | meta: 'meta', 192 | }) 193 | expect(actual.warnings).toHaveLength(2) 194 | expect(actual.warnings[1]).toEqual({ 195 | id: '2', 196 | pluginName: 'plugin', 197 | text: 'message', 198 | location: null, 199 | detail: 'meta', 200 | notes: [], 201 | }) 202 | }) 203 | }) 204 | 205 | describe('processCodeWithSourceMap', () => { 206 | it('when map is null, should return expected', () => { 207 | const actual = processCodeWithSourceMap(null, 'code') 208 | expect(actual).toEqual('code') 209 | }) 210 | 211 | it('when map is not null, should return expected', () => { 212 | const actual = processCodeWithSourceMap({ file: 'file', names: ['name'], sources: ['source'], sourcesContent: ['content'], version: 0 } as any, 'code') 213 | expect(actual).toEqual('code\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJmaWxlIjoiZmlsZSIsIm5hbWVzIjpbIm5hbWUiXSwic291cmNlcyI6WyJzb3VyY2UiXSwic291cmNlc0NvbnRlbnQiOlsiY29udGVudCJdLCJ2ZXJzaW9uIjowfQ==') 214 | }) 215 | }) 216 | }) 217 | -------------------------------------------------------------------------------- /test/unit-tests/farm/context.test.ts: -------------------------------------------------------------------------------- 1 | import type { CompilationContext } from '@farmfe/core' 2 | import { describe, expect, it, vi } from 'vitest' 3 | import { createFarmContext, unpluginContext } from '../../../src/farm/context' 4 | 5 | describe('createFarmContext', () => { 6 | it('should create a valid farm context with parse function', () => { 7 | const mockContext = { 8 | addWatchFile: vi.fn(), 9 | emitFile: vi.fn(), 10 | getWatchFiles: vi.fn().mockReturnValue(['file1', 'file2']), 11 | } as unknown as CompilationContext 12 | 13 | const farmContext = createFarmContext(mockContext) 14 | 15 | expect(farmContext.parse).toBeDefined() 16 | expect(farmContext.parse).toBeInstanceOf(Function) 17 | }) 18 | 19 | it('should add a watch file', () => { 20 | const mockContext = { 21 | addWatchFile: vi.fn(), 22 | } as unknown as CompilationContext 23 | 24 | const farmContext = createFarmContext(mockContext) 25 | farmContext.addWatchFile('test-file') 26 | 27 | expect(mockContext.addWatchFile).toHaveBeenCalledWith('test-file', 'test-file') 28 | }) 29 | 30 | it('should emit a file', () => { 31 | const mockContext = { 32 | emitFile: vi.fn(), 33 | } as unknown as CompilationContext 34 | 35 | const farmContext = createFarmContext(mockContext) 36 | farmContext.emitFile({ 37 | fileName: 'test-file.js', 38 | source: 'console.log("test")', 39 | } as any) 40 | 41 | expect(mockContext.emitFile).toHaveBeenCalledWith({ 42 | resolvedPath: 'test-file.js', 43 | name: 'test-file.js', 44 | content: expect.any(Array), 45 | resourceType: '.js', 46 | }) 47 | }) 48 | 49 | it('should emit a file by name', () => { 50 | const mockContext = { 51 | emitFile: vi.fn(), 52 | } as unknown as CompilationContext 53 | 54 | const farmContext = createFarmContext(mockContext) 55 | farmContext.emitFile({ 56 | name: 'test-file.js', 57 | source: 'console.log("test")', 58 | } as any) 59 | 60 | expect(mockContext.emitFile).toHaveBeenCalledWith({ 61 | resolvedPath: 'test-file.js', 62 | name: 'test-file.js', 63 | content: expect.any(Array), 64 | resourceType: '.js', 65 | }) 66 | }) 67 | 68 | it('should get watch files', () => { 69 | const mockContext = { 70 | getWatchFiles: vi.fn().mockReturnValue(['file1', 'file2']), 71 | } as unknown as CompilationContext 72 | 73 | const farmContext = createFarmContext(mockContext) 74 | const watchFiles = farmContext.getWatchFiles() 75 | 76 | expect(watchFiles).toEqual(['file1', 'file2']) 77 | }) 78 | 79 | it('should return native build context', () => { 80 | const mockContext = {} as CompilationContext 81 | 82 | const farmContext = createFarmContext(mockContext) 83 | const nativeBuildContext = farmContext.getNativeBuildContext!() 84 | 85 | expect(nativeBuildContext).toEqual({ framework: 'farm', context: mockContext }) 86 | }) 87 | }) 88 | 89 | describe('unpluginContext', () => { 90 | it('should call context.error with an Error object', () => { 91 | const mockContext = { 92 | error: vi.fn(), 93 | } as unknown as CompilationContext 94 | 95 | const pluginContext = unpluginContext(mockContext) 96 | pluginContext.error(new Error('Test error')) 97 | 98 | expect(mockContext.error).toHaveBeenCalledWith(new Error('Test error')) 99 | }) 100 | 101 | it('should call context.error with an Error String', () => { 102 | const mockContext = { 103 | error: vi.fn(), 104 | } as unknown as CompilationContext 105 | 106 | const pluginContext = unpluginContext(mockContext) 107 | pluginContext.error('Test error') 108 | 109 | expect(mockContext.error).toHaveBeenCalledWith(new Error('Test error')) 110 | }) 111 | 112 | it('should call context.warn with an Error object', () => { 113 | const mockContext = { 114 | warn: vi.fn(), 115 | } as unknown as CompilationContext 116 | 117 | const pluginContext = unpluginContext(mockContext) 118 | pluginContext.warn(new Error('Test warning')) 119 | 120 | expect(mockContext.warn).toHaveBeenCalledWith(new Error('Test warning')) 121 | }) 122 | 123 | it('should call context.warn with an Error String', () => { 124 | const mockContext = { 125 | warn: vi.fn(), 126 | } as unknown as CompilationContext 127 | 128 | const pluginContext = unpluginContext(mockContext) 129 | pluginContext.warn('Test warning') 130 | 131 | expect(mockContext.warn).toHaveBeenCalledWith(new Error('Test warning')) 132 | }) 133 | }) 134 | -------------------------------------------------------------------------------- /test/unit-tests/farm/index.test.ts: -------------------------------------------------------------------------------- 1 | import type { UnpluginOptions } from '../../../src/types' 2 | import { describe, expect, it, vi } from 'vitest' 3 | import { getFarmPlugin, toFarmPlugin } from '../../../src/farm/index' 4 | 5 | describe('getFarmPlugin', () => { 6 | it('should return a single plugin when factory returns one plugin', () => { 7 | const mockFactory = vi.fn(() => ({ 8 | name: 'test-plugin', 9 | })) 10 | 11 | const plugin = getFarmPlugin(mockFactory as any) 12 | 13 | expect(plugin).toBeDefined() 14 | }) 15 | 16 | it('should return an array of plugins when factory returns multiple plugins', () => { 17 | const mockFactory = vi.fn().mockReturnValue([ 18 | { name: 'test-plugin-1', farm: true }, 19 | { name: 'test-plugin-2', farm: true }, 20 | ]) 21 | 22 | const func = getFarmPlugin(mockFactory as any) 23 | const plugins: any = func({}) 24 | 25 | expect(plugins).toBeDefined() 26 | expect(plugins).toHaveLength(2) 27 | expect(plugins[0]).toHaveProperty('name', 'test-plugin-1') 28 | expect(plugins[1]).toHaveProperty('name', 'test-plugin-2') 29 | }) 30 | }) 31 | 32 | describe('toFarmPlugin', () => { 33 | it('should convert a basic plugin to a Farm plugin', () => { 34 | const plugin: UnpluginOptions = { 35 | name: 'test-plugin', 36 | } 37 | 38 | const farmPlugin = toFarmPlugin(plugin) 39 | 40 | expect(farmPlugin).toBeDefined() 41 | expect(farmPlugin).toHaveProperty('name', 'test-plugin') 42 | }) 43 | 44 | it('should handle buildStart hook', async () => { 45 | const buildStartMock = vi.fn() 46 | const plugin: UnpluginOptions = { 47 | name: 'test-plugin', 48 | buildStart: buildStartMock, 49 | } 50 | 51 | const farmPlugin = toFarmPlugin(plugin) 52 | 53 | expect(farmPlugin.buildStart).toBeDefined() 54 | await farmPlugin.buildStart?.executor({}, {} as any) 55 | 56 | expect(buildStartMock).toHaveBeenCalled() 57 | }) 58 | 59 | it('should handle resolveId hook', async () => { 60 | const resolveIdMock = vi.fn(() => 'resolved-id') 61 | const plugin: UnpluginOptions = { 62 | name: 'test-plugin', 63 | resolveId: resolveIdMock, 64 | } 65 | 66 | const farmPlugin = toFarmPlugin(plugin) 67 | 68 | expect(farmPlugin.resolve).toBeDefined() 69 | const result = await farmPlugin.resolve?.executor( 70 | { source: 'test-source', importer: 'test-importer' } as any, 71 | {} as any, 72 | ) 73 | 74 | expect(resolveIdMock).toHaveBeenCalled() 75 | expect(result).toHaveProperty('resolvedPath', 'resolved-id') 76 | }) 77 | 78 | it('should handle load hook', async () => { 79 | const loadMock = vi.fn(() => ({ code: 'test-content' })) 80 | const plugin: UnpluginOptions = { 81 | name: 'test-plugin', 82 | load: loadMock, 83 | } 84 | 85 | const farmPlugin = toFarmPlugin(plugin) 86 | 87 | expect(farmPlugin.load).toBeDefined() 88 | const result = await farmPlugin.load?.executor( 89 | { resolvedPath: 'test-path', query: [['', '']] } as any, 90 | {} as any, 91 | ) 92 | 93 | expect(loadMock).toHaveBeenCalled() 94 | expect(result).toHaveProperty('content', 'test-content') 95 | }) 96 | 97 | it('should handle transform hook', async () => { 98 | const transformMock = vi.fn(() => ({ code: 'transformed-content' })) 99 | const plugin: UnpluginOptions = { 100 | name: 'test-plugin', 101 | transform: transformMock, 102 | } 103 | 104 | const farmPlugin = toFarmPlugin(plugin) 105 | 106 | expect(farmPlugin.transform).toBeDefined() 107 | const result = await farmPlugin.transform?.executor( 108 | { resolvedPath: 'test-path', content: 'original-content', query: [['', '']] } as any, 109 | {} as any, 110 | ) 111 | 112 | expect(transformMock).toHaveBeenCalled() 113 | expect(result).toHaveProperty('content', 'transformed-content') 114 | }) 115 | 116 | it('should handle watchChange hook', async () => { 117 | const watchChangeMock = vi.fn() 118 | const plugin: UnpluginOptions = { 119 | name: 'test-plugin', 120 | watchChange: watchChangeMock, 121 | } 122 | 123 | const farmPlugin = toFarmPlugin(plugin) 124 | 125 | expect(farmPlugin.updateModules).toBeDefined() 126 | await farmPlugin.updateModules?.executor( 127 | { paths: [['test-path', 'change']] }, 128 | {} as any, 129 | ) 130 | 131 | expect(watchChangeMock).toHaveBeenCalled() 132 | }) 133 | 134 | it('should handle buildEnd hook', async () => { 135 | const buildEndMock = vi.fn() 136 | const plugin: UnpluginOptions = { 137 | name: 'test-plugin', 138 | buildEnd: buildEndMock, 139 | } 140 | 141 | const farmPlugin = toFarmPlugin(plugin) 142 | 143 | expect(farmPlugin.buildEnd).toBeDefined() 144 | await farmPlugin.buildEnd?.executor({}, {} as any) 145 | 146 | expect(buildEndMock).toHaveBeenCalled() 147 | }) 148 | 149 | it('should handle farm-specific properties in plugins', () => { 150 | const plugin = { 151 | name: 'test-plugin', 152 | farm: { 153 | customProperty: 'custom-value', 154 | }, 155 | } 156 | 157 | const farmPlugin = toFarmPlugin(plugin as any) 158 | 159 | expect(farmPlugin).toHaveProperty('customProperty', 'custom-value') 160 | }) 161 | 162 | it('should handle filters in resolveId hook', async () => { 163 | const resolveIdMock = vi.fn(() => 'resolved-id') 164 | const plugin: UnpluginOptions = { 165 | name: 'test-plugin', 166 | resolveId: resolveIdMock, 167 | } 168 | 169 | const farmPlugin = toFarmPlugin(plugin, { filters: ['custom-filter'] }) 170 | 171 | expect(farmPlugin.resolve).toBeDefined() 172 | expect(farmPlugin.resolve?.filters.sources).toContain('custom-filter') 173 | }) 174 | 175 | it('should handle isEntry in resolveId hook', async () => { 176 | const resolveIdMock = vi.fn(() => 'resolved-id') 177 | const plugin: UnpluginOptions = { 178 | name: 'test-plugin', 179 | resolveId: resolveIdMock, 180 | } 181 | 182 | const farmPlugin = toFarmPlugin(plugin) 183 | 184 | const result = await farmPlugin.resolve?.executor( 185 | { source: 'test-source', importer: 'test-importer', kind: { entry: 'index' } } as any, 186 | {} as any, 187 | ) 188 | 189 | expect(resolveIdMock).toHaveBeenCalledWith( 190 | 'test-source', 191 | expect.anything(), 192 | expect.objectContaining({ isEntry: true }), 193 | ) 194 | expect(result).toHaveProperty('resolvedPath', 'resolved-id') 195 | }) 196 | }) 197 | -------------------------------------------------------------------------------- /test/unit-tests/farm/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { 3 | appendQuery, 4 | convertEnforceToPriority, 5 | convertWatchEventChange, 6 | customParseQueryString, 7 | decodeStr, 8 | encodeStr, 9 | formatLoadModuleType, 10 | formatTransformModuleType, 11 | getContentValue, 12 | getCssModuleType, 13 | getJsModuleType, 14 | guessIdLoader, 15 | isObject, 16 | isStartsWithSlash, 17 | isString, 18 | removeQuery, 19 | stringifyQuery, 20 | transformQuery, 21 | } from '../../../src/farm/utils' 22 | 23 | describe('utils.ts', () => { 24 | it('guessIdLoader should return correct loader based on file extension', () => { 25 | expect(guessIdLoader('file.js')).toBe('js') 26 | expect(guessIdLoader('file.ts')).toBe('ts') 27 | expect(guessIdLoader('file.unknown')).toBe('js') 28 | }) 29 | 30 | it('transformQuery should append query string to resolvedPath', () => { 31 | const context = { 32 | query: [['key', 'value']], 33 | resolvedPath: '/path/to/file', 34 | } 35 | transformQuery(context) 36 | expect(context.resolvedPath).toBe('/path/to/file?key=value') 37 | }) 38 | 39 | it('convertEnforceToPriority should return correct priority', () => { 40 | expect(convertEnforceToPriority('pre')).toBe(102) 41 | expect(convertEnforceToPriority('post')).toBe(98) 42 | expect(convertEnforceToPriority(undefined)).toBe(100) 43 | }) 44 | 45 | it('convertWatchEventChange should map events correctly when Added', () => { 46 | const actual = convertWatchEventChange('Added' as any) 47 | expect(actual).toBe('create') 48 | }) 49 | 50 | it('convertWatchEventChange should map events correctly when Updated', () => { 51 | const actual = convertWatchEventChange('Updated' as any) 52 | expect(actual).toBe('update') 53 | }) 54 | 55 | it('convertWatchEventChange should map events correctly when Removed', () => { 56 | const actual = convertWatchEventChange('Removed' as any) 57 | expect(actual).toBe('delete') 58 | }) 59 | 60 | it('isString should correctly identify strings', () => { 61 | expect(isString('test')).toBe(true) 62 | expect(isString(123)).toBe(false) 63 | }) 64 | 65 | it('isObject should correctly identify objects', () => { 66 | expect(isObject({})).toBe(true) 67 | expect(isObject(null)).toBe(false) 68 | expect(isObject('string')).toBe(false) 69 | }) 70 | 71 | it('customParseQueryString should parse query strings correctly', () => { 72 | expect(customParseQueryString('http://example.com?key=value')).toEqual([['key', 'value']]) 73 | expect(customParseQueryString(null)).toEqual([]) 74 | }) 75 | 76 | it('encodeStr should encode null characters', () => { 77 | expect(encodeStr('hello\0world')).toBe('hello\\0world') 78 | expect(encodeStr('hello')).toBe('hello') 79 | }) 80 | 81 | it('decodeStr should decode null characters', () => { 82 | expect(decodeStr('hello\\0world')).toBe('hello\0world') 83 | expect(decodeStr('hello')).toBe('hello') 84 | }) 85 | 86 | it('getContentValue should return encoded content', () => { 87 | expect(getContentValue('test')).toBe('test') 88 | expect(getContentValue({ code: 'test' })).toBe('test') 89 | expect(() => getContentValue(null)).toThrow('Content cannot be null or undefined') 90 | }) 91 | 92 | it('removeQuery should remove query string from path', () => { 93 | expect(removeQuery('/path/to/file?query=1')).toBe('/path/to/file') 94 | expect(removeQuery('/path/to/file')).toBe('/path/to/file') 95 | }) 96 | 97 | it('isStartsWithSlash should check if string starts with a slash', () => { 98 | expect(isStartsWithSlash('/path')).toBe(true) 99 | expect(isStartsWithSlash('path')).toBe(false) 100 | }) 101 | 102 | it('appendQuery should append query to id', () => { 103 | expect(appendQuery('id', [['key', 'value']])).toBe('id?key=value') 104 | expect(appendQuery('id', [])).toBe('id') 105 | }) 106 | 107 | it('stringifyQuery should convert query array to string', () => { 108 | expect(stringifyQuery([['key', 'value']])).toBe('key=value') 109 | expect(stringifyQuery([])).toBe('') 110 | }) 111 | 112 | it('getCssModuleType should return correct CSS module type', () => { 113 | expect(getCssModuleType('file.less')).toBe('less') 114 | expect(getCssModuleType('file.unknown')).toBe(null) 115 | }) 116 | 117 | it('getJsModuleType should return correct JS module type', () => { 118 | expect(getJsModuleType('file.js')).toBe('js') 119 | expect(getJsModuleType('file.unknown')).toBe(null) 120 | }) 121 | 122 | it('formatLoadModuleType should return correct module type', () => { 123 | expect(formatLoadModuleType('file.css')).toBe('css') 124 | expect(formatLoadModuleType('file.js')).toBe('js') 125 | expect(formatLoadModuleType('file.unknown')).toBe('js') 126 | }) 127 | 128 | it('formatTransformModuleType should return correct module type', () => { 129 | expect(formatTransformModuleType('file.css')).toBe('css') 130 | expect(formatTransformModuleType('file.js')).toBe('js') 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /test/unit-tests/filter/filter.test.ts: -------------------------------------------------------------------------------- 1 | import type { UnpluginOptions, VitePlugin } from 'unplugin' 2 | import type { Mock } from 'vitest' 3 | import * as path from 'node:path' 4 | import { createUnplugin } from 'unplugin' 5 | import { afterEach, describe, expect, it, vi } from 'vitest' 6 | import { build, toArray } from '../utils' 7 | 8 | function createUnpluginWithHooks( 9 | resolveId: UnpluginOptions['resolveId'], 10 | load: UnpluginOptions['load'], 11 | transform: UnpluginOptions['transform'], 12 | ) { 13 | return createUnplugin(() => ({ 14 | name: 'test-plugin', 15 | resolveId, 16 | load, 17 | transform, 18 | })) 19 | } 20 | 21 | function createIdHook() { 22 | const handler = vi.fn() 23 | return { 24 | hook: { 25 | filter: { 26 | id: { include: [/\.js$/], exclude: ['**/entry.js', /not-expect/] }, 27 | }, 28 | handler, 29 | }, 30 | handler, 31 | } 32 | } 33 | 34 | function createTransformHook() { 35 | const handler = vi.fn() 36 | return { 37 | hook: { 38 | filter: { 39 | id: { include: [/\.js$/], exclude: ['**/entry.js', /not-expect/] }, 40 | code: { include: '42' }, 41 | }, 42 | handler, 43 | }, 44 | handler, 45 | } 46 | } 47 | 48 | function check(resolveIdHandler: Mock, loadHandler: Mock, transformHandler: Mock): void { 49 | expect(resolveIdHandler).toBeCalledTimes(1) 50 | expect(loadHandler).toBeCalledTimes(1) 51 | expect(transformHandler).toBeCalledTimes(1) 52 | 53 | const testName = expect.getState().currentTestName 54 | const hasExtraOptions = testName?.includes('vite') || testName?.includes('rolldown') 55 | 56 | expect(transformHandler).lastCalledWith( 57 | expect.stringMatching('export default 42'), 58 | expect.stringMatching(/\bmod\.js$/), 59 | ...hasExtraOptions ? [expect.anything()] : [], 60 | ) 61 | } 62 | 63 | describe('filter', () => { 64 | afterEach(() => { 65 | vi.restoreAllMocks() 66 | }) 67 | 68 | it('vite', async () => { 69 | const { hook: resolveId, handler: resolveIdHandler } = createIdHook() 70 | const { hook: load, handler: loadHandler } = createIdHook() 71 | const { hook: transform, handler: transformHandler } = createTransformHook() 72 | const plugin = createUnpluginWithHooks(resolveId, load, transform).vite 73 | // we need to define `enforce` here for the plugin to be run 74 | const plugins = toArray(plugin()).map((plugin): VitePlugin => ({ ...plugin, enforce: 'pre' })) 75 | 76 | await build.vite({ 77 | clearScreen: false, 78 | plugins: [plugins], 79 | build: { 80 | lib: { 81 | entry: path.resolve(__dirname, 'test-src/entry.js'), 82 | name: 'TestLib', 83 | }, 84 | write: false, // don't output anything 85 | }, 86 | }) 87 | 88 | check(resolveIdHandler, loadHandler, transformHandler) 89 | }) 90 | 91 | it('rollup', async () => { 92 | const { hook: resolveId, handler: resolveIdHandler } = createIdHook() 93 | const { hook: load, handler: loadHandler } = createIdHook() 94 | const { hook: transform, handler: transformHandler } = createTransformHook() 95 | const plugin = createUnpluginWithHooks(resolveId, load, transform).rollup 96 | 97 | await build.rollup({ 98 | input: path.resolve(__dirname, 'test-src/entry.js'), 99 | plugins: [plugin()], 100 | }) 101 | 102 | check(resolveIdHandler, loadHandler, transformHandler) 103 | }) 104 | 105 | it('rolldown', async () => { 106 | const { hook: resolveId, handler: resolveIdHandler } = createIdHook() 107 | const { hook: load, handler: loadHandler } = createIdHook() 108 | const { hook: transform, handler: transformHandler } = createTransformHook() 109 | const plugin = createUnpluginWithHooks(resolveId, load, transform).rolldown 110 | 111 | await build.rolldown({ 112 | input: path.resolve(__dirname, 'test-src/entry.js'), 113 | plugins: [plugin()], 114 | }) 115 | 116 | check(resolveIdHandler, loadHandler, transformHandler) 117 | }) 118 | 119 | it('webpack', async () => { 120 | const { hook: resolveId, handler: resolveIdHandler } = createIdHook() 121 | const { hook: load, handler: loadHandler } = createIdHook() 122 | const { hook: transform, handler: transformHandler } = createTransformHook() 123 | const plugin = createUnpluginWithHooks(resolveId, load, transform).webpack 124 | 125 | await new Promise((resolve) => { 126 | build.webpack( 127 | { 128 | entry: path.resolve(__dirname, 'test-src/entry.js'), 129 | plugins: [plugin()], 130 | }, 131 | resolve, 132 | ) 133 | }) 134 | 135 | check(resolveIdHandler, loadHandler, transformHandler) 136 | }) 137 | 138 | it('rspack', async () => { 139 | const { hook: resolveId, handler: resolveIdHandler } = createIdHook() 140 | const { hook: load, handler: loadHandler } = createIdHook() 141 | const { hook: transform, handler: transformHandler } = createTransformHook() 142 | const plugin = createUnpluginWithHooks(resolveId, load, transform).rspack 143 | 144 | await new Promise((resolve) => { 145 | build.rspack( 146 | { 147 | entry: path.resolve(__dirname, 'test-src/entry.js'), 148 | plugins: [plugin()], 149 | }, 150 | resolve, 151 | ) 152 | }) 153 | 154 | check(resolveIdHandler, loadHandler, transformHandler) 155 | }) 156 | 157 | it('esbuild', async () => { 158 | const { hook: resolveId, handler: resolveIdHandler } = createIdHook() 159 | const { hook: load, handler: loadHandler } = createIdHook() 160 | const { hook: transform, handler: transformHandler } = createTransformHook() 161 | const plugin = createUnpluginWithHooks(resolveId, load, transform).esbuild 162 | 163 | await build.esbuild({ 164 | entryPoints: [path.resolve(__dirname, 'test-src/entry.js')], 165 | plugins: [plugin()], 166 | bundle: true, // actually traverse imports 167 | write: false, // don't pollute console 168 | }) 169 | 170 | check(resolveIdHandler, loadHandler, transformHandler) 171 | }) 172 | }) 173 | -------------------------------------------------------------------------------- /test/unit-tests/filter/test-src/entry.js: -------------------------------------------------------------------------------- 1 | import mod from './mod.js' 2 | import val from './not-expect.js' 3 | 4 | export const hello = mod 5 | export default val 6 | -------------------------------------------------------------------------------- /test/unit-tests/filter/test-src/mod.js: -------------------------------------------------------------------------------- 1 | export default 42 2 | -------------------------------------------------------------------------------- /test/unit-tests/filter/test-src/not-expect.js: -------------------------------------------------------------------------------- 1 | export default 'foo' 2 | -------------------------------------------------------------------------------- /test/unit-tests/id-consistency/id-consistency.test.ts: -------------------------------------------------------------------------------- 1 | import type { UnpluginOptions, VitePlugin } from 'unplugin' 2 | import type { Mock } from 'vitest' 3 | import * as path from 'node:path' 4 | import { createUnplugin } from 'unplugin' 5 | import { afterEach, describe, expect, it, vi } from 'vitest' 6 | import { build, toArray } from '../utils' 7 | 8 | const entryFilePath = path.resolve(__dirname, './test-src/entry.js') 9 | const externals = ['node:path'] 10 | 11 | function createUnpluginWithCallback( 12 | resolveIdCallback: UnpluginOptions['resolveId'], 13 | transformIncludeCallback: UnpluginOptions['transformInclude'], 14 | transformCallback: UnpluginOptions['transform'], 15 | loadCallback: UnpluginOptions['load'], 16 | ) { 17 | return createUnplugin(() => ({ 18 | name: 'test-plugin', 19 | resolveId: resolveIdCallback, 20 | transformInclude: transformIncludeCallback, 21 | transform: transformCallback, 22 | load: loadCallback, 23 | })) 24 | } 25 | 26 | // We extract this check because all bundlers should behave the same 27 | function checkHookCalls( 28 | name: 'webpack' | 'rollup' | 'vite' | 'rspack' | 'esbuild', 29 | resolveIdCallback: Mock, 30 | transformIncludeCallback: Mock, 31 | transformCallback: Mock, 32 | loadCallback: Mock, 33 | ): void { 34 | const EXPECT_CALLED_TIMES = 4 35 | // Ensure that all bundlers call the hooks the same amount of times 36 | expect(resolveIdCallback).toHaveBeenCalledTimes(EXPECT_CALLED_TIMES) 37 | expect(transformIncludeCallback).toHaveBeenCalledTimes(EXPECT_CALLED_TIMES) 38 | expect(transformCallback).toHaveBeenCalledTimes(EXPECT_CALLED_TIMES) 39 | expect(loadCallback).toHaveBeenCalledTimes(EXPECT_CALLED_TIMES) 40 | 41 | // Ensure that each hook was called with unique ids 42 | expect(new Set(resolveIdCallback.mock.calls.map(call => call[0]))).toHaveLength(EXPECT_CALLED_TIMES) 43 | expect(new Set(transformIncludeCallback.mock.calls.map(call => call[0]))).toHaveLength(EXPECT_CALLED_TIMES) 44 | expect(new Set(transformCallback.mock.calls.map(call => call[1]))).toHaveLength(EXPECT_CALLED_TIMES) 45 | expect(new Set(loadCallback.mock.calls.map(call => call[0]))).toHaveLength(EXPECT_CALLED_TIMES) 46 | 47 | // Ensure that the `resolveId` hook was called with expected values 48 | expect(resolveIdCallback).toHaveBeenCalledWith(entryFilePath, undefined, expect.anything()) 49 | expect(resolveIdCallback).toHaveBeenCalledWith('./proxy-export', expect.anything(), expect.anything()) 50 | expect(resolveIdCallback).toHaveBeenCalledWith('./sub-folder/named-export', expect.anything(), expect.anything()) 51 | expect(resolveIdCallback).toHaveBeenCalledWith('./default-export', expect.anything(), expect.anything()) 52 | 53 | // Ensure that the `transformInclude`, `transform` and `load` hooks were called with the same (absolute) ids 54 | const ids = transformIncludeCallback.mock.calls.map(call => call[0]) 55 | ids.forEach((id) => { 56 | expect(path.isAbsolute(id)).toBe(true) 57 | if (name === 'vite') { 58 | expect(transformCallback).toHaveBeenCalledWith(expect.anything(), id, expect.anything()) 59 | } 60 | else { 61 | expect(transformCallback).toHaveBeenCalledWith(expect.anything(), id) 62 | } 63 | const isVite = expect.getState().currentTestName?.includes('vite') 64 | expect(loadCallback).toHaveBeenCalledWith( 65 | id, 66 | ...(isVite ? [expect.anything()] : []), 67 | ) 68 | }) 69 | } 70 | 71 | describe('id parameter should be consistent across hooks and plugins', () => { 72 | afterEach(() => { 73 | vi.restoreAllMocks() 74 | }) 75 | 76 | it('vite', async () => { 77 | const mockResolveIdHook = vi.fn(() => undefined) 78 | const mockTransformIncludeHook = vi.fn(() => true) 79 | const mockTransformHook = vi.fn(() => undefined) 80 | const mockLoadHook = vi.fn(() => undefined) 81 | 82 | const plugin = createUnpluginWithCallback( 83 | mockResolveIdHook, 84 | mockTransformIncludeHook, 85 | mockTransformHook, 86 | mockLoadHook, 87 | ).vite 88 | // we need to define `enforce` here for the plugin to be run 89 | const plugins = toArray(plugin()).map((plugin): VitePlugin => ({ ...plugin, enforce: 'pre' })) 90 | 91 | await build.vite({ 92 | clearScreen: false, 93 | plugins: [plugins], 94 | build: { 95 | lib: { 96 | entry: entryFilePath, 97 | name: 'TestLib', 98 | }, 99 | rollupOptions: { 100 | external: externals, 101 | }, 102 | write: false, // don't output anything 103 | }, 104 | }) 105 | 106 | checkHookCalls('vite', mockResolveIdHook, mockTransformIncludeHook, mockTransformHook, mockLoadHook) 107 | }) 108 | 109 | it('rollup', async () => { 110 | const mockResolveIdHook = vi.fn(() => undefined) 111 | const mockTransformIncludeHook = vi.fn(() => true) 112 | const mockTransformHook = vi.fn(() => undefined) 113 | const mockLoadHook = vi.fn(() => undefined) 114 | 115 | const plugin = createUnpluginWithCallback( 116 | mockResolveIdHook, 117 | mockTransformIncludeHook, 118 | mockTransformHook, 119 | mockLoadHook, 120 | ).rollup 121 | 122 | await build.rollup({ 123 | input: entryFilePath, 124 | plugins: [plugin()], 125 | external: externals, 126 | }) 127 | 128 | checkHookCalls('rollup', mockResolveIdHook, mockTransformIncludeHook, mockTransformHook, mockLoadHook) 129 | }) 130 | 131 | it('webpack', async () => { 132 | const mockResolveIdHook = vi.fn(() => undefined) 133 | const mockTransformIncludeHook = vi.fn(() => true) 134 | const mockTransformHook = vi.fn(() => undefined) 135 | const mockLoadHook = vi.fn(() => undefined) 136 | 137 | const plugin = createUnpluginWithCallback( 138 | mockResolveIdHook, 139 | mockTransformIncludeHook, 140 | mockTransformHook, 141 | mockLoadHook, 142 | ).webpack 143 | 144 | await new Promise((resolve) => { 145 | build.webpack( 146 | { 147 | entry: entryFilePath, 148 | plugins: [plugin()], 149 | externals, 150 | mode: 'production', 151 | target: 'node', // needed for webpack 4 so it doesn't try to "browserify" any node externals and load addtional modules 152 | }, 153 | () => { 154 | resolve() 155 | }, 156 | ) 157 | }) 158 | 159 | checkHookCalls('webpack', mockResolveIdHook, mockTransformIncludeHook, mockTransformHook, mockLoadHook) 160 | }) 161 | 162 | it('rspack', async () => { 163 | const mockResolveIdHook = vi.fn(() => undefined) 164 | const mockTransformIncludeHook = vi.fn(() => true) 165 | const mockTransformHook = vi.fn(() => undefined) 166 | const mockLoadHook = vi.fn(() => undefined) 167 | 168 | const plugin = createUnpluginWithCallback( 169 | mockResolveIdHook, 170 | mockTransformIncludeHook, 171 | mockTransformHook, 172 | mockLoadHook, 173 | ).rspack 174 | 175 | await new Promise((resolve) => { 176 | build.rspack( 177 | { 178 | entry: entryFilePath, 179 | plugins: [plugin()], 180 | externals, 181 | mode: 'production', 182 | target: 'node', 183 | }, 184 | () => { 185 | resolve() 186 | }, 187 | ) 188 | }) 189 | 190 | checkHookCalls('rspack', mockResolveIdHook, mockTransformIncludeHook, mockTransformHook, mockLoadHook) 191 | }) 192 | 193 | it('esbuild', async () => { 194 | const mockResolveIdHook = vi.fn(() => undefined) 195 | const mockTransformIncludeHook = vi.fn(() => true) 196 | const mockTransformHook = vi.fn(() => undefined) 197 | const mockLoadHook = vi.fn(() => undefined) 198 | 199 | const plugin = createUnpluginWithCallback( 200 | mockResolveIdHook, 201 | mockTransformIncludeHook, 202 | mockTransformHook, 203 | mockLoadHook, 204 | ).esbuild 205 | 206 | await build.esbuild({ 207 | entryPoints: [entryFilePath], 208 | plugins: [plugin()], 209 | bundle: true, // actually traverse imports 210 | write: false, // don't pollute console 211 | external: externals, 212 | }) 213 | 214 | checkHookCalls('esbuild', mockResolveIdHook, mockTransformIncludeHook, mockTransformHook, mockLoadHook) 215 | }) 216 | }) 217 | -------------------------------------------------------------------------------- /test/unit-tests/id-consistency/test-src/default-export.js: -------------------------------------------------------------------------------- 1 | export default 'some string' 2 | -------------------------------------------------------------------------------- /test/unit-tests/id-consistency/test-src/entry.js: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | 3 | // test external modules 4 | import { named, proxiedDefault } from './proxy-export' 5 | 6 | // just some random code to use the imports 7 | process.stdout.write(JSON.stringify({ 8 | named, 9 | proxiedDefault, 10 | path: path.join(__dirname, __filename), 11 | })) 12 | -------------------------------------------------------------------------------- /test/unit-tests/id-consistency/test-src/proxy-export.js: -------------------------------------------------------------------------------- 1 | export { default as proxiedDefault } from './default-export' 2 | export { named } from './sub-folder/named-export' 3 | -------------------------------------------------------------------------------- /test/unit-tests/id-consistency/test-src/sub-folder/named-export.js: -------------------------------------------------------------------------------- 1 | export const named = 'named export' 2 | -------------------------------------------------------------------------------- /test/unit-tests/resolve-id-external/resolve-id-external.test.ts: -------------------------------------------------------------------------------- 1 | import type { VitePlugin } from 'unplugin' 2 | import * as path from 'node:path' 3 | import { createUnplugin } from 'unplugin' 4 | import { afterEach, describe, expect, it, vi } from 'vitest' 5 | import { build, toArray } from '../utils' 6 | 7 | const entryFilePath = path.resolve(__dirname, './test-src/entry.js') 8 | const externals = ['node:path'] 9 | 10 | describe('load hook should not be called when resolveId hook returned `external: true`', () => { 11 | const mockResolveIdHook = vi.fn((id: string) => { 12 | if (id === 'external-module') { 13 | return { 14 | id, 15 | external: true, 16 | } 17 | } 18 | else { 19 | return null 20 | } 21 | }) 22 | const mockLoadHook = vi.fn(() => undefined) 23 | 24 | function createMockedUnplugin() { 25 | return createUnplugin(() => ({ 26 | name: 'test-plugin', 27 | resolveId: mockResolveIdHook, 28 | load: mockLoadHook, 29 | })) 30 | } 31 | // We extract this check because all bundlers should behave the same 32 | function checkHookCalls(): void { 33 | expect(mockResolveIdHook).toHaveBeenCalledTimes(3) 34 | expect(mockResolveIdHook).toHaveBeenCalledWith(entryFilePath, undefined, expect.anything()) 35 | expect(mockResolveIdHook).toHaveBeenCalledWith('./internal-module.js', expect.anything(), expect.anything()) 36 | expect(mockResolveIdHook).toHaveBeenCalledWith('external-module', expect.anything(), expect.anything()) 37 | 38 | const isVite = expect.getState().currentTestName?.includes('vite') 39 | expect(mockLoadHook).toHaveBeenCalledTimes(2) 40 | expect(mockLoadHook).toHaveBeenCalledWith( 41 | expect.stringMatching(/(?:\/|\\)entry\.js$/), 42 | ...(isVite ? [expect.anything()] : []), 43 | ) 44 | expect(mockLoadHook).toHaveBeenCalledWith( 45 | expect.stringMatching(/(?:\/|\\)internal-module\.js$/), 46 | ...(isVite ? [expect.anything()] : []), 47 | ) 48 | } 49 | 50 | afterEach(() => { 51 | vi.clearAllMocks() 52 | }) 53 | 54 | it('vite', async () => { 55 | const plugin = createMockedUnplugin().vite 56 | 57 | // we need to define `enforce` here for the plugin to be run 58 | const plugins = toArray(plugin()).map((plugin): VitePlugin => ({ ...plugin, enforce: 'pre' })) 59 | await build.vite({ 60 | clearScreen: false, 61 | plugins: [plugins], 62 | build: { 63 | lib: { 64 | entry: entryFilePath, 65 | name: 'TestLib', 66 | }, 67 | rollupOptions: { 68 | external: externals, 69 | }, 70 | write: false, // don't output anything 71 | }, 72 | }) 73 | 74 | checkHookCalls() 75 | }) 76 | 77 | it('rollup', async () => { 78 | const plugin = createMockedUnplugin().rollup 79 | 80 | await build.rollup({ 81 | input: entryFilePath, 82 | plugins: [plugin()], 83 | external: externals, 84 | }) 85 | 86 | checkHookCalls() 87 | }) 88 | 89 | it('webpack', async () => { 90 | const plugin = createMockedUnplugin().webpack 91 | 92 | await new Promise((resolve) => { 93 | build.webpack( 94 | { 95 | entry: entryFilePath, 96 | plugins: [plugin()], 97 | externals, 98 | mode: 'production', 99 | target: 'node', // needed for webpack 4 so it doesn't try to "browserify" any node externals and load addtional modules 100 | }, 101 | () => { 102 | resolve() 103 | }, 104 | ) 105 | }) 106 | 107 | checkHookCalls() 108 | }) 109 | 110 | it('rspack', async () => { 111 | const plugin = createMockedUnplugin().rspack 112 | 113 | await new Promise((resolve) => { 114 | build.rspack( 115 | { 116 | entry: entryFilePath, 117 | plugins: [plugin()], 118 | externals, 119 | mode: 'production', 120 | target: 'node', 121 | }, 122 | () => { 123 | resolve() 124 | }, 125 | ) 126 | }) 127 | 128 | checkHookCalls() 129 | }) 130 | 131 | it('esbuild', async () => { 132 | const plugin = createMockedUnplugin().esbuild 133 | 134 | await build.esbuild({ 135 | entryPoints: [entryFilePath], 136 | plugins: [plugin()], 137 | bundle: true, // actually traverse imports 138 | write: false, // don't pollute console 139 | external: externals, 140 | }) 141 | 142 | checkHookCalls() 143 | }) 144 | }) 145 | -------------------------------------------------------------------------------- /test/unit-tests/resolve-id-external/test-src/entry.js: -------------------------------------------------------------------------------- 1 | import external from 'external-module' 2 | import internal from './internal-module.js' 3 | 4 | // just some random code to use the imports 5 | process.stdout.write(JSON.stringify({ 6 | internal, 7 | external, 8 | })) 9 | -------------------------------------------------------------------------------- /test/unit-tests/resolve-id-external/test-src/internal-module.js: -------------------------------------------------------------------------------- 1 | export default 'some-internal-module' 2 | -------------------------------------------------------------------------------- /test/unit-tests/resolve-id/resolve-id.test.ts: -------------------------------------------------------------------------------- 1 | import type { UnpluginBuildContext, UnpluginContext, UnpluginOptions, VitePlugin } from 'unplugin' 2 | import type { Mock } from 'vitest' 3 | import * as path from 'node:path' 4 | import { createUnplugin } from 'unplugin' 5 | import { afterEach, describe, expect, it, vi } from 'vitest' 6 | import { build, toArray } from '../utils' 7 | 8 | function createUnpluginWithCallback(resolveIdCallback: UnpluginOptions['resolveId']) { 9 | return createUnplugin(() => ({ 10 | name: 'test-plugin', 11 | resolveId: resolveIdCallback, 12 | })) 13 | } 14 | 15 | // We extract this check because all bundlers should behave the same 16 | const propsToTest: (keyof (UnpluginContext & UnpluginBuildContext))[] = ['addWatchFile', 'emitFile', 'getWatchFiles', 'parse', 'error', 'warn'] 17 | 18 | function createResolveIdHook(): Mock { 19 | const mockResolveIdHook = vi.fn(function (this: UnpluginContext & UnpluginBuildContext) { 20 | for (const prop of propsToTest) { 21 | expect(this).toHaveProperty(prop) 22 | expect(this[prop]).toBeInstanceOf(Function) 23 | } 24 | }) 25 | return mockResolveIdHook 26 | } 27 | 28 | function checkResolveIdHook(resolveIdCallback: Mock): void { 29 | expect.assertions(4 * (1 + propsToTest.length * 2)) 30 | 31 | expect(resolveIdCallback).toHaveBeenCalledWith( 32 | expect.stringMatching(/(?:\/|\\)entry\.js$/), 33 | undefined, 34 | expect.objectContaining({ isEntry: true }), 35 | ) 36 | 37 | expect(resolveIdCallback).toHaveBeenCalledWith( 38 | './proxy-export', 39 | expect.stringMatching(/(?:\/|\\)entry\.js$/), 40 | expect.objectContaining({ isEntry: false }), 41 | ) 42 | 43 | expect(resolveIdCallback).toHaveBeenCalledWith( 44 | './default-export', 45 | expect.stringMatching(/(?:\/|\\)proxy-export\.js$/), 46 | expect.objectContaining({ isEntry: false }), 47 | ) 48 | 49 | expect(resolveIdCallback).toHaveBeenCalledWith( 50 | './named-export', 51 | expect.stringMatching(/(?:\/|\\)proxy-export\.js$/), 52 | expect.objectContaining({ isEntry: false }), 53 | ) 54 | } 55 | 56 | describe('resolveId hook', () => { 57 | afterEach(() => { 58 | vi.restoreAllMocks() 59 | }) 60 | 61 | it('vite', async () => { 62 | const mockResolveIdHook = createResolveIdHook() 63 | const plugin = createUnpluginWithCallback(mockResolveIdHook).vite 64 | // we need to define `enforce` here for the plugin to be run 65 | const plugins = toArray(plugin()).map((plugin): VitePlugin => ({ ...plugin, enforce: 'pre' })) 66 | 67 | await build.vite({ 68 | clearScreen: false, 69 | plugins: [plugins], 70 | build: { 71 | lib: { 72 | entry: path.resolve(__dirname, 'test-src/entry.js'), 73 | name: 'TestLib', 74 | }, 75 | write: false, // don't output anything 76 | }, 77 | }) 78 | 79 | checkResolveIdHook(mockResolveIdHook) 80 | }) 81 | 82 | it('rollup', async () => { 83 | const mockResolveIdHook = createResolveIdHook() 84 | const plugin = createUnpluginWithCallback(mockResolveIdHook).rollup 85 | 86 | await build.rollup({ 87 | input: path.resolve(__dirname, 'test-src/entry.js'), 88 | plugins: [plugin()], 89 | }) 90 | 91 | checkResolveIdHook(mockResolveIdHook) 92 | }) 93 | 94 | it('webpack', async () => { 95 | const mockResolveIdHook = createResolveIdHook() 96 | const plugin = createUnpluginWithCallback(mockResolveIdHook).webpack 97 | 98 | await new Promise((resolve) => { 99 | build.webpack( 100 | { 101 | entry: path.resolve(__dirname, 'test-src/entry.js'), 102 | plugins: [plugin()], 103 | }, 104 | resolve, 105 | ) 106 | }) 107 | 108 | checkResolveIdHook(mockResolveIdHook) 109 | }) 110 | 111 | it('rspack', async () => { 112 | const mockResolveIdHook = createResolveIdHook() 113 | const plugin = createUnpluginWithCallback(mockResolveIdHook).rspack 114 | 115 | await new Promise((resolve) => { 116 | build.rspack( 117 | { 118 | entry: path.resolve(__dirname, 'test-src/entry.js'), 119 | plugins: [plugin()], 120 | }, 121 | resolve, 122 | ) 123 | }) 124 | 125 | checkResolveIdHook(mockResolveIdHook) 126 | }) 127 | 128 | it('esbuild', async () => { 129 | const mockResolveIdHook = createResolveIdHook() 130 | const plugin = createUnpluginWithCallback(mockResolveIdHook).esbuild 131 | 132 | await build.esbuild({ 133 | entryPoints: [path.resolve(__dirname, 'test-src/entry.js')], 134 | plugins: [plugin()], 135 | bundle: true, // actually traverse imports 136 | write: false, // don't pollute console 137 | }) 138 | 139 | checkResolveIdHook(mockResolveIdHook) 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /test/unit-tests/resolve-id/test-src/default-export.js: -------------------------------------------------------------------------------- 1 | export default 'some string' 2 | -------------------------------------------------------------------------------- /test/unit-tests/resolve-id/test-src/entry.js: -------------------------------------------------------------------------------- 1 | import { named, proxiedDefault } from './proxy-export' 2 | 3 | process.stdout.write(JSON.stringify({ named, proxiedDefault })) 4 | -------------------------------------------------------------------------------- /test/unit-tests/resolve-id/test-src/named-export.js: -------------------------------------------------------------------------------- 1 | export const named = 'named export' 2 | -------------------------------------------------------------------------------- /test/unit-tests/resolve-id/test-src/proxy-export.js: -------------------------------------------------------------------------------- 1 | export { default as proxiedDefault } from './default-export' 2 | export { named } from './named-export' 3 | -------------------------------------------------------------------------------- /test/unit-tests/rolldown/index.test.ts: -------------------------------------------------------------------------------- 1 | import type { RolldownPlugin } from '../../../src/types' 2 | import { describe, expect, it, vi } from 'vitest' 3 | import { getRolldownPlugin } from '../../../src/rolldown/index' 4 | 5 | describe('getRolldownPlugin', () => { 6 | it('should return a function', () => { 7 | const factory = vi.fn() 8 | const plugin = getRolldownPlugin(factory) 9 | expect(typeof plugin).toBe('function') 10 | }) 11 | 12 | it('should call the factory function with the correct arguments', () => { 13 | const factory = vi.fn() 14 | const plugin = getRolldownPlugin(factory) 15 | plugin({ foo: 'bar' }) 16 | expect(factory).toHaveBeenCalledWith({ foo: 'bar' }, { framework: 'rolldown' }) 17 | }) 18 | 19 | it('should return an array of plugins if multiple plugins are returned', () => { 20 | const factory = vi.fn(() => [() => {}, () => {}]) 21 | const plugin = getRolldownPlugin(factory) 22 | const result = plugin({}) as RolldownPlugin[] 23 | expect(Array.isArray(result)).toBe(true) 24 | expect(result.length).toBe(2) 25 | }) 26 | 27 | it('should return a single plugin if only one is returned', () => { 28 | const factory = vi.fn(() => () => {}) 29 | const plugin = getRolldownPlugin(factory) 30 | const result = plugin({}) 31 | expect(typeof result).toBe('function') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/unit-tests/rspack/context.test.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer' 2 | import { describe, expect, it, vi } from 'vitest' 3 | import { createBuildContext, createContext } from '../../../src/rspack/context' 4 | 5 | describe('createBuildContext', () => { 6 | it('getNativeBuildContext - should return expected', () => { 7 | const compiler = { name: 'testCompiler' } 8 | const compilation = { name: 'testCompilation' } 9 | const loaderContext = { name: 'testLoaderContext' } 10 | 11 | const buildContext = createBuildContext(compiler as any, compilation as any, loaderContext as any) 12 | 13 | expect(buildContext.getNativeBuildContext!()).toEqual({ 14 | framework: 'rspack', 15 | compiler, 16 | compilation, 17 | loaderContext, 18 | }) 19 | }) 20 | 21 | it('emitFile - should return expected', () => { 22 | const emitAssetMock = vi.fn() 23 | const RawSourceMock = vi.fn(content => ({ content })) 24 | const compiler = { name: 'testCompiler' } 25 | const compilation = { 26 | name: 'testCompilation', 27 | compiler: { 28 | webpack: { 29 | sources: { 30 | RawSource: RawSourceMock, 31 | }, 32 | }, 33 | }, 34 | emitAsset: emitAssetMock, 35 | } 36 | const loaderContext = { name: 'testLoaderContext' } 37 | 38 | const buildContext = createBuildContext(compiler as any, compilation as any, loaderContext as any) 39 | 40 | buildContext.emitFile({ 41 | fileName: 'testFile.js', 42 | source: 'testSource', 43 | } as any) 44 | expect(emitAssetMock).toHaveBeenCalledWith( 45 | 'testFile.js', 46 | { 47 | content: 'testSource', 48 | }, 49 | ) 50 | emitAssetMock.mockClear() 51 | 52 | buildContext.emitFile({ 53 | name: 'testFile.js', 54 | source: Buffer.from('testBufferSource'), 55 | } as any) 56 | expect(emitAssetMock).toHaveBeenCalledWith( 57 | 'testFile.js', 58 | { 59 | content: Buffer.from('testBufferSource'), 60 | }, 61 | ) 62 | emitAssetMock.mockClear() 63 | }) 64 | 65 | it('createContext - should return expected', () => { 66 | const loaderContext = { 67 | emitError: vi.fn(), 68 | emitWarning: vi.fn(), 69 | } 70 | 71 | const context = createContext(loaderContext as any) 72 | 73 | context.error('testError') 74 | expect(loaderContext.emitError).toHaveBeenCalledWith(new Error('testError')) 75 | 76 | context.error({ message: 'testError' }) 77 | expect(loaderContext.emitError).toHaveBeenCalledWith(new Error('testError')) 78 | 79 | context.warn('testWarning') 80 | expect(loaderContext.emitWarning).toHaveBeenCalledWith(new Error('testWarning')) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /test/unit-tests/rspack/loaders/load.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | import load from '../../../../src/rspack/loaders/load' 3 | 4 | describe('load', () => { 5 | it('should call callback with source and map when plugin.load is not defined', async () => { 6 | const asyncMock = vi.fn() 7 | const query = { plugin: {} } 8 | await load.call({ async: () => asyncMock, query } as any, 'source', 'map') 9 | 10 | expect(asyncMock).toHaveBeenCalledWith(null, 'source', 'map') 11 | }) 12 | 13 | it('should call callback with transformed code and map when handler returns an object', async () => { 14 | const asyncMock = vi.fn() 15 | const handlerMock = vi.fn().mockResolvedValue({ code: 'transformedCode', map: 'transformedMap' }) 16 | const query = { 17 | plugin: { 18 | load: handlerMock, 19 | }, 20 | } 21 | 22 | await load.call( 23 | { 24 | async: () => asyncMock, 25 | query, 26 | resource: 'resourceId', 27 | } as any, 28 | 'source', 29 | 'map', 30 | ) 31 | 32 | expect(handlerMock).toHaveBeenCalled() 33 | expect(asyncMock).toHaveBeenCalledWith(null, 'transformedCode', 'transformedMap') 34 | }) 35 | 36 | it('should call callback with transformed code when handler returns a string', async () => { 37 | const asyncMock = vi.fn() 38 | const handlerMock = vi.fn().mockResolvedValue('transformedCode') 39 | const query = { 40 | plugin: { 41 | load: handlerMock, 42 | }, 43 | } 44 | 45 | await load.call( 46 | { 47 | async: () => asyncMock, 48 | query, 49 | resource: 'resourceId', 50 | } as any, 51 | 'source', 52 | 'map', 53 | ) 54 | 55 | expect(handlerMock).toHaveBeenCalled() 56 | expect(asyncMock).toHaveBeenCalledWith(null, 'transformedCode', 'map') 57 | }) 58 | 59 | it('should call callback with source and map when handler returns null', async () => { 60 | const asyncMock = vi.fn() 61 | const handlerMock = vi.fn().mockResolvedValue(null) 62 | const query = { 63 | plugin: { 64 | load: handlerMock, 65 | }, 66 | } 67 | 68 | await load.call( 69 | { 70 | async: () => asyncMock, 71 | query, 72 | resource: 'resourceId', 73 | } as any, 74 | 'source', 75 | 'map', 76 | ) 77 | 78 | expect(handlerMock).toHaveBeenCalled() 79 | expect(asyncMock).toHaveBeenCalledWith(null, 'source', 'map') 80 | }) 81 | 82 | it('should call callback with source and map when handler returns object', async () => { 83 | const asyncMock = vi.fn() 84 | const handlerMock = vi.fn().mockResolvedValue({ 85 | code: 'code', 86 | map: 'resmap', 87 | }) 88 | const query = { 89 | plugin: { 90 | load: handlerMock, 91 | }, 92 | } 93 | 94 | await load.call( 95 | { 96 | async: () => asyncMock, 97 | query, 98 | resource: 'resourceId', 99 | } as any, 100 | 'source', 101 | 'map', 102 | ) 103 | 104 | expect(handlerMock).toHaveBeenCalled() 105 | expect(asyncMock).toHaveBeenCalledWith(null, 'code', 'resmap') 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /test/unit-tests/rspack/loaders/transform.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | import transform from '../../../../src/rspack/loaders/transform' 3 | 4 | describe('transform', () => { 5 | it('should call callback with source and map if plugin.transform is not defined', async () => { 6 | const mockCallback = vi.fn() 7 | const mockLoaderContext = { 8 | async: () => mockCallback, 9 | query: {}, 10 | } as any 11 | 12 | const source = 'test source' 13 | const map = 'test map' 14 | 15 | await transform.call(mockLoaderContext, source, map) 16 | 17 | expect(mockCallback).toHaveBeenCalledWith(null, source, map) 18 | }) 19 | 20 | it('should call callback with an error if handler throws an error', async () => { 21 | const mockCallback = vi.fn() 22 | const mockLoaderContext = { 23 | async: () => mockCallback, 24 | query: { 25 | plugin: { 26 | transform: { 27 | handler: vi.fn().mockRejectedValue(new Error('Handler error')), 28 | filter: vi.fn().mockReturnValue(true), 29 | }, 30 | }, 31 | }, 32 | resource: 'test resource', 33 | } as any 34 | 35 | const source = 'test source' 36 | const map = 'test map' 37 | 38 | vi.mock('../../../../src/utils/filter', () => ({ 39 | normalizeObjectHook: vi.fn(() => ({ handler: vi.fn().mockRejectedValue(new Error('Handler error')), filter: vi.fn().mockReturnValue(true) })), 40 | })) 41 | 42 | await transform.call(mockLoaderContext, source, map) 43 | 44 | expect(mockCallback).toHaveBeenCalledWith(expect.any(Error)) 45 | expect(mockCallback.mock.calls[0][0].message).toBe('Handler error') 46 | }) 47 | 48 | it('should call callback with an error if handler throws string', async () => { 49 | const mockCallback = vi.fn() 50 | const mockLoaderContext = { 51 | async: () => mockCallback, 52 | query: { 53 | plugin: { 54 | transform: { 55 | handler: vi.fn().mockRejectedValue('Handler error'), 56 | filter: vi.fn().mockReturnValue(true), 57 | }, 58 | }, 59 | }, 60 | resource: 'test resource', 61 | } as any 62 | 63 | const source = 'test source' 64 | const map = 'test map' 65 | 66 | vi.mock('../../../../src/utils/filter', () => ({ 67 | normalizeObjectHook: vi.fn(() => ({ handler: vi.fn().mockRejectedValue(new Error('Handler error')), filter: vi.fn().mockReturnValue(true) })), 68 | })) 69 | 70 | await transform.call(mockLoaderContext, source, map) 71 | 72 | expect(mockCallback).toHaveBeenCalledWith(expect.any(Error)) 73 | expect(mockCallback.mock.calls[0][0].message).toBe('Handler error') 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /test/unit-tests/unloader/index.test.ts: -------------------------------------------------------------------------------- 1 | import type { UnloaderPlugin } from '../../../src/types' 2 | import { describe, expect, it, vi } from 'vitest' 3 | import { getUnloaderPlugin } from '../../../src/unloader/index' 4 | 5 | describe('getUnloaderPlugin', () => { 6 | it('should return a function', () => { 7 | const factory = vi.fn() 8 | const plugin = getUnloaderPlugin(factory) 9 | expect(typeof plugin).toBe('function') 10 | }) 11 | 12 | it('should call the factory function with the correct arguments', () => { 13 | const factory = vi.fn() 14 | const plugin = getUnloaderPlugin(factory) 15 | plugin({ foo: 'bar' }) 16 | expect(factory).toHaveBeenCalledWith({ foo: 'bar' }, { framework: 'unloader' }) 17 | }) 18 | 19 | it('should return an array of plugins if multiple plugins are returned', () => { 20 | const factory = vi.fn(() => [() => {}, () => {}]) 21 | const plugin = getUnloaderPlugin(factory) 22 | const result = plugin({}) as UnloaderPlugin[] 23 | expect(Array.isArray(result)).toBe(true) 24 | expect(result.length).toBe(2) 25 | }) 26 | 27 | it('should return a single plugin if only one is returned', () => { 28 | const factory = vi.fn(() => () => {}) 29 | const plugin = getUnloaderPlugin(factory) 30 | const result = plugin({}) 31 | expect(typeof result).toBe('function') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/unit-tests/utils.ts: -------------------------------------------------------------------------------- 1 | import * as rspack from '@rspack/core' 2 | import * as esbuild from 'esbuild' 3 | import * as rolldown from 'rolldown' 4 | import * as rollup from 'rollup' 5 | import * as vite from 'vite' 6 | import * as webpack from 'webpack' 7 | 8 | export * from '../../src/utils/general' 9 | 10 | export const viteBuild: typeof vite.build = vite.build 11 | export const rollupBuild: typeof rollup.rollup = rollup.rollup 12 | export const rolldownBuild: typeof rolldown.build = rolldown.build 13 | export const esbuildBuild: typeof esbuild.build = esbuild.build 14 | export const webpackBuild: typeof webpack.webpack = webpack.webpack || (webpack as any).default || webpack 15 | export const rspackBuild: typeof rspack.rspack = rspack.rspack 16 | 17 | export const webpackVersion: string = ((webpack as any).default || webpack).version 18 | 19 | export const build: { 20 | webpack: typeof webpack.webpack 21 | rspack: typeof rspackBuild 22 | rollup: typeof rollupBuild 23 | rolldown: typeof rolldownBuild 24 | vite: typeof viteBuild 25 | esbuild: typeof esbuildBuild 26 | } = { 27 | webpack: webpackBuild, 28 | rspack: rspackBuild, 29 | rollup: rollupBuild, 30 | rolldown: rolldownBuild, 31 | vite(config) { 32 | return viteBuild(vite.mergeConfig(config || {}, { 33 | build: { 34 | rollupOptions: { 35 | logLevel: 'silent', 36 | }, 37 | }, 38 | logLevel: 'silent', 39 | })) 40 | }, 41 | esbuild: esbuildBuild, 42 | } 43 | -------------------------------------------------------------------------------- /test/unit-tests/utils/context.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { parse } from '../../../src/utils/context' 3 | 4 | describe('parse', () => { 5 | it('should parse valid JavaScript code', () => { 6 | const code = 'const x = 42;' 7 | const result = parse(code) 8 | expect(result).toBeDefined() 9 | }) 10 | 11 | it('should throw an error for invalid JavaScript code', () => { 12 | const code = 'const x = ;' 13 | expect(() => parse(code)).toThrow() 14 | }) 15 | 16 | it('should accept custom options', () => { 17 | const code = 'const x = 42;' 18 | const opts = { ecmaVersion: 2020 } 19 | const result = parse(code, opts) 20 | expect(result).toBeDefined() 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/unit-tests/virtual-id/test-src/entry.js: -------------------------------------------------------------------------------- 1 | import './imported.js' 2 | -------------------------------------------------------------------------------- /test/unit-tests/virtual-id/test-src/imported.js: -------------------------------------------------------------------------------- 1 | export default 'test' 2 | -------------------------------------------------------------------------------- /test/unit-tests/virtual-id/virtual-id.test.ts: -------------------------------------------------------------------------------- 1 | import type { UnpluginOptions, VitePlugin } from 'unplugin' 2 | import type { Mock } from 'vitest' 3 | import * as fs from 'node:fs' 4 | import * as path from 'node:path' 5 | import { createUnplugin } from 'unplugin' 6 | import { afterEach, describe, expect, it, vi } from 'vitest' 7 | import { build, toArray } from '../utils' 8 | 9 | function createUnpluginWithCallbacks(resolveIdCallback: UnpluginOptions['resolveId'], loadCallback: UnpluginOptions['load']) { 10 | return createUnplugin(() => ({ 11 | name: 'test-plugin', 12 | resolveId: resolveIdCallback, 13 | load: loadCallback, 14 | })) 15 | } 16 | 17 | function createResolveIdHook(): Mock { 18 | const mockResolveIdHook = vi.fn((id: string, importer: string | undefined): string => { 19 | // rspack seems to generate paths of the form \C:\... on Windows. 20 | // Remove the leading \ 21 | if (importer && /^\\[A-Z]:\\/.test(importer)) 22 | importer = importer.slice(1) 23 | id = path.resolve(path.dirname(importer ?? ''), id) 24 | return `${id}.js` 25 | }) 26 | return mockResolveIdHook 27 | } 28 | 29 | function createLoadHook(): Mock { 30 | const mockLoadHook = vi.fn((id: string): string => { 31 | expect(id).toMatch(/\.js\.js$/) 32 | id = id.slice(0, -3) 33 | return fs.readFileSync(id, { encoding: 'utf-8' }) 34 | }) 35 | return mockLoadHook 36 | } 37 | 38 | function checkResolveIdHook(resolveIdCallback: Mock): void { 39 | expect(resolveIdCallback).toHaveBeenCalledWith( 40 | expect.stringMatching(/(?:\/|\\)entry\.js$/), 41 | undefined, 42 | expect.objectContaining({ isEntry: true }), 43 | ) 44 | 45 | expect(resolveIdCallback).toHaveBeenCalledWith( 46 | './imported.js', 47 | expect.stringMatching(/(?:\/|\\)entry\.js\.js$/), 48 | expect.objectContaining({ isEntry: false }), 49 | ) 50 | } 51 | 52 | function checkLoadHook(loadCallback: Mock): void { 53 | const isVite = expect.getState().currentTestName?.includes('vite') 54 | 55 | expect(loadCallback).toHaveBeenCalledWith( 56 | expect.stringMatching(/(?:\/|\\)entry\.js\.js$/), 57 | ...(isVite ? [expect.anything()] : []), 58 | ) 59 | 60 | expect(loadCallback).toHaveBeenCalledWith( 61 | expect.stringMatching(/(?:\/|\\)imported\.js\.js$/), 62 | ...(isVite ? [expect.anything()] : []), 63 | ) 64 | } 65 | 66 | describe('virtual ids', () => { 67 | afterEach(() => { 68 | vi.restoreAllMocks() 69 | }) 70 | 71 | it('vite', async () => { 72 | const mockResolveIdHook = createResolveIdHook() 73 | const mockLoadHook = createLoadHook() 74 | const plugin = createUnpluginWithCallbacks(mockResolveIdHook, mockLoadHook).vite 75 | // we need to define `enforce` here for the plugin to be run 76 | const plugins = toArray(plugin()).map((plugin): VitePlugin => ({ ...plugin, enforce: 'pre' })) 77 | 78 | await build.vite({ 79 | clearScreen: false, 80 | plugins: [plugins], 81 | build: { 82 | lib: { 83 | entry: path.resolve(__dirname, 'test-src/entry.js'), 84 | name: 'TestLib', 85 | }, 86 | write: false, // don't output anything 87 | }, 88 | }) 89 | 90 | checkResolveIdHook(mockResolveIdHook) 91 | checkLoadHook(mockLoadHook) 92 | }) 93 | 94 | it('rollup', async () => { 95 | const mockResolveIdHook = createResolveIdHook() 96 | const mockLoadHook = createLoadHook() 97 | const plugin = createUnpluginWithCallbacks(mockResolveIdHook, mockLoadHook).rollup 98 | 99 | await build.rollup({ 100 | input: path.resolve(__dirname, 'test-src/entry.js'), 101 | plugins: [plugin()], 102 | }) 103 | 104 | checkResolveIdHook(mockResolveIdHook) 105 | checkLoadHook(mockLoadHook) 106 | }) 107 | 108 | it('webpack', async () => { 109 | const mockResolveIdHook = createResolveIdHook() 110 | const mockLoadHook = createLoadHook() 111 | const plugin = createUnpluginWithCallbacks(mockResolveIdHook, mockLoadHook).webpack 112 | 113 | await new Promise((resolve) => { 114 | build.webpack( 115 | { 116 | entry: path.resolve(__dirname, 'test-src/entry.js'), 117 | plugins: [plugin()], 118 | }, 119 | resolve, 120 | ) 121 | }) 122 | 123 | checkResolveIdHook(mockResolveIdHook) 124 | checkLoadHook(mockLoadHook) 125 | }) 126 | 127 | it.skip('rspack', async () => { 128 | const mockResolveIdHook = createResolveIdHook() 129 | const mockLoadHook = createLoadHook() 130 | const plugin = createUnpluginWithCallbacks(mockResolveIdHook, mockLoadHook).rspack 131 | 132 | await new Promise((resolve) => { 133 | build.rspack( 134 | { 135 | entry: path.resolve(__dirname, 'test-src/entry.js'), 136 | plugins: [plugin()], 137 | }, 138 | resolve, 139 | ) 140 | }) 141 | 142 | checkResolveIdHook(mockResolveIdHook) 143 | checkLoadHook(mockLoadHook) 144 | }) 145 | 146 | it('esbuild', async () => { 147 | const mockResolveIdHook = createResolveIdHook() 148 | const mockLoadHook = createLoadHook() 149 | const plugin = createUnpluginWithCallbacks(mockResolveIdHook, mockLoadHook).esbuild 150 | 151 | await build.esbuild({ 152 | entryPoints: [path.resolve(__dirname, 'test-src/entry.js')], 153 | plugins: [plugin()], 154 | bundle: true, // actually traverse imports 155 | write: false, // don't pollute console 156 | }) 157 | 158 | checkResolveIdHook(mockResolveIdHook) 159 | checkLoadHook(mockLoadHook) 160 | }) 161 | }) 162 | -------------------------------------------------------------------------------- /test/unit-tests/webpack/context.test.ts: -------------------------------------------------------------------------------- 1 | import type { Compilation, Compiler, LoaderContext } from 'webpack' 2 | import { describe, expect, it, vi } from 'vitest' 3 | import { contextOptionsFromCompilation, createBuildContext, createContext, normalizeMessage } from '../../../src/webpack/context' 4 | 5 | describe('webpack - utils', () => { 6 | describe('contextOptionsFromCompilation', () => { 7 | it('should add and retrieve watch files', () => { 8 | const mockCompilation = { 9 | fileDependencies: new Set(), 10 | } as unknown as Compilation 11 | 12 | const contextOptions = contextOptionsFromCompilation(mockCompilation) 13 | contextOptions.addWatchFile('test-file.js') 14 | expect(contextOptions.getWatchFiles()).toContain('test-file.js') 15 | }) 16 | 17 | it('should add and retrieve compilation dependencies', () => { 18 | const mockCompilation = { 19 | compilationDependencies: new Set(), 20 | } as unknown as Compilation 21 | 22 | const contextOptions = contextOptionsFromCompilation(mockCompilation) 23 | contextOptions.addWatchFile('test-file.js') 24 | expect(contextOptions.getWatchFiles()).toContain('test-file.js') 25 | }) 26 | }) 27 | 28 | describe('createBuildContext', () => { 29 | it('should add watch files and emit assets', () => { 30 | const mockOptions = { 31 | addWatchFile: vi.fn(), 32 | getWatchFiles: vi.fn(() => ['file1.js']), 33 | } 34 | const mockCompiler = {} as Compiler 35 | const mockCompilation = { 36 | emitAsset: vi.fn(), 37 | } as unknown as Compilation 38 | 39 | const buildContext = createBuildContext(mockOptions, mockCompiler, mockCompilation) 40 | buildContext.addWatchFile('file2.js') 41 | expect(mockOptions.addWatchFile).toHaveBeenCalledWith(expect.stringContaining('file2.js')) 42 | 43 | buildContext.emitFile({ fileName: 'output.js', source: 'content' } as any) 44 | expect(mockCompilation.emitAsset).toHaveBeenCalledWith( 45 | 'output.js', 46 | expect.anything(), 47 | ) 48 | }) 49 | }) 50 | 51 | describe('createContext', () => { 52 | it('should emit errors and warnings', () => { 53 | const mockLoader = { 54 | emitError: vi.fn(), 55 | emitWarning: vi.fn(), 56 | } as unknown as LoaderContext<{ unpluginName: string }> 57 | 58 | const context = createContext(mockLoader) 59 | context.error('Test error') 60 | context.warn('Test warning') 61 | 62 | expect(mockLoader.emitError).toHaveBeenCalledWith(expect.any(Error)) 63 | expect(mockLoader.emitWarning).toHaveBeenCalledWith(expect.any(Error)) 64 | }) 65 | }) 66 | 67 | describe('normalizeMessage', () => { 68 | it('should normalize string messages', () => { 69 | const error = normalizeMessage('Test error') 70 | expect(error.message).toBe('Test error') 71 | }) 72 | 73 | it('should normalize object messages', () => { 74 | const error = normalizeMessage({ message: 'Test error', stack: 'stack trace', meta: 'meta info' }) 75 | expect(error.message).toBe('Test error') 76 | expect(error.stack).toBe('stack trace') 77 | expect(error.cause).toBe('meta info') 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /test/unit-tests/webpack/loaders/load.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | import load from '../../../../src/webpack/loaders/load' 3 | 4 | describe('load function', () => { 5 | const mockCallback = vi.fn() 6 | const mockLoaderContext = { 7 | async: () => mockCallback, 8 | query: { 9 | plugin: { 10 | load: vi.fn(), 11 | __virtualModulePrefix: '/virtual/', 12 | }, 13 | }, 14 | resource: '/virtual/test.js', 15 | addDependency: vi.fn(), 16 | getDependencies: vi.fn().mockReturnValue(['/dependency1', '/dependency2']), 17 | _compiler: {}, 18 | _compilation: {}, 19 | } 20 | 21 | it('should call callback with source and map if plugin.load is not defined', async () => { 22 | const context = { ...mockLoaderContext, query: { plugin: {} } } 23 | const source = 'source code' 24 | const map = 'source map' 25 | 26 | await load.call(context as any, source, map) 27 | 28 | expect(mockCallback).toHaveBeenCalledWith(null, source, map) 29 | }) 30 | 31 | it('should decode id if it starts with __virtualModulePrefix', async () => { 32 | const source = 'source code' 33 | const map = 'source map' 34 | const pluginLoadHandler = vi.fn().mockResolvedValue(null) 35 | mockLoaderContext.query.plugin.load = pluginLoadHandler 36 | 37 | await load.call(mockLoaderContext as any, source, map) 38 | 39 | expect(pluginLoadHandler).toHaveBeenCalledWith('test.js') 40 | }) 41 | 42 | it('should call callback with transformed code and map if handler returns an object', async () => { 43 | const source = 'source code' 44 | const map = 'source map' 45 | const transformedCode = { code: 'transformed code', map: 'transformed map' } 46 | const pluginLoadHandler = vi.fn().mockResolvedValue(transformedCode) 47 | mockLoaderContext.query.plugin.load = pluginLoadHandler 48 | 49 | await load.call(mockLoaderContext as any, source, map) 50 | 51 | expect(mockCallback).toHaveBeenCalledWith(null, transformedCode.code, transformedCode.map) 52 | }) 53 | 54 | it('should call callback with transformed code if handler returns a string', async () => { 55 | const source = 'source code' 56 | const map = 'source map' 57 | const transformedCode = 'transformed code' 58 | const pluginLoadHandler = vi.fn().mockResolvedValue(transformedCode) 59 | mockLoaderContext.query.plugin.load = pluginLoadHandler 60 | 61 | await load.call(mockLoaderContext as any, source, map) 62 | 63 | expect(mockCallback).toHaveBeenCalledWith(null, transformedCode, map) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/unit-tests/webpack/loaders/transform.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | import transform from '../../../../src/webpack/loaders/transform' 3 | 4 | describe('transform loader', () => { 5 | const mockCallback = vi.fn() 6 | const mockLoaderContext = { 7 | async: () => mockCallback, 8 | query: {}, 9 | resource: '/path/to/resource', 10 | addDependency: vi.fn(), 11 | getDependencies: vi.fn().mockReturnValue(['/path/to/dependency']), 12 | _compiler: {}, 13 | _compilation: {}, 14 | } 15 | 16 | it('should return source and map if plugin.transform is not defined', async () => { 17 | const source = 'source code' 18 | const map = 'source map' 19 | 20 | mockLoaderContext.query = {} 21 | 22 | await transform.call(mockLoaderContext as any, source, map) 23 | 24 | expect(mockCallback).toHaveBeenCalledWith(null, source, map) 25 | }) 26 | 27 | it('should return source and map if filter does not match', async () => { 28 | const source = 'source code' 29 | const map = 'source map' 30 | 31 | mockLoaderContext.query = { 32 | plugin: { 33 | transform: { 34 | handler: vi.fn(), 35 | filter: vi.fn().mockReturnValue(false), 36 | }, 37 | }, 38 | } 39 | 40 | await transform.call(mockLoaderContext as any, source, map) 41 | 42 | expect(mockCallback).toHaveBeenCalledWith(null, source, map) 43 | }) 44 | 45 | it('should call handler and return transformed code', async () => { 46 | const source = 'source code' 47 | const map = 'source map' 48 | const transformedCode = 'transformed code' 49 | 50 | const handlerMock = vi.fn().mockResolvedValue(transformedCode) 51 | mockLoaderContext.query = { 52 | plugin: { 53 | transform: { 54 | handler: handlerMock, 55 | filter: vi.fn().mockReturnValue(true), 56 | }, 57 | }, 58 | } 59 | 60 | await transform.call(mockLoaderContext as any, source, map) 61 | 62 | expect(handlerMock).toHaveBeenCalled() 63 | expect(mockCallback).toHaveBeenCalledWith(null, transformedCode, map) 64 | }) 65 | 66 | it('should call handler and return transformed code and map if handler returns an object', async () => { 67 | const source = 'source code' 68 | const map = 'source map' 69 | const transformedResult = { code: 'transformed code', map: 'transformed map' } 70 | 71 | const handlerMock = vi.fn().mockResolvedValue(transformedResult) 72 | mockLoaderContext.query = { 73 | plugin: { 74 | transform: { 75 | handler: handlerMock, 76 | filter: vi.fn().mockReturnValue(true), 77 | }, 78 | }, 79 | } 80 | 81 | await transform.call(mockLoaderContext as any, source, map) 82 | 83 | expect(handlerMock).toHaveBeenCalled() 84 | expect(mockCallback).toHaveBeenCalledWith(null, transformedResult.code, transformedResult.map) 85 | }) 86 | 87 | it('should handle errors thrown by the handler', async () => { 88 | const source = 'source code' 89 | const map = 'source map' 90 | const error = new Error('Handler error') 91 | 92 | const handlerMock = vi.fn().mockRejectedValue(error) 93 | mockLoaderContext.query = { 94 | plugin: { 95 | transform: { 96 | handler: handlerMock, 97 | filter: vi.fn().mockReturnValue(true), 98 | }, 99 | }, 100 | } 101 | 102 | await transform.call(mockLoaderContext as any, source, map) 103 | 104 | expect(handlerMock).toHaveBeenCalled() 105 | expect(mockCallback).toHaveBeenCalledWith(error) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /test/unit-tests/write-bundle/.gitignore: -------------------------------------------------------------------------------- 1 | test-out 2 | -------------------------------------------------------------------------------- /test/unit-tests/write-bundle/test-src/entry.js: -------------------------------------------------------------------------------- 1 | import someString, { someOtherString } from './import' 2 | 3 | process.stdout.write(JSON.stringify({ someString, someOtherString })) 4 | -------------------------------------------------------------------------------- /test/unit-tests/write-bundle/test-src/import.js: -------------------------------------------------------------------------------- 1 | export default 'some string' 2 | 3 | export const someOtherString = 'some other string' 4 | -------------------------------------------------------------------------------- /test/unit-tests/write-bundle/write-bundle.test.ts: -------------------------------------------------------------------------------- 1 | import type { RspackOptions } from '@rspack/core' 2 | import type { UnpluginOptions, VitePlugin } from 'unplugin' 3 | import type { Mock } from 'vitest' 4 | import * as fs from 'node:fs' 5 | import * as path from 'node:path' 6 | import { createUnplugin } from 'unplugin' 7 | import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' 8 | import { build, toArray, webpackVersion } from '../utils' 9 | 10 | function createUnpluginWithCallback(writeBundleCallback: UnpluginOptions['writeBundle']) { 11 | return createUnplugin(() => ({ 12 | name: 'test-plugin', 13 | writeBundle: writeBundleCallback, 14 | })) 15 | } 16 | 17 | function generateMockWriteBundleHook(outputPath: string) { 18 | return () => { 19 | // We want to check that at the time the `writeBundle` hook is called, all 20 | // build-artifacts have already been written to disk. 21 | 22 | const bundleExists = fs.existsSync(path.join(outputPath, 'output.js')) 23 | const sourceMapExists = fs.existsSync(path.join(outputPath, 'output.js.map')) 24 | 25 | expect(bundleExists).toBe(true) 26 | expect(sourceMapExists).toBe(true) 27 | 28 | return undefined 29 | } 30 | } 31 | 32 | // We extract this check because all bundlers should behave the same 33 | function checkWriteBundleHook(writeBundleCallback: Mock): void { 34 | expect(writeBundleCallback).toHaveBeenCalledOnce() 35 | } 36 | 37 | describe('writeBundle hook', () => { 38 | beforeAll(() => { 39 | fs.rmSync(path.resolve(__dirname, 'test-out'), { recursive: true, force: true }) 40 | }) 41 | 42 | afterEach(() => { 43 | vi.restoreAllMocks() 44 | }) 45 | 46 | it('vite', async () => { 47 | expect.assertions(3) 48 | const mockWriteBundleHook = vi.fn(generateMockWriteBundleHook(path.resolve(__dirname, 'test-out/vite'))) 49 | const plugin = createUnpluginWithCallback(mockWriteBundleHook).vite 50 | // we need to define `enforce` here for the plugin to be run 51 | const plugins = toArray(plugin()).map((plugin): VitePlugin => ({ ...plugin, enforce: 'pre' })) 52 | 53 | await build.vite({ 54 | clearScreen: false, 55 | plugins: [plugins], 56 | build: { 57 | lib: { 58 | entry: path.resolve(__dirname, 'test-src/entry.js'), 59 | name: 'TestLib', 60 | fileName: 'output', 61 | formats: ['es'], 62 | }, 63 | outDir: path.resolve(__dirname, 'test-out/vite'), 64 | sourcemap: true, 65 | }, 66 | }) 67 | 68 | checkWriteBundleHook(mockWriteBundleHook) 69 | }) 70 | 71 | it('rollup', async () => { 72 | expect.assertions(3) 73 | const mockResolveIdHook = vi.fn(generateMockWriteBundleHook(path.resolve(__dirname, 'test-out/rollup'))) 74 | const plugin = createUnpluginWithCallback(mockResolveIdHook).rollup 75 | 76 | const rollupBuild = await build.rollup({ 77 | input: path.resolve(__dirname, 'test-src/entry.js'), 78 | }) 79 | 80 | await rollupBuild.write({ 81 | plugins: [plugin()], 82 | file: path.resolve(__dirname, 'test-out/rollup/output.js'), 83 | format: 'cjs', 84 | exports: 'named', 85 | sourcemap: true, 86 | }) 87 | 88 | checkWriteBundleHook(mockResolveIdHook) 89 | }) 90 | 91 | it('webpack', async () => { 92 | expect.assertions(3) 93 | const mockResolveIdHook = vi.fn(generateMockWriteBundleHook(path.resolve(__dirname, 'test-out/webpack'))) 94 | const plugin = createUnpluginWithCallback(mockResolveIdHook).webpack 95 | 96 | const webpack4Options = { 97 | entry: path.resolve(__dirname, 'test-src/entry.js'), 98 | cache: false, 99 | output: { 100 | path: path.resolve(__dirname, 'test-out/webpack'), 101 | filename: 'output.js', 102 | libraryTarget: 'commonjs', 103 | }, 104 | plugins: [plugin()], 105 | devtool: 'source-map', 106 | } 107 | 108 | const webpack5Options = { 109 | entry: path.resolve(__dirname, 'test-src/entry.js'), 110 | plugins: [plugin()], 111 | devtool: 'source-map', 112 | output: { 113 | path: path.resolve(__dirname, 'test-out/webpack'), 114 | filename: 'output.js', 115 | library: { 116 | type: 'commonjs', 117 | }, 118 | }, 119 | } 120 | 121 | await new Promise((resolve) => { 122 | build.webpack(webpackVersion!.startsWith('4') ? webpack4Options : webpack5Options, resolve) 123 | }) 124 | 125 | checkWriteBundleHook(mockResolveIdHook) 126 | }) 127 | 128 | it('rspack', async () => { 129 | expect.assertions(3) 130 | const mockResolveIdHook = vi.fn(generateMockWriteBundleHook(path.resolve(__dirname, 'test-out/rspack'))) 131 | const plugin = createUnpluginWithCallback(mockResolveIdHook).rspack 132 | 133 | const rspackOptions: RspackOptions = { 134 | entry: path.resolve(__dirname, 'test-src/entry.js'), 135 | plugins: [plugin()], 136 | devtool: 'source-map', 137 | output: { 138 | path: path.resolve(__dirname, 'test-out/rspack'), 139 | filename: 'output.js', 140 | library: { 141 | type: 'commonjs', 142 | }, 143 | }, 144 | } 145 | 146 | await new Promise((resolve) => { 147 | build.rspack(rspackOptions, resolve) 148 | }) 149 | 150 | checkWriteBundleHook(mockResolveIdHook) 151 | }) 152 | 153 | it('esbuild', async () => { 154 | expect.assertions(3) 155 | const mockResolveIdHook = vi.fn(generateMockWriteBundleHook(path.resolve(__dirname, 'test-out/esbuild'))) 156 | const plugin = createUnpluginWithCallback(mockResolveIdHook).esbuild 157 | 158 | await build.esbuild({ 159 | entryPoints: [path.resolve(__dirname, 'test-src/entry.js')], 160 | plugins: [plugin()], 161 | bundle: true, // actually traverse imports 162 | outfile: path.resolve(__dirname, 'test-out/esbuild/output.js'), 163 | format: 'cjs', 164 | sourcemap: true, 165 | }) 166 | 167 | checkWriteBundleHook(mockResolveIdHook) 168 | }) 169 | }) 170 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["es2023"], 5 | "moduleDetection": "force", 6 | "module": "preserve", 7 | "moduleResolution": "bundler", 8 | "paths": { 9 | "unplugin": [ 10 | "./src/index.ts" 11 | ] 12 | }, 13 | "resolveJsonModule": true, 14 | "types": [ 15 | "node" 16 | ], 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "declaration": true, 20 | "noEmit": true, 21 | "esModuleInterop": true, 22 | "isolatedDeclarations": true, 23 | "isolatedModules": true, 24 | "verbatimModuleSyntax": true, 25 | "skipLibCheck": true 26 | }, 27 | "include": [ 28 | "src", 29 | "test" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/webpack/loaders/*', 'src/rspack/loaders/*'], 5 | format: ['cjs', 'esm'], 6 | clean: true, 7 | target: 'node18.12', 8 | dts: true, 9 | sourcemap: false, 10 | define: { 11 | __DEV__: 'false', 12 | }, 13 | shims: true, 14 | external: ['vite', 'webpack', 'rollup', 'esbuild', '@farmfe/core'], 15 | unused: { level: 'error' }, 16 | }) 17 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | define: { 6 | __DEV__: 'true', 7 | }, 8 | resolve: { 9 | alias: { 10 | unplugin: resolve('src/index.ts'), 11 | }, 12 | }, 13 | test: { 14 | coverage: { 15 | reporter: ['text', 'json', 'html'], 16 | include: ['src/**/*.{ts,tsx}'], 17 | }, 18 | }, 19 | }) 20 | --------------------------------------------------------------------------------