├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── renovate.json5 └── workflows │ ├── release.yml │ └── unit-test.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── eslint.config.js ├── jsr.json ├── package.json ├── pnpm-lock.yaml ├── src ├── core │ └── options.ts ├── esbuild.ts ├── farm.ts ├── index.ts ├── rolldown.ts ├── rollup.ts ├── rspack.ts ├── vite.ts └── webpack.ts ├── tests ├── __snapshots__ │ ├── esbuild.test.ts.snap │ ├── rolldown.test.ts.snap │ ├── rollup.test.ts.snap │ ├── vite.test.ts.snap │ └── webpack.test.ts.snap ├── esbuild.test.ts ├── fixtures │ └── main.js ├── rolldown.test.ts ├── rollup.test.ts ├── vite.test.ts └── webpack.test.ts ├── tsconfig.json └── tsdown.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | end_of_line = lf 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: sxzz 2 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: ['github>sxzz/renovate-config'], 3 | automerge: true, 4 | } 5 | -------------------------------------------------------------------------------- /.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 | 28 | publish-jsr: 29 | runs-on: ubuntu-latest 30 | 31 | permissions: 32 | contents: read 33 | id-token: write 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - name: Install pnpm 39 | uses: pnpm/action-setup@v4.1.0 40 | 41 | - name: Set node 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version: lts/* 45 | cache: pnpm 46 | 47 | - name: Install 48 | run: pnpm i 49 | 50 | - name: Publish package 51 | run: npx jsr publish 52 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v4.1.0 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | cache: pnpm 24 | 25 | - name: Install 26 | run: pnpm i 27 | 28 | - name: Lint 29 | run: pnpm lint 30 | 31 | - name: Typecheck 32 | run: pnpm typecheck 33 | 34 | test: 35 | runs-on: ${{ matrix.os }} 36 | 37 | strategy: 38 | matrix: 39 | os: [ubuntu-latest, windows-latest] 40 | node: [18, 20, 22] 41 | fail-fast: false 42 | 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v4 46 | 47 | - name: Install pnpm 48 | uses: pnpm/action-setup@v4.1.0 49 | 50 | - name: Set node ${{ matrix.node }} 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version: ${{ matrix.node }} 54 | cache: pnpm 55 | 56 | - name: Install 57 | run: pnpm i 58 | 59 | - name: Build 60 | run: pnpm run build 61 | 62 | - name: Test 63 | run: pnpm run test 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.log 5 | .vercel 6 | .eslintcache 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "eslint.experimental.useFlatConfig": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2024-PRESENT 三咲智子 (https://github.com/sxzz) 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-replace [![npm](https://img.shields.io/npm/v/unplugin-replace.svg)](https://npmjs.com/package/unplugin-replace) [![JSR](https://jsr.io/badges/@unplugin/replace)](https://jsr.io/@unplugin/replace) 2 | 3 | [![Unit Test](https://github.com/unplugin/unplugin-replace/actions/workflows/unit-test.yml/badge.svg)](https://github.com/unplugin/unplugin-replace/actions/workflows/unit-test.yml) 4 | 5 | 🍣 A universal bundler plugin which replaces targeted strings in files, based on [@rollup/plugin-replace](https://www.npmjs.com/package/@rollup/plugin-replace). 6 | 7 | ## Installation 8 | 9 | ```bash 10 | # npm 11 | npm i -D unplugin-replace 12 | 13 | # jsr 14 | npx jsr add -D @unplugin/replace 15 | ``` 16 | 17 |
18 | Vite
19 | 20 | ```ts 21 | // vite.config.ts 22 | import Replace from 'unplugin-replace/vite' 23 | 24 | export default defineConfig({ 25 | plugins: [Replace()], 26 | }) 27 | ``` 28 | 29 |
30 | 31 |
32 | Rollup
33 | 34 | ```ts 35 | // rollup.config.js 36 | import Replace from 'unplugin-replace/rollup' 37 | 38 | export default { 39 | plugins: [Replace()], 40 | } 41 | ``` 42 | 43 |
44 | 45 |
46 | esbuild
47 | 48 | ```ts 49 | // esbuild.config.js 50 | import { build } from 'esbuild' 51 | 52 | build({ 53 | plugins: [require('unplugin-replace/esbuild')()], 54 | }) 55 | ``` 56 | 57 |
58 | 59 |
60 | Webpack
61 | 62 | ```ts 63 | // webpack.config.js 64 | module.exports = { 65 | /* ... */ 66 | plugins: [require('unplugin-replace/webpack')()], 67 | } 68 | ``` 69 | 70 |
71 | 72 | ## Usage 73 | 74 | ### Options 75 | 76 | For all options please refer to [docs](https://github.com/rollup/plugins/tree/master/packages/replace#options). 77 | 78 | This plugin accepts all [@rollup/plugin-replace](https://github.com/rollup/plugins/tree/master/packages/replace#options) options, and some extra options that are specific to this plugin. 79 | 80 | ### `options.values` 81 | 82 | - Type: `{ [find: string]: Replacement } | ReplaceItem[]` 83 | - Default: `[]` 84 | 85 | ```ts 86 | type ReplaceItem = { 87 | /** Supply a string or RegExp to find what you are looking for. */ 88 | find: string | RegExp 89 | 90 | /** 91 | * Can be a string or a function. 92 | * - If it's a string, it will replace the substring matched by pattern. A number of special replacement patterns are supported 93 | * - If it's a function, it will be invoked for every match and its return value is used as the replacement text. 94 | */ 95 | replacement: Replacement 96 | } 97 | type Replacement = string | ((id: string, match: RegExpExecArray) => string) 98 | ``` 99 | 100 | Comparing with `@rollup/plugin-replace`, `find` option supports regex pattern. 101 | 102 | **Example:** 103 | 104 | ```ts 105 | Replace({ 106 | values: [ 107 | { 108 | find: /apples/gi, 109 | replacement: 'oranges', 110 | }, 111 | { 112 | find: 'process.env.NODE_ENV', 113 | replacement: '"production"', 114 | }, 115 | ], 116 | }) 117 | ``` 118 | 119 | ## Sponsors 120 | 121 |

122 | 123 | 124 | 125 |

126 | 127 | ## License 128 | 129 | [MIT](./LICENSE) License © 2024-PRESENT [三咲智子](https://github.com/sxzz) 130 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { sxzz } from '@sxzz/eslint-config' 2 | export default sxzz() 3 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unplugin/replace", 3 | "version": "0.6.0", 4 | "exports": { 5 | "./index": "./src/index.ts", 6 | "./esbuild": "./src/esbuild.ts", 7 | "./rollup": "./src/rollup.ts", 8 | "./rolldown": "./src/rolldown.ts", 9 | "./vite": "./src/vite.ts", 10 | "./webpack": "./src/webpack.ts", 11 | "./rspack": "./src/rspack.ts", 12 | "./farm": "./src/farm.ts" 13 | }, 14 | "publish": { 15 | "include": [ 16 | "src", 17 | "package.json", 18 | "jsr.json", 19 | "README.md", 20 | "LICENSE" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unplugin-replace", 3 | "version": "0.6.0", 4 | "packageManager": "pnpm@10.11.0", 5 | "description": "A universal bundler plugin which replaces targeted strings in files.", 6 | "type": "module", 7 | "keywords": [ 8 | "unplugin", 9 | "rollup", 10 | "vite", 11 | "esbuild", 12 | "webpack" 13 | ], 14 | "license": "MIT", 15 | "homepage": "https://github.com/unplugin/unplugin-replace#readme", 16 | "bugs": { 17 | "url": "https://github.com/unplugin/unplugin-replace/issues" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/unplugin/unplugin-replace.git" 22 | }, 23 | "author": "三咲智子 Kevin Deng ", 24 | "funding": "https://github.com/sponsors/sxzz", 25 | "files": [ 26 | "dist" 27 | ], 28 | "main": "./dist/index.js", 29 | "module": "./dist/index.js", 30 | "types": "./dist/index.d.ts", 31 | "exports": { 32 | ".": "./dist/index.js", 33 | "./vite": "./dist/vite.js", 34 | "./webpack": "./dist/webpack.js", 35 | "./rspack": "./dist/rspack.js", 36 | "./rollup": "./dist/rollup.js", 37 | "./rolldown": "./dist/rolldown.js", 38 | "./esbuild": "./dist/esbuild.js", 39 | "./farm": "./dist/farm.js", 40 | "./*": "./*" 41 | }, 42 | "typesVersions": { 43 | "*": { 44 | "*": [ 45 | "./dist/*", 46 | "./*" 47 | ] 48 | } 49 | }, 50 | "publishConfig": { 51 | "access": "public" 52 | }, 53 | "scripts": { 54 | "lint": "eslint --cache .", 55 | "lint:fix": "pnpm run lint --fix", 56 | "build": "tsdown", 57 | "dev": "tsdown --watch", 58 | "test": "vitest", 59 | "typecheck": "tsc --noEmit", 60 | "release": "bumpp && pnpm publish", 61 | "prepublishOnly": "pnpm run build" 62 | }, 63 | "dependencies": { 64 | "magic-string": "^0.30.17", 65 | "unplugin": "^2.3.4" 66 | }, 67 | "devDependencies": { 68 | "@sxzz/eslint-config": "^7.0.1", 69 | "@sxzz/prettier-config": "^2.2.1", 70 | "@sxzz/test-utils": "^0.5.6", 71 | "@types/node": "^22.15.21", 72 | "bumpp": "^10.1.1", 73 | "esbuild": "^0.25.4", 74 | "eslint": "^9.27.0", 75 | "fast-glob": "^3.3.3", 76 | "prettier": "^3.5.3", 77 | "rolldown": "nightly", 78 | "rollup": "^4.41.1", 79 | "tsdown": "^0.12.3", 80 | "tsx": "^4.19.4", 81 | "typescript": "^5.8.3", 82 | "vite": "^6.3.5", 83 | "vitest": "^3.1.4", 84 | "webpack": "^5.99.9" 85 | }, 86 | "engines": { 87 | "node": ">=20.18.0" 88 | }, 89 | "prettier": "@sxzz/prettier-config" 90 | } 91 | -------------------------------------------------------------------------------- /src/core/options.ts: -------------------------------------------------------------------------------- 1 | import type { FilterPattern } from 'unplugin' 2 | 3 | export type Replacement = 4 | | string 5 | | ((id: string, match: RegExpExecArray) => string) 6 | export type ReplaceItem = { 7 | find: F 8 | replacement: Replacement 9 | } 10 | export type ReplaceMap = { 11 | [str: string]: Replacement 12 | } 13 | 14 | export interface BaseOptions { 15 | /** 16 | * A picomatch pattern, or array of patterns, of files that should be 17 | * processed by this plugin (if omitted, all files are included by default) 18 | */ 19 | include?: FilterPattern 20 | /** 21 | * Files that should be excluded, if `include` is otherwise too permissive. 22 | */ 23 | exclude?: FilterPattern 24 | /** 25 | * If false, skips source map generation. This will improve performance. 26 | * 27 | * For Vite, this option will be enabled on build mode 28 | * and respect `build.sourcemap`. 29 | * @default true 30 | */ 31 | sourceMap?: boolean 32 | /** 33 | * To replace every occurrence of `<@foo@>` instead of every occurrence 34 | * of `foo`, supply delimiters 35 | */ 36 | delimiters?: [string, string] 37 | /** 38 | * Prevents replacing strings where they are followed by a single equals 39 | * sign. 40 | */ 41 | preventAssignment?: boolean 42 | objectGuards?: boolean 43 | /** 44 | * You can separate values to replace from other options. 45 | */ 46 | values?: ReplaceMap | ReplaceItem[] 47 | enforce?: 'pre' | 'post' | undefined 48 | } 49 | 50 | export interface Options extends BaseOptions { 51 | /** 52 | * All other options are treated as `string: replacement` replacers, 53 | * or `string: (id) => replacement` functions. 54 | */ 55 | [str: string]: 56 | | Replacement 57 | | BaseOptions['include'] 58 | | BaseOptions['values'] 59 | | BaseOptions['preventAssignment'] 60 | } 61 | type Overwrite = Pick> & U 62 | 63 | export type OptionsResolved = Overwrite< 64 | Required, 65 | { 66 | include: BaseOptions['include'] 67 | exclude: BaseOptions['exclude'] 68 | enforce: BaseOptions['enforce'] 69 | values: ReplaceItem[] 70 | } 71 | > 72 | 73 | export function resolveOptions(options: Options): OptionsResolved { 74 | return { 75 | include: options.include, 76 | exclude: options.exclude, 77 | sourceMap: options.sourceMap ?? true, 78 | delimiters: options.delimiters || [String.raw`\b`, String.raw`\b(?!\.)`], 79 | preventAssignment: options.preventAssignment ?? false, 80 | objectGuards: options.objectGuards ?? false, 81 | values: getReplacements(), 82 | enforce: 'enforce' in options ? options.enforce : 'pre', 83 | } 84 | 85 | function getReplacements() { 86 | if (options.values) { 87 | if (Array.isArray(options.values)) { 88 | return options.values 89 | } 90 | return normalizeObjectValues(options.values) 91 | } 92 | 93 | const values = Object.assign({}, options) 94 | delete values.delimiters 95 | delete values.include 96 | delete values.exclude 97 | delete values.sourcemap 98 | delete values.sourceMap 99 | delete values.objectGuards 100 | return normalizeObjectValues(values as ReplaceMap) 101 | } 102 | } 103 | 104 | function normalizeObjectValues(values: ReplaceMap): ReplaceItem[] { 105 | return Object.entries(values).map(([find, replacement]) => ({ 106 | find, 107 | replacement, 108 | })) 109 | } 110 | -------------------------------------------------------------------------------- /src/esbuild.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for esbuild plugin. Requires esbuild >= 0.15 3 | * 4 | * @module 5 | */ 6 | 7 | import unplugin from './index' 8 | 9 | /** 10 | * Esbuild plugin 11 | * 12 | * @example 13 | * ```ts 14 | * import { build } from 'esbuild' 15 | * import Replace from 'unplugin-replace/esbuild' 16 | * 17 | * build({ plugins: [Replace()] }) 18 | ``` 19 | */ 20 | const esbuild = unplugin.esbuild as typeof unplugin.esbuild 21 | export default esbuild 22 | -------------------------------------------------------------------------------- /src/farm.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for Farm plugin. 3 | * 4 | * @module 5 | */ 6 | 7 | import unplugin from './index' 8 | 9 | /** 10 | * Farm plugin 11 | * 12 | * @example 13 | * ```ts 14 | * // farm.config.js 15 | * import Replace from 'unplugin-replace/farm' 16 | * 17 | * export default { 18 | * plugins: [Replace()], 19 | * } 20 | * ``` 21 | */ 22 | const farm = unplugin.farm as typeof unplugin.farm 23 | export default farm 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for main unplugin. 3 | * @module 4 | */ 5 | 6 | import MagicString from 'magic-string' 7 | import { 8 | createUnplugin, 9 | type TransformResult, 10 | type UnpluginInstance, 11 | } from 'unplugin' 12 | import { resolveOptions, type Options, type ReplaceItem } from './core/options' 13 | 14 | /** 15 | * The main unplugin instance. 16 | */ 17 | const plugin: UnpluginInstance = createUnplugin< 18 | Options | undefined, 19 | false 20 | >((rawOptions = {}) => { 21 | const options = resolveOptions(rawOptions) 22 | const { 23 | include, 24 | exclude, 25 | enforce, 26 | delimiters, 27 | objectGuards, 28 | preventAssignment, 29 | } = options 30 | const stringValues = options.values.filter( 31 | (value): value is ReplaceItem => typeof value.find === 'string', 32 | ) 33 | const regexpValues = options.values.filter( 34 | (value): value is ReplaceItem => value.find instanceof RegExp, 35 | ) 36 | 37 | if (objectGuards) expandTypeofReplacements(stringValues) 38 | const pattern = buildStringPattern() 39 | 40 | const values = [...regexpValues] 41 | if (pattern) { 42 | values.unshift({ find: pattern, replacement: null! }) 43 | } 44 | 45 | const name = 'unplugin-replace' 46 | return { 47 | name, 48 | enforce, 49 | 50 | buildStart() { 51 | if (![true, false].includes(preventAssignment)) { 52 | console.warn({ 53 | message: `${name}: 'preventAssignment' currently defaults to false. It is recommended to set this option to \`true\`, as the next major version will default this option to \`true\`.`, 54 | }) 55 | } 56 | }, 57 | 58 | transform: { 59 | filter: { id: { include, exclude } }, 60 | handler(code, id) { 61 | if (values.length === 0) return 62 | 63 | return executeReplacement(code, id) 64 | }, 65 | }, 66 | 67 | vite: { 68 | configResolved(config) { 69 | options.sourceMap = 70 | config.command === 'build' ? !!config.build.sourcemap : true 71 | }, 72 | }, 73 | } 74 | 75 | function executeReplacement(code: string, id: string) { 76 | const magicString = new MagicString(code) 77 | if (!codeHasReplacements(code, id, magicString)) { 78 | return null 79 | } 80 | 81 | const result: TransformResult = { code: magicString.toString() } 82 | if (options.sourceMap) { 83 | result.map = magicString.generateMap({ hires: true }) 84 | } 85 | return result 86 | } 87 | 88 | function codeHasReplacements( 89 | code: string, 90 | id: string, 91 | magicString: MagicString, 92 | ) { 93 | let has = false 94 | let match: RegExpExecArray | null 95 | 96 | for (const { find, replacement } of values) { 97 | while ((match = find.exec(code))) { 98 | has = true 99 | 100 | const start = match.index 101 | const end = start + match[0].length 102 | 103 | const finalReplacement = 104 | find === pattern 105 | ? stringValues.find(({ find }) => find === match![1])!.replacement 106 | : replacement 107 | const result = String(ensureFunction(finalReplacement)(id, match)) 108 | magicString.overwrite(start, end, result) 109 | 110 | if (!find.global) break 111 | } 112 | } 113 | 114 | return has 115 | } 116 | 117 | function buildStringPattern(): RegExp | undefined { 118 | const escapedKeys = stringValues 119 | .map(({ find }) => find) 120 | .sort(longest) 121 | .map(escape) 122 | const lookbehind = preventAssignment 123 | ? String.raw`(? 0) { 132 | return pattern 133 | } 134 | } 135 | }) 136 | export default plugin 137 | 138 | function escape(str: string) { 139 | // eslint-disable-next-line unicorn/prefer-string-replace-all 140 | return str.replace(/[$()*+./?[\\\]^{|}-]/g, String.raw`\$&`) 141 | } 142 | 143 | function ensureFunction( 144 | functionOrValue: any, 145 | ): (id: string, match: RegExpExecArray) => any { 146 | if (typeof functionOrValue === 'function') return functionOrValue 147 | return () => functionOrValue 148 | } 149 | 150 | function longest(a: string, b: string) { 151 | return b.length - a.length 152 | } 153 | 154 | const objKeyRegEx = 155 | /^([$A-Z_\u00A0-\uFFFF][\w$\u00A0-\uFFFF]*)(\.([$A-Z_\u00A0-\uFFFF][\w$\u00A0-\uFFFF]*))+$/i 156 | function expandTypeofReplacements(values: ReplaceItem[]) { 157 | values.forEach(({ find }) => { 158 | const objMatch = find.match(objKeyRegEx) 159 | if (!objMatch) return 160 | let dotIndex = objMatch[1].length 161 | let lastIndex = 0 162 | do { 163 | values.push( 164 | { 165 | find: `typeof ${find.slice(lastIndex, dotIndex)} ===`, 166 | replacement: '"object" ===', 167 | }, 168 | { 169 | find: `typeof ${find.slice(lastIndex, dotIndex)} !==`, 170 | replacement: '"object" !==', 171 | }, 172 | { 173 | find: `typeof ${find.slice(lastIndex, dotIndex)}===`, 174 | replacement: '"object"===', 175 | }, 176 | { 177 | find: `typeof ${find.slice(lastIndex, dotIndex)}!==`, 178 | replacement: '"object"!==', 179 | }, 180 | { 181 | find: `typeof ${find.slice(lastIndex, dotIndex)} ==`, 182 | replacement: '"object" ===', 183 | }, 184 | { 185 | find: `typeof ${find.slice(lastIndex, dotIndex)} !=`, 186 | replacement: '"object" !==', 187 | }, 188 | { 189 | find: `typeof ${find.slice(lastIndex, dotIndex)}==`, 190 | replacement: '"object"===', 191 | }, 192 | { 193 | find: `typeof ${find.slice(lastIndex, dotIndex)}!=`, 194 | replacement: '"object"!==', 195 | }, 196 | ) 197 | lastIndex = dotIndex + 1 198 | dotIndex = find.indexOf('.', lastIndex) 199 | } while (dotIndex !== -1) 200 | }) 201 | } 202 | -------------------------------------------------------------------------------- /src/rolldown.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for Rolldown plugin. 3 | * 4 | * @module 5 | */ 6 | 7 | import unplugin from './index' 8 | 9 | /** 10 | * Rolldown plugin 11 | * 12 | * @example 13 | * ```ts 14 | * // rolldown.config.js 15 | * import Replace from 'unplugin-replace/rolldown' 16 | * 17 | * export default { 18 | * plugins: [Replace()], 19 | * } 20 | * ``` 21 | */ 22 | const rolldown = unplugin.rolldown as typeof unplugin.rolldown 23 | export default rolldown 24 | -------------------------------------------------------------------------------- /src/rollup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for Rollup plugin. 3 | * 4 | * @module 5 | */ 6 | 7 | import unplugin from './index' 8 | 9 | /** 10 | * Rollup plugin 11 | * 12 | * @example 13 | * ```ts 14 | * // rollup.config.js 15 | * import Replace from 'unplugin-replace/rollup' 16 | * 17 | * export default { 18 | * plugins: [Replace()], 19 | * } 20 | * ``` 21 | */ 22 | const rollup = unplugin.rollup as typeof unplugin.rollup 23 | export default rollup 24 | -------------------------------------------------------------------------------- /src/rspack.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for Rspack plugin. 3 | * 4 | * @module 5 | */ 6 | 7 | import unplugin from './index' 8 | 9 | /** 10 | * Rspack plugin 11 | * 12 | * @example 13 | * ```js 14 | * // rspack.config.js 15 | * import Replace from 'unplugin-replace/rspack' 16 | * 17 | * default export { 18 | * plugins: [Replace()], 19 | * } 20 | * ``` 21 | */ 22 | const rspack = unplugin.rspack as typeof unplugin.rspack 23 | export default rspack 24 | -------------------------------------------------------------------------------- /src/vite.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for Vite plugin. 3 | * 4 | * @module 5 | */ 6 | 7 | import unplugin from './index' 8 | 9 | /** 10 | * Vite plugin 11 | * 12 | * @example 13 | * ```ts 14 | * // vite.config.ts 15 | * import Replace from 'unplugin-replace/vite' 16 | * 17 | * export default defineConfig({ 18 | * plugins: [Replace()], 19 | * }) 20 | * ``` 21 | */ 22 | const vite = unplugin.vite as typeof unplugin.vite 23 | export default vite 24 | -------------------------------------------------------------------------------- /src/webpack.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for webpack plugin. 3 | * 4 | * @module 5 | */ 6 | 7 | import unplugin from './index' 8 | 9 | /** 10 | * Webpack plugin 11 | * 12 | * @example 13 | * ```js 14 | * // webpack.config.js 15 | * import Replace from 'unplugin-replace/webpack' 16 | * 17 | * default export { 18 | * plugins: [Replace()], 19 | * } 20 | * ``` 21 | */ 22 | const webpack = unplugin.webpack as typeof unplugin.webpack 23 | export default webpack 24 | -------------------------------------------------------------------------------- /tests/__snapshots__/esbuild.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`esbuild 1`] = ` 4 | "// tests/fixtures/main.js 5 | var platform = "darwin"; 6 | console.log('hello "darwin"', platform); 7 | console.log(DEV); 8 | export { 9 | platform 10 | }; 11 | " 12 | `; 13 | -------------------------------------------------------------------------------- /tests/__snapshots__/rolldown.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`rolldown 1`] = ` 4 | "import "node:process"; 5 | 6 | //#region tests/fixtures/main.js 7 | const platform = "darwin"; 8 | console.log("hello \\"darwin\\"", platform); 9 | console.log(DEV); 10 | 11 | //#endregion 12 | export { platform };" 13 | `; 14 | -------------------------------------------------------------------------------- /tests/__snapshots__/rollup.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`rollup > find regexp 1`] = ` 4 | "// main.js 5 | import 'node:process'; 6 | 7 | const platform = null; 8 | 9 | console.info('hello null', platform); 10 | console.info(dEV); 11 | 12 | export { platform }; 13 | " 14 | `; 15 | 16 | exports[`rollup > find string 1`] = ` 17 | "// main.js 18 | import 'node:process'; 19 | 20 | const platform = "darwin"; 21 | 22 | console.log('hello "darwin"', platform); 23 | console.log(DEV); 24 | 25 | export { platform }; 26 | " 27 | `; 28 | -------------------------------------------------------------------------------- /tests/__snapshots__/vite.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`vite 1`] = ` 4 | "import "node:process"; 5 | const platform = "darwin"; 6 | console.log('hello "darwin"', platform); 7 | console.log(DEV); 8 | " 9 | `; 10 | -------------------------------------------------------------------------------- /tests/__snapshots__/webpack.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`webpack 1`] = ` 4 | "/******/ (() => { // webpackBootstrap 5 | /******/ "use strict"; 6 | 7 | // UNUSED EXPORTS: platform 8 | 9 | ;// external "node:process" 10 | const external_node_process_namespaceObject = require("node:process"); 11 | ;// ./tests/fixtures/main.js 12 | 13 | 14 | const platform = "darwin" 15 | 16 | console.log('hello "darwin"', platform) 17 | console.log(DEV) 18 | 19 | /******/ })() 20 | ;" 21 | `; 22 | -------------------------------------------------------------------------------- /tests/esbuild.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { build } from 'esbuild' 3 | import { expect, test } from 'vitest' 4 | import UnpluginReplace from '../src/esbuild' 5 | 6 | test('esbuild', async () => { 7 | const { outputFiles } = await build({ 8 | entryPoints: [path.resolve(__dirname, 'fixtures/main.js')], 9 | format: 'esm', 10 | write: false, 11 | bundle: true, 12 | platform: 'node', 13 | plugins: [ 14 | UnpluginReplace({ 15 | 'process.platform': '"darwin"', 16 | }), 17 | ], 18 | }) 19 | expect(outputFiles[0].text).toMatchSnapshot() 20 | expect(outputFiles[0].text).not.contains('process.platform') 21 | expect(outputFiles[0].text).contains('"darwin"') 22 | }) 23 | -------------------------------------------------------------------------------- /tests/fixtures/main.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | export const platform = process.platform 4 | 5 | console.log('hello process.platform', platform) 6 | console.log(DEV) 7 | -------------------------------------------------------------------------------- /tests/rolldown.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { rolldown } from 'rolldown' 3 | import { expect, test } from 'vitest' 4 | import UnpluginReplace from '../src/rolldown' 5 | 6 | test('rolldown', async () => { 7 | const build = await rolldown({ 8 | input: path.resolve(__dirname, 'fixtures/main.js'), 9 | external: ['node:process'], 10 | plugins: [ 11 | UnpluginReplace({ 12 | 'process.platform': '"darwin"', 13 | }), 14 | ], 15 | }) 16 | const { output } = await build.generate({ format: 'es' }) 17 | const code = output[0].code 18 | expect(code).toMatchSnapshot() 19 | expect(code).not.contains('process.platform') 20 | expect(code).contains('"darwin"') 21 | }) 22 | -------------------------------------------------------------------------------- /tests/rollup.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { rollupBuild } from '@sxzz/test-utils' 3 | import { describe, expect, test } from 'vitest' 4 | import UnpluginReplace from '../src/rollup' 5 | 6 | describe('rollup', () => { 7 | test('find string', async () => { 8 | const { snapshot } = await rollupBuild( 9 | path.resolve(__dirname, 'fixtures/main.js'), 10 | [ 11 | UnpluginReplace({ 12 | 'process.platform': '"darwin"', 13 | }), 14 | ], 15 | ) 16 | expect(snapshot).toMatchSnapshot() 17 | expect(snapshot).not.contains('process.platform') 18 | expect(snapshot).contains('"darwin"') 19 | }) 20 | 21 | test('find regexp', async () => { 22 | const { snapshot } = await rollupBuild( 23 | path.resolve(__dirname, 'fixtures/main.js'), 24 | [ 25 | UnpluginReplace({ 26 | values: [ 27 | { find: /process\.\w+/g, replacement: 'null' }, 28 | { 29 | find: /[A-Z]/, 30 | replacement: (id, match) => match[0].toLowerCase(), 31 | }, 32 | { 33 | find: /console\.\w+/g, 34 | replacement: () => `console.info`, 35 | }, 36 | ], 37 | }), 38 | ], 39 | ) 40 | expect(snapshot).toMatchSnapshot() 41 | expect(snapshot).contains('console.info(dEV)') 42 | expect(snapshot).contains(`console.info('hello null', platform)`) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /tests/vite.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { build } from 'vite' 3 | import { expect, test } from 'vitest' 4 | import UnpluginReplace from '../src/vite' 5 | import type { RollupOutput } from 'rollup' 6 | 7 | test('vite', async () => { 8 | const root = path.resolve(__dirname, 'fixtures') 9 | const { output } = (await build({ 10 | root, 11 | build: { 12 | minify: false, 13 | rollupOptions: { 14 | input: [path.resolve(root, 'main.js')], 15 | external: (id) => id === 'node:process', 16 | logLevel: 'silent', 17 | }, 18 | write: false, 19 | }, 20 | logLevel: 'silent', 21 | plugins: [ 22 | UnpluginReplace({ 23 | 'process.platform': '"darwin"', 24 | }), 25 | ], 26 | })) as RollupOutput 27 | expect(output[0].code).toMatchSnapshot() 28 | expect(output[0].code).not.contains('process.platform') 29 | expect(output[0].code).contains('"darwin"') 30 | }) 31 | -------------------------------------------------------------------------------- /tests/webpack.test.ts: -------------------------------------------------------------------------------- 1 | import { mkdtemp, readFile, rm } from 'node:fs/promises' 2 | import os from 'node:os' 3 | import path from 'node:path' 4 | import { expect, onTestFinished, test } from 'vitest' 5 | import webpack from 'webpack' 6 | import UnpluginReplace from '../src/webpack' 7 | 8 | test('webpack', async () => { 9 | const tmp = await mkdtemp(path.join(os.tmpdir(), 'unplugin-replace-')) 10 | onTestFinished(() => rm(tmp, { recursive: true, force: true })) 11 | 12 | await new Promise((resolve, reject) => { 13 | const compiler = webpack({ 14 | entry: path.resolve(__dirname, 'fixtures/main.js'), 15 | output: { 16 | path: tmp, 17 | }, 18 | plugins: [ 19 | UnpluginReplace({ 20 | 'process.platform': '"darwin"', 21 | }), 22 | ], 23 | mode: 'production', 24 | target: 'node', 25 | optimization: { 26 | minimize: false, 27 | }, 28 | }) 29 | compiler.run((error, stats) => { 30 | if (error) return reject(error) 31 | resolve(stats!) 32 | }) 33 | }) 34 | 35 | const result = await readFile(path.resolve(tmp, 'main.js'), 'utf-8') 36 | expect(result).toMatchSnapshot() 37 | expect(result).not.contains('process.platform') 38 | expect(result).contains('"darwin"') 39 | }) 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["es2022"], 5 | "moduleDetection": "force", 6 | "module": "preserve", 7 | "moduleResolution": "bundler", 8 | "resolveJsonModule": true, 9 | "types": ["node"], 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "declaration": true, 13 | "isolatedDeclarations": true, 14 | "esModuleInterop": true, 15 | "isolatedModules": true, 16 | "verbatimModuleSyntax": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src", "tests"] 20 | } 21 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | entry: './src/*.ts', 5 | }) 6 | --------------------------------------------------------------------------------