├── .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 |
9 |
10 |
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 |
9 |
10 |
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 |
8 |
9 |
10 |
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 |
10 | {{ msg }}
11 |
12 |
13 | Recommended IDE setup:
14 | VSCode
15 | +
16 | Volar
17 |
18 |
19 | See README.md
for more information.
20 |
21 |
22 |
23 | Vite Docs
24 |
25 | |
26 | Vue 3 Docs
27 |
28 |
29 |
30 |
31 | Edit
32 | components/HelloWorld.vue
to test hot module replacement.
33 |
34 |
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 |
--------------------------------------------------------------------------------