├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── README_zh-CN.md ├── build.config.js ├── client.d.ts ├── example ├── style │ ├── example-transformer.scss │ ├── example.css │ ├── example.module.scss │ ├── example.scss │ └── share-to-js │ │ └── index.scss └── vue │ ├── App.vue │ ├── assets │ └── logo.png │ ├── components │ └── HelloWorld.vue │ ├── env.d.ts │ ├── index.html │ ├── main.ts │ ├── public │ └── favicon.ico │ └── vite.config.ts ├── jest.config.cjs ├── package.json ├── pnpm-lock.yaml ├── src ├── index.ts ├── interface.ts ├── transformer.ts └── utils.ts ├── test └── transformer.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .prettierignore 2 | pnpm-lock.yaml 3 | dist 4 | LICENSE -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.lint.unknownProperties": "ignore", 3 | "scss.lint.unknownProperties": "ignore", 4 | "less.lint.unknownProperties": "ignore", 5 | "editor.formatOnSave": true, 6 | "editor.tabSize": 2, 7 | "[json]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[javascript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[html]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[typescript]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[css]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "[scss]": { 23 | "editor.defaultFormatter": "esbenp.prettier-vscode" 24 | }, 25 | "[less]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[vue]": { 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[markdown]": { 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 shixuanhong 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-css-export 🥰 2 | 3 | [中文](https://github.com/shixuanhong/vite-plugin-css-export/blob/main/README_zh-CN.md) | [English](https://github.com/shixuanhong/vite-plugin-css-export/blob/main/README.md) 4 | 5 | **Export variables from CSS to JS, and support nested rules.** 6 | 7 |

8 | npm package 9 | node compatibility 10 | vite compatibility 11 |

12 | 13 | This plugin allows you to use a pseudo-class called `:export` in CSS, and properties in this pseudo-class will be exported to JavaScript. 14 | 15 | Besides that, with the help of Vite, we can use `:export` in .scss, .sass, .less, .styl and .stylus files. 16 | 17 | [How to use css pre-processors in Vite](https://vitejs.dev/guide/features.html#css-pre-processors) 18 | 19 | > Note: Please use 3.x for Vite5, 2.x for Vite4, and 1.x for Vite2 and Vite3. 20 | 21 | ## Install ❤️ 22 | 23 | ```shell 24 | npm install vite-plugin-css-export -D 25 | ``` 26 | 27 | or 28 | 29 | ```shell 30 | yarn add vite-plugin-css-export -D 31 | ``` 32 | 33 | or 34 | 35 | ```shell 36 | pnpm add vite-plugin-css-export -D 37 | ``` 38 | 39 | ## Usage 💡 40 | 41 | ### Quick Start 42 | 43 | ```typescript 44 | // vite.config.ts 45 | import ViteCSSExportPlugin from 'vite-plugin-css-export' 46 | import { defineConfig } from 'vite' 47 | 48 | export default defineConfig({ 49 | plugins: [ViteCSSExportPlugin()] 50 | }) 51 | ``` 52 | 53 | ```css 54 | /* example.css */ 55 | :root { 56 | --font-color: #333; 57 | } 58 | 59 | :export { 60 | fontColor: var(--font-color); 61 | fontSize: 14px; 62 | } 63 | 64 | :export button { 65 | bgColor: #462dd3; 66 | color: #fff; 67 | } 68 | 69 | :export menu menuItem { 70 | bgColor: #1d243a; 71 | color: #fff; 72 | } 73 | ``` 74 | 75 | ```typescript 76 | // if you use in Typescript. wildcard module declarations 77 | // env.d.ts 78 | /// 79 | 80 | // if you want IntelliSense 81 | interface CSSPropertiesExportedData { 82 | fontColor: string 83 | fontSize: string 84 | button: { 85 | bgColor: string 86 | color: string 87 | } 88 | menu: { 89 | menuItem: { 90 | bgColor: string 91 | color: string 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | Use the suffix `?export`. 98 | 99 | ```typescript 100 | // main.ts 101 | import cssResult from './assets/style/example.css?export' 102 | 103 | console.log(cssResult) 104 | 105 | // output 106 | // { 107 | // fontColor: "var(--font-color)", 108 | // fontSize: "14px", 109 | // button: { 110 | // bgColor: "#462dd3", 111 | // color: "#fff" 112 | // }, 113 | // menu: { 114 | // menuItem: { 115 | // bgColor: "#1d243a", 116 | // color: "#fff" 117 | // } 118 | // } 119 | // } 120 | ``` 121 | 122 | ### CSS Pre-processor 123 | 124 | If you are using CSS pre-processor then you can use nested rules. 125 | 126 | ```scss 127 | // .scss 128 | :root { 129 | --font-color: #333; 130 | } 131 | 132 | $menuItemBgColor: #1d243a; 133 | 134 | :export { 135 | fontColor: var(--font-color); 136 | fontSize: 14px; 137 | button { 138 | bgcolor: #462dd3; 139 | color: #fff; 140 | } 141 | menu { 142 | menuItem { 143 | bgcolor: $menuItemBgColor; 144 | color: #fff; 145 | } 146 | } 147 | } 148 | ``` 149 | 150 | ### CSS Module 151 | 152 | When used with CSS module, some simple configuration is required. By default, the exported results will not include CSS module related content (except what's in `:export`) . 153 | 154 | ```typescript 155 | // vite.config.ts 156 | import ViteCSSExportPlugin from 'vite-plugin-css-export' 157 | import { defineConfig } from 'vite' 158 | 159 | export default defineConfig({ 160 | plugins: [ 161 | ViteCSSExportPlugin({ 162 | cssModule: { 163 | isGlobalCSSModule: false, 164 | enableExportMerge: true, // default false 165 | sharedDataExportName: 'cssExportedData' // default 'sharedData' 166 | } 167 | }) 168 | ] 169 | }) 170 | ``` 171 | 172 | ```scss 173 | // example.module.scss 174 | :root { 175 | --font-color: #333; 176 | } 177 | 178 | $menuItemBgColor: #1d243a; 179 | 180 | .base-button { 181 | background-color: transparent; 182 | } 183 | 184 | // alias for :export 185 | :share { 186 | fontcolor: var(--font-color); 187 | fontsize: 14px; 188 | 189 | button { 190 | bgcolor: #462dd3; 191 | color: #fff; 192 | } 193 | 194 | menu { 195 | menuItem { 196 | bgcolor: $menuItemBgColor; 197 | color: #fff; 198 | } 199 | } 200 | } 201 | 202 | :export { 203 | fontColor: var(--font-color); 204 | fontSize: 14px; 205 | } 206 | ``` 207 | 208 | ```typescript 209 | // main.ts 210 | import cssModuleResult from './assets/style/example.module.scss?export' 211 | 212 | console.log(cssModuleResult) 213 | 214 | // output 215 | // { 216 | // cssExportedData: { 217 | // fontColor: "var(--font-color)", 218 | // fontSize: "14px", 219 | // button: { 220 | // bgColor: "#462dd3", 221 | // color: "#fff" 222 | // }, 223 | // menu: { 224 | // menuItem: { 225 | // bgColor: "#1d243a", 226 | // color: "#fff" 227 | // } 228 | // } 229 | // }, 230 | // fontColor: "var(--font-color)", 231 | // fontSize: "14px", 232 | // "base-button": "_base-button_1k9w3_5" 233 | // } 234 | 235 | // when enableExportMerge is false 236 | // output 237 | // { 238 | // fontColor: "var(--font-color)", 239 | // fontSize: "14px", 240 | // button: { 241 | // bgColor: "#462dd3", 242 | // color: "#fff" 243 | // }, 244 | // menu: { 245 | // menuItem: { 246 | // bgColor: "#1d243a", 247 | // color: "#fff" 248 | // } 249 | // } 250 | // } 251 | ``` 252 | 253 | ### Note ⚠ 254 | 255 | If the plugin is used with CSS module, please replace `:export` with `:share` to avoid unknown conflicts with `:export` provided by CSS module. 256 | 257 | > In fact you can still use `:export`, which won't cause a runtime error, `:share` is an alias for `:export`. 258 | 259 | Please do not type the following characters in property names: 260 | 261 | ```bash 262 | 263 | "/", "~", ">", "<", "[", "]", "(", ")", ".", "#", "@", ":", "*" 264 | ``` 265 | 266 | Because this plugin is applied after `vite:css`, all parsing actions are based on the result returned by `vite:css`. When you type the above characters, there are some characters that the plugin cannot give correct warning/error message, for example: `@` 267 | 268 | ```scss 269 | // your code 270 | :export { 271 | fontColor: var(--font-color); 272 | fontSize: 14px; 273 | 274 | button { 275 | bgcolor: #462dd3; 276 | color: #fff; 277 | } 278 | 279 | @menu { 280 | menuItem { 281 | bgcolor: $menuItemBgColor; 282 | color: #fff; 283 | } 284 | } 285 | } 286 | ``` 287 | 288 | ```css 289 | /** after vite:css */ 290 | :export { 291 | fontColor: var(--font-color); 292 | fontSize: 14px; 293 | } 294 | :export button { 295 | bgColor: #462dd3; 296 | color: #fff; 297 | } 298 | /** unable to track the error @menu */ 299 | @menu { 300 | :export menuItem { 301 | bgColor: #1d243a; 302 | color: #fff; 303 | } 304 | } 305 | ``` 306 | 307 | ```javascript 308 | // after vite:css-export 309 | { 310 | fontColor: "var(--font-color)", 311 | fontSize: "14px", 312 | button: { 313 | bgColor: "#462dd3", 314 | color: "#fff" 315 | }, 316 | // menu is missing 317 | menuItem: { 318 | bgColor: "#1d243a", 319 | color: "#fff" 320 | } 321 | } 322 | ``` 323 | 324 | ### Lint 325 | 326 | You may get some warnings from the editor or Stylelint, you can disable related rules. 327 | 328 | #### VS Code 329 | 330 | ```json 331 | { 332 | "css.lint.unknownProperties": "ignore", 333 | "scss.lint.unknownProperties": "ignore", 334 | "less.lint.unknownProperties": "ignore" 335 | } 336 | ``` 337 | 338 | #### Stylelint 339 | 340 | ```json 341 | { 342 | "rules": { 343 | "property-no-unknown": [ 344 | true, 345 | { 346 | "ignoreSelectors": [":export", ":share"] 347 | } 348 | ], 349 | "property-case": null, 350 | "selector-pseudo-class-no-unknown": [ 351 | true, 352 | { 353 | "ignorePseudoClasses": ["export", "share"] 354 | } 355 | ], 356 | "selector-type-no-unknown": [ 357 | true, 358 | { 359 | "ignore": ["default-namespace"] 360 | } 361 | ] 362 | } 363 | } 364 | ``` 365 | 366 | ## Options ⚙️ 367 | 368 | ### shouldTransform 369 | 370 | - **type:** `(id: string) => boolean` 371 | 372 | - **default:** `undefined` 373 | 374 | - **description:** This option allows you to additionally specify which style files should be transformed, not just `?export`. Usage: 375 | 376 | ```typescript 377 | // vite.config.ts 378 | export default defineConfig({ 379 | plugins: [ 380 | ViteCSSExportPlugin({ 381 | shouldTransform(id) { 382 | const include = path.resolve( 383 | process.cwd(), 384 | 'example/assets/style/share-to-js' 385 | ) 386 | return path.resolve(id).indexOf(include) > -1 387 | } 388 | }) 389 | ] 390 | }) 391 | ``` 392 | 393 | ### propertyNameTransformer 394 | 395 | - **type:** `(key: string) => string` 396 | 397 | - **default:** `undefined` 398 | 399 | - **description:** The option allows you to define a method for transforming CSS property names, but doesn`t transform additionalData. The plugin has some built-in methods. Usage: 400 | 401 | ```typescript 402 | // vite.config.ts 403 | import { 404 | default as ViteCSSExportPlugin, 405 | kebabCaseToUpperCamelCase, 406 | kebabCaseToLowerCamelCase, 407 | kebabCaseToPascalCase 408 | } from 'vite-plugin-css-export' 409 | 410 | export default defineConfig({ 411 | plugins: [ 412 | ViteCSSExportPlugin({ 413 | propertyNameTransformer: kebabCaseToUpperCamelCase 414 | }) 415 | ] 416 | }) 417 | ``` 418 | 419 | ### additionalData 420 | 421 | - **type:** `SharedCSSData` 422 | 423 | - **default:** `{}` 424 | 425 | - **description:** The option allows you to append data to all processed results, we can share some common variables here. 426 | 427 | ### cssModule 428 | 429 | #### cssModule.isGlobalCSSModule 430 | 431 | - **type:** `boolean` 432 | 433 | - **default:** `false` 434 | 435 | - **description:** Whether the CSS module is used globally, not just in the `.module.[suffix]` file. 436 | 437 | #### cssModule.enableExportMerge 438 | 439 | - **type:** `boolean` 440 | 441 | - **default:** `false` 442 | 443 | - **description:** When value is true, `sharedData` will be merged with the result of CSS module, otherwise only `sharedData` will be exported. It won't work when using `?inline` 444 | 445 | > _`sharedData` is the parsed result of the plugin._ 446 | 447 | #### cssModule.sharedDataExportName 448 | 449 | - **type:** `string` 450 | 451 | - **default:** `'sharedData'` 452 | 453 | - **description:** When `cssModule.enableExportMerge` is true, modify the property name of `sharedData` in the merged result. It won't work when using `?inline` 454 | -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-css-export 🥰 2 | 3 | [中文](https://github.com/shixuanhong/vite-plugin-css-export/blob/main/README_zh-CN.md) | [English](https://github.com/shixuanhong/vite-plugin-css-export/blob/main/README.md) 4 | 5 | **从 CSS 导出变量到 JS 中,并且支持嵌套规则。** 6 | 7 |

8 | npm package 9 | node compatibility 10 | vite compatibility 11 |

12 | 13 | 这个插件允许你在 CSS 中使用 `:export` 伪类,并且这个伪类下的属性将会被导出到 JavaScript 中。 14 | 15 | 除此之外,如果在 Vite 中启用了 CSS 预处理器,那我们就可以在 .scss、.sass、.less、.styl 和 .stylus 文件中使用 `:export`。 16 | 17 | [如何在 Vite 中使用 CSS 预处理器](https://vitejs.dev/guide/features.html#css-pre-processors) 18 | 19 | > 注意:Vite5 请使用 3.x,Vite4 请使用 2.x,Vite2 和 Vite3 请使用 1.x。 20 | 21 | ## 安装 ❤️ 22 | 23 | ```shell 24 | npm install vite-plugin-css-export -D 25 | ``` 26 | 27 | or 28 | 29 | ```shell 30 | yarn add vite-plugin-css-export -D 31 | ``` 32 | 33 | or 34 | 35 | ```shell 36 | pnpm add vite-plugin-css-export -D 37 | ``` 38 | 39 | ## 使用 💡 40 | 41 | ### 快速上手 42 | 43 | ```typescript 44 | // vite.config.ts 45 | import ViteCSSExportPlugin from 'vite-plugin-css-export' 46 | import { defineConfig } from 'vite' 47 | 48 | export default defineConfig({ 49 | plugins: [ViteCSSExportPlugin()] 50 | }) 51 | ``` 52 | 53 | ```css 54 | /* example.css */ 55 | :root { 56 | --font-color: #333; 57 | } 58 | 59 | :export { 60 | fontColor: var(--font-color); 61 | fontSize: 14px; 62 | } 63 | 64 | :export button { 65 | bgColor: #462dd3; 66 | color: #fff; 67 | } 68 | 69 | :export menu menuItem { 70 | bgColor: #1d243a; 71 | color: #fff; 72 | } 73 | ``` 74 | 75 | ```typescript 76 | // 如果使用了 Typescript ,你需要引用这个声明文件 77 | // 里面包含了所需的通配符模块声明,如 *.css?export 78 | // env.d.ts 79 | /// 80 | 81 | // 如果你想要代码提示 82 | interface CSSPropertiesExportedData { 83 | fontColor: string 84 | fontSize: string 85 | button: { 86 | bgColor: string 87 | color: string 88 | } 89 | menu: { 90 | menuItem: { 91 | bgColor: string 92 | color: string 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | 在导入时,路径中需加入后缀 `?export`。 99 | 100 | ```typescript 101 | // main.ts 102 | import cssResult from './assets/style/example.css?export' 103 | 104 | console.log(cssResult) 105 | 106 | // output 107 | // { 108 | // fontColor: "var(--font-color)", 109 | // fontSize: "14px", 110 | // button: { 111 | // bgColor: "#462dd3", 112 | // color: "#fff" 113 | // }, 114 | // menu: { 115 | // menuItem: { 116 | // bgColor: "#1d243a", 117 | // color: "#fff" 118 | // } 119 | // } 120 | // } 121 | ``` 122 | 123 | ### CSS 预处理器 124 | 125 | 如果你启用了 CSS 预处理器,那么你可以使用嵌套规则,便于我们定义一些复杂的结构。 126 | 127 | ```scss 128 | // .scss 129 | :root { 130 | --font-color: #333; 131 | } 132 | 133 | $menuItemBgColor: #1d243a; 134 | 135 | :export { 136 | fontColor: var(--font-color); 137 | fontSize: 14px; 138 | button { 139 | bgcolor: #462dd3; 140 | color: #fff; 141 | } 142 | menu { 143 | menuItem { 144 | bgcolor: $menuItemBgColor; 145 | color: #fff; 146 | } 147 | } 148 | } 149 | ``` 150 | 151 | ### CSS Module 152 | 153 | 与 CSS module 一起使用时,需要进行一些简单的配置,默认情况下,导出的结果中不会包含 CSS module 的相关内容(除了`:export`下的内容)。 154 | 155 | ```typescript 156 | // vite.config.ts 157 | import ViteCSSExportPlugin from 'vite-plugin-css-export' 158 | import { defineConfig } from 'vite' 159 | 160 | export default defineConfig({ 161 | plugins: [ 162 | ViteCSSExportPlugin({ 163 | cssModule: { 164 | isGlobalCSSModule: false, 165 | enableExportMerge: true, // default false 166 | sharedDataExportName: 'cssExportedData' // default 'sharedData' 167 | } 168 | }) 169 | ] 170 | }) 171 | ``` 172 | 173 | ```scss 174 | // example.module.scss 175 | :root { 176 | --font-color: #333; 177 | } 178 | 179 | $menuItemBgColor: #1d243a; 180 | 181 | .base-button { 182 | background-color: transparent; 183 | } 184 | 185 | // :export 的别名 186 | :share { 187 | fontcolor: var(--font-color); 188 | fontsize: 14px; 189 | 190 | button { 191 | bgcolor: #462dd3; 192 | color: #fff; 193 | } 194 | 195 | menu { 196 | menuItem { 197 | bgcolor: $menuItemBgColor; 198 | color: #fff; 199 | } 200 | } 201 | } 202 | 203 | :export { 204 | fontColor: var(--font-color); 205 | fontSize: 14px; 206 | } 207 | ``` 208 | 209 | ```typescript 210 | // main.ts 211 | import cssModuleResult from './assets/style/example.module.scss?export' 212 | 213 | console.log(cssModuleResult) 214 | 215 | // output 216 | // { 217 | // cssExportedData: { 218 | // fontColor: "var(--font-color)", 219 | // fontSize: "14px", 220 | // button: { 221 | // bgColor: "#462dd3", 222 | // color: "#fff" 223 | // }, 224 | // menu: { 225 | // menuItem: { 226 | // bgColor: "#1d243a", 227 | // color: "#fff" 228 | // } 229 | // } 230 | // }, 231 | // fontColor: "var(--font-color)", 232 | // fontSize: "14px", 233 | // "base-button": "_base-button_1k9w3_5" // css module 234 | // } 235 | 236 | // 当 enableExportMerge 为 false时,将不会包含CSS module的相关内容 237 | // output 238 | // { 239 | // fontColor: "var(--font-color)", 240 | // fontSize: "14px", 241 | // button: { 242 | // bgColor: "#462dd3", 243 | // color: "#fff" 244 | // }, 245 | // menu: { 246 | // menuItem: { 247 | // bgColor: "#1d243a", 248 | // color: "#fff" 249 | // } 250 | // } 251 | // } 252 | ``` 253 | 254 | ### 注意 ⚠ 255 | 256 | 如果插件与 CSS module 一起使用,请将 `:export` 替换为 `:share` ,这样做可以避免与 CSS module 提供的`:export`之间的未知冲突。 257 | 258 | > 实际上你仍然可以使用`:export`,它并不会导致运行错误,`:share` 是 `:export` 的别名。 259 | 260 | 请不要在属性名称中键入以下字符: 261 | 262 | ```bash 263 | 264 | "/", "~", ">", "<", "[", "]", "(", ")", ".", "#", "@", ":", "*" 265 | ``` 266 | 267 | 由于本插件应用在`vite:css`之后,所以一切解析行为都基于`vite:css`返回的结果,当你键入以上字符时,存在一些字符本插件无法给出正确的警告/错误信息,例如:`@` 268 | 269 | ```scss 270 | // your code 271 | :export { 272 | fontColor: var(--font-color); 273 | fontSize: 14px; 274 | 275 | button { 276 | bgcolor: #462dd3; 277 | color: #fff; 278 | } 279 | 280 | @menu { 281 | menuItem { 282 | bgcolor: $menuItemBgColor; 283 | color: #fff; 284 | } 285 | } 286 | } 287 | ``` 288 | 289 | ```css 290 | /** after vite:css */ 291 | :export { 292 | fontColor: var(--font-color); 293 | fontSize: 14px; 294 | } 295 | :export button { 296 | bgColor: #462dd3; 297 | color: #fff; 298 | } 299 | /** 无法捕捉 @menu */ 300 | @menu { 301 | :export menuItem { 302 | bgColor: #1d243a; 303 | color: #fff; 304 | } 305 | } 306 | ``` 307 | 308 | ```javascript 309 | // after vite:css-export 310 | { 311 | fontColor: "var(--font-color)", 312 | fontSize: "14px", 313 | button: { 314 | bgColor: "#462dd3", 315 | color: "#fff" 316 | }, 317 | // menu 丢失 318 | menuItem: { 319 | bgColor: "#1d243a", 320 | color: "#fff" 321 | } 322 | } 323 | ``` 324 | 325 | ### 代码检查 326 | 327 | 你可能会得到编辑器或者 Stylelint 的一些警告,你可以把相关规则关闭。 328 | 329 | #### VS Code 330 | 331 | ```json 332 | { 333 | "css.lint.unknownProperties": "ignore", 334 | "scss.lint.unknownProperties": "ignore", 335 | "less.lint.unknownProperties": "ignore" 336 | } 337 | ``` 338 | 339 | #### Stylelint 340 | 341 | ```json 342 | { 343 | "rules": { 344 | "property-no-unknown": [ 345 | true, 346 | { 347 | "ignoreSelectors": [":export", ":share"] 348 | } 349 | ], 350 | "property-case": null, 351 | "selector-pseudo-class-no-unknown": [ 352 | true, 353 | { 354 | "ignorePseudoClasses": ["export", "share"] 355 | } 356 | ], 357 | "selector-type-no-unknown": [ 358 | true, 359 | { 360 | "ignore": ["default-namespace"] 361 | } 362 | ] 363 | } 364 | } 365 | ``` 366 | 367 | ## 配置项 ⚙️ 368 | 369 | ### shouldTransform 370 | 371 | - **type:** `(id: string) => boolean` 372 | 373 | - **default:** `undefined` 374 | 375 | - **description:** 该选项允许你额外指定哪些样式文件应该被转换,而不仅仅是`?export`,用法如下: 376 | 377 | ```typescript 378 | // vite.config.ts 379 | export default defineConfig({ 380 | plugins: [ 381 | ViteCSSExportPlugin({ 382 | shouldTransform(id) { 383 | const include = path.resolve( 384 | process.cwd(), 385 | 'example/assets/style/share-to-js' 386 | ) 387 | return path.resolve(id).indexOf(include) > -1 388 | } 389 | }) 390 | ] 391 | }) 392 | ``` 393 | 394 | ### propertyNameTransformer 395 | 396 | - **type:** `(key: string) => string` 397 | 398 | - **default:** `undefined` 399 | 400 | - **description:** 该选项允许你定义一个转换 CSS 属性名称的方法,它并不会处理 additionalData。插件内置了一些方法,用法如下: 401 | 402 | ```typescript 403 | // vite.config.ts 404 | import { 405 | default as ViteCSSExportPlugin, 406 | kebabCaseToUpperCamelCase, 407 | kebabCaseToLowerCamelCase, 408 | kebabCaseToPascalCase 409 | } from 'vite-plugin-css-export' 410 | 411 | export default defineConfig({ 412 | plugins: [ 413 | ViteCSSExportPlugin({ 414 | propertyNameTransformer: kebabCaseToUpperCamelCase 415 | }) 416 | ] 417 | }) 418 | ``` 419 | 420 | ### additionalData 421 | 422 | - **type:** `SharedCSSData` 423 | 424 | - **default:** `{}` 425 | 426 | - **description:** 该选项允许你将指定的数据附加到所有的已处理的结果中,我们可以在这里分享一些常用的属性值。 427 | 428 | ### cssModule 429 | 430 | #### cssModule.isGlobalCSSModule 431 | 432 | - **type:** `boolean` 433 | 434 | - **default:** `false` 435 | 436 | - **description:** 是否在全局启用了 CSS module,而不仅仅是在 `.module.[suffix]` 文件中。 437 | 438 | #### cssModule.enableExportMerge 439 | 440 | - **type:** `boolean` 441 | 442 | - **default:** `false` 443 | 444 | - **description:** 当值为 true 时, `sharedData` 将会和 CSS module 的内容合并后再导出, 否则只有 `sharedData` 会被导出。它在使用`?inline`时不会生效。 445 | 446 | > _`sharedData` 是本插件处理 CSS 内容后的结果_ 447 | 448 | #### cssModule.sharedDataExportName 449 | 450 | - **type:** `string` 451 | 452 | - **default:** `'sharedData'` 453 | 454 | - **description:** 当 `cssModule.enableExportMerge` 值为 true 时, 修改导出结果中 `sharedData` 的属性名称。它在使用`?inline`时不会生效。 455 | -------------------------------------------------------------------------------- /build.config.js: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: ['src/index'], 5 | clean: true, 6 | declaration: true, 7 | externals: ['vite'], 8 | rollup: { 9 | emitCJS: true 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /client.d.ts: -------------------------------------------------------------------------------- 1 | interface CSSPropertiesExportedData { 2 | readonly [key: string]: any 3 | } 4 | 5 | // css module 6 | declare module '*.module.css?export' {} 7 | 8 | declare module '*.module.scss?export' {} 9 | 10 | declare module '*.module.sass?export' {} 11 | 12 | declare module '*.module.less?export' {} 13 | 14 | declare module '*.module.styl?export' {} 15 | 16 | declare module '*.module.stylus?export' {} 17 | 18 | // css pre-processor 19 | declare module '*.css?export' { 20 | const cssExportVariables: CSSPropertiesExportedData 21 | export default cssExportVariables 22 | } 23 | 24 | declare module '*.scss?export' { 25 | const cssExportVariables: CSSPropertiesExportedData 26 | export default cssExportVariables 27 | } 28 | 29 | declare module '*.sass?export' { 30 | const cssExportVariables: CSSPropertiesExportedData 31 | export default cssExportVariables 32 | } 33 | 34 | declare module '*.less?export' { 35 | const cssExportVariables: CSSPropertiesExportedData 36 | export default cssExportVariables 37 | } 38 | 39 | declare module '*.styl?export' { 40 | const cssExportVariables: CSSPropertiesExportedData 41 | export default cssExportVariables 42 | } 43 | 44 | declare module '*.stylus?export' { 45 | const cssExportVariables: CSSPropertiesExportedData 46 | export default cssExportVariables 47 | } 48 | 49 | // inline 50 | declare module '*.css?export&inline' { 51 | const cssExportVariables: CSSPropertiesExportedData 52 | export default cssExportVariables 53 | } 54 | 55 | declare module '*.scss?export&inline' { 56 | const cssExportVariables: CSSPropertiesExportedData 57 | export default cssExportVariables 58 | } 59 | 60 | declare module '*.sass?export&inline' { 61 | const cssExportVariables: CSSPropertiesExportedData 62 | export default cssExportVariables 63 | } 64 | 65 | declare module '*.less?export&inline' { 66 | const cssExportVariables: CSSPropertiesExportedData 67 | export default cssExportVariables 68 | } 69 | 70 | declare module '*.styl?export&inline' { 71 | const cssExportVariables: CSSPropertiesExportedData 72 | export default cssExportVariables 73 | } 74 | 75 | declare module '*.stylus?export&inline' { 76 | const cssExportVariables: CSSPropertiesExportedData 77 | export default cssExportVariables 78 | } 79 | -------------------------------------------------------------------------------- /example/style/example-transformer.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-color: #333; 3 | } 4 | 5 | $menuItemBgColor: #1d243a; 6 | 7 | .base-button { 8 | background-color: transparent; 9 | } 10 | 11 | :export { 12 | font-color: var(--font-color); 13 | font-size: 14px; 14 | 15 | button { 16 | bg-color: #462dd3; 17 | color: #fff; 18 | } 19 | 20 | menu { 21 | menu-item { 22 | bg-color: $menuItemBgColor; 23 | color: #fff; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/style/example.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-color: #333; 3 | } 4 | 5 | .base-button { 6 | background-color: transparent; 7 | } 8 | 9 | :export { 10 | fontColor: var(--font-color); 11 | fontSize: 14px; 12 | } 13 | 14 | :export button { 15 | bgColor: #462dd3; 16 | color: #fff; 17 | } 18 | 19 | :export menu menuItem { 20 | bgColor: #1d243a; 21 | color: #fff; 22 | } 23 | -------------------------------------------------------------------------------- /example/style/example.module.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-color: #333; 3 | } 4 | 5 | $menuItemBgColor: #1d243a; 6 | 7 | .base-button { 8 | background-color: transparent; 9 | } 10 | 11 | :share { 12 | fontcolor: var(--font-color); 13 | fontsize: 14px; 14 | 15 | button { 16 | bgcolor: #462dd3; 17 | color: #fff; 18 | } 19 | 20 | menu { 21 | menuItem { 22 | bgcolor: $menuItemBgColor; 23 | color: #fff; 24 | } 25 | } 26 | } 27 | 28 | // conflict 29 | :export { 30 | fontColor: var(--font-color); 31 | fontSize: 14px; 32 | } 33 | -------------------------------------------------------------------------------- /example/style/example.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-color: #333; 3 | } 4 | 5 | $menuItemBgColor: #1d243a; 6 | 7 | .base-button { 8 | background-color: transparent; 9 | } 10 | 11 | :export { 12 | fontColor: var(--font-color); 13 | fontSize: 14px; 14 | 15 | button { 16 | bgcolor: #462dd3; 17 | color: #fff; 18 | } 19 | 20 | menu { 21 | menuItem { 22 | bgcolor: $menuItemBgColor; 23 | color: #fff; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/style/share-to-js/index.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-color: #333; 3 | } 4 | 5 | $menuItemBgColor: #1d243a; 6 | 7 | .base-button { 8 | background-color: transparent; 9 | } 10 | 11 | :export { 12 | fontColor: var(--font-color); 13 | fontSize: 14px; 14 | 15 | button { 16 | bgcolor: #462dd3; 17 | color: #fff; 18 | } 19 | 20 | menu { 21 | menuItem { 22 | bgcolor: $menuItemBgColor; 23 | color: #fff; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/vue/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 22 | -------------------------------------------------------------------------------- /example/vue/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shixuanhong/vite-plugin-css-export/65a8e5834e121ce07a339ad451e4731bd64209da/example/vue/assets/logo.png -------------------------------------------------------------------------------- /example/vue/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 35 | 36 | 53 | -------------------------------------------------------------------------------- /example/vue/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | interface CSSPropertiesExportedData { 5 | fontColor: string 6 | fontSize: string 7 | button: { 8 | bgColor: string 9 | color: string 10 | } 11 | menu: { 12 | menuItem: { 13 | bgColor: string 14 | color: string 15 | } 16 | } 17 | } 18 | 19 | declare module '*.vue' { 20 | import { DefineComponent } from 'vue' 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 22 | const component: DefineComponent<{}, {}, any> 23 | export default component 24 | } 25 | 26 | declare module '*.module.scss?export' { 27 | export const cssExportedData: CSSPropertiesExportedData 28 | const cssModuleResult: { 29 | [key: string]: any 30 | cssExportedData: CSSPropertiesExportedData 31 | } 32 | export default cssModuleResult 33 | } 34 | -------------------------------------------------------------------------------- /example/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/vue/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | // will be injected into the page 5 | import cssResult from '../style/example.css?export' 6 | // will not be injected 7 | import cssResultWithInline from '../style/example.css?export&inline' 8 | import scssResult from '../style/example.scss?export&inline' 9 | import { 10 | default as cssModuleResult, 11 | cssExportedData 12 | } from '../style/example.module.scss?export' 13 | import cssModuleInlineResult from '../style/example.module.scss?export&inline' 14 | import transformerResult from '../style/example-transformer.scss?export&inline' 15 | import shouldTransformResult from '../style/share-to-js/index.scss?inline' 16 | 17 | console.log('.css', cssResult) 18 | console.log('.css inline', cssResultWithInline) 19 | console.log('.scss', scssResult) 20 | console.log('css module', cssModuleResult, cssExportedData) 21 | console.log('css module inline', cssModuleInlineResult) 22 | console.log('transformer', transformerResult) 23 | console.log('shouldTransformResult', shouldTransformResult) 24 | 25 | createApp(App).mount('#app') 26 | -------------------------------------------------------------------------------- /example/vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shixuanhong/vite-plugin-css-export/65a8e5834e121ce07a339ad451e4731bd64209da/example/vue/public/favicon.ico -------------------------------------------------------------------------------- /example/vue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import Inspect from 'vite-plugin-inspect' 4 | import { 5 | default as ViteCSSExportPlugin, 6 | kebabCaseToUpperCamelCase 7 | } from '../../src' 8 | import path from 'node:path' 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | vue(), 13 | ViteCSSExportPlugin({ 14 | shouldTransform(id) { 15 | const include = path.resolve(process.cwd(), 'example/style/share-to-js') 16 | return path.resolve(id).indexOf(include) > -1 17 | }, 18 | additionalData: { 19 | nav: { 20 | navBgColor: '#fff' 21 | } 22 | }, 23 | cssModule: { 24 | isGlobalCSSModule: false, 25 | enableExportMerge: true, 26 | sharedDataExportName: 'cssExportedData' 27 | }, 28 | propertyNameTransformer: kebabCaseToUpperCamelCase 29 | }), 30 | Inspect() 31 | ] 32 | }) 33 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node' 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-css-export", 3 | "author": { 4 | "name": "shixuanhong" 5 | }, 6 | "description": "A Vite plugin for sharing variables between Javascript and CSS (or Sass, Less, etc.)", 7 | "version": "3.0.2", 8 | "keywords": [ 9 | "vite", 10 | "vite-plugin", 11 | "vite-plugin-css-export" 12 | ], 13 | "license": "MIT", 14 | "homepage": "https://github.com/shixuanhong/vite-plugin-css-export#readme", 15 | "bugs": { 16 | "url": "https://github.com/shixuanhong/vite-plugin-css-export/issues" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/shixuanhong/vite-plugin-css-export.git" 21 | }, 22 | "scripts": { 23 | "dev": "vite example/vue", 24 | "build": "vite build example/vue", 25 | "build:lib": "unbuild", 26 | "preview": "vite preview example/vue", 27 | "test": "jest test", 28 | "format": "prettier --write .", 29 | "publish:lib": "unbuild && npm publish" 30 | }, 31 | "type": "module", 32 | "exports": { 33 | ".": { 34 | "require": "./dist/index.cjs", 35 | "import": "./dist/index.mjs", 36 | "types": "./dist/index.d.ts" 37 | }, 38 | "./*": "./*" 39 | }, 40 | "main": "dist/index.cjs", 41 | "module": "dist/index.mjs", 42 | "types": "dist/index.d.ts", 43 | "files": [ 44 | "dist", 45 | "client.d.ts" 46 | ], 47 | "devDependencies": { 48 | "@types/estree": "^1.0.0", 49 | "@types/jest": "^27.4.1", 50 | "@types/node": "^20.11.5", 51 | "@vitejs/plugin-vue": "^5.2.1", 52 | "jest": "^29.7.0", 53 | "prettier": "^3.2.4", 54 | "rollup": "^4.17.0", 55 | "sass": "^1.83.0", 56 | "ts-jest": "^29.2.5", 57 | "typescript": "^5.7.2", 58 | "unbuild": "^3.2.0", 59 | "vite": "^6.0.7", 60 | "vite-plugin-inspect": "^0.10.6", 61 | "vue": "^3.5.13", 62 | "vue-tsc": "^2.0.21" 63 | }, 64 | "peerDependencies": { 65 | "vite": "^5.0.0 || ^6.0.0" 66 | }, 67 | "dependencies": { 68 | "@rollup/pluginutils": "^5.1.0", 69 | "acorn-walk": "^8.3.2", 70 | "escodegen": "^2.1.0", 71 | "postcss": "^8.4.33" 72 | }, 73 | "engines": { 74 | "node": ">=18.0.0" 75 | }, 76 | "packageManager": "pnpm@9.0.0+" 77 | } 78 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, ResolvedConfig } from 'vite' 2 | import { dataToEsm } from '@rollup/pluginutils' 3 | import { parse } from 'postcss' 4 | import { 5 | drillDown, 6 | clearExportNamedDeclaration, 7 | isSourceDescription, 8 | getPluginTransformHandler, 9 | getCSSVirtualId 10 | } from './utils' 11 | import type { 12 | CSSModuleOptions, 13 | ViteCSSExportPluginOptions, 14 | SharedCSSData, 15 | ParsedResult 16 | } from './interface' 17 | import type { Program } from 'estree' 18 | import type { TransformPluginContext, TransformResult } from 'rollup' 19 | import escodegen from 'escodegen' 20 | 21 | export { CSSModuleOptions, ViteCSSExportPluginOptions, SharedCSSData } 22 | 23 | export * from './transformer' 24 | 25 | let shouldTransform: ViteCSSExportPluginOptions['shouldTransform'] 26 | const exportRE = /(?:\?|&)export\b/ 27 | const inlineRE = /(?:\?|&)inline\b/ 28 | const cssLangs = `\\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\\?)` 29 | const cssLangRE = new RegExp(cssLangs) 30 | const cssModuleRE = new RegExp(`\\.module${cssLangs}`) 31 | 32 | const matchedList = [':export', ':share'] 33 | const macthingRE = new RegExp( 34 | `(^(${matchedList.join('|')})$|^(${matchedList.join('|')})\\s)` 35 | ) 36 | const errorCharacterArr = [ 37 | '/', 38 | '~', 39 | '>', 40 | '<', 41 | '[', 42 | ']', 43 | '(', 44 | ')', 45 | '.', 46 | '#', 47 | '@', 48 | ':', 49 | '*' 50 | ] 51 | const warnCharacterArr = ['\\', '-'] 52 | 53 | const errorCharacters = '[/~><\\[\\]\\(\\)\\.#@\\:\\*]' 54 | const warnCharacters = '[\\-]' 55 | const nameErrorValidRE = new RegExp( 56 | `(?!${macthingRE.source})${errorCharacters}` 57 | ) 58 | const nameWarnValidRE = new RegExp(`(?!${macthingRE.source})${warnCharacters}`) 59 | 60 | const defaultCSSModuleOptions: CSSModuleOptions = { 61 | isGlobalCSSModule: false, 62 | enableExportMerge: false, 63 | sharedDataExportName: 'sharedData' 64 | } 65 | 66 | const isCSSRequest = (request: string): boolean => cssLangRE.test(request) 67 | 68 | const isTransform = (id: string): boolean => 69 | shouldTransform ? shouldTransform(id) || exportRE.test(id) : exportRE.test(id) 70 | 71 | /** 72 | * parse css code after vite:css 73 | * 74 | * @param {TransformPluginContext} this 75 | * @param {string} cssCode 76 | * @return {ParsedResult} 77 | */ 78 | function parseCode( 79 | this: TransformPluginContext, 80 | cssCode: string, 81 | propertyNameTransformer?: (key: string) => string 82 | ): ParsedResult { 83 | const sharedData: SharedCSSData = {} 84 | let otherCode = '' 85 | 86 | parse(cssCode).walkRules((ruleNode) => { 87 | const selector = ruleNode.selector 88 | if (macthingRE.test(selector)) { 89 | // error 90 | let nameErrorValidResult = nameErrorValidRE.exec(selector) 91 | if (nameErrorValidResult && nameErrorValidResult.length > 0) { 92 | this.error( 93 | `The property name cannot contain the characters: ${errorCharacterArr 94 | .map((c) => `"${c}"`) 95 | .join(', ')}.\n`, 96 | ruleNode.positionInside(nameErrorValidResult.index) 97 | ) 98 | } else { 99 | // warning 100 | let nameWarnValidResult = nameWarnValidRE.exec(selector) 101 | if ( 102 | !propertyNameTransformer && 103 | nameWarnValidResult && 104 | nameWarnValidResult.length > 0 105 | ) { 106 | this.warn( 107 | `The property name should not contain the characters: ${warnCharacterArr 108 | .map((c) => `"${c}"`) 109 | .join( 110 | ', ' 111 | )}. Use option propertyNameTransformer to suppress this warning.`, 112 | ruleNode.positionInside(nameWarnValidResult.index) 113 | ) 114 | } 115 | // assign values to sharedData 116 | const levelNames = selector 117 | .split(' ') 118 | .slice(1) 119 | .map((levelName) => 120 | propertyNameTransformer 121 | ? propertyNameTransformer(levelName) 122 | : levelName 123 | ) 124 | const target = drillDown(sharedData, levelNames) 125 | ruleNode.walkDecls((declNode) => { 126 | let propertyName = propertyNameTransformer 127 | ? propertyNameTransformer(declNode.prop) 128 | : declNode.prop 129 | target[propertyName] = declNode.value 130 | }) 131 | } 132 | } else { 133 | // unprocessed css code will be injected into the index.html by vite:css-post 134 | otherCode += `\n${ruleNode.toString()}` 135 | } 136 | }) 137 | return { 138 | sharedData, 139 | otherCode 140 | } as ParsedResult 141 | } 142 | 143 | function vitePostCodeHandler( 144 | this: any, 145 | id: string, 146 | code: string, 147 | cssModuleOptions: CSSModuleOptions, 148 | parsedResultCache: Map 149 | ): string { 150 | const { 151 | isGlobalCSSModule = false, 152 | enableExportMerge = false, 153 | sharedDataExportName = 'sharedData' 154 | } = cssModuleOptions 155 | if (!parsedResultCache.has(id)) { 156 | return '' 157 | } 158 | const parsedResult = parsedResultCache.get(id) 159 | const sharedDataStr = dataToEsm(parsedResult.sharedData, { 160 | namedExports: true, 161 | preferConst: true 162 | }) 163 | let sharedAst = this.parse(sharedDataStr) 164 | if (typeof code === 'string' && code !== '') { 165 | const ast = this.parse(code) as Program 166 | // compatible with css module 167 | if (cssModuleRE.test(id) || isGlobalCSSModule) { 168 | if (enableExportMerge && !inlineRE.test(id)) { 169 | return code.replace( 170 | /export default\s*\{/, 171 | [ 172 | `export const ${sharedDataExportName} = ${JSON.stringify( 173 | parsedResult.sharedData 174 | )}`, 175 | `export default { ${sharedDataExportName},` 176 | ].join('\n') 177 | ) 178 | } else { 179 | // override 180 | // clear all named exports, exclude /^__vite__/ 181 | clearExportNamedDeclaration(ast, /^__vite__/) 182 | } 183 | } 184 | // remove the original default export 185 | const defaultIndex = ast.body.findIndex( 186 | (item: { type: string }) => item.type === 'ExportDefaultDeclaration' 187 | ) 188 | defaultIndex > -1 && ast.body.splice(defaultIndex, 1) 189 | ast.body = ast.body.concat(sharedAst.body) 190 | 191 | return escodegen.generate(ast) 192 | } else { 193 | return sharedDataStr 194 | } 195 | } 196 | 197 | function hijackCSSPostPlugin( 198 | cssPostPlugin: Plugin, 199 | config: ResolvedConfig, 200 | cssModuleOptions: CSSModuleOptions, 201 | parsedResultCache: Map, 202 | codeCacheMap: Map 203 | ): void { 204 | if (cssPostPlugin.transform) { 205 | const _transform = getPluginTransformHandler(cssPostPlugin.transform) 206 | cssPostPlugin.transform = async function (...args) { 207 | const [cssCode, id, ...restArgs] = args 208 | if (isCSSRequest(id) && isTransform(id)) { 209 | const { isGlobalCSSModule = false } = cssModuleOptions 210 | // result of vite:post 211 | // this result will be modified if the conditions of vite:css-export are met. 212 | let result: TransformResult = '' 213 | if (cssModuleRE.test(id) || isGlobalCSSModule) { 214 | result = await _transform.apply(this, ['', id, ...restArgs]) 215 | } 216 | let jsCode 217 | if (isSourceDescription(result)) { 218 | jsCode = vitePostCodeHandler.call( 219 | this, 220 | id, 221 | result.code, 222 | cssModuleOptions, 223 | parsedResultCache 224 | ) 225 | } else { 226 | jsCode = vitePostCodeHandler.call( 227 | this, 228 | id, 229 | result as string, 230 | cssModuleOptions, 231 | parsedResultCache 232 | ) 233 | } 234 | const output = [] 235 | if (!inlineRE.test(id)) { 236 | const cssVirtualId = getCSSVirtualId(id) 237 | codeCacheMap.set(cssVirtualId, cssCode) 238 | output.push(`import "${cssVirtualId}"`) 239 | } 240 | output.push(jsCode) 241 | 242 | return { 243 | code: output.join('\n'), 244 | map: { mappings: '' }, 245 | moduleSideEffects: true 246 | } 247 | } else { 248 | return await _transform.apply(this, args) 249 | } 250 | } 251 | } 252 | } 253 | 254 | /** 255 | * the plugin is applied after vite:css and before vite:post 256 | * @param {ViteCSSExportPluginOptions} [options={}] 257 | * @return {Plugin} 258 | */ 259 | export default function ViteCSSExportPlugin( 260 | options: ViteCSSExportPluginOptions = {} 261 | ): Plugin { 262 | const pluginName = 'vite:css-export' 263 | const { 264 | cssModule = defaultCSSModuleOptions, 265 | additionalData = {}, 266 | propertyNameTransformer, 267 | shouldTransform: _shouldTransform 268 | } = options 269 | 270 | shouldTransform = _shouldTransform 271 | 272 | const parsedResultCache = new Map() 273 | const codeCacheMap = new Map() 274 | let config 275 | return { 276 | name: pluginName, 277 | configResolved(resolvedConfig) { 278 | config = resolvedConfig 279 | const cssPostPlugin = config.plugins.find( 280 | (item) => item.name === 'vite:css-post' 281 | ) 282 | cssPostPlugin && 283 | hijackCSSPostPlugin( 284 | cssPostPlugin, 285 | config, 286 | cssModule, 287 | parsedResultCache, 288 | codeCacheMap 289 | ) 290 | }, 291 | buildStart() { 292 | codeCacheMap.clear() 293 | parsedResultCache.clear() 294 | }, 295 | resolveId(id) { 296 | if (codeCacheMap.has(id)) { 297 | return id 298 | } 299 | }, 300 | load(id, options) { 301 | if (codeCacheMap.has(id)) { 302 | return codeCacheMap.get(id) ?? '' 303 | } 304 | }, 305 | async transform(code, id, _options) { 306 | if (isCSSRequest(id) && isTransform(id)) { 307 | const parsedResult = parseCode.call(this, code, propertyNameTransformer) 308 | // append additionalData 309 | Object.assign(parsedResult.sharedData, additionalData) 310 | // cache the current parsedResult for use in vite:post 311 | parsedResultCache.set(id, parsedResult) 312 | return { 313 | code: parsedResult.otherCode, 314 | map: { mappings: '' } 315 | } 316 | } 317 | return null 318 | } 319 | } as Plugin 320 | } 321 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | export interface CSSModuleOptions { 2 | /** 3 | * Whether the CSS Module is used globally, not just in the `.module.[suffix]` file. 4 | * 5 | * @default false 6 | * @type {boolean} 7 | * @memberof CSSModuleOptions 8 | */ 9 | isGlobalCSSModule?: boolean 10 | /** 11 | * When value is true, `sharedData` will be merged with the result of CSS Module, 12 | * otherwise only `sharedData` will be exported. 13 | * 14 | * `sharedData` is the parsed result of the plugin. 15 | * 16 | * @default false 17 | * @type {boolean} 18 | * @memberof CSSModuleOptions 19 | */ 20 | enableExportMerge?: boolean 21 | /** 22 | * When `cssModule.enableExportMerge` is true, modify the property name of `sharedData` in the merged result. 23 | * 24 | * @default "sharedData" 25 | * @type {string} 26 | * @memberof CSSModuleOptions 27 | */ 28 | sharedDataExportName?: string 29 | } 30 | export interface ViteCSSExportPluginOptions { 31 | /** 32 | * This option allows you to additionally specify which style files should be transformed, not just `?export`. 33 | * 34 | * ``` typescript 35 | * shouldTransform(id) { 36 | * const include = path.resolve(process.cwd(), 'example/assets/style/share-to-js') 37 | * return path.resolve(id).indexOf(include) > -1 38 | * } 39 | * ``` 40 | * 41 | * @memberof ViteCSSExportPluginOptions 42 | */ 43 | shouldTransform?: (id: string) => boolean 44 | /** 45 | * The option allows you to define a method for transforming CSS property names, but doesn`t transform additionalData. 46 | * 47 | * @memberof ViteCSSExportPluginOptions 48 | */ 49 | propertyNameTransformer?: (key: string) => string 50 | /** 51 | * The option allows you to append data to all processed results, we can share some common variables here. 52 | * 53 | * @type {SharedCSSData} 54 | * @memberof ViteCSSExportPluginOptions 55 | */ 56 | additionalData?: SharedCSSData 57 | /** 58 | * Options related to css module. 59 | * 60 | * @type {CSSModuleOptions} 61 | * @memberof ViteCSSExportPluginOptions 62 | */ 63 | cssModule?: CSSModuleOptions 64 | } 65 | 66 | export interface SharedCSSData { 67 | [key: string]: SharedCSSData | string 68 | } 69 | 70 | export interface ParsedResult { 71 | /** 72 | * Data shared with JavaScript. 73 | * 74 | * @type {SharedCSSData} 75 | */ 76 | sharedData: SharedCSSData 77 | /** 78 | * Unprocessed css code. 79 | * 80 | * @type {string} 81 | */ 82 | otherCode: string 83 | } 84 | -------------------------------------------------------------------------------- /src/transformer.ts: -------------------------------------------------------------------------------- 1 | export function kebabCaseToUpperCamelCase(name: string): string { 2 | return name 3 | .split('-') 4 | .map((item) => 5 | Array.from(item) 6 | .map((char, j) => (j === 0 ? char.toUpperCase() : char.toLowerCase())) 7 | .join('') 8 | ) 9 | .join('') 10 | } 11 | 12 | export function kebabCaseToLowerCamelCase(name: string): string { 13 | return name 14 | .split('-') 15 | .map((item, i) => 16 | Array.from(item) 17 | .map((char, j) => 18 | i !== 0 && j === 0 ? char.toUpperCase() : char.toLowerCase() 19 | ) 20 | .join('') 21 | ) 22 | .join('') 23 | } 24 | 25 | export const kebabCaseToPascalCase = kebabCaseToUpperCamelCase 26 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Program, Identifier, VariableDeclarator } from 'estree' 2 | import { simple } from 'acorn-walk' 3 | import { SourceDescription, TransformResult } from 'rollup' 4 | import { Plugin } from 'vite' 5 | import { makeLegalIdentifier } from '@rollup/pluginutils' 6 | 7 | export const drillDown = (obj: any, keys: Array): any => { 8 | if (!obj) { 9 | return obj 10 | } 11 | const key = keys.shift() 12 | if (key) { 13 | if (!obj[key]) { 14 | obj[key] = {} 15 | } 16 | if (keys.length > 0) { 17 | return drillDown(obj[key], keys) 18 | } else { 19 | return obj[key] 20 | } 21 | } else { 22 | return obj 23 | } 24 | } 25 | export function clearExportNamedDeclaration( 26 | ast: Program, 27 | exclude?: RegExp 28 | ): void { 29 | const filteredNodeList: Array = [] 30 | simple(ast as any, { 31 | ExportNamedDeclaration(node) { 32 | simple(node, { 33 | //@ts-ignore acorn-walk v8.3.2, its type declaration is incorrect 34 | VariableDeclarator(variableDeclaratorNode: unknown) { 35 | let name = (( 36 | (variableDeclaratorNode).id 37 | )).name 38 | if (exclude && !exclude.test(name)) { 39 | filteredNodeList.push(node) 40 | } 41 | } 42 | }) 43 | }, 44 | ExportDefaultDeclaration(node) { 45 | filteredNodeList.push(node) 46 | } 47 | }) 48 | ast.body = ast.body.filter((item) => filteredNodeList.indexOf(item) === -1) 49 | } 50 | export function isSourceDescription( 51 | obj: TransformResult 52 | ): obj is SourceDescription { 53 | return obj && typeof obj !== 'string' && typeof obj.code !== 'undefined' 54 | } 55 | 56 | export type PluginTransformHandler = ( 57 | this: any, 58 | code: string, 59 | id: string, 60 | options?: { 61 | ssr?: boolean 62 | } 63 | ) => Promise | TransformResult 64 | export function getPluginTransformHandler(transform: Plugin['transform']) { 65 | if (typeof transform === 'function') { 66 | return transform as PluginTransformHandler 67 | } else { 68 | return transform.handler as PluginTransformHandler 69 | } 70 | } 71 | 72 | export function getCSSVirtualId(id: string) { 73 | const [filename] = id.split('?') 74 | const cssVirtualId = `${makeLegalIdentifier(filename)}?lang.css` 75 | return cssVirtualId 76 | } 77 | -------------------------------------------------------------------------------- /test/transformer.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | kebabCaseToUpperCamelCase, 3 | kebabCaseToLowerCamelCase 4 | } from '../src/transformer' 5 | 6 | // kebabCaseToLowerCamelCase 7 | 8 | test('kebabCaseToLowerCamelCase: background-color to backgroundColor', () => { 9 | expect(kebabCaseToLowerCamelCase('background-color')).toBe('backgroundColor') 10 | }) 11 | 12 | test('kebabCaseToLowerCamelCase: BACKGROUND-COLOR to backgroundColor', () => { 13 | expect(kebabCaseToLowerCamelCase('background-color')).toBe('backgroundColor') 14 | }) 15 | 16 | test('kebabCaseToLowerCamelCase: bacKgrOund-cOlor to backgroundColor', () => { 17 | expect(kebabCaseToLowerCamelCase('background-color')).toBe('backgroundColor') 18 | }) 19 | 20 | test('kebabCaseToLowerCamelCase: backgroundColor to backgroundcolor', () => { 21 | expect(kebabCaseToLowerCamelCase('backgroundColor')).toBe('backgroundcolor') 22 | }) 23 | 24 | test('kebabCaseToLowerCamelCase: animation-iteration-count to animationIterationCount', () => { 25 | expect(kebabCaseToLowerCamelCase('animation-iteration-count')).toBe( 26 | 'animationIterationCount' 27 | ) 28 | }) 29 | 30 | test('kebabCaseToLowerCamelCase: animation-iteration-count- to animationIterationCount', () => { 31 | expect(kebabCaseToLowerCamelCase('animation-iteration-count-')).toBe( 32 | 'animationIterationCount' 33 | ) 34 | }) 35 | 36 | test('kebabCaseToLowerCamelCase: 1-2-3 to 123', () => { 37 | expect(kebabCaseToLowerCamelCase('1-2-3')).toBe('123') 38 | }) 39 | 40 | test('kebabCaseToLowerCamelCase: 1RRR-2AAAA-3DDDD to 1rrr2aaaa3dddd', () => { 41 | expect(kebabCaseToLowerCamelCase('1RRR-2AAAA-3DDDD')).toBe('1rrr2aaaa3dddd') 42 | }) 43 | 44 | test('kebabCaseToLowerCamelCase: $12D-2AA5DA-dD4dD to $12d2aa5daDd4dd', () => { 45 | expect(kebabCaseToLowerCamelCase('$12D-2AA5DA-dD4dD')).toBe('$12d2aa5daDd4dd') 46 | }) 47 | 48 | // kebabCaseToUpperCamelCase 49 | 50 | test('kebabCaseToUpperCamelCase: background-color to BackgroundColor', () => { 51 | expect(kebabCaseToUpperCamelCase('background-color')).toBe('BackgroundColor') 52 | }) 53 | 54 | test('kebabCaseToUpperCamelCase: BACKGROUND-COLOR to BackgroundColor', () => { 55 | expect(kebabCaseToUpperCamelCase('background-color')).toBe('BackgroundColor') 56 | }) 57 | 58 | test('kebabCaseToUpperCamelCase: bacKgrOund-cOlor to BackgroundColor', () => { 59 | expect(kebabCaseToUpperCamelCase('background-color')).toBe('BackgroundColor') 60 | }) 61 | 62 | test('kebabCaseToUpperCamelCase: BackgroundColor to Backgroundcolor', () => { 63 | expect(kebabCaseToUpperCamelCase('BackgroundColor')).toBe('Backgroundcolor') 64 | }) 65 | 66 | test('kebabCaseToUpperCamelCase: animation-iteration-count to AnimationIterationCount', () => { 67 | expect(kebabCaseToUpperCamelCase('animation-iteration-count')).toBe( 68 | 'AnimationIterationCount' 69 | ) 70 | }) 71 | 72 | test('kebabCaseToUpperCamelCase: animation-iteration-count- to AnimationIterationCount', () => { 73 | expect(kebabCaseToUpperCamelCase('animation-iteration-count-')).toBe( 74 | 'AnimationIterationCount' 75 | ) 76 | }) 77 | 78 | test('kebabCaseToUpperCamelCase: 1-2-3 to 123', () => { 79 | expect(kebabCaseToUpperCamelCase('1-2-3')).toBe('123') 80 | }) 81 | 82 | test('kebabCaseToUpperCamelCase: 1RRR-2AAAA-3DDDD to 1rrr2aaaa3dddd', () => { 83 | expect(kebabCaseToUpperCamelCase('1RRR-2AAAA-3DDDD')).toBe('1rrr2aaaa3dddd') 84 | }) 85 | 86 | test('kebabCaseToUpperCamelCase: $12D-2AA5DA-dD4dD to $12d2aa5daDd4dd', () => { 87 | expect(kebabCaseToUpperCamelCase('$12D-2AA5DA-dD4dD')).toBe('$12d2aa5daDd4dd') 88 | }) 89 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "lib": ["esnext", "DOM"] 12 | } 13 | } 14 | --------------------------------------------------------------------------------