├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── package.json ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | yarn.lock 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.json 3 | .prettierrc 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "useTabs": false, 8 | "quoteProps": "consistent", 9 | "bracketSpacing": true, 10 | "jsxBracketSameLine": false, 11 | "arrowParens": "always" 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ikeq 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 | # vite-plugin-filter-replace [![npm](https://img.shields.io/npm/v/vite-plugin-filter-replace.svg)](https://npmjs.com/package/vite-plugin-filter-replace) 2 | 3 | Apply filename based replacements. 4 | 5 | ```ts 6 | import vue from '@vitejs/plugin-vue'; 7 | import replace from 'vite-plugin-filter-replace'; 8 | 9 | export default { 10 | plugins: [ 11 | replace( 12 | [ 13 | { 14 | filter: /\.css$/, 15 | replace: { 16 | from: /__foo__/g, 17 | to: 'foo', 18 | }, 19 | }, 20 | { 21 | filter: /\.css$/, 22 | replace: [ 23 | { from: /__foo__/g, to: 'foo' }, 24 | { from: /__(foo)__/g, to: '$1' }, 25 | ], 26 | }, 27 | { 28 | filter: ['node_modules/moment/dist/moment.js'], 29 | replace(source, path) { 30 | return 'some code'; 31 | }, 32 | }, 33 | ], 34 | { enforce: 'pre' }, 35 | ), 36 | ], 37 | }; 38 | ``` 39 | 40 | ## Options 41 | 42 | ```ts 43 | function replace(replacements: Replacement[]): Plugin; 44 | function replace(replacements: Replacement[], options: Options): Plugin; 45 | 46 | interface Replacement { 47 | filter: RegExp | string | string[]; 48 | replace: { from: RegExp | string | string[]; to: string | number }; 49 | } 50 | 51 | interface Replacement { 52 | filter: RegExp | string | string[]; 53 | replace: (source: string, path: string) => string; 54 | } 55 | 56 | interface Replacement { 57 | filter: RegExp | string | string[]; 58 | replace: { from: RegExp | string | string[]; to: string | number }[]; 59 | } 60 | 61 | interface Replacement { 62 | filter: RegExp | string | string[]; 63 | replace: ((source: string, path: string) => string)[]; 64 | } 65 | 66 | interface Options { 67 | enforce?: 'pre' | 'post'; 68 | apply?: 'serve' | 'build'; 69 | } 70 | ``` 71 | 72 | ## License 73 | 74 | MIT 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-filter-replace", 3 | "version": "0.1.14", 4 | "description": "Apply filename based replacements.", 5 | "files": [ 6 | "dist" 7 | ], 8 | "main": "./dist/index.cjs", 9 | "module": "./dist/index.mjs", 10 | "types": "./dist/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "import": "./dist/index.mjs", 15 | "require": "./dist/index.cjs" 16 | } 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/ikeq/vite-plugin-filter-replace.git" 21 | }, 22 | "keywords": [ 23 | "vite", 24 | "replace" 25 | ], 26 | "author": "ikeq", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/ikeq/vite-plugin-filter-replace/issues" 30 | }, 31 | "homepage": "https://github.com/ikeq/vite-plugin-filter-replace#readme", 32 | "devDependencies": { 33 | "@types/node": "^18.11.18", 34 | "typescript": "^4.9.4", 35 | "vite": "^4.0.4" 36 | }, 37 | "scripts": { 38 | "build:esm": "tsc --module esnext && cd dist && mv index.js index.mjs", 39 | "build:cjs": "tsc && cd dist && mv index.js index.cjs", 40 | "build": "yarn build:esm && yarn build:cjs" 41 | }, 42 | "dependencies": { 43 | "magic-string": "^0.30.17" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import { Plugin } from 'vite'; 3 | import { PluginBuild } from 'esbuild'; 4 | import MagicString, { SourceMap } from 'magic-string'; 5 | 6 | type ReplaceFn = (source: string, path: string) => string; 7 | type ReplacePair = { from: RegExp | string | string[]; to: string | number }; 8 | 9 | interface Replacement { 10 | /** 11 | * for debugging purpose 12 | */ 13 | id?: string | number; 14 | filter: RegExp | string | string[]; 15 | replace: ReplacePair | ReplaceFn | Array; 16 | } 17 | 18 | interface Options extends Pick {} 19 | 20 | function escape(str: string): string { 21 | return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); 22 | } 23 | 24 | function parseReplacements( 25 | replacements: Replacement[], 26 | ): Array & { filter: RegExp; replace: ReplaceFn[] }> { 27 | if (!replacements || !replacements.length) return []; 28 | 29 | // TODO: 30 | // re-group replacements to ensure filter is unique 31 | 32 | return replacements.reduce((entries: any[], replacement) => { 33 | const filter = 34 | replacement.filter instanceof RegExp 35 | ? replacement.filter 36 | : new RegExp( 37 | `(${[] 38 | .concat(replacement.filter as any) 39 | .filter((i) => i) 40 | .map((i: string) => escape(i.trim().replace(/\\+/g, '/'))) 41 | .join('|')})`, 42 | ); 43 | let { replace = [] } = replacement; 44 | 45 | if (!filter) return entries; 46 | if (typeof replace === 'function' || !Array.isArray(replace)) { 47 | replace = [replace]; 48 | } 49 | 50 | replace = replace.reduce((entries: ReplaceFn[], rp) => { 51 | if (typeof rp === 'function') return entries.concat(rp); 52 | 53 | const { from, to } = rp; 54 | 55 | if (from === undefined || to === undefined) return entries; 56 | 57 | return entries.concat((source) => 58 | source.replace( 59 | from instanceof RegExp 60 | ? from 61 | : new RegExp( 62 | `(${[] 63 | .concat(from as any) 64 | .map(escape) 65 | .join('|')})`, 66 | 'g', 67 | ), 68 | String(to), 69 | ), 70 | ); 71 | }, []); 72 | 73 | if (!replace.length) return entries; 74 | 75 | return entries.concat({ ...replacement, filter, replace }); 76 | }, []); 77 | } 78 | 79 | export default function (replacements: Replacement[] = [], options: Options = {}): Plugin { 80 | const resolvedReplacements = parseReplacements(replacements); 81 | let isServe = true; 82 | let sourcemap = false; 83 | 84 | if (!resolvedReplacements.length) return {} as any; 85 | 86 | function replace(code: string, id: string): string; 87 | function replace(code: string, id: string, sourcemap: boolean): { code: string; map: SourceMap }; 88 | function replace( 89 | code: string, 90 | id: string, 91 | sourcemap?: boolean, 92 | ): string | { code: string; map: SourceMap } { 93 | const replaced = resolvedReplacements.reduce((code, rp) => { 94 | if (!rp.filter.test(id)) return code; 95 | return rp.replace.reduce((text, replace) => replace(text, id), code); 96 | }, code); 97 | 98 | if (!sourcemap) return replaced; 99 | 100 | return { 101 | code: replaced, 102 | map: new MagicString(replaced).generateMap({ hires: true }), 103 | }; 104 | } 105 | 106 | return { 107 | name: 'vite-plugin-filter-replace', 108 | enforce: options.enforce, 109 | apply: options.apply, 110 | config: (config, env) => { 111 | isServe = env.command === 'serve'; 112 | sourcemap = !!config.build?.sourcemap; 113 | 114 | if (!isServe) return; 115 | 116 | if (!config.optimizeDeps) { 117 | config.optimizeDeps = {}; 118 | } 119 | if (!config.optimizeDeps.esbuildOptions) { 120 | config.optimizeDeps.esbuildOptions = {}; 121 | } 122 | if (!config.optimizeDeps.esbuildOptions.plugins) { 123 | config.optimizeDeps.esbuildOptions.plugins = []; 124 | } 125 | 126 | config.optimizeDeps.esbuildOptions.plugins.unshift( 127 | ...resolvedReplacements.map((option) => { 128 | return { 129 | name: 'vite-plugin-filter-replace' + (option.id ? `:${option.id}` : ''), 130 | setup(build: PluginBuild) { 131 | build.onLoad({ filter: option.filter, namespace: 'file' }, async ({ path }) => { 132 | const source = await fs.readFile(path, 'utf8'); 133 | 134 | return { 135 | loader: 'default', 136 | contents: option.replace.reduce((text, replace) => replace(text, path), source), 137 | }; 138 | }); 139 | }, 140 | }; 141 | }), 142 | ); 143 | 144 | return config; 145 | }, 146 | renderChunk(code, chunk) { 147 | if (isServe) return null; 148 | return replace(code, chunk.fileName, sourcemap); 149 | }, 150 | transform(code, id) { 151 | return replace(code, id, sourcemap); 152 | }, 153 | async handleHotUpdate(ctx) { 154 | const defaultRead = ctx.read; 155 | ctx.read = async function () { 156 | return replace(await defaultRead(), ctx.file); 157 | }; 158 | }, 159 | }; 160 | } 161 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "exclude": ["**/*.spec.ts"], 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "target": "ESNext", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "strict": true, 10 | "declaration": true, 11 | "sourceMap": false, 12 | "noUnusedLocals": true, 13 | "esModuleInterop": true, 14 | "baseUrl": "." 15 | } 16 | } 17 | --------------------------------------------------------------------------------