升级vite7,主题插件迁移到本地

This commit is contained in:
JEECG 2026-05-11 18:32:31 +08:00
parent 7df07a823f
commit 3582595317
14 changed files with 1093 additions and 8 deletions

View File

@ -1,7 +1,7 @@
import { generate } from '@ant-design/colors';
import setting from '/@/settings/projectSetting';
// : JHHB-579
// src/settings MIX APP_PRESET_COLOR_LIST[2]
// store build
export const primaryColor = setting.themeColor;
export const darkMode = setting.themeMode;
type Fn = (...arg: any) => any;

View File

@ -0,0 +1,224 @@
import type { Plugin, ResolvedConfig } from 'vite';
import path from 'path';
import fs from 'fs-extra';
import less from 'less';
import { createFileHash, minifyCSS, extractVariable } from './utils';
import chalk from 'chalk';
import { colorRE, linkID } from './constants';
import { injectClientPlugin } from './injectClientPlugin';
import { lessPlugin } from './preprocessor/less';
export interface AntdDarkThemeOption {
darkModifyVars?: any;
fileName?: string;
verbose?: boolean;
selector?: string;
filter?: (id: string) => boolean;
extractCss?: boolean;
preloadFiles?: string[];
loadMethod?: 'link' | 'ajax';
}
export function antdDarkThemePlugin(options: AntdDarkThemeOption): Plugin[] {
const {
darkModifyVars,
verbose = true,
fileName = 'app-antd-dark-theme-style',
selector,
filter,
extractCss = true,
preloadFiles = [],
loadMethod = 'link',
} = options;
let isServer = false;
let needSourcemap = false;
let config: ResolvedConfig;
let extCssString = '';
const styleMap = new Map<string, string>();
const codeCache = new Map<string, { code: string; css: string }>();
const cssOutputName = `${fileName}.${createFileHash()}.css`;
const hrefProtocals = ['http://'];
const getCss = (css: string) => {
return `[${selector || 'data-theme="dark"'}] {${css}}`;
};
async function preloadLess() {
if (!preloadFiles || !preloadFiles.length) return;
for (const id of preloadFiles) {
const code = fs.readFileSync(id, 'utf-8');
less
.render(code, {
javascriptEnabled: true,
modifyVars: darkModifyVars,
filename: path.resolve(id),
plugins: [lessPlugin(id, config)],
})
.then(({ css }) => {
const colors = css.match(colorRE);
if (colors) {
css = extractVariable(css, colors.concat(['transparent']));
codeCache.set(id, { code, css });
}
})
.catch(() => void 0);
}
}
function getProtocal(p: string): string | undefined {
let protocal: string | undefined;
hrefProtocals.forEach((hrefProtocal) => {
if (p.startsWith(hrefProtocal)) protocal = hrefProtocal;
});
return protocal;
}
return [
injectClientPlugin('antdDarkPlugin', {
antdDarkCssOutputName: cssOutputName,
antdDarkExtractCss: extractCss,
antdDarkLoadLink: loadMethod === 'link',
}),
{
name: 'vite:antd-dark-theme',
enforce: 'pre',
configResolved(resolvedConfig) {
config = resolvedConfig;
isServer = resolvedConfig.command === 'serve';
needSourcemap = !!resolvedConfig.build.sourcemap;
if (isServer) preloadLess();
},
transformIndexHtml: {
order: 'pre',
handler(html) {
let href: string;
const protocal = getProtocal(config.base);
if (isServer || loadMethod !== 'link') return html;
if (protocal) {
href =
protocal +
path.posix.join(
config.base.slice(protocal.length),
config.build.assetsDir,
cssOutputName,
);
} else {
href = path.posix.join(config.base, config.build.assetsDir, cssOutputName);
}
return {
html,
tags: [
{
tag: 'link',
attrs: {
disabled: true,
id: linkID,
rel: 'alternate stylesheet',
href,
},
injectTo: 'head',
},
],
};
},
},
async transform(code, id) {
if (!id.endsWith('.less') || !code.includes('@')) return null;
if (typeof filter === 'function' && !filter(id)) return null;
const getResult = (content: string) => ({
map: needSourcemap ? this.getCombinedSourcemap() : null,
code: content,
});
let processCss = '';
const cache = codeCache.get(id);
const isUpdate = !cache || cache.code !== code;
if (isUpdate) {
let renderedCss = '';
try {
const { css } = await less.render(code, {
javascriptEnabled: true,
modifyVars: darkModifyVars,
filename: path.resolve(id),
plugins: [lessPlugin(id, config)],
});
renderedCss = css;
} catch {
// less less vite alias / virtual import
// dark theme
return null;
}
const colors = renderedCss.match(colorRE);
if (colors) {
processCss = extractVariable(renderedCss, colors.concat(['transparent']));
}
} else {
processCss = cache!.css;
}
if (isServer || !extractCss) {
if (isUpdate) codeCache.set(id, { code, css: processCss });
return getResult(`${getCss(processCss)}\n` + code);
} else {
if (!styleMap.has(id) && processCss) {
try {
const { css } = await less.render(getCss(processCss), {
filename: path.resolve(id),
plugins: [lessPlugin(id, config)],
});
extCssString += `${css}\n`;
} catch {
// selector wrapper less
}
}
styleMap.set(id, processCss);
}
return null;
},
async writeBundle() {
if (!extractCss) return;
const {
root,
build: { outDir, assetsDir, minify },
} = config;
if (minify) {
extCssString = await minifyCSS(extCssString, config);
}
const cssOutputPath = path.resolve(root, outDir, assetsDir, cssOutputName);
fs.writeFileSync(cssOutputPath, extCssString);
},
closeBundle() {
if (verbose && !isServer && extractCss) {
const {
build: { outDir, assetsDir },
} = config;
console.log(
chalk.cyan('\n✨ [vite-plugin-theme:antd-dark]') +
` - extract antd dark css code file is successfully:`,
);
try {
const { size } = fs.statSync(path.join(outDir, assetsDir, cssOutputName));
console.log(
chalk.dim(outDir + '/') +
chalk.magentaBright(`${assetsDir}/${cssOutputName}`) +
`\t\t${chalk.dim((size / 1024).toFixed(2) + 'kb')}` +
'\n',
);
} catch {}
}
},
},
];
}

View File

@ -0,0 +1,191 @@
// @ts-nocheck
// 6 placeholder build/vite/plugin/theme-plugin/injectClientPlugin.ts
// transform hook
// `declare const __X__: T` declare
// vite oxc/esbuild .ts
export const globalField = '__VITE_THEME__';
export const styleTagId = '__VITE_PLUGIN_THEME__';
export const darkStyleTagId = '__VITE_PLUGIN_DARK_THEME__';
export const linkID = '__VITE_PLUGIN_THEME-ANTD_DARK_THEME_LINK__';
const colorPluginOutputFileName = __COLOR_PLUGIN_OUTPUT_FILE_NAME__;
const isProd = __PROD__;
const colorPluginOptions = __COLOR_PLUGIN_OPTIONS__;
const injectTo = colorPluginOptions && colorPluginOptions.injectTo;
const debounceThemeRender = debounce(200, renderTheme);
export let darkCssIsReady = false;
(() => {
if (!(window as any)[globalField]) {
(window as any)[globalField] = {
styleIdMap: new Map(),
styleRenderQueueMap: new Map(),
};
}
setGlobalOptions('replaceStyleVariables', replaceStyleVariables);
if (!getGlobalOptions('defaultOptions')) {
setGlobalOptions('defaultOptions', colorPluginOptions);
}
})();
export function addCssToQueue(id: string, styleString: string) {
const styleIdMap: Map<string, string> = getGlobalOptions('styleIdMap');
if (!styleIdMap.get(id)) {
(window as any)[globalField].styleRenderQueueMap.set(id, styleString);
debounceThemeRender();
}
}
function renderTheme() {
const variables = getGlobalOptions('colorVariables');
if (!variables) return;
const styleRenderQueueMap: Map<string, string> = getGlobalOptions('styleRenderQueueMap');
const styleDom = getStyleDom(styleTagId);
let html = styleDom.innerHTML;
for (const [id, css] of styleRenderQueueMap.entries()) {
html += css;
(window as any)[globalField].styleRenderQueueMap.delete(id);
(window as any)[globalField].styleIdMap.set(id, css);
}
replaceCssColors(html, variables).then((processCss) => {
appendCssToDom(styleDom, processCss, injectTo);
});
}
export async function replaceStyleVariables({
colorVariables,
customCssHandler,
}: {
colorVariables: string[];
customCssHandler?: (css: string) => string;
}) {
setGlobalOptions('colorVariables', colorVariables);
const styleIdMap: Map<string, string> = getGlobalOptions('styleIdMap');
const styleRenderQueueMap: Map<string, string> = getGlobalOptions('styleRenderQueueMap');
if (!isProd) {
for (const [id, css] of styleIdMap.entries()) {
styleRenderQueueMap.set(id, css);
}
renderTheme();
} else {
try {
const cssText = await fetchCss(colorPluginOutputFileName);
const styleDom = getStyleDom(styleTagId);
const processCss = await replaceCssColors(cssText, colorVariables, customCssHandler);
appendCssToDom(styleDom, processCss, injectTo);
} catch (error: any) {
throw new Error(error);
}
}
}
export async function loadDarkThemeCss() {
if (darkCssIsReady || !__ANTD_DARK_PLUGIN_EXTRACT_CSS__) return;
if (__ANTD_DARK_PLUGIN_LOAD_LINK__) {
const linkTag = document.getElementById(linkID);
if (linkTag) {
linkTag.removeAttribute('disabled');
linkTag.setAttribute('rel', 'stylesheet');
}
} else {
const cssText = await fetchCss(__ANTD_DARK_PLUGIN_OUTPUT_FILE_NAME__);
const styleDom = getStyleDom(darkStyleTagId);
appendCssToDom(styleDom, cssText, injectTo);
}
darkCssIsReady = true;
}
export async function replaceCssColors(
css: string,
colors: string[],
customCssHandler?: (css: string) => string,
) {
let retCss = css;
const defaultOptions = getGlobalOptions('defaultOptions');
const colorVariables: string[] = defaultOptions ? defaultOptions.colorVariables || [] : [];
colorVariables.forEach(function (color, index) {
const reg = new RegExp(
color.replace(/,/g, ',\\s*').replace(/\s/g, '').replace('(', `\\(`).replace(')', `\\)`) +
'([\\da-f]{2})?(\\b|\\)|,|\\s)?',
'ig',
);
retCss = retCss.replace(reg, colors[index] + '$1$2').replace('$1$2', '');
if (customCssHandler && typeof customCssHandler === 'function') {
retCss = customCssHandler(retCss) || retCss;
}
});
return retCss;
}
export function setGlobalOptions(key: string, value: any) {
(window as any)[globalField][key] = value;
}
export function getGlobalOptions(key: string) {
return (window as any)[globalField][key];
}
export function getStyleDom(id: string): HTMLStyleElement {
let style = document.getElementById(id) as HTMLStyleElement | null;
if (!style) {
style = document.createElement('style');
style.setAttribute('id', id);
}
return style;
}
export async function appendCssToDom(
styleDom: HTMLStyleElement,
cssText: string,
appendTo: 'head' | 'body' | 'body-prepend' = 'body',
) {
styleDom.innerHTML = cssText;
if (appendTo === 'head') {
document.head.appendChild(styleDom);
} else if (appendTo === 'body') {
document.body.appendChild(styleDom);
} else if (appendTo === 'body-prepend') {
const firstChildren = document.body.firstChild;
document.body.insertBefore(styleDom, firstChildren);
}
}
function fetchCss(fileName: string): Promise<string> {
return new Promise((resolve, reject) => {
const append = getGlobalOptions('appended');
if (append) {
setGlobalOptions('appended', false);
resolve('');
return;
}
const xhr = new XMLHttpRequest();
xhr.onload = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) resolve(xhr.responseText);
else reject(xhr.status);
}
};
xhr.onerror = function (e) {
reject(e);
};
xhr.ontimeout = function (e) {
reject(e);
};
xhr.open('GET', fileName, true);
xhr.send();
});
}
function debounce<T extends (...args: any[]) => any>(delay: number, fn: T) {
let timer: ReturnType<typeof setTimeout> | undefined;
return function (this: any, ...args: any[]) {
const ctx = this;
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(ctx, args);
}, delay);
};
}

View File

@ -0,0 +1,50 @@
import tinycolor from 'tinycolor2';
export function mixLighten(colorStr: string, weight: number) {
return mix('fff', colorStr, weight);
}
export function mixDarken(colorStr: string, weight: number) {
return mix('000', colorStr, weight);
}
export function mix(color1: string, color2: string, weight: number, alpha1?: number, alpha2?: number) {
color1 = dropPrefix(color1);
color2 = dropPrefix(color2);
if (weight === undefined) weight = 0.5;
if (alpha1 === undefined) alpha1 = 1;
if (alpha2 === undefined) alpha2 = 1;
const w = 2 * weight - 1;
const alphaDelta = alpha1 - alpha2;
const w1 = ((w * alphaDelta === -1 ? w : (w + alphaDelta) / (1 + w * alphaDelta)) + 1) / 2;
const w2 = 1 - w1;
const rgb1 = toNum3(color1);
const rgb2 = toNum3(color2);
const r = Math.round(w1 * rgb1[0] + w2 * rgb2[0]);
const g = Math.round(w1 * rgb1[1] + w2 * rgb2[1]);
const b = Math.round(w1 * rgb1[2] + w2 * rgb2[2]);
return '#' + pad2(r) + pad2(g) + pad2(b);
}
export function toNum3(colorStr: string) {
colorStr = dropPrefix(colorStr);
if (colorStr.length === 3) {
colorStr = colorStr[0] + colorStr[0] + colorStr[1] + colorStr[1] + colorStr[2] + colorStr[2];
}
const r = parseInt(colorStr.slice(0, 2), 16);
const g = parseInt(colorStr.slice(2, 4), 16);
const b = parseInt(colorStr.slice(4, 6), 16);
return [r, g, b];
}
export function dropPrefix(colorStr: string) {
return colorStr.replace('#', '');
}
export function pad2(num: number) {
let t = num.toString(16);
if (t.length === 1) t = '0' + t;
return t;
}
export { tinycolor };

View File

@ -0,0 +1,36 @@
import path from 'path';
import { normalizePath } from 'vite';
export const VITE_CLIENT_ENTRY = '/@vite/client';
// client.ts node_modules/.vite
// injectClientPlugin transform id
export const VITE_PLUGIN_THEME_CLIENT_ENTRY = normalizePath(
path.resolve(process.cwd(), 'build/vite/plugin/theme-plugin/client'),
);
export const CLIENT_PUBLIC_ABSOLUTE_PATH = normalizePath(
VITE_PLUGIN_THEME_CLIENT_ENTRY + '/client.ts',
);
// import 使 normalizePath
export const CLIENT_PUBLIC_PATH_FRAGMENT = 'theme-plugin/client/client';
export const commentRE = /\\\\?n|\n|\\\\?r|\/\*[\s\S]+?\*\//g;
const cssLangs = `\\.(css|less|sass|scss|styl|stylus|postcss)($|\\?)`;
export const colorRE =
/#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})|rgba?\((.*),\s*(.*),\s*(.*)(?:,\s*(.*(?:.*)?))?\)/gi;
export const cssVariableString = `const css = "`;
export const cssBlockRE = /[^}]*\{[^{]*\}/g;
export const cssLangRE = new RegExp(cssLangs);
export const ruleRE = /(\w+-)*\w+:/;
export const cssValueRE = /(\s?[a-z0-9]+\s)*/;
export const safeEmptyRE = /\s?/;
export const importSafeRE = /(\s*!important)?/;
export const linkID = '__VITE_PLUGIN_THEME-ANTD_DARK_THEME_LINK__';

View File

@ -0,0 +1,185 @@
import type { Plugin, ResolvedConfig } from 'vite';
import path from 'path';
import fs from 'fs-extra';
import { debug as Debug } from 'debug';
import chalk from 'chalk';
import { extractVariable, minifyCSS, createFileHash, formatCss } from './utils';
import { VITE_CLIENT_ENTRY, cssLangRE, cssVariableString, CLIENT_PUBLIC_ABSOLUTE_PATH } from './constants';
import { injectClientPlugin } from './injectClientPlugin';
export * from './client/colorUtils';
export { antdDarkThemePlugin } from './antdDarkThemePlugin';
export type ResolveSelector = (selector: string) => string;
export type InjectTo = 'head' | 'body' | 'body-prepend';
export interface ViteThemeOptions {
colorVariables: string[];
wrapperCssSelector?: string;
resolveSelector?: ResolveSelector;
customerExtractVariable?: (code: string) => string;
fileName?: string;
injectTo?: InjectTo;
verbose?: boolean;
}
const debug = Debug('vite-plugin-theme');
export function viteThemePlugin(opt: ViteThemeOptions): Plugin[] {
let isServer = false;
let config: ResolvedConfig;
let clientPath = '';
const styleMap = new Map<string, string>();
const extCssSet = new Set<string>();
const emptyPlugin: Plugin = { name: 'vite:theme' };
const options: ViteThemeOptions = Object.assign(
{
colorVariables: [],
wrapperCssSelector: '',
fileName: 'app-theme-style',
injectTo: 'body',
verbose: true,
},
opt,
);
debug('plugin options:', options);
const {
colorVariables,
wrapperCssSelector,
resolveSelector,
customerExtractVariable,
fileName,
verbose,
} = options;
if (!colorVariables || colorVariables.length === 0) {
console.error('colorVariables is not empty!');
return [emptyPlugin];
}
const resolveSelectorFn =
resolveSelector || ((s: string) => `${wrapperCssSelector} ${s}`);
const cssOutputName = `${fileName}.${createFileHash()}.css`;
let needSourcemap = false;
return [
injectClientPlugin('colorPlugin', {
colorPluginCssOutputName: cssOutputName,
colorPluginOptions: options,
}),
{
...emptyPlugin,
enforce: 'post',
configResolved(resolvedConfig) {
config = resolvedConfig;
isServer = resolvedConfig.command === 'serve';
// dev /@fs/<absolute> client.ts
clientPath = JSON.stringify(
isServer
? '/@fs/' + CLIENT_PUBLIC_ABSOLUTE_PATH
: path.posix.join(config.base, CLIENT_PUBLIC_ABSOLUTE_PATH),
);
needSourcemap = !!resolvedConfig.build.sourcemap;
debug('plugin config base:', resolvedConfig.base);
},
async transform(code, id) {
if (!cssLangRE.test(id)) return null;
const getResult = (content: string) => ({
map: needSourcemap ? this.getCombinedSourcemap() : null,
code: content,
});
const clientCode = isServer
? await getClientStyleString(code)
: code.replace('export default', '').replace('"', '');
const extractCssCodeTemplate =
typeof customerExtractVariable === 'function'
? customerExtractVariable(clientCode)
: extractVariable(clientCode, colorVariables, resolveSelectorFn);
debug('extractCssCodeTemplate:', id, extractCssCodeTemplate?.slice(0, 100));
if (!extractCssCodeTemplate) return null;
if (isServer) {
const retCode = [
`import { addCssToQueue } from ${clientPath}`,
`const themeCssId = ${JSON.stringify(id)}`,
`const themeCssStr = ${JSON.stringify(formatCss(extractCssCodeTemplate))}`,
`addCssToQueue(themeCssId, themeCssStr)`,
code,
];
return getResult(retCode.join('\n'));
} else {
if (!styleMap.has(id)) {
extCssSet.add(extractCssCodeTemplate);
}
styleMap.set(id, extractCssCodeTemplate);
}
return null;
},
async writeBundle() {
const {
root,
build: { outDir, assetsDir, minify },
} = config;
let extCssString = '';
for (const css of extCssSet) {
extCssString += css;
}
if (minify) {
extCssString = await minifyCSS(extCssString, config);
}
const cssOutputPath = path.resolve(root, outDir, assetsDir, cssOutputName);
fs.ensureDirSync(path.dirname(cssOutputPath));
fs.writeFileSync(cssOutputPath, extCssString);
},
closeBundle() {
if (verbose && !isServer) {
const {
build: { outDir, assetsDir },
} = config;
console.log(
chalk.cyan('\n✨ [vite-plugin-theme]') +
` - extract css code file is successfully:`,
);
try {
const { size } = fs.statSync(path.join(outDir, assetsDir, cssOutputName));
console.log(
chalk.dim(outDir + '/') +
chalk.magentaBright(`${assetsDir}/${cssOutputName}`) +
`\t\t${chalk.dim((size / 1024).toFixed(2) + 'kb')}` +
'\n',
);
} catch {}
}
},
},
];
}
async function getClientStyleString(code: string) {
if (!code.includes(VITE_CLIENT_ENTRY)) return code;
code = code.replace(/\\n/g, '');
const cssPrefix = cssVariableString;
const cssPrefixLen = cssPrefix.length;
const cssPrefixIndex = code.indexOf(cssPrefix);
const len = cssPrefixIndex + cssPrefixLen;
const cssLastIndex = code.indexOf('\n', len + 1);
if (cssPrefixIndex !== -1) {
code = code.slice(len, cssLastIndex);
}
return code;
}

View File

@ -0,0 +1,94 @@
import path from 'path';
import { normalizePath } from 'vite';
import type { Plugin, ResolvedConfig } from 'vite';
import type { ViteThemeOptions } from './index';
import { CLIENT_PUBLIC_ABSOLUTE_PATH, CLIENT_PUBLIC_PATH_FRAGMENT } from './constants';
type PluginType = 'colorPlugin' | 'antdDarkPlugin';
/**
* 通过 transform hook 直接对内置 client.ts 中的 6 个占位符做字符串替换
*
* vite 8 `define` dev 模式下并未对项目源码.ts中的纯标识符占位做编译期替换
* 因此回到 transform-based 替换 与原 @rys-fe/vite-plugin-theme 行为一致
*
* 同名 plugin 注册多次colorPlugin + antdDarkPlugin两次 transform 都会跑
* 各自只替换自己负责的占位互不干扰
*/
export function injectClientPlugin(
type: PluginType,
{
colorPluginOptions,
colorPluginCssOutputName,
antdDarkCssOutputName,
antdDarkExtractCss,
antdDarkLoadLink,
}: {
colorPluginOptions?: ViteThemeOptions;
antdDarkCssOutputName?: string;
colorPluginCssOutputName?: string;
antdDarkExtractCss?: boolean;
antdDarkLoadLink?: boolean;
},
): Plugin {
let config: ResolvedConfig;
let isServer = false;
return {
name: `vite:inject-vite-plugin-theme-client(${type})`,
enforce: 'pre',
configResolved(resolvedConfig) {
config = resolvedConfig;
isServer = resolvedConfig.command === 'serve';
},
transform(code, id) {
const nid = normalizePath(id);
const isClientFile =
nid === CLIENT_PUBLIC_ABSOLUTE_PATH ||
nid.endsWith(CLIENT_PUBLIC_PATH_FRAGMENT + '.ts') ||
nid.endsWith(CLIENT_PUBLIC_PATH_FRAGMENT + '.js') ||
nid.includes(CLIENT_PUBLIC_PATH_FRAGMENT.replace(/\//gi, '_'));
if (!isClientFile) return;
const assetsDir = config.build?.assetsDir ?? 'assets';
const base = config.base ?? '/';
const getOutputFile = (name?: string) =>
JSON.stringify(`${base}${assetsDir}/${name}`);
// __PROD__: plugin
code = code.replace(/\b__PROD__\b/g, JSON.stringify(!isServer));
if (type === 'colorPlugin') {
code = code
.replace(/\b__COLOR_PLUGIN_OUTPUT_FILE_NAME__\b/g, getOutputFile(colorPluginCssOutputName))
.replace(/\b__COLOR_PLUGIN_OPTIONS__\b/g, JSON.stringify(colorPluginOptions ?? {}));
}
if (type === 'antdDarkPlugin') {
code = code.replace(
/\b__ANTD_DARK_PLUGIN_OUTPUT_FILE_NAME__\b/g,
getOutputFile(antdDarkCssOutputName),
);
if (typeof antdDarkExtractCss === 'boolean') {
code = code.replace(
/\b__ANTD_DARK_PLUGIN_EXTRACT_CSS__\b/g,
JSON.stringify(antdDarkExtractCss),
);
}
if (typeof antdDarkLoadLink === 'boolean') {
code = code.replace(
/\b__ANTD_DARK_PLUGIN_LOAD_LINK__\b/g,
JSON.stringify(antdDarkLoadLink),
);
}
}
return { code, map: null };
},
};
}
// path tree-shake
export const _PATH = path.posix;

View File

@ -0,0 +1,147 @@
import path from 'path';
import fs from 'fs';
import type { Alias, ResolvedConfig } from 'vite';
import { normalizePath } from 'vite';
import less from 'less';
export type ResolveFn = (
id: string,
importer?: string,
aliasOnly?: boolean,
) => Promise<string | undefined>;
type CssUrlReplacer = (url: string, importer?: string) => string | Promise<string>;
export const externalRE = /^(https?:)?\/\//;
export const isExternalUrl = (url: string) => externalRE.test(url);
export const dataUrlRE = /^\s*data:/i;
export const isDataUrl = (url: string) => dataUrlRE.test(url);
const cssUrlRE = /url\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/;
let ViteLessManager: any;
function createViteLessPlugin(
rootFile: string,
alias: Alias[],
resolvers: { less: ResolveFn },
): Less.Plugin {
if (!ViteLessManager) {
ViteLessManager = class ViteManager extends (less as any).FileManager {
resolvers: any;
rootFile: string;
alias: Alias[];
constructor(rootFile: string, resolvers: { less: ResolveFn }, alias: Alias[]) {
super();
this.rootFile = rootFile;
this.resolvers = resolvers;
this.alias = alias;
}
supports() {
return true;
}
supportsSync() {
return false;
}
async loadFile(filename: string, dir: string, opts: any, env: any) {
const resolved = await this.resolvers.less(filename, path.join(dir, '*'));
if (resolved) {
const result = await rebaseUrls(resolved, this.rootFile, this.alias);
let contents: string;
if (result && 'contents' in result) {
contents = result.contents as string;
} else {
contents = fs.readFileSync(resolved, 'utf-8');
}
return {
filename: path.resolve(resolved),
contents,
};
} else {
return super.loadFile(filename, dir, opts, env);
}
}
};
}
return {
install(_: any, pluginManager: any) {
pluginManager.addFileManager(new ViteLessManager(rootFile, resolvers, alias));
},
minVersion: [3, 0, 0],
};
}
export function lessPlugin(id: string, config: ResolvedConfig) {
const resolvers = createCSSResolvers(config);
return createViteLessPlugin(id, (config.resolve as any).alias as Alias[], resolvers);
}
// vite 8 ResolvedConfig.createResolver config.environments
// "Cannot read properties of undefined (reading 'environments')"
// antd dark theme less less alias import vite 线
// less FileManager alias import antdDark transform
// try/catch build
function createCSSResolvers(_config: ResolvedConfig): { less: ResolveFn } {
return {
less: (async (_id: string) => undefined) as ResolveFn,
};
}
async function rebaseUrls(file: string, rootFile: string, alias: Alias[]): Promise<any> {
file = path.resolve(file);
const fileDir = path.dirname(file);
const rootDir = path.dirname(rootFile);
if (fileDir === rootDir) return { file };
const content = fs.readFileSync(file, 'utf-8');
if (!cssUrlRE.test(content)) return { file };
const rebased = await rewriteCssUrls(content, (url) => {
if (url.startsWith('/')) return url;
for (const { find } of alias) {
const matches = typeof find === 'string' ? url.startsWith(find) : (find as RegExp).test(url);
if (matches) return url;
}
const absolute = path.resolve(fileDir, url);
const relative = path.relative(rootDir, absolute);
return normalizePath(relative);
});
return { file, contents: rebased };
}
function rewriteCssUrls(css: string, replacer: CssUrlReplacer): Promise<string> {
return asyncReplace(css, cssUrlRE, async (match) => {
const [matched, rawUrl] = match;
return await doUrlReplace(rawUrl, matched, replacer);
});
}
export async function asyncReplace(
input: string,
re: RegExp,
replacer: (match: RegExpExecArray) => string | Promise<string>,
) {
let match: RegExpExecArray | null;
let remaining = input;
let rewritten = '';
while ((match = re.exec(remaining))) {
rewritten += remaining.slice(0, match.index);
rewritten += await replacer(match);
remaining = remaining.slice(match.index + match[0].length);
}
rewritten += remaining;
return rewritten;
}
async function doUrlReplace(rawUrl: string, matched: string, replacer: CssUrlReplacer) {
let wrap = '';
const first = rawUrl[0];
if (first === `"` || first === `'`) {
wrap = first;
rawUrl = rawUrl.slice(1, -1);
}
if (isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl.startsWith('#')) {
return matched;
}
return `url(${wrap}${await replacer(rawUrl)}${wrap})`;
}

View File

@ -0,0 +1,129 @@
import type { ResolvedConfig } from 'vite';
import { createHash } from 'crypto';
import CleanCSS from 'clean-css';
import {
commentRE,
cssBlockRE,
ruleRE,
cssValueRE,
safeEmptyRE,
importSafeRE,
} from './constants';
export type ResolveSelector = (selector: string) => string;
export function getVariablesReg(colors: string[]) {
return new RegExp(
colors
.map(
(i) =>
`(${i
.replace(/\s/g, ' ?')
.replace(/\(/g, `\\(`)
.replace(/\)/g, `\\)`)
.replace(/0?\./g, `0?\\.`)})`,
)
.join('|'),
);
}
export function combineRegs(decorator = '', joinString = '', ...args: any[]) {
const regString = args
.map((item) => {
const str = item.toString();
return `(${str.slice(1, str.length - 1)})`;
})
.join(joinString);
return new RegExp(regString, decorator);
}
export function formatCss(s: string) {
s = s.replace(/\s*([{}:;,])\s*/g, '$1');
s = s.replace(/;\s*;/g, ';');
s = s.replace(/,[\s.#\d]*{/g, '{');
s = s.replace(/([^\s])\{([^\s])/g, '$1 {\n\t$2');
s = s.replace(/([^\s])\}([^\n]*)/g, '$1\n}\n$2');
s = s.replace(/([^\s]);([^\s}])/g, '$1;\n\t$2');
return s;
}
export function createFileHash() {
return createHash('sha256').update(Date.now().toString()).digest('hex').substr(0, 8);
}
/**
* Compress the generated css.
* vite 8 已移除 config.build.cleanCssOptions这里直接使用 CleanCSS 默认配置
*/
export async function minifyCSS(css: string, config: ResolvedConfig) {
const res = new CleanCSS({ rebase: false }).minify(css);
if (res.errors && res.errors.length) {
console.error(`error when minifying css:\n${res.errors}`);
throw res.errors[0];
}
if (res.warnings && res.warnings.length) {
config.logger.warn(`warnings when minifying css:\n${res.warnings}`);
}
return res.styles;
}
// Used to extract relevant color configuration in css
export function extractVariable(
code: string,
colorVariables: string[],
resolveSelector?: ResolveSelector,
colorRE?: RegExp,
): string {
colorVariables = Array.from(new Set(colorVariables));
code = code.replace(commentRE, '');
const cssBlocks = code.match(cssBlockRE);
if (!cssBlocks || cssBlocks.length === 0) {
return '';
}
let allExtractedVariable = '';
const variableReg = getVariablesReg(colorVariables);
for (let index = 0; index < cssBlocks.length; index++) {
const cssBlock = cssBlocks[index];
if (!variableReg.test(cssBlock) || !cssBlock) continue;
const cssSelector = cssBlock.match(/[^{]*/)?.[0] ?? '';
if (!cssSelector) continue;
if (/^@.*keyframes/.test(cssSelector)) {
allExtractedVariable += `${cssSelector}{${extractVariable(
cssBlock.replace(/[^{]*\{/, '').replace(/}$/, ''),
colorVariables,
resolveSelector,
colorRE,
)}}`;
continue;
}
const colorReg = combineRegs(
'g',
'',
ruleRE,
cssValueRE,
safeEmptyRE,
variableReg,
importSafeRE,
);
const colorReplaceTemplates = cssBlock.match(colorRE || colorReg);
if (!colorReplaceTemplates) continue;
allExtractedVariable += `${
resolveSelector ? resolveSelector(cssSelector) : cssSelector
} {${colorReplaceTemplates.join(';')}}`;
}
return allExtractedVariable;
}

View File

@ -4,7 +4,8 @@
*/
import type { PluginOption } from 'vite';
import path from 'path';
import { viteThemePlugin, antdDarkThemePlugin, mixLighten, mixDarken, tinycolor } from '@rys-fe/vite-plugin-theme';
// vite 8 @rys-fe/vite-plugin-theme vite 8 ./theme-plugin
import { viteThemePlugin, antdDarkThemePlugin, mixLighten, mixDarken, tinycolor } from './theme-plugin';
import { getThemeColors, generateColors } from '../../config/themeConfig';
import { generateModifyVars } from '../../generate/generateModifyVars';

View File

@ -112,8 +112,8 @@
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "5.2.4",
"@vitejs/plugin-vue-jsx": "4.1.1",
"@vitejs/plugin-vue": "^6.0.6",
"@vitejs/plugin-vue-jsx": "^5.1.5",
"@vue/compiler-sfc": "^3.5.22",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.21",
@ -158,7 +158,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"unplugin-vue-components": "~0.24.1",
"vite": "^6.3.6",
"vite": "^7.3.3",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-mkcert": "^1.17.9",
@ -170,9 +170,8 @@
"vite-plugin-pwa": "^1.1.0",
"workbox-window": "^7.3.0",
"vite-plugin-qiankun": "^1.0.15",
"@rys-fe/vite-plugin-theme": "^0.8.6",
"vite-plugin-vue-setup-extend-plus": "^0.1.0",
"unocss": "^0.58.9",
"unocss": "^66.6.8",
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "^1.8.27",
"dingtalk-jsapi": "^3.2.0",

View File

@ -22,6 +22,9 @@
"noImplicitAny": false,
"skipLibCheck": true,
"paths": {
"@rys-fe/vite-plugin-theme/es/client": ["build/vite/plugin/theme-plugin/client/client.ts"],
"@rys-fe/vite-plugin-theme/es/colorUtils": ["build/vite/plugin/theme-plugin/client/colorUtils.ts"],
"@rys-fe/vite-plugin-theme": ["build/vite/plugin/theme-plugin/index.ts"],
"/@/*": ["src/*"],
"/#/*": ["types/*"],
"@/*": ["src/*"],

View File

@ -46,6 +46,11 @@ declare global {
path?: EventTarget[];
}
interface ImportMetaEnv extends ViteEnv {
readonly BASE_URL: string;
readonly MODE: string;
readonly DEV: boolean;
readonly PROD: boolean;
readonly SSR: boolean;
__: unknown;
}

View File

@ -51,6 +51,27 @@ export default async ({ command, mode }: ConfigEnv): Promise<UserConfig> => {
root,
resolve: {
alias: [
// @logicflow/vue-node-registry 1.1.13 npm src/ package.json
// main/module lib/es/vite 6 esbuild srcrolldown
// import src/index.ts
{
find: /^@logicflow\/vue-node-registry$/,
replacement: pathResolve('node_modules/@logicflow/vue-node-registry/src/index.ts'),
},
// @rys-fe/vite-plugin-theme vite 8
// RegExp vite resolve.extensions
{
find: /^@rys-fe\/vite-plugin-theme\/es\/client$/,
replacement: pathResolve('build/vite/plugin/theme-plugin/client/client'),
},
{
find: /^@rys-fe\/vite-plugin-theme\/es\/colorUtils$/,
replacement: pathResolve('build/vite/plugin/theme-plugin/client/colorUtils'),
},
{
find: /^@rys-fe\/vite-plugin-theme$/,
replacement: pathResolve('build/vite/plugin/theme-plugin/index'),
},
{
find: 'vue-i18n',
replacement: 'vue-i18n/dist/vue-i18n.cjs.js',