├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── README.zh-cn.md ├── assets └── logo.svg ├── eslint.config.js ├── examples ├── vite-vue-ts │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── vue-cli │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── App.vue │ └── main.js │ └── vue.config.js ├── package.json ├── playground ├── index.html ├── main.ts ├── package.json └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── postbuild.ts ├── src ├── core │ ├── constant.ts │ ├── context │ │ ├── generator.ts │ │ ├── index.ts │ │ ├── lexer.ts │ │ ├── parser.ts │ │ └── transformer.ts │ ├── directive.ts │ ├── directives │ │ ├── define.ts │ │ ├── if.ts │ │ ├── index.ts │ │ └── message.ts │ ├── index.ts │ ├── types │ │ ├── comment.ts │ │ ├── directive.ts │ │ ├── index.ts │ │ ├── node.ts │ │ └── token.ts │ ├── unplugin.ts │ └── utils.ts ├── esbuild.ts ├── index.ts ├── nuxt.ts ├── rollup.ts ├── rspack.ts ├── types.ts ├── vite.ts └── webpack.ts ├── test ├── __snapshots__ │ └── if.test.ts.snap ├── define.test.ts ├── fixtures │ ├── define.txt │ ├── if.css │ ├── if.html │ ├── if.js │ ├── if.jsx │ └── undef.txt ├── generator.test.ts ├── if.test.ts ├── lexer.test.ts ├── parser.test.ts └── transformer.test.ts ├── tsconfig.json └── tsup.config.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 16.x 21 | 22 | - name: Setup 23 | run: npm i -g @antfu/ni 24 | 25 | - name: Install 26 | run: nci 27 | 28 | - name: Lint 29 | run: nr lint 30 | 31 | test: 32 | runs-on: ${{ matrix.os }} 33 | 34 | strategy: 35 | matrix: 36 | node: [16.x, 18.x] 37 | os: [ubuntu-latest, windows-latest, macos-latest] 38 | fail-fast: false 39 | 40 | steps: 41 | - uses: actions/checkout@v3 42 | - name: Set node ${{ matrix.node }} 43 | uses: actions/setup-node@v3 44 | with: 45 | node-version: ${{ matrix.node }} 46 | 47 | - name: Setup 48 | run: npm i -g @antfu/ni 49 | 50 | - name: Install 51 | run: nci 52 | 53 | - name: Build 54 | run: nr build 55 | 56 | - name: Test 57 | run: nr test 58 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v2 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 16.x 23 | cache: pnpm 24 | registry-url: 'https://registry.npmjs.org' 25 | 26 | - run: npx changelogithub 27 | continue-on-error: true 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | 31 | - name: Install Dependencies 32 | run: pnpm i 33 | 34 | - name: Publish to NPM 35 | run: pnpm publish --access public --no-git-checks 36 | env: 37 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE 81 | .idea 82 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.experimental.useFlatConfig": true, 4 | // Disable the default formatter, use eslint instead 5 | "prettier.enable": false, 6 | "editor.formatOnSave": false, 7 | // Auto fix 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit", 10 | "source.organizeImports": "never" 11 | }, 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { 15 | "rule": "style/*", 16 | "severity": "off" 17 | }, 18 | { 19 | "rule": "format/*", 20 | "severity": "off" 21 | }, 22 | { 23 | "rule": "*-indent", 24 | "severity": "off" 25 | }, 26 | { 27 | "rule": "*-spacing", 28 | "severity": "off" 29 | }, 30 | { 31 | "rule": "*-spaces", 32 | "severity": "off" 33 | }, 34 | { 35 | "rule": "*-order", 36 | "severity": "off" 37 | }, 38 | { 39 | "rule": "*-dangle", 40 | "severity": "off" 41 | }, 42 | { 43 | "rule": "*-newline", 44 | "severity": "off" 45 | }, 46 | { 47 | "rule": "*quotes", 48 | "severity": "off" 49 | }, 50 | { 51 | "rule": "*semi", 52 | "severity": "off" 53 | } 54 | ], 55 | // Enable eslint for all supported languages 56 | "eslint.validate": [ 57 | "javascript", 58 | "javascriptreact", 59 | "typescript", 60 | "typescriptreact", 61 | "vue", 62 | "html", 63 | "markdown", 64 | "json", 65 | "jsonc", 66 | "yaml", 67 | "toml" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 KeJun 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 | logo 2 | 3 | # unplugin-preprocessor-directives 4 | 5 | [![npm version][npm-version-src]][npm-version-href] 6 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 7 | [![github stars][github-stars-src]][github-stars-href] 8 | [![bundle][bundle-src]][bundle-href] 9 | [![License][license-src]][license-href] 10 | [![JSDocs][jsdocs-src]][jsdocs-href] 11 | 12 | English | [简体中文](./README.zh-cn.md) 13 | 14 | >[!IMPORTANT] 15 | > If you like this project, please consider giving it a star ⭐️. Your support will help this project become a part of the [unplugin organization](https://github.com/unplugin/.github/issues/5)! 16 | 17 | ## Install 18 | 19 | ```bash 20 | npm i unplugin-preprocessor-directives 21 | ``` 22 | 23 |
24 | Vite
25 | 26 | ```ts 27 | // vite.config.ts 28 | import PreprocessorDirectives from 'unplugin-preprocessor-directives/vite' 29 | 30 | export default defineConfig({ 31 | plugins: [ 32 | PreprocessorDirectives({ /* options */ }), 33 | ], 34 | }) 35 | ``` 36 | 37 | Example: [`playground/`](./playground/) 38 | 39 |
40 | 41 |
42 | Rollup
43 | 44 | ```ts 45 | // rollup.config.js 46 | import PreprocessorDirectives from 'unplugin-preprocessor-directives/rollup' 47 | 48 | export default { 49 | plugins: [ 50 | PreprocessorDirectives({ /* options */ }), 51 | ], 52 | } 53 | ``` 54 | 55 |
56 | 57 |
58 | Webpack
59 | 60 | ```ts 61 | // webpack.config.js 62 | module.exports = { 63 | /* ... */ 64 | plugins: [ 65 | require('unplugin-preprocessor-directives/webpack')({ /* options */ }) 66 | ] 67 | } 68 | ``` 69 | 70 |
71 | 72 |
73 | Nuxt
74 | 75 | ```ts 76 | // nuxt.config.js 77 | export default defineNuxtConfig({ 78 | modules: [ 79 | ['unplugin-preprocessor-directives/nuxt', { /* options */ }], 80 | ], 81 | }) 82 | ``` 83 | 84 | > This module works for both Nuxt 2 and [Nuxt Vite](https://github.com/nuxt/vite) 85 | 86 |
87 | 88 |
89 | Vue CLI
90 | 91 | ```ts 92 | // vue.config.js 93 | module.exports = { 94 | configureWebpack: { 95 | plugins: [ 96 | require('unplugin-preprocessor-directives/webpack')({ /* options */ }), 97 | ], 98 | }, 99 | } 100 | ``` 101 | 102 |
103 | 104 |
105 | esbuild
106 | 107 | ```ts 108 | // esbuild.config.js 109 | import { build } from 'esbuild' 110 | import PreprocessorDirectives from 'unplugin-preprocessor-directives/esbuild' 111 | 112 | build({ 113 | plugins: [PreprocessorDirectives()], 114 | }) 115 | ``` 116 | 117 |
118 | 119 |
120 | Rspack (⚠️ experimental)
121 | 122 | ```ts 123 | // rspack.config.js 124 | module.exports = { 125 | plugins: [ 126 | require('unplugin-preprocessor-directives/rspack')({ /* options */ }), 127 | ], 128 | } 129 | ``` 130 | 131 |
132 | 133 | ## Usage 134 | 135 | ### Defining symbols 136 | 137 | You use the following two preprocessor directives to define or undefine symbols for conditional compilation: 138 | 139 | - `#define`: Define a symbol. 140 | - `#undef`: Undefine a symbol. 141 | 142 | You use `#define` to define a symbol. When you use the symbol as the expression that's passed to the `#if` directive, the expression will evaluate to `true`, as the following example shows: 143 | 144 | ```ts 145 | // #define VERBOSE 146 | 147 | // #if VERBOSE 148 | console.log('Verbose output version') 149 | // #endif 150 | ``` 151 | 152 | ### Conditional compilation 153 | 154 | - `#if`: Opens a conditional compilation, where code is compiled only if the specified symbol is defined and evaluated to true. 155 | - `#elif`: Closes the preceding conditional compilation and opens a new conditional compilation based on if the specified symbol is defined and evaluated to true. 156 | - `#else`: Closes the preceding conditional compilation and opens a new conditional compilation if the previous specified symbol isn't defined or evaluated to false. 157 | - `#endif`: Closes the preceding conditional compilation. 158 | 159 | > [!NOTE] 160 | > By default, use vite's `loadEnv` function to load environment variables based on `process.env.NODE_ENV` and compile symbols as conditions. 161 | 162 | ```ts 163 | // src/index.ts 164 | 165 | // #if DEV 166 | console.log('Debug version') 167 | // #endif 168 | 169 | // #if !MYTEST 170 | console.log('MYTEST is not defined or false') 171 | // #endif 172 | ``` 173 | 174 | You can use the operators `==` (equality) and `!=` (inequality) to test for the bool values `true` or `false`. `true` means the symbol is defined. The statement `#if DEBUG` has the same meaning as `#if (DEBUG == true)`. You can use the `&&` (and), `||` (or), and `!` (not) operators to evaluate whether multiple symbols have been defined. You can also group symbols and operators with parentheses. 175 | 176 | ```ts 177 | class MyClass { 178 | constructor() { 179 | // #if (DEBUG && MYTEST) 180 | console.log('DEBUG and MYTEST are defined') 181 | // #elif (DEBUG==false && !MYTEST) 182 | console.log('DEBUG and MYTEST are not defined') 183 | // #endif 184 | } 185 | } 186 | ``` 187 | ### Error and warning and info messages 188 | 189 | You instruct the compiler to generate user-defined compiler errors and warnings and informational messages. 190 | 191 | - `#error`: Generates an error. 192 | - `#warning`: Generates a warning. 193 | - `#info`: Generates an informational message. 194 | 195 | ```ts 196 | // #error this is an error message 197 | // #warning this is a warning message 198 | // #info this is an info message 199 | ``` 200 | ## Custom directive 201 | 202 | You can used `defineDirective` to define your own directive. 203 | 204 | Taking the built-in directive as an example: 205 | 206 | ```ts 207 | export const MessageDirective = defineDirective(context => ({ 208 | lex(comment) { 209 | return simpleMatchToken(comment, /#(error|warning|info)\s*(.*)/) 210 | }, 211 | parse(token) { 212 | if (token.type === 'error' || token.type === 'warning' || token.type === 'info') { 213 | this.current++ 214 | return { 215 | type: 'MessageStatement', 216 | kind: token.type, 217 | value: token.value, 218 | } 219 | } 220 | }, 221 | transform(node) { 222 | if (node.type === 'MessageStatement') { 223 | switch (node.kind) { 224 | case 'error': 225 | context.logger.error(node.value, { timestamp: true }) 226 | break 227 | case 'warning': 228 | context.logger.warn(node.value, { timestamp: true }) 229 | break 230 | case 'info': 231 | context.logger.info(node.value, { timestamp: true }) 232 | break 233 | } 234 | return createProgramNode() 235 | } 236 | }, 237 | generate(node, comment) { 238 | if (node.type === 'MessageStatement' && comment) 239 | return `${comment.start} #${node.kind} ${node.value} ${comment.end}` 240 | }, 241 | })) 242 | ``` 243 | 244 | ### `enforce: 'pre' | 'post'` 245 | 246 | Execution priority of directives 247 | 248 | - `pre`: Execute as early as possible 249 | - `post`: Execute as late as possible 250 | 251 | [npm-version-src]: https://img.shields.io/npm/v/unplugin-preprocessor-directives?style=flat&colorA=18181B&colorB=F0DB4F 252 | [npm-version-href]: https://npmjs.com/package/unplugin-preprocessor-directives 253 | [npm-downloads-src]: https://img.shields.io/npm/dw/unplugin-preprocessor-directives?style=flat&colorA=18181B&colorB=F0DB4F 254 | [npm-downloads-href]: https://npmjs.com/package/unplugin-preprocessor-directives 255 | [github-stars-src]: https://img.shields.io/github/stars/kejunmao/unplugin-preprocessor-directives?style=flat&colorA=18181B&colorB=F0DB4F 256 | [github-stars-href]: https://github.com/kejunmao/unplugin-preprocessor-directives 257 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/unplugin-preprocessor-directives?style=flat&colorA=18181B&colorB=F0DB4F 258 | [bundle-href]: https://bundlephobia.com/result?p=unplugin-preprocessor-directives 259 | [license-src]: https://img.shields.io/github/license/kejunmao/unplugin-preprocessor-directives.svg?style=flat&colorA=18181B&colorB=F0DB4F 260 | [license-href]: https://github.com/kejunmao/unplugin-preprocessor-directives/blob/main/LICENSE 261 | [jsdocs-src]: https://img.shields.io/badge/jsDocs.io-reference-18181B?style=flat&colorA=18181B&colorB=F0DB4F 262 | [jsdocs-href]: https://www.jsdocs.io/package/unplugin-preprocessor-directives 263 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | logo 2 | 3 | # unplugin-preprocessor-directives 4 | 5 | [![npm version][npm-version-src]][npm-version-href] 6 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 7 | [![bundle][bundle-src]][bundle-href] 8 | [![License][license-src]][license-href] 9 | [![JSDocs][jsdocs-src]][jsdocs-href] 10 | 11 | [English](./README.md) | 简体中文 12 | 13 | >[!IMPORTANT] 14 | > 如果你喜欢这个项目,希望你能给这个仓库点一个 star ⭐,你的支持能帮助这个项目加入到 [unplugin 组织](https://github.com/unplugin/.github/issues/5)! 15 | 16 | ## 安装 17 | 18 | ```bash 19 | npm i unplugin-preprocessor-directives 20 | ``` 21 | 22 |
23 | Vite
24 | 25 | ```ts 26 | // vite.config.ts 27 | import PreprocessorDirectives from 'unplugin-preprocessor-directives/vite' 28 | 29 | export default defineConfig({ 30 | plugins: [ 31 | PreprocessorDirectives({ /* options */ }), 32 | ], 33 | }) 34 | ``` 35 | 36 | Example: [`playground/`](./playground/) 37 | 38 |
39 | 40 |
41 | Rollup
42 | 43 | ```ts 44 | // rollup.config.js 45 | import PreprocessorDirectives from 'unplugin-preprocessor-directives/rollup' 46 | 47 | export default { 48 | plugins: [ 49 | PreprocessorDirectives({ /* options */ }), 50 | ], 51 | } 52 | ``` 53 | 54 |
55 | 56 |
57 | Webpack
58 | 59 | ```ts 60 | // webpack.config.js 61 | module.exports = { 62 | /* ... */ 63 | plugins: [ 64 | require('unplugin-preprocessor-directives/webpack')({ /* options */ }) 65 | ] 66 | } 67 | ``` 68 | 69 |
70 | 71 |
72 | Nuxt
73 | 74 | ```ts 75 | // nuxt.config.js 76 | export default defineNuxtConfig({ 77 | modules: [ 78 | ['unplugin-preprocessor-directives/nuxt', { /* options */ }], 79 | ], 80 | }) 81 | ``` 82 | 83 | > This module works for both Nuxt 2 and [Nuxt Vite](https://github.com/nuxt/vite) 84 | 85 |
86 | 87 |
88 | Vue CLI
89 | 90 | ```ts 91 | // vue.config.js 92 | module.exports = { 93 | configureWebpack: { 94 | plugins: [ 95 | require('unplugin-preprocessor-directives/webpack')({ /* options */ }), 96 | ], 97 | }, 98 | } 99 | ``` 100 | 101 |
102 | 103 |
104 | esbuild
105 | 106 | ```ts 107 | // esbuild.config.js 108 | import { build } from 'esbuild' 109 | import PreprocessorDirectives from 'unplugin-preprocessor-directives/esbuild' 110 | 111 | build({ 112 | plugins: [PreprocessorDirectives()], 113 | }) 114 | ``` 115 | 116 |
117 | 118 |
119 | Rspack (⚠️ 实验性)
120 | 121 | ```ts 122 | // rspack.config.js 123 | module.exports = { 124 | plugins: [ 125 | require('unplugin-preprocessor-directives/rspack')({ /* options */ }), 126 | ], 127 | } 128 | ``` 129 | 130 |
131 | 132 | ## 使用 133 | 134 | ### 定义 symbols 135 | 136 | 您可以使用以下两个预处理器指令来定义或取消定义 symbols,以便进行条件编译: 137 | 138 | - `#define`: 定义一个 symbol. 139 | - `#undef`: 取消定义一个 symbol. 140 | 141 | 使用 `#define` 可以定义一个 symbol。将 symbol 作为表达式传递给 `#if` 指令时,表达式的值将为 `true`,如下例所示: 142 | 143 | ```ts 144 | // #define VERBOSE 145 | 146 | // #if VERBOSE 147 | console.log('Verbose output version') 148 | // #endif 149 | ``` 150 | 151 | ### 条件编译 152 | 153 | - `#if`: 打开条件编译,只有当指定的 symbol 被定义并求值为 true 时,代码才会被编译。 154 | - `#elif`:关闭前面的条件编译,并判断是否定义了指定的 symbol 并求值为 true 时,打开一个新的条件编译。 155 | - `#else`: 如果前一个指定的 symbol 未定义或求值为 false,则关闭前一个条件编译,并打开一个新的条件编译。 156 | - `#endif`: 关闭前面的条件编译。 157 | 158 | > [!NOTE] 159 | > 默认情况下,使用 vite 的 `loadEnv` 函数根据`process.env.NODE_ENV` 加载环境变量并作为条件编译 symbols。 160 | 161 | ```ts 162 | // src/index.ts 163 | 164 | // #if DEV 165 | console.log('Debug version') 166 | // #endif 167 | 168 | // #if !MYTEST 169 | console.log('MYTEST is not defined or false') 170 | // #endif 171 | ``` 172 | 173 | 可以使用运算符 `==` (相等)和 `!=` (不等)来测试 `true` 或 `false`。`true` 表示 symbol 已定义。语句 `#if DEBUG` 与 `#if (DEBUG == true)` 意义相同。支持使用 `&&` (与)、`||` (或) 和 `!` (非) 操作符来判断是否定义了多个 symbols。还可以用括号将 symbols 和运算符分组。 174 | 175 | ```ts 176 | class MyClass { 177 | constructor() { 178 | // #if (DEBUG && MYTEST) 179 | console.log('DEBUG and MYTEST are defined') 180 | // #elif (DEBUG==false && !MYTEST) 181 | console.log('DEBUG and MYTEST are not defined') 182 | // #endif 183 | } 184 | } 185 | ``` 186 | ### 错误、警告和信息提示 187 | 188 | 可以指示编译器生成用户定义的编译器错误、警告和信息。 189 | 190 | - `#error`: 生成一条错误消息。 191 | - `#warning`: 生成一条警告消息。 192 | - `#info`: 生成一条信息消息。 193 | 194 | ```ts 195 | // #error this is an error message 196 | // #warning this is a warning message 197 | // #info this is an info message 198 | ``` 199 | 200 | ## 自定义指令 201 | 202 | 您可以使用 `defineDirective` 定义自己的指令。 203 | 204 | 以内置指令为例: 205 | 206 | ```ts 207 | export const MessageDirective = defineDirective(context => ({ 208 | lex(comment) { 209 | return simpleMatchToken(comment, /#(error|warning|info)\s*(.*)/) 210 | }, 211 | parse(token) { 212 | if (token.type === 'error' || token.type === 'warning' || token.type === 'info') { 213 | this.current++ 214 | return { 215 | type: 'MessageStatement', 216 | kind: token.type, 217 | value: token.value, 218 | } 219 | } 220 | }, 221 | transform(node) { 222 | if (node.type === 'MessageStatement') { 223 | switch (node.kind) { 224 | case 'error': 225 | context.logger.error(node.value, { timestamp: true }) 226 | break 227 | case 'warning': 228 | context.logger.warn(node.value, { timestamp: true }) 229 | break 230 | case 'info': 231 | context.logger.info(node.value, { timestamp: true }) 232 | break 233 | } 234 | return createProgramNode() 235 | } 236 | }, 237 | generate(node, comment) { 238 | if (node.type === 'MessageStatement' && comment) 239 | return `${comment.start} #${node.kind} ${node.value} ${comment.end}` 240 | }, 241 | })) 242 | ``` 243 | 244 | ### `enforce: 'pre' | 'post'` 245 | 246 | 指令的执行优先级 247 | 248 | - `pre` 尽可能早执行 249 | - `post` 尽可能晚执行 250 | 251 | [npm-version-src]: https://img.shields.io/npm/v/unplugin-preprocessor-directives?style=flat&colorA=18181B&colorB=F0DB4F 252 | [npm-version-href]: https://npmjs.com/package/unplugin-preprocessor-directives 253 | [npm-downloads-src]: https://img.shields.io/npm/dm/unplugin-preprocessor-directives?style=flat&colorA=18181B&colorB=F0DB4F 254 | [npm-downloads-href]: https://npmjs.com/package/unplugin-preprocessor-directives 255 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/unplugin-preprocessor-directives?style=flat&colorA=18181B&colorB=F0DB4F 256 | [bundle-href]: https://bundlephobia.com/result?p=unplugin-preprocessor-directives 257 | [license-src]: https://img.shields.io/github/license/kejunmao/unplugin-preprocessor-directives.svg?style=flat&colorA=18181B&colorB=F0DB4F 258 | [license-href]: https://github.com/kejunmao/unplugin-preprocessor-directives/blob/main/LICENSE 259 | [jsdocs-src]: https://img.shields.io/badge/jsDocs.io-reference-18181B?style=flat&colorA=18181B&colorB=F0DB4F 260 | [jsdocs-href]: https://www.jsdocs.io/package/unplugin-preprocessor-directives 261 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | ignores: ['**/test/fixtures/**/*.*'], 5 | }) 6 | -------------------------------------------------------------------------------- /examples/vite-vue-ts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Vue + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/vite-vue-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-vue-ts", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.4.15" 13 | }, 14 | "devDependencies": { 15 | "@vitejs/plugin-vue": "^5.0.3", 16 | "typescript": "^5.3.3", 17 | "unplugin-preprocessor-directives": "workspace:*", 18 | "vite": "^5.0.12", 19 | "vue-tsc": "^1.8.27" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/vite-vue-ts/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 48 | -------------------------------------------------------------------------------- /examples/vite-vue-ts/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /examples/vite-vue-ts/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/vite-vue-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "jsx": "preserve", 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "useDefineForClassFields": true, 11 | "module": "ESNext", 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "allowImportingTsExtensions": true, 16 | /* Linting */ 17 | "strict": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noEmit": true, 22 | "isolatedModules": true, 23 | "skipLibCheck": true 24 | }, 25 | "references": [ 26 | { 27 | "path": "./tsconfig.node.json" 28 | } 29 | ], 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.d.ts", 33 | "src/**/*.tsx", 34 | "src/**/*.vue" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/vite-vue-ts/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/vite-vue-ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import PreprocessorDirectives from 'unplugin-preprocessor-directives/vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue(), PreprocessorDirectives()], 8 | }) 9 | -------------------------------------------------------------------------------- /examples/vue-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cli", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "vue": "^3.4.15" 11 | }, 12 | "devDependencies": { 13 | "@vue/cli-service": "~5.0.8", 14 | "unplugin-preprocessor-directives": "workspace:*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/vue-cli/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/vue-cli/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /examples/vue-cli/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /examples/vue-cli/vue.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@vue/cli-service').ProjectOptions} 3 | */ 4 | 5 | module.exports = { 6 | configureWebpack: { 7 | plugins: [ 8 | require('unplugin-preprocessor-directives/webpack')(), 9 | ], 10 | }, 11 | chainWebpack: (config) => { 12 | config.module 13 | .rule('html') 14 | .test(/\.html$/) 15 | .use('html-webpack-plugin/loader') 16 | .loader(require.resolve('html-webpack-plugin/lib/loader.js')) 17 | .options({ 18 | force: true, 19 | }) 20 | .end() 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unplugin-preprocessor-directives", 3 | "type": "module", 4 | "version": "1.0.3", 5 | "packageManager": "pnpm@8.14.1", 6 | "description": "", 7 | "license": "MIT", 8 | "homepage": "https://github.com/kejunmao/unplugin-preprocessor-directives#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/kejunmao/unplugin-preprocessor-directives.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/kejunmao/unplugin-preprocessor-directives/issues" 15 | }, 16 | "keywords": [ 17 | "unplugin", 18 | "vite", 19 | "webpack", 20 | "rollup", 21 | "rspack", 22 | "transform", 23 | "vite-plugin", 24 | "webpack-plugin", 25 | "rollup-plugin", 26 | "esbuild-plugin", 27 | "rspack-plugin", 28 | "nuxt-plugin", 29 | "directives", 30 | "preprocessor" 31 | ], 32 | "exports": { 33 | ".": { 34 | "import": { 35 | "types": "./dist/index.d.ts", 36 | "default": "./dist/index.js" 37 | }, 38 | "require": { 39 | "types": "./dist/index.d.cts", 40 | "default": "./dist/index.cjs" 41 | } 42 | }, 43 | "./vite": { 44 | "import": { 45 | "types": "./dist/vite.d.ts", 46 | "default": "./dist/vite.js" 47 | }, 48 | "require": { 49 | "types": "./dist/vite.d.cts", 50 | "default": "./dist/vite.cjs" 51 | } 52 | }, 53 | "./webpack": { 54 | "import": { 55 | "types": "./dist/webpack.d.ts", 56 | "default": "./dist/webpack.js" 57 | }, 58 | "require": { 59 | "types": "./dist/webpack.d.cts", 60 | "default": "./dist/webpack.cjs" 61 | } 62 | }, 63 | "./rollup": { 64 | "import": { 65 | "types": "./dist/rollup.d.ts", 66 | "default": "./dist/rollup.js" 67 | }, 68 | "require": { 69 | "types": "./dist/rollup.d.cts", 70 | "default": "./dist/rollup.cjs" 71 | } 72 | }, 73 | "./esbuild": { 74 | "import": { 75 | "types": "./dist/esbuild.d.ts", 76 | "default": "./dist/esbuild.js" 77 | }, 78 | "require": { 79 | "types": "./dist/esbuild.d.cts", 80 | "default": "./dist/esbuild.cjs" 81 | } 82 | }, 83 | "./rspack": { 84 | "import": { 85 | "types": "./dist/rspack.d.ts", 86 | "default": "./dist/rspack.js" 87 | }, 88 | "require": { 89 | "types": "./dist/rspack.d.cts", 90 | "default": "./dist/rspack.cjs" 91 | } 92 | }, 93 | "./nuxt": { 94 | "import": { 95 | "types": "./dist/nuxt.d.ts", 96 | "default": "./dist/nuxt.js" 97 | }, 98 | "require": { 99 | "types": "./dist/nuxt.d.cts", 100 | "default": "./dist/nuxt.cjs" 101 | } 102 | }, 103 | "./types": { 104 | "import": { 105 | "types": "./dist/types.d.ts", 106 | "default": "./dist/types.js" 107 | }, 108 | "require": { 109 | "types": "./dist/types.d.cts", 110 | "default": "./dist/types.cjs" 111 | } 112 | }, 113 | "./*": "./*" 114 | }, 115 | "main": "dist/index.cjs", 116 | "module": "dist/index.js", 117 | "types": "dist/index.d.ts", 118 | "typesVersions": { 119 | "*": { 120 | "*": [ 121 | "./dist/*", 122 | "./*" 123 | ] 124 | } 125 | }, 126 | "files": [ 127 | "dist" 128 | ], 129 | "scripts": { 130 | "build": "tsup", 131 | "dev": "tsup --watch src", 132 | "build:fix": "esno scripts/postbuild.ts", 133 | "lint": "eslint .", 134 | "play": "npm -C playground run dev", 135 | "prepublishOnly": "npm run build", 136 | "release": "bumpp", 137 | "start": "esno src/index.ts", 138 | "test": "vitest" 139 | }, 140 | "peerDependencies": { 141 | "@nuxt/kit": "^3", 142 | "@nuxt/schema": "^3", 143 | "@rspack/core": "*", 144 | "esbuild": "*", 145 | "rollup": "^3", 146 | "vite": ">=3", 147 | "webpack": "^4 || ^5" 148 | }, 149 | "peerDependenciesMeta": { 150 | "@nuxt/kit": { 151 | "optional": true 152 | }, 153 | "@nuxt/schema": { 154 | "optional": true 155 | }, 156 | "@rspack/core": { 157 | "optional": true 158 | }, 159 | "esbuild": { 160 | "optional": true 161 | }, 162 | "rollup": { 163 | "optional": true 164 | }, 165 | "vite": { 166 | "optional": true 167 | }, 168 | "webpack": { 169 | "optional": true 170 | } 171 | }, 172 | "dependencies": { 173 | "@ampproject/remapping": "^2.2.1", 174 | "magic-string": "^0.30.5", 175 | "unplugin": "^1.6.0" 176 | }, 177 | "devDependencies": { 178 | "@antfu/eslint-config": "^2.6.3", 179 | "@nuxt/kit": "^3.9.3", 180 | "@nuxt/schema": "^3.9.3", 181 | "@types/node": "^20.11.5", 182 | "bumpp": "^9.3.0", 183 | "chalk": "^5.3.0", 184 | "eslint": "^8.56.0", 185 | "esno": "^4.0.0", 186 | "fast-glob": "^3.3.2", 187 | "nodemon": "^3.0.3", 188 | "rimraf": "^5.0.5", 189 | "rollup": "^4.9.5", 190 | "tsup": "^8.0.1", 191 | "typescript": "^5.3.3", 192 | "vite": "^5.0.12", 193 | "vitest": "^1.2.1", 194 | "webpack": "^5.89.0" 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | visit /__inspect/ to inspect the intermediate state 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/main.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | // eslint-disable @typescript-eslint/indent 3 | /* prettier-ignore */ 4 | document.getElementById('app')!.innerHTML = '__UNPLUGIN__' 5 | // #undef DEV 6 | // #error this is an error message 7 | // #warning this is a warning message 8 | // #info this is an info message 9 | 10 | console.log(1) 11 | // #if DEV 12 | console.log(2) 13 | // #if ASD != '233' 14 | console.log(3) 15 | // #endif 16 | console.log(4) 17 | // #elif NODE_ENV == 'development' 18 | console.log(5) 19 | // #elif VC6 20 | console.log(6) 21 | // #else 22 | console.log(7) 23 | // #endif 24 | 25 | console.log(8) 26 | 27 | // #if DEBUG 28 | console.log(9) 29 | // #else 30 | console.log(10) 31 | // #endif 32 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "nodemon -w '../src/**/*.ts' -e .ts -x vite dev" 5 | }, 6 | "devDependencies": { 7 | "vite": "^5.0.12", 8 | "vite-plugin-inspect": "^0.8.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import Inspect from 'vite-plugin-inspect' 3 | import Unplugin from '../src/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | Inspect(), 8 | Unplugin(), 9 | ], 10 | }) 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - examples/* 4 | -------------------------------------------------------------------------------- /scripts/postbuild.ts: -------------------------------------------------------------------------------- 1 | import { basename, dirname, resolve } from 'node:path' 2 | import { promises as fs } from 'node:fs' 3 | import { fileURLToPath } from 'node:url' 4 | import fg from 'fast-glob' 5 | import chalk from 'chalk' 6 | 7 | async function run() { 8 | // fix cjs exports 9 | const files = await fg('*.cjs', { 10 | ignore: ['chunk-*'], 11 | absolute: true, 12 | cwd: resolve(dirname(fileURLToPath(import.meta.url)), '../dist'), 13 | }) 14 | for (const file of files) { 15 | console.log(chalk.cyan.inverse(' POST '), `Fix ${basename(file)}`) 16 | let code = await fs.readFile(file, 'utf8') 17 | code = code.replace('exports.default =', 'module.exports =') 18 | code += 'exports.default = module.exports;' 19 | await fs.writeFile(file, code) 20 | } 21 | } 22 | 23 | run() 24 | -------------------------------------------------------------------------------- /src/core/constant.ts: -------------------------------------------------------------------------------- 1 | import type { Comment } from './types' 2 | 3 | export const comments: Comment[] = [ 4 | // js 5 | { 6 | type: 'js', 7 | start: '// ', 8 | end: '', 9 | regex: /^\/\/\s?(.*)$/, 10 | }, 11 | // jsx 12 | { 13 | type: 'jsx', 14 | start: '{/* ', 15 | end: ' */}', 16 | regex: /^\{\s?\/\*\s?(.*)\s?\*\/\s?\}$/, 17 | }, 18 | // css 19 | { 20 | type: 'css', 21 | start: '/* ', 22 | end: ' */', 23 | regex: /^\/\*\s?(.*)\*\/$/, 24 | }, 25 | // html 26 | { 27 | type: 'html', 28 | start: '', 30 | regex: /^$/, 31 | }, 32 | ] 33 | -------------------------------------------------------------------------------- /src/core/context/generator.ts: -------------------------------------------------------------------------------- 1 | import type { Generate, SimpleNode } from '../types' 2 | import { findComment } from '../utils' 3 | 4 | export class Generator { 5 | constructor(public node: SimpleNode, public generates: Generate[] = []) { 6 | } 7 | 8 | walk(node: SimpleNode): string | void { 9 | switch (node.type) { 10 | case 'Program': 11 | return node.body.map(this.walk.bind(this)).filter((n: any) => !!n).join('\n') 12 | case 'CodeStatement': 13 | return node.value 14 | } 15 | 16 | for (const generate of this.generates) { 17 | const comment = findComment(node.comment!) 18 | const generated = generate.call(this, node, comment) 19 | if (generated) 20 | return generated 21 | } 22 | 23 | throw new Error(`Generator: Unknown node type: ${node.type}`) 24 | } 25 | 26 | private generate(): string { 27 | return this.walk(this.node) as string 28 | } 29 | 30 | static generate(node: SimpleNode, generates: Generate[] = []) { 31 | return new Generator(node, generates).generate() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/core/context/index.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import type { Logger } from 'vite' 3 | import { createFilter, createLogger, loadEnv } from 'vite' 4 | import MagicString from 'magic-string' 5 | import type { UserOptions } from '../../types' 6 | import type { Generate, Lex, ObjectDirective, Parse, Transform } from '../types' 7 | import { Generator } from './generator' 8 | import { Lexer } from './lexer' 9 | import { Parser } from './parser' 10 | import { Transformer } from './transformer' 11 | 12 | export * from './lexer' 13 | export * from './parser' 14 | 15 | export function resolveOptions(options?: UserOptions): Required { 16 | return { 17 | cwd: process.cwd(), 18 | include: ['**/*'], 19 | exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/], 20 | directives: [], 21 | ...options, 22 | } 23 | } 24 | 25 | export function sortUserDirectives( 26 | directives: ObjectDirective[], 27 | ): [ObjectDirective[], ObjectDirective[], ObjectDirective[]] { 28 | const preDirectives: ObjectDirective[] = [] 29 | const postDirectives: ObjectDirective[] = [] 30 | const normalDirectives: ObjectDirective[] = [] 31 | 32 | if (directives) { 33 | directives.forEach((p) => { 34 | if (p.enforce === 'pre') 35 | preDirectives.push(p) 36 | else if (p.enforce === 'post') 37 | postDirectives.push(p) 38 | else normalDirectives.push(p) 39 | }) 40 | } 41 | 42 | return [preDirectives, normalDirectives, postDirectives] 43 | } 44 | 45 | export class Context { 46 | options: Required 47 | directives: ObjectDirective[] 48 | lexers: Lex[] 49 | parsers: Parse[] 50 | transforms: Transform[] 51 | generates: Generate[] 52 | filter: (id: string) => boolean 53 | env: Record = process.env 54 | logger: Logger 55 | constructor(options?: UserOptions) { 56 | this.options = resolveOptions(options) 57 | this.directives = sortUserDirectives(this.options.directives.map(d => typeof d === 'function' ? d(this) : d)).flat() 58 | 59 | this.lexers = this.directives.map(d => d.lex) 60 | this.parsers = this.directives.map(d => d.parse) 61 | this.transforms = this.directives.map(d => d.transform) 62 | this.generates = this.directives.map(d => d.generate) 63 | 64 | this.filter = createFilter(this.options.include, this.options.exclude) 65 | this.logger = createLogger(undefined, { 66 | prefix: 'unplugin-preprocessor-directives', 67 | }) 68 | this.env = this.loadEnv() 69 | } 70 | 71 | loadEnv(mode = process.env.NODE_ENV || 'development') { 72 | return loadEnv( 73 | mode, 74 | this.options.cwd, 75 | '', 76 | ) 77 | } 78 | 79 | transform(code: string, _id: string) { 80 | const tokens = Lexer.lex(code, this.lexers) 81 | const ast = Parser.parse(tokens, this.parsers) 82 | 83 | const transformed = Transformer.transform(ast, this.transforms) 84 | if (transformed) 85 | return Generator.generate(transformed, this.generates) 86 | } 87 | 88 | transformWithMap(code: string, _id: string) { 89 | const generated = this.transform(code, _id) 90 | if (generated) { 91 | const ms = new MagicString(code, { filename: _id }) 92 | ms.overwrite(0, code.length, generated) 93 | return { 94 | code: ms.toString(), 95 | map: ms.generateMap({ hires: true }), 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/core/context/lexer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | /* eslint-disable no-labels */ 3 | import { isComment, parseComment } from '../utils' 4 | import type { CodeToken, Lex, SimpleToken } from '../types' 5 | 6 | export class Lexer { 7 | current = 0 8 | tokens: SimpleToken[] = [] 9 | constructor(public code: string, public lexers: Lex[] = []) { 10 | } 11 | 12 | private lex() { 13 | const code = this.code 14 | scanner: 15 | while (this.current < code.length) { 16 | const startIndex = this.current 17 | let endIndex = code.indexOf('\n', startIndex + 1) 18 | if (endIndex === -1) 19 | endIndex = code.length 20 | 21 | const line = code.slice(startIndex, endIndex).trim() 22 | if (isComment(line)) { 23 | for (const lex of this.lexers) { 24 | const comment = parseComment(line) 25 | 26 | const token = lex.bind(this)(comment.content!) 27 | if (token) { 28 | this.tokens.push({ comment: comment.type, ...token }) 29 | this.current = endIndex 30 | continue scanner 31 | } 32 | } 33 | } 34 | this.tokens.push({ 35 | type: 'code', 36 | value: line, 37 | } as CodeToken) 38 | this.current = endIndex 39 | } 40 | return this.tokens 41 | } 42 | 43 | static lex(code: string, lexers: Lex[] = []) { 44 | const lexer = new Lexer(code, lexers) 45 | return lexer.lex() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/core/context/parser.ts: -------------------------------------------------------------------------------- 1 | import type { CodeStatement, Parse, SimpleToken } from '../types' 2 | import { createProgramNode } from '../utils' 3 | 4 | export class Parser { 5 | ast = createProgramNode() 6 | current = 0 7 | constructor(public tokens: SimpleToken[], public parsers: Parse[] = []) { 8 | } 9 | 10 | walk() { 11 | const token = this.tokens[this.current] 12 | 13 | if (token.type === 'code') { 14 | this.current++ 15 | return { type: 'CodeStatement', value: token.value } as CodeStatement 16 | } 17 | 18 | for (const parser of this.parsers) { 19 | const node = parser.bind(this)(token) 20 | if (node) 21 | return { comment: token.comment, ...node } 22 | } 23 | 24 | throw new Error(`Parser: Unknown token type: ${token.type}`) 25 | } 26 | 27 | private parse() { 28 | while (this.current < this.tokens.length) 29 | this.ast.body.push(this.walk()) 30 | 31 | return this.ast 32 | } 33 | 34 | static parse(tokens: SimpleToken[], parsers: Parse[] = []) { 35 | const parser = new Parser(tokens, parsers) 36 | return parser.parse() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/core/context/transformer.ts: -------------------------------------------------------------------------------- 1 | import type { ProgramNode, SimpleNode, Transform } from '../types' 2 | 3 | export class Transformer { 4 | constructor(public program: ProgramNode, public transforms: Transform[] = []) { 5 | } 6 | 7 | public walk(node: SimpleNode): SimpleNode | void { 8 | switch (node.type) { 9 | case 'Program': 10 | return { 11 | ...node, 12 | body: node.body.map(this.walk.bind(this)).filter((n: any) => !!n), 13 | } as ProgramNode 14 | case 'CodeStatement': 15 | return node 16 | } 17 | 18 | for (const transformer of this.transforms) { 19 | const transformed = transformer.bind(this)(node) 20 | if (transformed) 21 | return transformed 22 | } 23 | 24 | throw new Error(`Transformer: Unknown node type: ${node.type}`) 25 | } 26 | 27 | private transform(): SimpleNode | void { 28 | const ast = this.walk(this.program) 29 | 30 | return ast 31 | } 32 | 33 | static transform(program: ProgramNode, transforms: Transform[] = []) { 34 | const transformer = new Transformer(program, transforms) 35 | return transformer.transform() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/core/directive.ts: -------------------------------------------------------------------------------- 1 | import type { Directive, SimpleNode, SimpleToken } from './types/index' 2 | 3 | export function defineDirective(directive: Directive) { 4 | return directive 5 | } 6 | -------------------------------------------------------------------------------- /src/core/directives/define.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable curly */ 2 | /* eslint-disable no-new-func */ 3 | import process from 'node:process' 4 | import { defineDirective } from '../directive' 5 | import type { DefineStatement, DefineToken } from '../types' 6 | import { createProgramNode } from '../utils' 7 | 8 | function resolveDefineNameAndValue(expression: string, env = process.env): [string, boolean] { 9 | if (/^\w*$/.test(expression)) { 10 | return [expression, true] 11 | } 12 | 13 | else { 14 | const evaluateExpression = new Function('env', `with (env){ return {${expression.replace('=', ':')}} }`) 15 | return Object.entries(evaluateExpression(env))[0] as any 16 | } 17 | } 18 | 19 | export const theDefineDirective = defineDirective(context => ({ 20 | lex(comment) { 21 | const defineMath = comment.match(/#define\s?(.*)/) 22 | 23 | if (defineMath) { 24 | return { 25 | type: 'define', 26 | value: defineMath[1]?.trim(), 27 | } 28 | } 29 | const undefMatch = comment.match(/#undef\s?(\w*)/) 30 | if (undefMatch) { 31 | return { 32 | type: 'undef', 33 | value: undefMatch[1]?.trim(), 34 | } 35 | } 36 | }, 37 | parse(token) { 38 | if (token.type === 'define' || token.type === 'undef') { 39 | this.current++ 40 | return { 41 | type: 'DefineStatement', 42 | kind: token.type, 43 | value: token.value, 44 | } 45 | } 46 | }, 47 | transform(node) { 48 | if (node.type === 'DefineStatement') { 49 | if (node.kind === 'define') { 50 | const [name, value] = resolveDefineNameAndValue(node.value, context.env) 51 | context.env[name] = value 52 | } 53 | else if (node.kind === 'undef') 54 | context.env[node.value] = false 55 | 56 | return createProgramNode() 57 | } 58 | }, 59 | generate(node, comment) { 60 | if (node.type === 'DefineStatement' && comment) { 61 | if (node.kind === 'define') 62 | return `${comment.start} #${node.kind} ${node.value} ${comment.end}` 63 | 64 | else if (node.kind === 'undef') 65 | return `${comment.start} #${node.kind} ${node.value} ${comment.end}` 66 | } 67 | }, 68 | })) 69 | -------------------------------------------------------------------------------- /src/core/directives/if.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineDirective } from '../directive' 3 | import type { IfStatement, IfToken } from '../types' 4 | import { simpleMatchToken } from '../utils' 5 | 6 | export function resolveConditional(test: string, env = process.env) { 7 | test = test || 'true' 8 | test = test.trim() 9 | test = test.replace(/([^=!])=([^=])/g, '$1==$2') 10 | 11 | // eslint-disable-next-line no-new-func 12 | const evaluateCondition = new Function('env', `with (env){ return ( ${test} ) }`) 13 | 14 | try { 15 | return evaluateCondition(env) === 'false' ? false : !!evaluateCondition(env) 16 | } 17 | catch (error) { 18 | if (error instanceof ReferenceError) { 19 | const match = /(\w*?) is not defined/g.exec(error.message) 20 | if (match && match[1]) { 21 | const name = match[1] 22 | // @ts-expect-error ignore 23 | env[name] = false 24 | return resolveConditional(test, env) 25 | } 26 | } 27 | return false 28 | } 29 | } 30 | 31 | export const ifDirective = defineDirective((context) => { 32 | return { 33 | lex(comment) { 34 | return simpleMatchToken(comment, /#(if|else|elif|endif)\s?(.*)/) 35 | }, 36 | parse(token) { 37 | if (token.type === 'if' || token.type === 'elif' || token.type === 'else') { 38 | const node: IfStatement = { 39 | type: 'IfStatement', 40 | test: token.value, 41 | consequent: [], 42 | alternate: [], 43 | kind: token.type, 44 | } 45 | this.current++ 46 | 47 | while (this.current < this.tokens.length) { 48 | const nextToken = this.tokens[this.current] 49 | 50 | if (nextToken.type === 'elif' || nextToken.type === 'else') { 51 | node.alternate.push(this.walk()) 52 | break 53 | } 54 | else if (nextToken.type === 'endif') { 55 | this.current++ // Skip 'endif' 56 | break 57 | } 58 | else { 59 | node.consequent.push(this.walk()) 60 | } 61 | } 62 | return node 63 | } 64 | }, 65 | transform(node) { 66 | if (node.type === 'IfStatement') { 67 | if (resolveConditional(node.test, context.env)) { 68 | return { 69 | type: 'Program', 70 | body: node.consequent.map(this.walk.bind(this)).filter(n => n != null), 71 | } 72 | } 73 | else if (node.alternate) { 74 | return { 75 | type: 'Program', 76 | body: node.alternate.map(this.walk.bind(this)).filter(n => n != null), 77 | } 78 | } 79 | } 80 | }, 81 | generate(node, comment) { 82 | if (node.type === 'IfStatement' && comment) { 83 | let code = '' 84 | if (node.kind === 'else') 85 | code = `${comment.start} ${node.kind} ${comment.end}` 86 | 87 | else 88 | code = `${comment.start} #${node.kind} ${node.test}${comment.end}` 89 | 90 | const consequentCode = node.consequent.map(this.walk.bind(this)).join('\n') 91 | code += `\n${consequentCode}` 92 | if (node.alternate.length) { 93 | const alternateCode = node.alternate.map(this.walk.bind(this)).join('\n') 94 | code += `\n${alternateCode}` 95 | } 96 | else { 97 | code += `\n${comment.start} #endif ${comment.end}` 98 | } 99 | return code 100 | } 101 | }, 102 | } 103 | }) 104 | -------------------------------------------------------------------------------- /src/core/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './if' 2 | export * from './define' 3 | export * from './message' 4 | -------------------------------------------------------------------------------- /src/core/directives/message.ts: -------------------------------------------------------------------------------- 1 | import { defineDirective } from '../directive' 2 | import type { MessageStatement, MessageToken } from '../types' 3 | import { createProgramNode, simpleMatchToken } from '../utils' 4 | 5 | export const MessageDirective = defineDirective(context => ({ 6 | lex(comment) { 7 | return simpleMatchToken(comment, /#(error|warning|info)\s*(.*)/) 8 | }, 9 | parse(token) { 10 | if (token.type === 'error' || token.type === 'warning' || token.type === 'info') { 11 | this.current++ 12 | return { 13 | type: 'MessageStatement', 14 | kind: token.type, 15 | value: token.value, 16 | } 17 | } 18 | }, 19 | transform(node) { 20 | if (node.type === 'MessageStatement') { 21 | switch (node.kind) { 22 | case 'error': 23 | context.logger.error(node.value, { timestamp: true }) 24 | break 25 | case 'warning': 26 | context.logger.warn(node.value, { timestamp: true }) 27 | break 28 | case 'info': 29 | context.logger.info(node.value, { timestamp: true }) 30 | break 31 | } 32 | return createProgramNode() 33 | } 34 | }, 35 | generate(node, comment) { 36 | if (node.type === 'MessageStatement' && comment) 37 | return `${comment.start} #${node.kind} ${node.value} ${comment.end}` 38 | }, 39 | })) 40 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './unplugin' 2 | export * from './context' 3 | export * from './directives' 4 | export * from './directive' 5 | export * from './types' 6 | export * from './utils' 7 | export * from './constant' 8 | -------------------------------------------------------------------------------- /src/core/types/comment.ts: -------------------------------------------------------------------------------- 1 | export interface Comment { 2 | type: string 3 | start: string 4 | end: string 5 | regex: RegExp 6 | } 7 | -------------------------------------------------------------------------------- /src/core/types/directive.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from '../context' 2 | import type { Lexer } from '../context/lexer' 3 | import type { Parser } from '../context/parser' 4 | import type { Transformer } from '../context/transformer' 5 | import type { Generator } from '../context/generator' 6 | import type { Comment } from '../types' 7 | 8 | export interface SimpleToken { 9 | comment?: string 10 | type: string 11 | value: string 12 | [x: string]: any 13 | } 14 | 15 | export interface SimpleNode { 16 | comment?: string 17 | type: string 18 | [x: string]: any 19 | } 20 | 21 | export type Lex = (this: Lexer, currentLine: string) => (T | void) 22 | export type Parse = (this: Parser, currentToken: T) => (N | void) 23 | export type Transform = (this: Transformer, currentNode: N) => (ResultN | void) 24 | export type Generate = (this: Generator, ast: SimpleNode, comment?: Comment) => (string | void) 25 | 26 | export interface ObjectDirective { 27 | enforce?: 'pre' | 'post' 28 | lex: Lex 29 | parse: Parse 30 | transform: Transform 31 | generate: Generate 32 | } 33 | 34 | export interface FunctionDirective { 35 | (context: Context): ObjectDirective 36 | } 37 | 38 | export type Directive = ObjectDirective | FunctionDirective 39 | -------------------------------------------------------------------------------- /src/core/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './directive' 2 | export * from './token' 3 | export * from './node' 4 | export * from './comment' 5 | -------------------------------------------------------------------------------- /src/core/types/node.ts: -------------------------------------------------------------------------------- 1 | import type { IfToken, SimpleNode } from '.' 2 | 3 | export interface ProgramNode extends SimpleNode { 4 | type: 'Program' 5 | body: SimpleNode[] 6 | } 7 | 8 | export interface CodeStatement extends SimpleNode { 9 | type: 'CodeStatement' 10 | value: string 11 | } 12 | 13 | export interface IfStatement extends SimpleNode { 14 | type: 'IfStatement' 15 | test: string 16 | consequent: SimpleNode[] 17 | alternate: SimpleNode[] 18 | kind: IfToken['type'] 19 | } 20 | 21 | export interface DefineStatement extends SimpleNode { 22 | type: 'DefineStatement' 23 | kind: 'define' | 'undef' 24 | value: string 25 | } 26 | 27 | export interface MessageStatement extends SimpleNode { 28 | type: 'MessageStatement' 29 | kind: 'error' | 'warning' | 'info' 30 | value: string 31 | } 32 | -------------------------------------------------------------------------------- /src/core/types/token.ts: -------------------------------------------------------------------------------- 1 | import type { SimpleToken } from '.' 2 | 3 | export interface CodeToken extends SimpleToken { 4 | type: 'code' 5 | value: string 6 | } 7 | export interface IfToken extends SimpleToken { 8 | type: 'if' | 'else' | 'elif' | 'endif' 9 | value: string 10 | } 11 | export interface DefineToken extends SimpleToken { 12 | type: 'define' | 'undef' 13 | value: string 14 | } 15 | 16 | export interface MessageToken extends SimpleToken { 17 | type: 'error' | 'warning' | 'info' 18 | value: string 19 | } 20 | -------------------------------------------------------------------------------- /src/core/unplugin.ts: -------------------------------------------------------------------------------- 1 | import type { UnpluginFactory } from 'unplugin' 2 | import { createUnplugin } from 'unplugin' 3 | import remapping from '@ampproject/remapping' 4 | import type { UserOptions } from '../types' 5 | import { Context } from './context' 6 | import { MessageDirective, ifDirective, theDefineDirective } from './directives' 7 | 8 | export const unpluginFactory: UnpluginFactory = ( 9 | options, 10 | ) => { 11 | // @ts-expect-error ignore 12 | const ctx = new Context({ ...options, directives: [ifDirective, theDefineDirective, MessageDirective, ...options?.directives ?? []] }) 13 | return { 14 | name: 'unplugin-preprocessor-directives', 15 | enforce: 'pre', 16 | transform: (code, id) => ctx.transform(code, id), 17 | transformInclude(id) { 18 | return ctx.filter(id) 19 | }, 20 | vite: { 21 | configResolved(config) { 22 | ctx.env = { 23 | ...ctx.loadEnv(config.mode), 24 | ...config.env, 25 | } 26 | }, 27 | transform(code, id) { 28 | if (ctx.filter(id)) { 29 | const transformed = ctx.transformWithMap(code, id) 30 | if (transformed) { 31 | const map = remapping( 32 | [this.getCombinedSourcemap() as any, transformed.map], 33 | () => null, 34 | ) as any 35 | return { 36 | code: transformed.code, 37 | map, 38 | } 39 | } 40 | } 41 | }, 42 | }, 43 | } 44 | } 45 | 46 | export const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory) 47 | 48 | export default unplugin 49 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import { comments } from './constant' 2 | import type { ProgramNode, SimpleNode, SimpleToken } from './types' 3 | 4 | export function simpleMatchToken(comment: string, regex: RegExp) { 5 | const match = comment.match(regex) 6 | if (match) { 7 | return { 8 | type: match[1], 9 | value: match[2]?.trim(), 10 | } as T 11 | } 12 | } 13 | 14 | export function createProgramNode(body: SimpleNode[] = []) { 15 | return { 16 | type: 'Program', 17 | body, 18 | } as ProgramNode 19 | } 20 | 21 | export function isComment(line: string) { 22 | return comments.some(comment => comment.regex.test(line)) 23 | } 24 | 25 | export function parseComment(line: string) { 26 | const comment = comments.find(comment => comment.start === line.slice(0, comment.start.length)) 27 | const content = comment?.regex.exec(line)?.[1] 28 | 29 | return { 30 | type: comment?.type, 31 | content: content?.trim() ?? '', 32 | } 33 | } 34 | 35 | export function findComment(type: string) { 36 | return comments.find(comment => comment.type === type)! 37 | } 38 | -------------------------------------------------------------------------------- /src/esbuild.ts: -------------------------------------------------------------------------------- 1 | import { createEsbuildPlugin } from 'unplugin' 2 | import { unpluginFactory } from './core/unplugin' 3 | 4 | export default createEsbuildPlugin(unpluginFactory) 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './core' 3 | -------------------------------------------------------------------------------- /src/nuxt.ts: -------------------------------------------------------------------------------- 1 | import { addVitePlugin, addWebpackPlugin, defineNuxtModule } from '@nuxt/kit' 2 | import vite from './vite' 3 | import webpack from './webpack' 4 | import type { UserOptions } from './types' 5 | import '@nuxt/schema' 6 | 7 | export interface ModuleOptions extends UserOptions { 8 | 9 | } 10 | 11 | export default defineNuxtModule({ 12 | meta: { 13 | name: 'nuxt-unplugin-preprocessor-directives', 14 | configKey: 'unpluginManifest', 15 | }, 16 | defaults: { 17 | // ...default options 18 | }, 19 | setup(options, _nuxt) { 20 | addVitePlugin(() => vite(options)) 21 | addWebpackPlugin(() => webpack(options)) 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /src/rollup.ts: -------------------------------------------------------------------------------- 1 | import { createRollupPlugin } from 'unplugin' 2 | import { unpluginFactory } from './core/unplugin' 3 | 4 | export default createRollupPlugin(unpluginFactory) 5 | -------------------------------------------------------------------------------- /src/rspack.ts: -------------------------------------------------------------------------------- 1 | import { createRspackPlugin } from 'unplugin' 2 | import { unpluginFactory } from './core/unplugin' 3 | 4 | export default createRspackPlugin(unpluginFactory) 5 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { FilterPattern } from 'vite' 2 | import type { Directive } from './core/types' 3 | 4 | export interface Options { 5 | cwd: string 6 | directives: Directive[] 7 | include: FilterPattern 8 | exclude: FilterPattern 9 | } 10 | 11 | export interface UserOptions extends Partial { } 12 | -------------------------------------------------------------------------------- /src/vite.ts: -------------------------------------------------------------------------------- 1 | import { createVitePlugin } from 'unplugin' 2 | import { unpluginFactory } from './core/unplugin' 3 | 4 | export default createVitePlugin(unpluginFactory) 5 | -------------------------------------------------------------------------------- /src/webpack.ts: -------------------------------------------------------------------------------- 1 | import { createWebpackPlugin } from 'unplugin' 2 | import { unpluginFactory } from './core/unplugin' 3 | 4 | export default createWebpackPlugin(unpluginFactory) 5 | -------------------------------------------------------------------------------- /test/__snapshots__/if.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`if > should parse if.css, dev = false 1`] = ` 4 | "body { 5 | } 6 | body { 7 | content: "!DEV"; 8 | } 9 | body { 10 | content: "!DEV"; 11 | } 12 | body { 13 | }" 14 | `; 15 | 16 | exports[`if > should parse if.css, dev = true 1`] = ` 17 | "body { 18 | content: "DEV"; 19 | } 20 | body { 21 | content: "!DEV else"; 22 | } 23 | body { 24 | content: "TEST"; 25 | } 26 | body { 27 | content: "else"; 28 | }" 29 | `; 30 | 31 | exports[`if > should parse if.html, dev = false 1`] = ` 32 | "
!DEV
33 |
!DEV
" 34 | `; 35 | 36 | exports[`if > should parse if.html, dev = true 1`] = ` 37 | "
DEV
38 |
!DEV else
39 |
TEST
40 |
41 |
else
42 |
" 43 | `; 44 | 45 | exports[`if > should parse if.js, dev = false 1`] = ` 46 | "console.log('!DEV') 47 | console.log('!DEV')" 48 | `; 49 | 50 | exports[`if > should parse if.js, dev = true 1`] = ` 51 | "console.log('DEV') 52 | console.log('!DEV else') 53 | console.log('TEST') 54 | console.log('else')" 55 | `; 56 | 57 | exports[`if > should parse if.jsx, dev = false 1`] = ` 58 | "const Component = () =>
59 |
;" 60 | `; 61 | 62 | exports[`if > should parse if.jsx, dev = true 1`] = ` 63 | "const Component = () =>
64 | DEV 65 | DEV=true 66 |
;" 67 | `; 68 | -------------------------------------------------------------------------------- /test/define.test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { readFileSync } from 'node:fs' 3 | import { describe, expect, it } from 'vitest' 4 | import { Context, theDefineDirective } from '../src' 5 | 6 | describe('define', () => { 7 | const root = resolve(__dirname, './fixtures') 8 | const context = new Context({ 9 | // @ts-expect-error ignore 10 | directives: [theDefineDirective], 11 | }) 12 | 13 | it('should define env', () => { 14 | const code = readFileSync(resolve(root, 'define.txt'), 'utf-8') 15 | context.transform(code, 'define.txt') 16 | 17 | expect(context.env.html).toBeTruthy() 18 | expect(context.env.css).toBeTruthy() 19 | expect(context.env.js).toBeTruthy() 20 | expect(context.env.DEV).toBe('2') 21 | }) 22 | it('should undef env', () => { 23 | const code = readFileSync(resolve(root, 'undef.txt'), 'utf-8') 24 | context.transform(code, 'undef.txt') 25 | 26 | expect(context.env.html).toBeFalsy() 27 | expect(context.env.css).toBeFalsy() 28 | expect(context.env.js).toBeFalsy() 29 | expect(context.env.DEV).toBeFalsy() 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/fixtures/define.txt: -------------------------------------------------------------------------------- 1 | 2 | /* #define css */ 3 | // #define js 4 | // #define DEV = !TEST ? '1' : '2' 5 | -------------------------------------------------------------------------------- /test/fixtures/if.css: -------------------------------------------------------------------------------- 1 | body { 2 | /* #if DEV */ 3 | content: "DEV"; 4 | /* #endif */ 5 | } 6 | 7 | 8 | body { 9 | /* #if !DEV */ 10 | content: "!DEV"; 11 | /* #else */ 12 | content: "!DEV else"; 13 | /* #endif */ 14 | } 15 | 16 | 17 | body { 18 | /* #if !DEV */ 19 | content: "!DEV"; 20 | /* #elif TEST */ 21 | content: "TEST"; 22 | /* #else */ 23 | content: "!DEV else"; 24 | /* #endif */ 25 | } 26 | 27 | 28 | body { 29 | /* #if DEV */ 30 | /* #if !TEST */ 31 | content: "!DEV !TEST"; 32 | /* #else */ 33 | content: "else"; 34 | /* #endif */ 35 | /* #endif */ 36 | } 37 | -------------------------------------------------------------------------------- /test/fixtures/if.html: -------------------------------------------------------------------------------- 1 | 2 |
DEV
3 | 4 | 5 | 6 |
!DEV
7 | 8 |
!DEV else
9 | 10 | 11 | 12 |
!DEV
13 | 14 |
TEST
15 | 16 |
!DEV else
17 | 18 | 19 | 20 |
21 | 22 |
!DEV !TEST
23 | 24 |
else
25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /test/fixtures/if.js: -------------------------------------------------------------------------------- 1 | // #if DEV 2 | console.log('DEV') 3 | // #endif 4 | 5 | // #if !DEV 6 | console.log('!DEV') 7 | // #else 8 | console.log('!DEV else') 9 | // #endif 10 | 11 | // #if !DEV 12 | console.log('!DEV') 13 | // #elif TEST 14 | console.log('TEST') 15 | // #else 16 | console.log('!DEV else') 17 | // #endif 18 | 19 | // #if DEV 20 | // #if !TEST 21 | console.log('!DEV !TEST') 22 | // #else 23 | console.log('else') 24 | // #endif 25 | // #endif 26 | -------------------------------------------------------------------------------- /test/fixtures/if.jsx: -------------------------------------------------------------------------------- 1 | const Component = () =>
2 | {/* #if DEV */} 3 | DEV 4 | {/* #endif */} 5 | {/* #if DEV=true */} 6 | DEV=true 7 | {/* #endif */} 8 | {/* #if !PROD!=true */} 9 | !PROD!=true 10 | {/* #endif */} 11 |
; 12 | -------------------------------------------------------------------------------- /test/fixtures/undef.txt: -------------------------------------------------------------------------------- 1 | 2 | /* #undef css */ 3 | // #undef js 4 | // #undef DEV 5 | -------------------------------------------------------------------------------- /test/generator.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import { describe, expect, it } from 'vitest' 4 | import { Generator } from '../src/core/context/generator' 5 | import { Context, Lexer, Parser } from '../src' 6 | 7 | describe('generator', () => { 8 | it('should generate code for Program node', () => { 9 | const node = { 10 | type: 'Program', 11 | body: [ 12 | { type: 'CodeStatement', value: 'console.log("Hello, World!");' }, 13 | { type: 'CodeStatement', value: 'console.log("Hello, KeJun");' }, 14 | ], 15 | } 16 | const result = Generator.generate(node) 17 | expect(result).toBe('console.log("Hello, World!");\nconsole.log("Hello, KeJun");') 18 | }) 19 | 20 | it('should generate code for CodeStatement node', () => { 21 | const node = { 22 | type: 'CodeStatement', 23 | value: 'console.log("Hello, World!");', 24 | } 25 | const result = Generator.generate(node) 26 | expect(result).toBe('console.log("Hello, World!");') 27 | }) 28 | 29 | it('should generate code without transform', () => { 30 | const ctx = new Context() 31 | const code = readFileSync(resolve(__dirname, './fixtures/if.html'), 'utf-8') 32 | const tokens = Lexer.lex(code, ctx.lexers) 33 | const ast = Parser.parse(tokens, ctx.parsers) 34 | const generated = Generator.generate(ast, ctx.generates) 35 | expect(generated.replaceAll(/\s/g, '')).toBe(code.replaceAll(/\s/g, '')) 36 | }) 37 | 38 | it('should throw an error for unknown node type', () => { 39 | const node = { 40 | type: 'UnknownNode', 41 | value: 'console.log("Hello, World!");', 42 | } 43 | expect(() => Generator.generate(node)).toThrowError('Generator: Unknown node type: UnknownNode') 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/if.test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { readFileSync } from 'node:fs' 3 | import { describe, expect, it } from 'vitest' 4 | import fg from 'fast-glob' 5 | import { Context, ifDirective } from '../src' 6 | 7 | describe('if', () => { 8 | const root = resolve(__dirname, './fixtures') 9 | const context = new Context({ 10 | // @ts-expect-error ignore 11 | directives: [ifDirective], 12 | }) 13 | 14 | fg.sync('if.*', { cwd: root }).forEach((file) => { 15 | it(`should parse ${file}, dev = true`, () => { 16 | context.env.DEV = true 17 | const code = readFileSync(resolve(root, file), 'utf-8') 18 | const result = context.transform(code, file) 19 | expect(result).toMatchSnapshot() 20 | }) 21 | }) 22 | 23 | fg.sync('if.*', { cwd: root }).forEach((file) => { 24 | it(`should parse ${file}, dev = false`, () => { 25 | context.env.DEV = false 26 | const code = readFileSync(resolve(root, file), 'utf-8') 27 | const result = context.transform(code, file) 28 | expect(result).toMatchSnapshot() 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/lexer.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { Lexer } from '../src' 3 | 4 | describe('lexer', () => { 5 | it('should tokenize code with comments', () => { 6 | const code = ` 7 | // This is a comment 8 | const foo = 'bar'; 9 | // Another comment 10 | const baz = 'qux';` 11 | const expectedTokens = [ 12 | { type: 'code', value: '// This is a comment' }, 13 | { type: 'code', value: 'const foo = \'bar\';' }, 14 | { type: 'code', value: '// Another comment' }, 15 | { type: 'code', value: 'const baz = \'qux\';' }, 16 | ] 17 | 18 | const tokens = Lexer.lex(code) 19 | 20 | expect(tokens).toEqual(expectedTokens) 21 | }) 22 | 23 | it('should tokenize code without comments', () => { 24 | const code = ` 25 | const foo = 'bar'; 26 | const baz = 'qux';` 27 | const expectedTokens = [ 28 | { type: 'code', value: 'const foo = \'bar\';' }, 29 | { type: 'code', value: 'const baz = \'qux\';' }, 30 | ] 31 | 32 | const tokens = Lexer.lex(code) 33 | 34 | expect(tokens).toEqual(expectedTokens) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { Parser } from '../src' 3 | 4 | describe('parser', () => { 5 | it('should parse code statements', () => { 6 | const tokens = [{ type: 'code', value: 'console.log("Hello, World!")' }] 7 | const ast = Parser.parse(tokens) 8 | expect(ast.body).toHaveLength(1) 9 | expect(ast.body[0].type).toBe('CodeStatement') 10 | expect(ast.body[0].value).toBe('console.log("Hello, World!")') 11 | }) 12 | 13 | it('should throw an error for unknown token type', () => { 14 | const tokens = [{ type: 'unknown', value: 'unknown token' }] 15 | expect(() => Parser.parse(tokens)).toThrowError('Parser: Unknown token type: unknown') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/transformer.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { Transformer } from '../src/core/context/transformer' 3 | import type { ProgramNode } from '../src' 4 | import { createProgramNode } from '../src' 5 | 6 | describe('transformer', () => { 7 | it('should transform the program correctly', () => { 8 | const program = createProgramNode([ 9 | { 10 | type: 'CodeStatement', 11 | code: 'console.log("Hello, world!");', 12 | }, 13 | ]) 14 | const transformedProgram = Transformer.transform(program) 15 | 16 | // Assert the transformed program 17 | expect(transformedProgram).toEqual({ 18 | type: 'Program', 19 | body: [ 20 | { 21 | type: 'CodeStatement', 22 | code: 'console.log("Hello, world!");', 23 | }, 24 | ], 25 | }) 26 | }) 27 | 28 | it('should throw an error for unknown node type', () => { 29 | const program = { 30 | type: 'UnknownNodeType', 31 | body: [], 32 | } as unknown as ProgramNode 33 | expect(() => { 34 | Transformer.transform(program) 35 | }).toThrowError('Transformer: Unknown node type: UnknownNodeType') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["esnext", "DOM"], 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "esModuleInterop": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup' 2 | 3 | export default { 4 | entryPoints: [ 5 | 'src/*.ts', 6 | ], 7 | clean: true, 8 | format: ['cjs', 'esm'], 9 | dts: true, 10 | shims: false, 11 | splitting: true, 12 | onSuccess: 'npm run build:fix', 13 | } 14 | --------------------------------------------------------------------------------