vite-plugin-theme-css 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # vite-plugin-theme-css
2
+
3
+ Vite 插件:用一份主题配置生成 CSS 变量、SCSS helper 与 JS 运行时,支持多主题切换与热更新。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add vite-plugin-theme-css
9
+ # 或
10
+ npm i vite-plugin-theme-css
11
+ ```
12
+
13
+ 需配合 Vite `>=5`,若使用 SCSS helper,请自行安装 `sass`。
14
+
15
+ ## 快速开始
16
+
17
+ ### 1. 创建主题配置
18
+
19
+ 默认读取项目根目录的 `theme.config.ts`:
20
+
21
+ ```ts
22
+ import { defineTheme } from 'vite-plugin-theme-css'
23
+
24
+ export default defineTheme({
25
+ themes: {
26
+ light: {
27
+ text: {
28
+ primary: '#111',
29
+ secondary: '#666'
30
+ },
31
+ bg: {
32
+ page: '#fff'
33
+ }
34
+ },
35
+ dark: {
36
+ text: {
37
+ primary: '#f5f5f5',
38
+ secondary: '#aaa'
39
+ },
40
+ bg: {
41
+ page: '#111'
42
+ }
43
+ }
44
+ },
45
+ common: {
46
+ font: {
47
+ size: {
48
+ h1: '32px',
49
+ body: '14px'
50
+ }
51
+ }
52
+ }
53
+ })
54
+ ```
55
+
56
+ 嵌套对象会扁平化为 CSS 变量:
57
+
58
+ | 配置路径 | CSS 变量 |
59
+ | --- | --- |
60
+ | `themes.light.text.primary` | `--text-primary` |
61
+ | `common.font.size.h1` | `--common-font-size-h1` |
62
+
63
+ 主题变量挂在 `:root[data-theme="..."]`,公共变量挂在 `:root`。
64
+
65
+ > `defineTheme` 为类型辅助(恒等函数)。配置里调用了 `defineTheme` 但未 import 时,插件会自动注入,类似 Vite 的 `defineConfig`。
66
+
67
+ ### 2. 注册插件
68
+
69
+ ```ts
70
+ // vite.config.ts
71
+ import { defineConfig } from 'vite'
72
+ import { themeCss } from 'vite-plugin-theme-css'
73
+
74
+ export default defineConfig({
75
+ plugins: [themeCss()]
76
+ })
77
+ ```
78
+
79
+ ### 3. 引入 CSS 变量
80
+
81
+ 在入口文件中导入虚拟 CSS:
82
+
83
+ ```ts
84
+ import 'virtual:theme-css/css'
85
+ ```
86
+
87
+ 生成示例:
88
+
89
+ ```css
90
+ :root {
91
+ --common-font-size-h1: 32px;
92
+ --common-font-size-body: 14px;
93
+ }
94
+
95
+ :root[data-theme="light"] {
96
+ --text-primary: #111;
97
+ --text-secondary: #666;
98
+ --bg-page: #fff;
99
+ }
100
+
101
+ :root[data-theme="dark"] {
102
+ --text-primary: #f5f5f5;
103
+ --text-secondary: #aaa;
104
+ --bg-page: #111;
105
+ }
106
+ ```
107
+
108
+ ## 用法
109
+
110
+ ### CSS / 内联样式
111
+
112
+ ```css
113
+ .card {
114
+ color: var(--text-primary);
115
+ background: var(--bg-page);
116
+ }
117
+ ```
118
+
119
+ ### SCSS
120
+
121
+ 插件会通过 `css.preprocessorOptions.scss.additionalData` 自动注入 helper(命名空间默认 `token`),可直接使用:
122
+
123
+ ```scss
124
+ .title {
125
+ color: token.theme(text, primary); // var(--text-primary)
126
+ font-size: token.common(font, size, h1); // var(--common-font-size-h1)
127
+ }
128
+
129
+ .banner {
130
+ @include token.on-theme(dark) {
131
+ opacity: 0.9;
132
+ }
133
+ }
134
+ ```
135
+
136
+ | API | 说明 |
137
+ | --- | --- |
138
+ | `token.theme($keys...)` | 主题变量 → `var(--a-b-c)` |
139
+ | `token.common($keys...)` | 公共变量 → `var(--common-a-b-c)` |
140
+ | `@include token.on-theme($name)` | 在指定主题下生效 |
141
+
142
+ 也可手动导入:
143
+
144
+ ```scss
145
+ @use 'virtual:theme-css/scss' as token;
146
+ ```
147
+
148
+ ### JS 运行时
149
+
150
+ ```ts
151
+ import { theme, common, setTheme, getTheme } from '@scope/theme-css'
152
+
153
+ theme('text', 'primary') // 当前主题下的原始值,如 '#111'
154
+ common('font', 'size', 'h1') // '32px'
155
+
156
+ setTheme('dark') // 设置 html[data-theme="dark"]
157
+ getTheme() // 'dark'
158
+ ```
159
+
160
+ `setTheme` 默认在主题名为 `dark` 时同步给 `<html>` 加上 `dark` class(可用 `syncHtmlDark: false` 关闭)。
161
+
162
+ ## 插件选项
163
+
164
+ ```ts
165
+ themeCss({
166
+ /** 配置文件路径,默认 './theme.config.ts' */
167
+ config: './theme.config.ts',
168
+
169
+ /** SCSS 命名空间,默认 'token' */
170
+ scssNamespace: 'token',
171
+
172
+ /** 是否同步 html.dark class,默认 true */
173
+ syncHtmlDark: true,
174
+
175
+ /** 排除自动注入 SCSS helper 的文件 */
176
+ exclude: [/node_modules/]
177
+ // 或: (filename) => filename.includes('legacy')
178
+ })
179
+ ```
180
+
181
+ ## 虚拟模块
182
+
183
+ | 模块 ID | 内容 |
184
+ | --- | --- |
185
+ | `virtual:theme-css/css` | 生成的 CSS 变量 |
186
+ | `virtual:theme-css/scss` | SCSS helper(`theme` / `common` / `on-theme`) |
187
+ | `@scope/theme-css` | 运行时 API(`theme` / `common` / `setTheme` / `getTheme`) |
188
+
189
+ ## 热更新
190
+
191
+ 修改 `theme.config.ts` 后会重新生成上述模块并触发页面 full-reload。
192
+
193
+ ## 开发
194
+
195
+ ```bash
196
+ pnpm test # 单元测试
197
+ pnpm build # 构建 dist
198
+ ```
199
+
200
+ ## License
201
+
202
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { ThemeConfig } from './types';
2
+ /** 类型辅助,运行时恒等返回 */
3
+ export declare function defineTheme<T extends ThemeConfig>(config: T): T;
@@ -0,0 +1,5 @@
1
+ import type { ThemeConfig } from '../../types';
2
+ /**
3
+ * 根据配置生成 CSS 变量字符串
4
+ */
5
+ export declare const generateThemeCss: (config: ThemeConfig) => string;
@@ -0,0 +1,8 @@
1
+ import type { ThemeConfig } from '../../types';
2
+ export interface JsDataOptions {
3
+ syncHtmlDark: boolean;
4
+ }
5
+ /**
6
+ * 只生成 JS 配置数据虚拟模块,不含运行时逻辑。
7
+ */
8
+ export declare function generateJs(config: ThemeConfig, options: JsDataOptions): string;
@@ -0,0 +1,2 @@
1
+ /** 返回 SCSS helper 模块内容(helper.scss 原文) */
2
+ export declare const generateScssHelper: () => string;
package/dist/index.cjs ADDED
@@ -0,0 +1,306 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ createThemeRuntime: () => createThemeRuntime,
34
+ defineTheme: () => defineTheme,
35
+ themeCss: () => themeCss
36
+ });
37
+ module.exports = __toCommonJS(src_exports);
38
+ var import_node_fs = require("fs");
39
+ var import_node_path = __toESM(require("path"), 1);
40
+ var import_node_url = require("url");
41
+ var import_node_module = require("module");
42
+ var import_jiti = require("jiti");
43
+
44
+ // src/types.ts
45
+ var defaultOptions = {
46
+ config: "./theme.config.ts",
47
+ scssNamespace: "token",
48
+ syncHtmlDark: true,
49
+ exclude: []
50
+ };
51
+
52
+ // src/utils/flatten.ts
53
+ var flattenTokens = (obj, prefix = "") => {
54
+ const result = {};
55
+ for (const [key, value] of Object.entries(obj)) {
56
+ const newKey = prefix ? `${prefix}-${key}` : key;
57
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
58
+ Object.assign(result, flattenTokens(value, newKey));
59
+ } else {
60
+ result[newKey] = String(value);
61
+ }
62
+ }
63
+ return result;
64
+ };
65
+
66
+ // src/generate/css/index.ts
67
+ var generateThemeCss = (config) => {
68
+ const { themes, common } = config;
69
+ const lines = [];
70
+ if (common) {
71
+ const flatCommon = flattenTokens(common, "common");
72
+ lines.push(":root {");
73
+ for (const [key, value] of Object.entries(flatCommon)) {
74
+ lines.push(` --${key}: ${value};`);
75
+ }
76
+ lines.push("}");
77
+ }
78
+ for (const [themeName, tokens] of Object.entries(themes)) {
79
+ const flatTokens = flattenTokens(tokens);
80
+ lines.push(`:root[data-theme="${themeName}"] {`);
81
+ for (const [key, value] of Object.entries(flatTokens)) {
82
+ lines.push(` --${key}: ${value};`);
83
+ }
84
+ lines.push("}");
85
+ }
86
+ return lines.join("\n");
87
+ };
88
+
89
+ // src/generate/scss/helper.scss
90
+ var helper_default = `// \u5185\u90E8\u5DE5\u5177\uFF1A\u62FC\u63A5\u591A\u4E2A key\r
91
+ @function _join-keys($keys...) {\r
92
+ $result: '';\r
93
+ @each $key in $keys {\r
94
+ @if $result == '' {\r
95
+ $result: $key;\r
96
+ } @else {\r
97
+ $result: #{$result}-#{$key};\r
98
+ }\r
99
+ }\r
100
+ @return $result;\r
101
+ }\r
102
+ \r
103
+ // \u83B7\u53D6\u4E3B\u9898\u53D8\u91CF\uFF1Atoken.theme(text, primary) -> var(--text-primary)\r
104
+ @function theme($keys...) {\r
105
+ @return var(--#{_join-keys($keys...)});\r
106
+ }\r
107
+ \r
108
+ // \u83B7\u53D6\u516C\u5171\u53D8\u91CF\uFF1Atoken.common(font, size, h1) -> var(--common-font-size-h1)\r
109
+ @function common($keys...) {\r
110
+ @return var(--common-#{_join-keys($keys...)});\r
111
+ }\r
112
+ \r
113
+ // \u7279\u5B9A\u4E3B\u9898\u9009\u62E9\u5668\uFF1A@include token.on-theme(dark) { ... }\r
114
+ @mixin on-theme($theme) {\r
115
+ :root[data-theme="#{$theme}"] & {\r
116
+ @content;\r
117
+ }\r
118
+ }\r
119
+ `;
120
+
121
+ // src/generate/scss/index.ts
122
+ var generateScssHelper = () => helper_default;
123
+
124
+ // src/generate/js/index.ts
125
+ function generateJs(config, options) {
126
+ return `export const config = ${JSON.stringify(config, null, 2)}
127
+ export const options = ${JSON.stringify(options)}
128
+ `;
129
+ }
130
+
131
+ // src/defineTheme.ts
132
+ function defineTheme(config) {
133
+ return config;
134
+ }
135
+
136
+ // src/runtime/index.ts
137
+ function getByPath(obj, keys) {
138
+ return keys.reduce((acc, key) => {
139
+ if (acc && typeof acc === "object" && key in acc) {
140
+ return acc[key];
141
+ }
142
+ return void 0;
143
+ }, obj);
144
+ }
145
+ function createThemeRuntime(config, options = {}) {
146
+ const syncHtmlDark = options.syncHtmlDark ?? true;
147
+ let currentTheme = typeof document !== "undefined" && document.documentElement.dataset.theme || "light";
148
+ return {
149
+ theme(...keys) {
150
+ const themeTokens = config.themes?.[currentTheme] ?? {};
151
+ const value = getByPath(themeTokens, keys);
152
+ return value !== void 0 ? String(value) : "";
153
+ },
154
+ common(...keys) {
155
+ const value = getByPath(config.common ?? {}, keys);
156
+ return value !== void 0 ? String(value) : "";
157
+ },
158
+ setTheme(name) {
159
+ currentTheme = name;
160
+ if (typeof document !== "undefined") {
161
+ document.documentElement.dataset.theme = name;
162
+ if (syncHtmlDark) {
163
+ document.documentElement.classList.toggle("dark", name === "dark");
164
+ }
165
+ }
166
+ },
167
+ getTheme() {
168
+ return currentTheme;
169
+ }
170
+ };
171
+ }
172
+
173
+ // src/index.ts
174
+ var import_meta = {};
175
+ var require2 = (0, import_node_module.createRequire)(import_meta.url);
176
+ var babelTransform = (0, import_node_module.createRequire)(require2.resolve("jiti/package.json"))("./dist/babel.cjs");
177
+ var VIRTUAL_CSS_ID = "virtual:theme-css/css";
178
+ var VIRTUAL_CSS_RESOLVED = "\0virtual:theme-css.css";
179
+ var VIRTUAL_SCSS_ID = "virtual:theme-css/scss";
180
+ var VIRTUAL_SCSS_RESOLVED = "\0virtual:theme-css.scss";
181
+ var VIRTUAL_RUNTIME_ID = "@scope/theme-css";
182
+ var VIRTUAL_RUNTIME_DATA_ID = "virtual:theme-css/runtime-data";
183
+ var VIRTUAL_RUNTIME_DATA_RESOLVED = "\0virtual:theme-css/runtime-data";
184
+ function resolveRuntimeEntryPath() {
185
+ const here = import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
186
+ const distEntry = import_node_path.default.join(here, "runtime-entry.mjs");
187
+ if ((0, import_node_fs.existsSync)(distEntry)) return distEntry;
188
+ return import_node_path.default.join(here, "runtime", "virtual-entry.ts");
189
+ }
190
+ function themeCss(options = {}) {
191
+ const resolvedOptions = { ...defaultOptions, ...options };
192
+ const { config: configPath, scssNamespace, syncHtmlDark, exclude } = resolvedOptions;
193
+ let viteConfig;
194
+ let themeConfig;
195
+ let cssContent = "";
196
+ let scssHelperContent = "";
197
+ let runtimeDataContent = "";
198
+ const runtimeEntryPath = resolveRuntimeEntryPath();
199
+ async function loadConfigAndGenerate() {
200
+ const jiti = (0, import_jiti.createJiti)(import_meta.url, {
201
+ interopDefault: true,
202
+ transform: (opts) => {
203
+ let { source } = opts;
204
+ const usesDefineTheme = /\bdefineTheme\s*\(/.test(source);
205
+ const hasBinding = /\bimport\s[\s\S]*?\bdefineTheme\b/.test(source) || /\b(?:const|let|var|function)\s+defineTheme\b/.test(source);
206
+ if (usesDefineTheme && !hasBinding) {
207
+ source = `const defineTheme = (c) => c;
208
+ ${source}`;
209
+ }
210
+ return babelTransform({ ...opts, source });
211
+ }
212
+ });
213
+ const mod = await jiti.import(import_node_path.default.resolve(configPath));
214
+ themeConfig = mod.default;
215
+ cssContent = generateThemeCss(themeConfig);
216
+ scssHelperContent = generateScssHelper();
217
+ runtimeDataContent = generateJs(themeConfig, { syncHtmlDark });
218
+ }
219
+ return {
220
+ name: "vite-plugin-theme-css",
221
+ async configResolved(config) {
222
+ viteConfig = config;
223
+ await loadConfigAndGenerate();
224
+ },
225
+ resolveId(id) {
226
+ switch (id) {
227
+ case VIRTUAL_CSS_ID:
228
+ return VIRTUAL_CSS_RESOLVED;
229
+ case VIRTUAL_SCSS_ID:
230
+ return VIRTUAL_SCSS_RESOLVED;
231
+ case VIRTUAL_RUNTIME_ID:
232
+ return runtimeEntryPath;
233
+ case VIRTUAL_RUNTIME_DATA_ID:
234
+ return VIRTUAL_RUNTIME_DATA_RESOLVED;
235
+ default:
236
+ return null;
237
+ }
238
+ },
239
+ load(id) {
240
+ switch (id) {
241
+ case VIRTUAL_CSS_RESOLVED:
242
+ return cssContent;
243
+ case VIRTUAL_SCSS_RESOLVED:
244
+ return scssHelperContent;
245
+ case VIRTUAL_RUNTIME_DATA_RESOLVED:
246
+ return runtimeDataContent;
247
+ default:
248
+ return null;
249
+ }
250
+ },
251
+ config(userConfig) {
252
+ const scssOptions = userConfig.css?.preprocessorOptions?.scss ?? {};
253
+ const userAdditionalData = scssOptions.additionalData;
254
+ const ourPrefix = `@use "${VIRTUAL_SCSS_ID}" as ${scssNamespace};
255
+ `;
256
+ const wrappedAdditionalData = async (source, filename) => {
257
+ if (filename.includes("virtual:theme-css")) {
258
+ return typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
259
+ }
260
+ const shouldExclude = Array.isArray(exclude) ? exclude.some((rule) => {
261
+ if (typeof rule === "string") return filename.includes(rule);
262
+ if (rule instanceof RegExp) return rule.test(filename);
263
+ return false;
264
+ }) : typeof exclude === "function" ? exclude(filename) : false;
265
+ if (shouldExclude) {
266
+ return typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
267
+ }
268
+ const userContent = typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
269
+ return ourPrefix + userContent;
270
+ };
271
+ return {
272
+ css: {
273
+ preprocessorOptions: {
274
+ scss: {
275
+ ...scssOptions,
276
+ additionalData: wrappedAdditionalData
277
+ }
278
+ }
279
+ }
280
+ };
281
+ },
282
+ async handleHotUpdate(ctx) {
283
+ const configAbsPath = import_node_path.default.resolve(configPath);
284
+ if (ctx.file === configAbsPath) {
285
+ await loadConfigAndGenerate();
286
+ const moduleGraph = ctx.server.moduleGraph;
287
+ for (const id of [
288
+ VIRTUAL_CSS_RESOLVED,
289
+ VIRTUAL_SCSS_RESOLVED,
290
+ VIRTUAL_RUNTIME_DATA_RESOLVED,
291
+ runtimeEntryPath
292
+ ]) {
293
+ const mod = moduleGraph.getModuleById(id);
294
+ if (mod) moduleGraph.invalidateModule(mod);
295
+ }
296
+ ctx.server.ws.send({ type: "full-reload" });
297
+ }
298
+ }
299
+ };
300
+ }
301
+ // Annotate the CommonJS export names for ESM import in node:
302
+ 0 && (module.exports = {
303
+ createThemeRuntime,
304
+ defineTheme,
305
+ themeCss
306
+ });
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { ThemeCssOptions } from './types';
3
+ export declare function themeCss(options?: ThemeCssOptions): Plugin;
4
+ export type { ThemeCssOptions, ThemeConfig } from './types';
5
+ export type { ThemeRuntime, ThemeRuntimeOptions } from './runtime';
6
+ export { defineTheme } from './defineTheme';
7
+ export { createThemeRuntime } from './runtime';
package/dist/index.mjs ADDED
@@ -0,0 +1,268 @@
1
+ // src/index.ts
2
+ import { existsSync } from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { createRequire } from "module";
6
+ import { createJiti } from "jiti";
7
+
8
+ // src/types.ts
9
+ var defaultOptions = {
10
+ config: "./theme.config.ts",
11
+ scssNamespace: "token",
12
+ syncHtmlDark: true,
13
+ exclude: []
14
+ };
15
+
16
+ // src/utils/flatten.ts
17
+ var flattenTokens = (obj, prefix = "") => {
18
+ const result = {};
19
+ for (const [key, value] of Object.entries(obj)) {
20
+ const newKey = prefix ? `${prefix}-${key}` : key;
21
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
22
+ Object.assign(result, flattenTokens(value, newKey));
23
+ } else {
24
+ result[newKey] = String(value);
25
+ }
26
+ }
27
+ return result;
28
+ };
29
+
30
+ // src/generate/css/index.ts
31
+ var generateThemeCss = (config) => {
32
+ const { themes, common } = config;
33
+ const lines = [];
34
+ if (common) {
35
+ const flatCommon = flattenTokens(common, "common");
36
+ lines.push(":root {");
37
+ for (const [key, value] of Object.entries(flatCommon)) {
38
+ lines.push(` --${key}: ${value};`);
39
+ }
40
+ lines.push("}");
41
+ }
42
+ for (const [themeName, tokens] of Object.entries(themes)) {
43
+ const flatTokens = flattenTokens(tokens);
44
+ lines.push(`:root[data-theme="${themeName}"] {`);
45
+ for (const [key, value] of Object.entries(flatTokens)) {
46
+ lines.push(` --${key}: ${value};`);
47
+ }
48
+ lines.push("}");
49
+ }
50
+ return lines.join("\n");
51
+ };
52
+
53
+ // src/generate/scss/helper.scss
54
+ var helper_default = `// \u5185\u90E8\u5DE5\u5177\uFF1A\u62FC\u63A5\u591A\u4E2A key\r
55
+ @function _join-keys($keys...) {\r
56
+ $result: '';\r
57
+ @each $key in $keys {\r
58
+ @if $result == '' {\r
59
+ $result: $key;\r
60
+ } @else {\r
61
+ $result: #{$result}-#{$key};\r
62
+ }\r
63
+ }\r
64
+ @return $result;\r
65
+ }\r
66
+ \r
67
+ // \u83B7\u53D6\u4E3B\u9898\u53D8\u91CF\uFF1Atoken.theme(text, primary) -> var(--text-primary)\r
68
+ @function theme($keys...) {\r
69
+ @return var(--#{_join-keys($keys...)});\r
70
+ }\r
71
+ \r
72
+ // \u83B7\u53D6\u516C\u5171\u53D8\u91CF\uFF1Atoken.common(font, size, h1) -> var(--common-font-size-h1)\r
73
+ @function common($keys...) {\r
74
+ @return var(--common-#{_join-keys($keys...)});\r
75
+ }\r
76
+ \r
77
+ // \u7279\u5B9A\u4E3B\u9898\u9009\u62E9\u5668\uFF1A@include token.on-theme(dark) { ... }\r
78
+ @mixin on-theme($theme) {\r
79
+ :root[data-theme="#{$theme}"] & {\r
80
+ @content;\r
81
+ }\r
82
+ }\r
83
+ `;
84
+
85
+ // src/generate/scss/index.ts
86
+ var generateScssHelper = () => helper_default;
87
+
88
+ // src/generate/js/index.ts
89
+ function generateJs(config, options) {
90
+ return `export const config = ${JSON.stringify(config, null, 2)}
91
+ export const options = ${JSON.stringify(options)}
92
+ `;
93
+ }
94
+
95
+ // src/defineTheme.ts
96
+ function defineTheme(config) {
97
+ return config;
98
+ }
99
+
100
+ // src/runtime/index.ts
101
+ function getByPath(obj, keys) {
102
+ return keys.reduce((acc, key) => {
103
+ if (acc && typeof acc === "object" && key in acc) {
104
+ return acc[key];
105
+ }
106
+ return void 0;
107
+ }, obj);
108
+ }
109
+ function createThemeRuntime(config, options = {}) {
110
+ const syncHtmlDark = options.syncHtmlDark ?? true;
111
+ let currentTheme = typeof document !== "undefined" && document.documentElement.dataset.theme || "light";
112
+ return {
113
+ theme(...keys) {
114
+ const themeTokens = config.themes?.[currentTheme] ?? {};
115
+ const value = getByPath(themeTokens, keys);
116
+ return value !== void 0 ? String(value) : "";
117
+ },
118
+ common(...keys) {
119
+ const value = getByPath(config.common ?? {}, keys);
120
+ return value !== void 0 ? String(value) : "";
121
+ },
122
+ setTheme(name) {
123
+ currentTheme = name;
124
+ if (typeof document !== "undefined") {
125
+ document.documentElement.dataset.theme = name;
126
+ if (syncHtmlDark) {
127
+ document.documentElement.classList.toggle("dark", name === "dark");
128
+ }
129
+ }
130
+ },
131
+ getTheme() {
132
+ return currentTheme;
133
+ }
134
+ };
135
+ }
136
+
137
+ // src/index.ts
138
+ var require2 = createRequire(import.meta.url);
139
+ var babelTransform = createRequire(require2.resolve("jiti/package.json"))("./dist/babel.cjs");
140
+ var VIRTUAL_CSS_ID = "virtual:theme-css/css";
141
+ var VIRTUAL_CSS_RESOLVED = "\0virtual:theme-css.css";
142
+ var VIRTUAL_SCSS_ID = "virtual:theme-css/scss";
143
+ var VIRTUAL_SCSS_RESOLVED = "\0virtual:theme-css.scss";
144
+ var VIRTUAL_RUNTIME_ID = "@scope/theme-css";
145
+ var VIRTUAL_RUNTIME_DATA_ID = "virtual:theme-css/runtime-data";
146
+ var VIRTUAL_RUNTIME_DATA_RESOLVED = "\0virtual:theme-css/runtime-data";
147
+ function resolveRuntimeEntryPath() {
148
+ const here = path.dirname(fileURLToPath(import.meta.url));
149
+ const distEntry = path.join(here, "runtime-entry.mjs");
150
+ if (existsSync(distEntry)) return distEntry;
151
+ return path.join(here, "runtime", "virtual-entry.ts");
152
+ }
153
+ function themeCss(options = {}) {
154
+ const resolvedOptions = { ...defaultOptions, ...options };
155
+ const { config: configPath, scssNamespace, syncHtmlDark, exclude } = resolvedOptions;
156
+ let viteConfig;
157
+ let themeConfig;
158
+ let cssContent = "";
159
+ let scssHelperContent = "";
160
+ let runtimeDataContent = "";
161
+ const runtimeEntryPath = resolveRuntimeEntryPath();
162
+ async function loadConfigAndGenerate() {
163
+ const jiti = createJiti(import.meta.url, {
164
+ interopDefault: true,
165
+ transform: (opts) => {
166
+ let { source } = opts;
167
+ const usesDefineTheme = /\bdefineTheme\s*\(/.test(source);
168
+ const hasBinding = /\bimport\s[\s\S]*?\bdefineTheme\b/.test(source) || /\b(?:const|let|var|function)\s+defineTheme\b/.test(source);
169
+ if (usesDefineTheme && !hasBinding) {
170
+ source = `const defineTheme = (c) => c;
171
+ ${source}`;
172
+ }
173
+ return babelTransform({ ...opts, source });
174
+ }
175
+ });
176
+ const mod = await jiti.import(path.resolve(configPath));
177
+ themeConfig = mod.default;
178
+ cssContent = generateThemeCss(themeConfig);
179
+ scssHelperContent = generateScssHelper();
180
+ runtimeDataContent = generateJs(themeConfig, { syncHtmlDark });
181
+ }
182
+ return {
183
+ name: "vite-plugin-theme-css",
184
+ async configResolved(config) {
185
+ viteConfig = config;
186
+ await loadConfigAndGenerate();
187
+ },
188
+ resolveId(id) {
189
+ switch (id) {
190
+ case VIRTUAL_CSS_ID:
191
+ return VIRTUAL_CSS_RESOLVED;
192
+ case VIRTUAL_SCSS_ID:
193
+ return VIRTUAL_SCSS_RESOLVED;
194
+ case VIRTUAL_RUNTIME_ID:
195
+ return runtimeEntryPath;
196
+ case VIRTUAL_RUNTIME_DATA_ID:
197
+ return VIRTUAL_RUNTIME_DATA_RESOLVED;
198
+ default:
199
+ return null;
200
+ }
201
+ },
202
+ load(id) {
203
+ switch (id) {
204
+ case VIRTUAL_CSS_RESOLVED:
205
+ return cssContent;
206
+ case VIRTUAL_SCSS_RESOLVED:
207
+ return scssHelperContent;
208
+ case VIRTUAL_RUNTIME_DATA_RESOLVED:
209
+ return runtimeDataContent;
210
+ default:
211
+ return null;
212
+ }
213
+ },
214
+ config(userConfig) {
215
+ const scssOptions = userConfig.css?.preprocessorOptions?.scss ?? {};
216
+ const userAdditionalData = scssOptions.additionalData;
217
+ const ourPrefix = `@use "${VIRTUAL_SCSS_ID}" as ${scssNamespace};
218
+ `;
219
+ const wrappedAdditionalData = async (source, filename) => {
220
+ if (filename.includes("virtual:theme-css")) {
221
+ return typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
222
+ }
223
+ const shouldExclude = Array.isArray(exclude) ? exclude.some((rule) => {
224
+ if (typeof rule === "string") return filename.includes(rule);
225
+ if (rule instanceof RegExp) return rule.test(filename);
226
+ return false;
227
+ }) : typeof exclude === "function" ? exclude(filename) : false;
228
+ if (shouldExclude) {
229
+ return typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
230
+ }
231
+ const userContent = typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
232
+ return ourPrefix + userContent;
233
+ };
234
+ return {
235
+ css: {
236
+ preprocessorOptions: {
237
+ scss: {
238
+ ...scssOptions,
239
+ additionalData: wrappedAdditionalData
240
+ }
241
+ }
242
+ }
243
+ };
244
+ },
245
+ async handleHotUpdate(ctx) {
246
+ const configAbsPath = path.resolve(configPath);
247
+ if (ctx.file === configAbsPath) {
248
+ await loadConfigAndGenerate();
249
+ const moduleGraph = ctx.server.moduleGraph;
250
+ for (const id of [
251
+ VIRTUAL_CSS_RESOLVED,
252
+ VIRTUAL_SCSS_RESOLVED,
253
+ VIRTUAL_RUNTIME_DATA_RESOLVED,
254
+ runtimeEntryPath
255
+ ]) {
256
+ const mod = moduleGraph.getModuleById(id);
257
+ if (mod) moduleGraph.invalidateModule(mod);
258
+ }
259
+ ctx.server.ws.send({ type: "full-reload" });
260
+ }
261
+ }
262
+ };
263
+ }
264
+ export {
265
+ createThemeRuntime,
266
+ defineTheme,
267
+ themeCss
268
+ };
@@ -0,0 +1,7 @@
1
+ import type { ThemeConfig } from '../types';
2
+ import type { ThemeRuntime, ThemeRuntimeOptions } from './types';
3
+ /**
4
+ * 创建主题运行时 API。虚拟模块只负责注入 config,逻辑写在这里。
5
+ */
6
+ export declare function createThemeRuntime(config: ThemeConfig, options?: ThemeRuntimeOptions): ThemeRuntime;
7
+ export type { ThemeRuntime, ThemeRuntimeOptions };
@@ -0,0 +1,9 @@
1
+ export interface ThemeRuntimeOptions {
2
+ syncHtmlDark?: boolean;
3
+ }
4
+ export interface ThemeRuntime {
5
+ theme: (...keys: string[]) => string;
6
+ common: (...keys: string[]) => string;
7
+ setTheme: (name: string) => void;
8
+ getTheme: () => string;
9
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 用户侧 `@scope/theme-css` 的真实入口。
3
+ * 配置由 virtual:theme-css/runtime-data 注入,此处是普通 TS。
4
+ */
5
+ declare const theme: (...keys: string[]) => string, common: (...keys: string[]) => string, setTheme: (name: string) => void, getTheme: () => string;
6
+ export { theme, common, setTheme, getTheme };
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/runtime/virtual-entry.ts
21
+ var virtual_entry_exports = {};
22
+ __export(virtual_entry_exports, {
23
+ common: () => common,
24
+ getTheme: () => getTheme,
25
+ setTheme: () => setTheme,
26
+ theme: () => theme
27
+ });
28
+ module.exports = __toCommonJS(virtual_entry_exports);
29
+
30
+ // src/runtime/index.ts
31
+ function getByPath(obj, keys) {
32
+ return keys.reduce((acc, key) => {
33
+ if (acc && typeof acc === "object" && key in acc) {
34
+ return acc[key];
35
+ }
36
+ return void 0;
37
+ }, obj);
38
+ }
39
+ function createThemeRuntime(config2, options2 = {}) {
40
+ const syncHtmlDark = options2.syncHtmlDark ?? true;
41
+ let currentTheme = typeof document !== "undefined" && document.documentElement.dataset.theme || "light";
42
+ return {
43
+ theme(...keys) {
44
+ const themeTokens = config2.themes?.[currentTheme] ?? {};
45
+ const value = getByPath(themeTokens, keys);
46
+ return value !== void 0 ? String(value) : "";
47
+ },
48
+ common(...keys) {
49
+ const value = getByPath(config2.common ?? {}, keys);
50
+ return value !== void 0 ? String(value) : "";
51
+ },
52
+ setTheme(name) {
53
+ currentTheme = name;
54
+ if (typeof document !== "undefined") {
55
+ document.documentElement.dataset.theme = name;
56
+ if (syncHtmlDark) {
57
+ document.documentElement.classList.toggle("dark", name === "dark");
58
+ }
59
+ }
60
+ },
61
+ getTheme() {
62
+ return currentTheme;
63
+ }
64
+ };
65
+ }
66
+
67
+ // src/runtime/virtual-entry.ts
68
+ var import_runtime_data = require("virtual:theme-css/runtime-data");
69
+ var { theme, common, setTheme, getTheme } = createThemeRuntime(import_runtime_data.config, import_runtime_data.options);
70
+ // Annotate the CommonJS export names for ESM import in node:
71
+ 0 && (module.exports = {
72
+ common,
73
+ getTheme,
74
+ setTheme,
75
+ theme
76
+ });
@@ -0,0 +1,46 @@
1
+ // src/runtime/index.ts
2
+ function getByPath(obj, keys) {
3
+ return keys.reduce((acc, key) => {
4
+ if (acc && typeof acc === "object" && key in acc) {
5
+ return acc[key];
6
+ }
7
+ return void 0;
8
+ }, obj);
9
+ }
10
+ function createThemeRuntime(config2, options2 = {}) {
11
+ const syncHtmlDark = options2.syncHtmlDark ?? true;
12
+ let currentTheme = typeof document !== "undefined" && document.documentElement.dataset.theme || "light";
13
+ return {
14
+ theme(...keys) {
15
+ const themeTokens = config2.themes?.[currentTheme] ?? {};
16
+ const value = getByPath(themeTokens, keys);
17
+ return value !== void 0 ? String(value) : "";
18
+ },
19
+ common(...keys) {
20
+ const value = getByPath(config2.common ?? {}, keys);
21
+ return value !== void 0 ? String(value) : "";
22
+ },
23
+ setTheme(name) {
24
+ currentTheme = name;
25
+ if (typeof document !== "undefined") {
26
+ document.documentElement.dataset.theme = name;
27
+ if (syncHtmlDark) {
28
+ document.documentElement.classList.toggle("dark", name === "dark");
29
+ }
30
+ }
31
+ },
32
+ getTheme() {
33
+ return currentTheme;
34
+ }
35
+ };
36
+ }
37
+
38
+ // src/runtime/virtual-entry.ts
39
+ import { config, options } from "virtual:theme-css/runtime-data";
40
+ var { theme, common, setTheme, getTheme } = createThemeRuntime(config, options);
41
+ export {
42
+ common,
43
+ getTheme,
44
+ setTheme,
45
+ theme
46
+ };
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/runtime/index.ts
21
+ var runtime_exports = {};
22
+ __export(runtime_exports, {
23
+ createThemeRuntime: () => createThemeRuntime
24
+ });
25
+ module.exports = __toCommonJS(runtime_exports);
26
+ function getByPath(obj, keys) {
27
+ return keys.reduce((acc, key) => {
28
+ if (acc && typeof acc === "object" && key in acc) {
29
+ return acc[key];
30
+ }
31
+ return void 0;
32
+ }, obj);
33
+ }
34
+ function createThemeRuntime(config, options = {}) {
35
+ const syncHtmlDark = options.syncHtmlDark ?? true;
36
+ let currentTheme = typeof document !== "undefined" && document.documentElement.dataset.theme || "light";
37
+ return {
38
+ theme(...keys) {
39
+ const themeTokens = config.themes?.[currentTheme] ?? {};
40
+ const value = getByPath(themeTokens, keys);
41
+ return value !== void 0 ? String(value) : "";
42
+ },
43
+ common(...keys) {
44
+ const value = getByPath(config.common ?? {}, keys);
45
+ return value !== void 0 ? String(value) : "";
46
+ },
47
+ setTheme(name) {
48
+ currentTheme = name;
49
+ if (typeof document !== "undefined") {
50
+ document.documentElement.dataset.theme = name;
51
+ if (syncHtmlDark) {
52
+ document.documentElement.classList.toggle("dark", name === "dark");
53
+ }
54
+ }
55
+ },
56
+ getTheme() {
57
+ return currentTheme;
58
+ }
59
+ };
60
+ }
61
+ // Annotate the CommonJS export names for ESM import in node:
62
+ 0 && (module.exports = {
63
+ createThemeRuntime
64
+ });
@@ -0,0 +1,39 @@
1
+ // src/runtime/index.ts
2
+ function getByPath(obj, keys) {
3
+ return keys.reduce((acc, key) => {
4
+ if (acc && typeof acc === "object" && key in acc) {
5
+ return acc[key];
6
+ }
7
+ return void 0;
8
+ }, obj);
9
+ }
10
+ function createThemeRuntime(config, options = {}) {
11
+ const syncHtmlDark = options.syncHtmlDark ?? true;
12
+ let currentTheme = typeof document !== "undefined" && document.documentElement.dataset.theme || "light";
13
+ return {
14
+ theme(...keys) {
15
+ const themeTokens = config.themes?.[currentTheme] ?? {};
16
+ const value = getByPath(themeTokens, keys);
17
+ return value !== void 0 ? String(value) : "";
18
+ },
19
+ common(...keys) {
20
+ const value = getByPath(config.common ?? {}, keys);
21
+ return value !== void 0 ? String(value) : "";
22
+ },
23
+ setTheme(name) {
24
+ currentTheme = name;
25
+ if (typeof document !== "undefined") {
26
+ document.documentElement.dataset.theme = name;
27
+ if (syncHtmlDark) {
28
+ document.documentElement.classList.toggle("dark", name === "dark");
29
+ }
30
+ }
31
+ },
32
+ getTheme() {
33
+ return currentTheme;
34
+ }
35
+ };
36
+ }
37
+ export {
38
+ createThemeRuntime
39
+ };
@@ -0,0 +1,15 @@
1
+ export interface ThemeConfig {
2
+ themes: Record<string, Record<string, any>>;
3
+ common?: Record<string, any>;
4
+ }
5
+ export interface ThemeCssOptions {
6
+ /** 配置文件路径,默认 './theme.config.ts' */
7
+ config?: string;
8
+ /** SCSS 命名空间,默认 'token' */
9
+ scssNamespace?: string;
10
+ /** 是否同步 html.dark,默认 true */
11
+ syncHtmlDark?: boolean;
12
+ /** 排除注入 SCSS helper 的文件规则 */
13
+ exclude?: (string | RegExp)[] | ((filename: string) => boolean);
14
+ }
15
+ export declare const defaultOptions: Required<ThemeCssOptions>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 将嵌套对象扁平化为 { 'key1-key2': value } 形式
3
+ * @param obj 原始嵌套对象
4
+ * @param prefix 当前前缀
5
+ */
6
+ export declare const flattenTokens: (obj: Record<string, any>, prefix?: string) => Record<string, string>;
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "vite-plugin-theme-css",
3
+ "version": "0.1.0",
4
+ "description": "Vite 插件:用一份主题配置生成 CSS 变量、SCSS helper 与 JS 运行时,支持多主题切换与热更新",
5
+ "author": "yuzinan <1589937631@qq.com>",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/yvygyyth/theme-css.git"
10
+ },
11
+ "homepage": "https://github.com/yvygyyth/theme-css#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/yvygyyth/theme-css/issues"
14
+ },
15
+ "keywords": [
16
+ "vite",
17
+ "vite-plugin",
18
+ "theme",
19
+ "css",
20
+ "css-variables",
21
+ "scss",
22
+ "dark-mode",
23
+ "theme-switcher"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "registry": "https://registry.npmjs.org/"
28
+ },
29
+ "type": "module",
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.mjs",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.mjs",
37
+ "require": "./dist/index.cjs"
38
+ },
39
+ "./runtime": {
40
+ "types": "./dist/runtime.d.ts",
41
+ "import": "./dist/runtime.mjs",
42
+ "require": "./dist/runtime.cjs"
43
+ }
44
+ },
45
+ "files": [
46
+ "dist"
47
+ ],
48
+ "dependencies": {
49
+ "jiti": "^2.7.0"
50
+ },
51
+ "peerDependencies": {
52
+ "vite": ">=5.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^26.2.0",
56
+ "prettier": "3.9.6",
57
+ "sass": "^1.103.0",
58
+ "tsup": "^8.5.1",
59
+ "typescript": "^7.0.2",
60
+ "vite": "^8.2.2",
61
+ "vitest": "^4.1.11"
62
+ },
63
+ "scripts": {
64
+ "build": "tsup && tsc -p tsconfig.build.json",
65
+ "dev": "tsup --watch",
66
+ "test": "vitest",
67
+ "format": "prettier --write .",
68
+ "format:check": "prettier --check .",
69
+ "release": "pnpm publish --access public"
70
+ }
71
+ }