vite-plugin-theme-css 0.1.0 → 0.2.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 CHANGED
@@ -76,14 +76,16 @@ export default defineConfig({
76
76
  })
77
77
  ```
78
78
 
79
- ### 3. 引入 CSS 变量
79
+ ### 3. CSS 变量(自动注入)
80
80
 
81
- 在入口文件中导入虚拟 CSS:
81
+ 默认会在 HTML 里自动注入主题 CSS 变量,**无需**在入口手动写:
82
82
 
83
83
  ```ts
84
- import 'virtual:theme-css/css'
84
+ import 'virtual:theme-css/css' // 可省略
85
85
  ```
86
86
 
87
+ 若需关闭自动注入,可设置 `autoImportCss: false`,再自行在入口 import。
88
+
87
89
  生成示例:
88
90
 
89
91
  ```css
@@ -173,8 +175,11 @@ themeCss({
173
175
  syncHtmlDark: true,
174
176
 
175
177
  /** 排除自动注入 SCSS helper 的文件 */
176
- exclude: [/node_modules/]
178
+ exclude: [/node_modules/],
177
179
  // 或: (filename) => filename.includes('legacy')
180
+
181
+ /** 是否自动在 HTML 注入 CSS 变量,默认 true */
182
+ autoImportCss: true
178
183
  })
179
184
  ```
180
185
 
@@ -0,0 +1,7 @@
1
+ export declare const VIRTUAL_CSS_ID = "virtual:theme-css/css";
2
+ export declare const VIRTUAL_CSS_RESOLVED = "\0virtual:theme-css.css";
3
+ export declare const VIRTUAL_SCSS_ID = "virtual:theme-css/scss";
4
+ export declare const VIRTUAL_SCSS_RESOLVED = "\0virtual:theme-css.scss";
5
+ export declare const VIRTUAL_RUNTIME_ID = "@scope/theme-css";
6
+ export declare const VIRTUAL_RUNTIME_DATA_ID = "virtual:theme-css/runtime-data";
7
+ export declare const VIRTUAL_RUNTIME_DATA_RESOLVED = "\0virtual:theme-css/runtime-data";
package/dist/index.cjs CHANGED
@@ -41,12 +41,22 @@ var import_node_url = require("url");
41
41
  var import_node_module = require("module");
42
42
  var import_jiti = require("jiti");
43
43
 
44
+ // src/constants.ts
45
+ var VIRTUAL_CSS_ID = "virtual:theme-css/css";
46
+ var VIRTUAL_CSS_RESOLVED = "\0virtual:theme-css.css";
47
+ var VIRTUAL_SCSS_ID = "virtual:theme-css/scss";
48
+ var VIRTUAL_SCSS_RESOLVED = "\0virtual:theme-css.scss";
49
+ var VIRTUAL_RUNTIME_ID = "@scope/theme-css";
50
+ var VIRTUAL_RUNTIME_DATA_ID = "virtual:theme-css/runtime-data";
51
+ var VIRTUAL_RUNTIME_DATA_RESOLVED = "\0virtual:theme-css/runtime-data";
52
+
44
53
  // src/types.ts
45
54
  var defaultOptions = {
46
55
  config: "./theme.config.ts",
47
56
  scssNamespace: "token",
48
57
  syncHtmlDark: true,
49
- exclude: []
58
+ exclude: [],
59
+ autoImportCss: true
50
60
  };
51
61
 
52
62
  // src/utils/flatten.ts
@@ -86,8 +96,8 @@ var generateThemeCss = (config) => {
86
96
  return lines.join("\n");
87
97
  };
88
98
 
89
- // src/generate/scss/helper.scss
90
- var helper_default = `// \u5185\u90E8\u5DE5\u5177\uFF1A\u62FC\u63A5\u591A\u4E2A key\r
99
+ // src/generate/scss/index.ts
100
+ var generateScssHelper = () => `// \u5185\u90E8\u5DE5\u5177\uFF1A\u62FC\u63A5\u591A\u4E2A key\r
91
101
  @function _join-keys($keys...) {\r
92
102
  $result: '';\r
93
103
  @each $key in $keys {\r
@@ -118,9 +128,6 @@ var helper_default = `// \u5185\u90E8\u5DE5\u5177\uFF1A\u62FC\u63A5\u591A\u4E2A
118
128
  }\r
119
129
  `;
120
130
 
121
- // src/generate/scss/index.ts
122
- var generateScssHelper = () => helper_default;
123
-
124
131
  // src/generate/js/index.ts
125
132
  function generateJs(config, options) {
126
133
  return `export const config = ${JSON.stringify(config, null, 2)}
@@ -128,6 +135,23 @@ export const options = ${JSON.stringify(options)}
128
135
  `;
129
136
  }
130
137
 
138
+ // src/utils/sass-importer.ts
139
+ var canonicalUrl = new URL(`${VIRTUAL_SCSS_ID}.scss`);
140
+ function createVirtualScssImporter(getContent) {
141
+ return {
142
+ canonicalize(url) {
143
+ if (url === VIRTUAL_SCSS_ID) return canonicalUrl;
144
+ return null;
145
+ },
146
+ load(canonical) {
147
+ if (canonical.href === canonicalUrl.href) {
148
+ return { contents: getContent(), syntax: "scss" };
149
+ }
150
+ return null;
151
+ }
152
+ };
153
+ }
154
+
131
155
  // src/defineTheme.ts
132
156
  function defineTheme(config) {
133
157
  return config;
@@ -174,13 +198,6 @@ function createThemeRuntime(config, options = {}) {
174
198
  var import_meta = {};
175
199
  var require2 = (0, import_node_module.createRequire)(import_meta.url);
176
200
  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
201
  function resolveRuntimeEntryPath() {
185
202
  const here = import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
186
203
  const distEntry = import_node_path.default.join(here, "runtime-entry.mjs");
@@ -189,11 +206,8 @@ function resolveRuntimeEntryPath() {
189
206
  }
190
207
  function themeCss(options = {}) {
191
208
  const resolvedOptions = { ...defaultOptions, ...options };
192
- const { config: configPath, scssNamespace, syncHtmlDark, exclude } = resolvedOptions;
193
- let viteConfig;
194
- let themeConfig;
209
+ const { config: configPath, scssNamespace, syncHtmlDark, exclude, autoImportCss } = resolvedOptions;
195
210
  let cssContent = "";
196
- let scssHelperContent = "";
197
211
  let runtimeDataContent = "";
198
212
  const runtimeEntryPath = resolveRuntimeEntryPath();
199
213
  async function loadConfigAndGenerate() {
@@ -211,15 +225,13 @@ ${source}`;
211
225
  }
212
226
  });
213
227
  const mod = await jiti.import(import_node_path.default.resolve(configPath));
214
- themeConfig = mod.default;
228
+ const themeConfig = mod.default;
215
229
  cssContent = generateThemeCss(themeConfig);
216
- scssHelperContent = generateScssHelper();
217
230
  runtimeDataContent = generateJs(themeConfig, { syncHtmlDark });
218
231
  }
219
232
  return {
220
233
  name: "vite-plugin-theme-css",
221
- async configResolved(config) {
222
- viteConfig = config;
234
+ async configResolved() {
223
235
  await loadConfigAndGenerate();
224
236
  },
225
237
  resolveId(id) {
@@ -241,7 +253,7 @@ ${source}`;
241
253
  case VIRTUAL_CSS_RESOLVED:
242
254
  return cssContent;
243
255
  case VIRTUAL_SCSS_RESOLVED:
244
- return scssHelperContent;
256
+ return generateScssHelper();
245
257
  case VIRTUAL_RUNTIME_DATA_RESOLVED:
246
258
  return runtimeDataContent;
247
259
  default:
@@ -253,32 +265,53 @@ ${source}`;
253
265
  const userAdditionalData = scssOptions.additionalData;
254
266
  const ourPrefix = `@use "${VIRTUAL_SCSS_ID}" as ${scssNamespace};
255
267
  `;
256
- const wrappedAdditionalData = async (source, filename) => {
257
- if (filename.includes("virtual:theme-css")) {
258
- return typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
268
+ const applyUserAdditionalData = async (source, filename) => {
269
+ if (typeof userAdditionalData === "function") {
270
+ return userAdditionalData(source, filename);
259
271
  }
260
- const shouldExclude = Array.isArray(exclude) ? exclude.some((rule) => {
272
+ if (userAdditionalData) {
273
+ return userAdditionalData + source;
274
+ }
275
+ return source;
276
+ };
277
+ const wrappedAdditionalData = async (source, filename) => {
278
+ const skipInject = filename.includes("virtual:theme-css") || (Array.isArray(exclude) ? exclude.some((rule) => {
261
279
  if (typeof rule === "string") return filename.includes(rule);
262
280
  if (rule instanceof RegExp) return rule.test(filename);
263
281
  return false;
264
- }) : typeof exclude === "function" ? exclude(filename) : false;
265
- if (shouldExclude) {
266
- return typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
282
+ }) : typeof exclude === "function" ? exclude(filename) : false);
283
+ if (skipInject) {
284
+ return applyUserAdditionalData(source, filename);
267
285
  }
268
- const userContent = typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
269
- return ourPrefix + userContent;
286
+ return ourPrefix + await applyUserAdditionalData(source, filename);
270
287
  };
288
+ const userImporters = Array.isArray(scssOptions.importers) ? scssOptions.importers : [];
271
289
  return {
272
290
  css: {
273
291
  preprocessorOptions: {
274
292
  scss: {
275
293
  ...scssOptions,
276
- additionalData: wrappedAdditionalData
294
+ additionalData: wrappedAdditionalData,
295
+ importers: [
296
+ createVirtualScssImporter(generateScssHelper),
297
+ ...userImporters
298
+ ]
277
299
  }
278
300
  }
279
301
  }
280
302
  };
281
303
  },
304
+ transformIndexHtml(html) {
305
+ if (!autoImportCss || html.includes(VIRTUAL_CSS_ID)) return;
306
+ return [
307
+ {
308
+ tag: "script",
309
+ attrs: { type: "module" },
310
+ children: `import ${JSON.stringify(VIRTUAL_CSS_ID)};`,
311
+ injectTo: "head-prepend"
312
+ }
313
+ ];
314
+ },
282
315
  async handleHotUpdate(ctx) {
283
316
  const configAbsPath = import_node_path.default.resolve(configPath);
284
317
  if (ctx.file === configAbsPath) {
@@ -286,7 +319,6 @@ ${source}`;
286
319
  const moduleGraph = ctx.server.moduleGraph;
287
320
  for (const id of [
288
321
  VIRTUAL_CSS_RESOLVED,
289
- VIRTUAL_SCSS_RESOLVED,
290
322
  VIRTUAL_RUNTIME_DATA_RESOLVED,
291
323
  runtimeEntryPath
292
324
  ]) {
package/dist/index.mjs CHANGED
@@ -5,12 +5,22 @@ import { fileURLToPath } from "url";
5
5
  import { createRequire } from "module";
6
6
  import { createJiti } from "jiti";
7
7
 
8
+ // src/constants.ts
9
+ var VIRTUAL_CSS_ID = "virtual:theme-css/css";
10
+ var VIRTUAL_CSS_RESOLVED = "\0virtual:theme-css.css";
11
+ var VIRTUAL_SCSS_ID = "virtual:theme-css/scss";
12
+ var VIRTUAL_SCSS_RESOLVED = "\0virtual:theme-css.scss";
13
+ var VIRTUAL_RUNTIME_ID = "@scope/theme-css";
14
+ var VIRTUAL_RUNTIME_DATA_ID = "virtual:theme-css/runtime-data";
15
+ var VIRTUAL_RUNTIME_DATA_RESOLVED = "\0virtual:theme-css/runtime-data";
16
+
8
17
  // src/types.ts
9
18
  var defaultOptions = {
10
19
  config: "./theme.config.ts",
11
20
  scssNamespace: "token",
12
21
  syncHtmlDark: true,
13
- exclude: []
22
+ exclude: [],
23
+ autoImportCss: true
14
24
  };
15
25
 
16
26
  // src/utils/flatten.ts
@@ -50,8 +60,8 @@ var generateThemeCss = (config) => {
50
60
  return lines.join("\n");
51
61
  };
52
62
 
53
- // src/generate/scss/helper.scss
54
- var helper_default = `// \u5185\u90E8\u5DE5\u5177\uFF1A\u62FC\u63A5\u591A\u4E2A key\r
63
+ // src/generate/scss/index.ts
64
+ var generateScssHelper = () => `// \u5185\u90E8\u5DE5\u5177\uFF1A\u62FC\u63A5\u591A\u4E2A key\r
55
65
  @function _join-keys($keys...) {\r
56
66
  $result: '';\r
57
67
  @each $key in $keys {\r
@@ -82,9 +92,6 @@ var helper_default = `// \u5185\u90E8\u5DE5\u5177\uFF1A\u62FC\u63A5\u591A\u4E2A
82
92
  }\r
83
93
  `;
84
94
 
85
- // src/generate/scss/index.ts
86
- var generateScssHelper = () => helper_default;
87
-
88
95
  // src/generate/js/index.ts
89
96
  function generateJs(config, options) {
90
97
  return `export const config = ${JSON.stringify(config, null, 2)}
@@ -92,6 +99,23 @@ export const options = ${JSON.stringify(options)}
92
99
  `;
93
100
  }
94
101
 
102
+ // src/utils/sass-importer.ts
103
+ var canonicalUrl = new URL(`${VIRTUAL_SCSS_ID}.scss`);
104
+ function createVirtualScssImporter(getContent) {
105
+ return {
106
+ canonicalize(url) {
107
+ if (url === VIRTUAL_SCSS_ID) return canonicalUrl;
108
+ return null;
109
+ },
110
+ load(canonical) {
111
+ if (canonical.href === canonicalUrl.href) {
112
+ return { contents: getContent(), syntax: "scss" };
113
+ }
114
+ return null;
115
+ }
116
+ };
117
+ }
118
+
95
119
  // src/defineTheme.ts
96
120
  function defineTheme(config) {
97
121
  return config;
@@ -137,13 +161,6 @@ function createThemeRuntime(config, options = {}) {
137
161
  // src/index.ts
138
162
  var require2 = createRequire(import.meta.url);
139
163
  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
164
  function resolveRuntimeEntryPath() {
148
165
  const here = path.dirname(fileURLToPath(import.meta.url));
149
166
  const distEntry = path.join(here, "runtime-entry.mjs");
@@ -152,11 +169,8 @@ function resolveRuntimeEntryPath() {
152
169
  }
153
170
  function themeCss(options = {}) {
154
171
  const resolvedOptions = { ...defaultOptions, ...options };
155
- const { config: configPath, scssNamespace, syncHtmlDark, exclude } = resolvedOptions;
156
- let viteConfig;
157
- let themeConfig;
172
+ const { config: configPath, scssNamespace, syncHtmlDark, exclude, autoImportCss } = resolvedOptions;
158
173
  let cssContent = "";
159
- let scssHelperContent = "";
160
174
  let runtimeDataContent = "";
161
175
  const runtimeEntryPath = resolveRuntimeEntryPath();
162
176
  async function loadConfigAndGenerate() {
@@ -174,15 +188,13 @@ ${source}`;
174
188
  }
175
189
  });
176
190
  const mod = await jiti.import(path.resolve(configPath));
177
- themeConfig = mod.default;
191
+ const themeConfig = mod.default;
178
192
  cssContent = generateThemeCss(themeConfig);
179
- scssHelperContent = generateScssHelper();
180
193
  runtimeDataContent = generateJs(themeConfig, { syncHtmlDark });
181
194
  }
182
195
  return {
183
196
  name: "vite-plugin-theme-css",
184
- async configResolved(config) {
185
- viteConfig = config;
197
+ async configResolved() {
186
198
  await loadConfigAndGenerate();
187
199
  },
188
200
  resolveId(id) {
@@ -204,7 +216,7 @@ ${source}`;
204
216
  case VIRTUAL_CSS_RESOLVED:
205
217
  return cssContent;
206
218
  case VIRTUAL_SCSS_RESOLVED:
207
- return scssHelperContent;
219
+ return generateScssHelper();
208
220
  case VIRTUAL_RUNTIME_DATA_RESOLVED:
209
221
  return runtimeDataContent;
210
222
  default:
@@ -216,32 +228,53 @@ ${source}`;
216
228
  const userAdditionalData = scssOptions.additionalData;
217
229
  const ourPrefix = `@use "${VIRTUAL_SCSS_ID}" as ${scssNamespace};
218
230
  `;
219
- const wrappedAdditionalData = async (source, filename) => {
220
- if (filename.includes("virtual:theme-css")) {
221
- return typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
231
+ const applyUserAdditionalData = async (source, filename) => {
232
+ if (typeof userAdditionalData === "function") {
233
+ return userAdditionalData(source, filename);
222
234
  }
223
- const shouldExclude = Array.isArray(exclude) ? exclude.some((rule) => {
235
+ if (userAdditionalData) {
236
+ return userAdditionalData + source;
237
+ }
238
+ return source;
239
+ };
240
+ const wrappedAdditionalData = async (source, filename) => {
241
+ const skipInject = filename.includes("virtual:theme-css") || (Array.isArray(exclude) ? exclude.some((rule) => {
224
242
  if (typeof rule === "string") return filename.includes(rule);
225
243
  if (rule instanceof RegExp) return rule.test(filename);
226
244
  return false;
227
- }) : typeof exclude === "function" ? exclude(filename) : false;
228
- if (shouldExclude) {
229
- return typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
245
+ }) : typeof exclude === "function" ? exclude(filename) : false);
246
+ if (skipInject) {
247
+ return applyUserAdditionalData(source, filename);
230
248
  }
231
- const userContent = typeof userAdditionalData === "function" ? await userAdditionalData(source, filename) : userAdditionalData ?? "";
232
- return ourPrefix + userContent;
249
+ return ourPrefix + await applyUserAdditionalData(source, filename);
233
250
  };
251
+ const userImporters = Array.isArray(scssOptions.importers) ? scssOptions.importers : [];
234
252
  return {
235
253
  css: {
236
254
  preprocessorOptions: {
237
255
  scss: {
238
256
  ...scssOptions,
239
- additionalData: wrappedAdditionalData
257
+ additionalData: wrappedAdditionalData,
258
+ importers: [
259
+ createVirtualScssImporter(generateScssHelper),
260
+ ...userImporters
261
+ ]
240
262
  }
241
263
  }
242
264
  }
243
265
  };
244
266
  },
267
+ transformIndexHtml(html) {
268
+ if (!autoImportCss || html.includes(VIRTUAL_CSS_ID)) return;
269
+ return [
270
+ {
271
+ tag: "script",
272
+ attrs: { type: "module" },
273
+ children: `import ${JSON.stringify(VIRTUAL_CSS_ID)};`,
274
+ injectTo: "head-prepend"
275
+ }
276
+ ];
277
+ },
245
278
  async handleHotUpdate(ctx) {
246
279
  const configAbsPath = path.resolve(configPath);
247
280
  if (ctx.file === configAbsPath) {
@@ -249,7 +282,6 @@ ${source}`;
249
282
  const moduleGraph = ctx.server.moduleGraph;
250
283
  for (const id of [
251
284
  VIRTUAL_CSS_RESOLVED,
252
- VIRTUAL_SCSS_RESOLVED,
253
285
  VIRTUAL_RUNTIME_DATA_RESOLVED,
254
286
  runtimeEntryPath
255
287
  ]) {
package/dist/types.d.ts CHANGED
@@ -11,5 +11,7 @@ export interface ThemeCssOptions {
11
11
  syncHtmlDark?: boolean;
12
12
  /** 排除注入 SCSS helper 的文件规则 */
13
13
  exclude?: (string | RegExp)[] | ((filename: string) => boolean);
14
+ /** 是否自动在 HTML 注入 CSS 变量,默认 true */
15
+ autoImportCss?: boolean;
14
16
  }
15
17
  export declare const defaultOptions: Required<ThemeCssOptions>;
@@ -0,0 +1,3 @@
1
+ import type { Importer } from 'sass';
2
+ /** 让 Sass 能解析 additionalData 里的 virtual:theme-css/scss */
3
+ export declare function createVirtualScssImporter(getContent: () => string): Importer<'sync'>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-theme-css",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Vite 插件:用一份主题配置生成 CSS 变量、SCSS helper 与 JS 运行时,支持多主题切换与热更新",
5
5
  "author": "yuzinan <1589937631@qq.com>",
6
6
  "license": "MIT",
@@ -53,6 +53,7 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^26.2.0",
56
+ "esbuild": "^0.28.2",
56
57
  "prettier": "3.9.6",
57
58
  "sass": "^1.103.0",
58
59
  "tsup": "^8.5.1",
@@ -63,6 +64,7 @@
63
64
  "scripts": {
64
65
  "build": "tsup && tsc -p tsconfig.build.json",
65
66
  "dev": "tsup --watch",
67
+ "play": "vite --config playground/vite.config.ts",
66
68
  "test": "vitest",
67
69
  "format": "prettier --write .",
68
70
  "format:check": "prettier --check .",