push
This commit is contained in:
103
build/plugin/mock/client.ts
Normal file
103
build/plugin/mock/client.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/* eslint-disable */
|
||||
import type { MockMethod } from './types';
|
||||
|
||||
export async function createProdMockServer(mockList: any[]) {
|
||||
const Mock: any = await import('mockjs');
|
||||
const { pathToRegexp } = await import('path-to-regexp');
|
||||
Mock.XHR.prototype.__send = Mock.XHR.prototype.send;
|
||||
Mock.XHR.prototype.send = function () {
|
||||
if (this.custom.xhr) {
|
||||
this.custom.xhr.withCredentials = this.withCredentials || false;
|
||||
|
||||
if (this.responseType) {
|
||||
this.custom.xhr.responseType = this.responseType;
|
||||
}
|
||||
}
|
||||
if (this.custom.requestHeaders) {
|
||||
const headers: any = {};
|
||||
for (let k in this.custom.requestHeaders) {
|
||||
headers[k.toString().toLowerCase()] = this.custom.requestHeaders[k];
|
||||
}
|
||||
this.custom.options = Object.assign({}, this.custom.options, { headers });
|
||||
}
|
||||
this.__send.apply(this, arguments);
|
||||
};
|
||||
|
||||
Mock.XHR.prototype.proxy_open = Mock.XHR.prototype.open;
|
||||
|
||||
Mock.XHR.prototype.open = function () {
|
||||
let responseType = this.responseType;
|
||||
this.proxy_open(...arguments);
|
||||
if (this.custom.xhr) {
|
||||
if (responseType) {
|
||||
this.custom.xhr.responseType = responseType;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const { url, method, response, timeout } of mockList) {
|
||||
__setupMock__(Mock, timeout);
|
||||
Mock.mock(
|
||||
pathToRegexp(url, undefined, { end: false }),
|
||||
method || 'get',
|
||||
__XHR2ExpressReqWrapper__(Mock, response),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function __param2Obj__(url: string) {
|
||||
const search = url.split('?')[1];
|
||||
if (!search) {
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(
|
||||
'{"' +
|
||||
decodeURIComponent(search)
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/&/g, '","')
|
||||
.replace(/=/g, '":"')
|
||||
.replace(/\+/g, ' ') +
|
||||
'"}',
|
||||
);
|
||||
}
|
||||
|
||||
function __XHR2ExpressReqWrapper__(_Mock: any, handle: (d: any) => any) {
|
||||
return function (options: any) {
|
||||
let result = null;
|
||||
if (typeof handle === 'function') {
|
||||
const { body, type, url, headers } = options;
|
||||
|
||||
let b = body;
|
||||
try {
|
||||
b = JSON.parse(body);
|
||||
} catch {}
|
||||
result = handle({
|
||||
method: type,
|
||||
body: b,
|
||||
query: __param2Obj__(url),
|
||||
headers,
|
||||
});
|
||||
} else {
|
||||
result = handle;
|
||||
}
|
||||
|
||||
return _Mock.mock(result);
|
||||
};
|
||||
}
|
||||
|
||||
function __setupMock__(mock: any, timeout = 0) {
|
||||
timeout &&
|
||||
mock.setup({
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
export function defineMockModule(
|
||||
fn: (config: {
|
||||
env: Record<string, any>;
|
||||
mode: string;
|
||||
command: 'build' | 'serve';
|
||||
}) => Promise<MockMethod[]> | MockMethod[],
|
||||
) {
|
||||
return fn;
|
||||
}
|
||||
255
build/plugin/mock/createMockServer.ts
Normal file
255
build/plugin/mock/createMockServer.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import type { ViteMockOptions, MockMethod, Recordable, RespThisType } from './types';
|
||||
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import chokidar from 'chokidar';
|
||||
import colors from 'picocolors';
|
||||
import url from 'url';
|
||||
import fg from 'fast-glob';
|
||||
import Mock from 'mockjs';
|
||||
import { pathToRegexp, match } from 'path-to-regexp';
|
||||
import { isArray, isFunction, sleep, isRegExp, isAbsPath } from './utils';
|
||||
import { IncomingMessage, NextHandleFunction } from 'connect';
|
||||
import { bundleRequire } from 'bundle-require';
|
||||
import type { ResolvedConfig } from 'vite';
|
||||
|
||||
export let mockData: MockMethod[] = [];
|
||||
|
||||
export async function createMockServer(
|
||||
opt: ViteMockOptions = { mockPath: 'mock', configPath: 'vite.mock.config' },
|
||||
config: ResolvedConfig,
|
||||
) {
|
||||
opt = {
|
||||
mockPath: 'mock',
|
||||
watchFiles: true,
|
||||
configPath: 'vite.mock.config.ts',
|
||||
logger: true,
|
||||
...opt,
|
||||
};
|
||||
|
||||
if (mockData.length > 0) return;
|
||||
mockData = await getMockConfig(opt, config);
|
||||
await createWatch(opt, config);
|
||||
}
|
||||
|
||||
// request match
|
||||
export async function requestMiddleware(opt: ViteMockOptions) {
|
||||
const { logger = true } = opt;
|
||||
const middleware: NextHandleFunction = async (req, res, next) => {
|
||||
let queryParams: {
|
||||
query?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
pathname?: string | null;
|
||||
} = {};
|
||||
|
||||
if (req.url) {
|
||||
queryParams = url.parse(req.url, true);
|
||||
}
|
||||
|
||||
const reqUrl = queryParams.pathname;
|
||||
|
||||
const matchRequest = mockData.find((item) => {
|
||||
if (!reqUrl || !item || !item.url) {
|
||||
return false;
|
||||
}
|
||||
if (item.method && item.method.toUpperCase() !== req.method) {
|
||||
return false;
|
||||
}
|
||||
return pathToRegexp(item.url).test(reqUrl);
|
||||
});
|
||||
|
||||
if (matchRequest) {
|
||||
const isGet = req.method && req.method.toUpperCase() === 'GET';
|
||||
const { response, rawResponse, timeout, statusCode, url } = matchRequest;
|
||||
|
||||
if (timeout) {
|
||||
await sleep(timeout);
|
||||
}
|
||||
|
||||
const urlMatch = match(url, { decode: decodeURIComponent });
|
||||
|
||||
let query = queryParams.query as any;
|
||||
if (reqUrl) {
|
||||
if ((isGet && JSON.stringify(query) === '{}') || !isGet) {
|
||||
const params = (urlMatch(reqUrl) as any).params;
|
||||
if (JSON.stringify(params) !== '{}') {
|
||||
query = (urlMatch(reqUrl) as any).params || {};
|
||||
} else {
|
||||
query = queryParams.query || {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const self: RespThisType = { req, res, parseJson: parseJson.bind(null, req) };
|
||||
if (isFunction(rawResponse)) {
|
||||
await rawResponse.bind(self)(req, res);
|
||||
} else {
|
||||
const body = await parseJson(req);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.statusCode = statusCode || 200;
|
||||
const mockResponse = isFunction(response)
|
||||
? response.bind(self)({ url: req.url as any, body, query, headers: req.headers })
|
||||
: response;
|
||||
res.end(JSON.stringify(Mock.mock(mockResponse)));
|
||||
}
|
||||
|
||||
logger && loggerOutput('request invoke', req.url!);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
return middleware;
|
||||
}
|
||||
|
||||
// create watch mock
|
||||
function createWatch(opt: ViteMockOptions, config: ResolvedConfig) {
|
||||
const { configPath, logger, watchFiles } = opt;
|
||||
|
||||
if (!watchFiles) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { absConfigPath, absMockPath } = getPath(opt);
|
||||
|
||||
if (process.env.VITE_DISABLED_WATCH_MOCK === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
const watchDir = [];
|
||||
const exitsConfigPath = fs.existsSync(absConfigPath);
|
||||
|
||||
exitsConfigPath && configPath ? watchDir.push(absConfigPath) : watchDir.push(absMockPath);
|
||||
|
||||
const watcher = chokidar.watch(watchDir, {
|
||||
ignored: opt.ignore || /.mjs$/,
|
||||
ignoreInitial: true,
|
||||
});
|
||||
|
||||
watcher.on('all', async (event, file) => {
|
||||
logger && loggerOutput(`mock file ${event}`, file);
|
||||
mockData = await getMockConfig(opt, config);
|
||||
});
|
||||
}
|
||||
|
||||
// clear cache
|
||||
function cleanRequireCache(opt: ViteMockOptions) {
|
||||
if (!require.cache) {
|
||||
return;
|
||||
}
|
||||
const { absConfigPath, absMockPath } = getPath(opt);
|
||||
Object.keys(require.cache).forEach((file) => {
|
||||
if (file === absConfigPath || file.indexOf(absMockPath) > -1) {
|
||||
delete require.cache[file];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseJson(req: IncomingMessage): Promise<Recordable> {
|
||||
return new Promise((resolve) => {
|
||||
let body = '';
|
||||
let jsonStr = '';
|
||||
req.on('data', function (chunk) {
|
||||
body += chunk;
|
||||
});
|
||||
req.on('end', function () {
|
||||
try {
|
||||
jsonStr = JSON.parse(body);
|
||||
} catch (err) {
|
||||
jsonStr = '';
|
||||
}
|
||||
resolve(jsonStr as any);
|
||||
return;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// load mock .ts files and watch
|
||||
async function getMockConfig(opt: ViteMockOptions, config: ResolvedConfig) {
|
||||
cleanRequireCache(opt);
|
||||
const { absConfigPath, absMockPath } = getPath(opt);
|
||||
const { ignore, configPath, logger } = opt;
|
||||
|
||||
let ret: MockMethod[] = [];
|
||||
|
||||
if (configPath && fs.existsSync(absConfigPath)) {
|
||||
logger && loggerOutput(`load mock data from`, absConfigPath);
|
||||
ret = await resolveModule(absConfigPath, config);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const mockFiles = fg
|
||||
.sync(`**/*.{ts,mjs,js}`, {
|
||||
cwd: absMockPath,
|
||||
})
|
||||
.filter((item) => {
|
||||
if (!ignore) {
|
||||
return true;
|
||||
}
|
||||
if (isFunction(ignore)) {
|
||||
return ignore(item);
|
||||
}
|
||||
if (isRegExp(ignore)) {
|
||||
return !ignore.test(path.basename(item));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
try {
|
||||
ret = [];
|
||||
const resolveModulePromiseList = [];
|
||||
|
||||
for (let index = 0; index < mockFiles.length; index++) {
|
||||
const mockFile = mockFiles[index];
|
||||
resolveModulePromiseList.push(resolveModule(path.join(absMockPath, mockFile), config));
|
||||
}
|
||||
|
||||
const loadAllResult = await Promise.all(resolveModulePromiseList);
|
||||
for (const resultModule of loadAllResult) {
|
||||
let mod = resultModule;
|
||||
if (!isArray(mod)) {
|
||||
mod = [mod];
|
||||
}
|
||||
ret = [...ret, ...mod];
|
||||
}
|
||||
} catch (error: any) {
|
||||
loggerOutput(`mock reload error`, error);
|
||||
ret = [];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Inspired by vite
|
||||
// support mock .ts files
|
||||
async function resolveModule(p: string, config: ResolvedConfig): Promise<any> {
|
||||
const mockData = await bundleRequire({
|
||||
filepath: p,
|
||||
});
|
||||
|
||||
let mod = mockData.mod.default || mockData.mod;
|
||||
if (isFunction(mod)) {
|
||||
mod = await mod({ env: config.env, mode: config.mode, command: config.command });
|
||||
}
|
||||
return mod;
|
||||
}
|
||||
|
||||
// get custom config file path and mock dir path
|
||||
function getPath(opt: ViteMockOptions) {
|
||||
const { mockPath, configPath } = opt;
|
||||
const cwd = process.cwd();
|
||||
const absMockPath = isAbsPath(mockPath) ? mockPath! : path.join(cwd, mockPath || '');
|
||||
const absConfigPath = path.join(cwd, configPath || '');
|
||||
return {
|
||||
absMockPath,
|
||||
absConfigPath,
|
||||
};
|
||||
}
|
||||
|
||||
function loggerOutput(title: string, msg: string, type: 'info' | 'error' = 'info') {
|
||||
const tag = type === 'info' ? colors.cyan(`[vite:mock]`) : colors.red(`[vite:mock-server]`);
|
||||
return console.log(
|
||||
`${colors.dim(new Date().toLocaleTimeString())} ${tag} ${colors.green(title)} ${colors.dim(
|
||||
msg,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
30
build/plugin/mock/index.ts
Normal file
30
build/plugin/mock/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { ViteMockOptions } from './types';
|
||||
import type { Plugin } from 'vite';
|
||||
import { ResolvedConfig } from 'vite';
|
||||
import { createMockServer, requestMiddleware } from './createMockServer';
|
||||
|
||||
export function viteMockServe(opt: ViteMockOptions = {}): Plugin {
|
||||
let isDev = false;
|
||||
let config: ResolvedConfig;
|
||||
|
||||
return {
|
||||
name: 'vite:mock',
|
||||
enforce: 'pre' as const,
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
isDev = config.command === 'serve';
|
||||
isDev && createMockServer(opt, config);
|
||||
},
|
||||
|
||||
configureServer: async ({ middlewares }) => {
|
||||
const { enable = isDev } = opt;
|
||||
if (!enable) {
|
||||
return;
|
||||
}
|
||||
const middleware = await requestMiddleware(opt);
|
||||
middlewares.use(middleware);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export * from './types';
|
||||
9
build/plugin/mock/mock.ts
Normal file
9
build/plugin/mock/mock.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { viteMockServe } from './index';
|
||||
|
||||
export function configMockPlugin({ enable, mockPath }: { enable: boolean; mockPath: string }) {
|
||||
return viteMockServe({
|
||||
// ignore: /.mjs$/,
|
||||
mockPath: mockPath,
|
||||
enable,
|
||||
});
|
||||
}
|
||||
38
build/plugin/mock/types.ts
Normal file
38
build/plugin/mock/types.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
|
||||
export interface ViteMockOptions {
|
||||
mockPath?: string;
|
||||
configPath?: string;
|
||||
ignore?: RegExp | ((fileName: string) => boolean);
|
||||
watchFiles?: boolean;
|
||||
enable?: boolean;
|
||||
logger?: boolean;
|
||||
}
|
||||
|
||||
export interface RespThisType {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
parseJson: () => any;
|
||||
}
|
||||
|
||||
export type MethodType = 'get' | 'post' | 'put' | 'delete' | 'patch';
|
||||
|
||||
export type Recordable<T = any> = Record<string, T>;
|
||||
|
||||
export declare interface MockMethod {
|
||||
url: string;
|
||||
method?: MethodType;
|
||||
timeout?: number;
|
||||
statusCode?: number;
|
||||
response?: (
|
||||
this: RespThisType,
|
||||
opt: { url: Recordable; body: Recordable; query: Recordable; headers: Recordable },
|
||||
) => any;
|
||||
rawResponse?: (this: RespThisType, req: IncomingMessage, res: ServerResponse) => void;
|
||||
}
|
||||
|
||||
export interface MockConfig {
|
||||
env: Record<string, any>;
|
||||
mode: string;
|
||||
command: 'build' | 'serve';
|
||||
}
|
||||
40
build/plugin/mock/utils.ts
Normal file
40
build/plugin/mock/utils.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import fs from 'fs';
|
||||
|
||||
const toString = Object.prototype.toString;
|
||||
|
||||
export function is(val: unknown, type: string) {
|
||||
return toString.call(val) === `[object ${type}]`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export function isFunction<T = Function>(val: unknown): val is T {
|
||||
return is(val, 'Function') || is(val, 'AsyncFunction');
|
||||
}
|
||||
|
||||
export function isArray(val: any): val is Array<any> {
|
||||
return val && Array.isArray(val);
|
||||
}
|
||||
|
||||
export function isRegExp(val: unknown): val is RegExp {
|
||||
return is(val, 'RegExp');
|
||||
}
|
||||
|
||||
export function isAbsPath(path: string | undefined) {
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
// Windows 路径格式:C:\ 或 \\ 开头,或已含盘符(D:\path\to\file)
|
||||
if (/^([a-zA-Z]:\\|\\\\|(?:\/|\uFF0F){2,})/.test(path)) {
|
||||
return true;
|
||||
}
|
||||
// Unix/Linux 路径格式:/ 开头
|
||||
return /^\/[^/]/.test(path);
|
||||
}
|
||||
|
||||
export function sleep(time: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve('');
|
||||
}, time);
|
||||
});
|
||||
}
|
||||
224
build/plugin/theme/antdDarkThemePlugin.ts
Normal file
224
build/plugin/theme/antdDarkThemePlugin.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getProtocal(path): string | undefined {
|
||||
let protocal:string | undefined;
|
||||
|
||||
hrefProtocals.forEach(hrefProtocal => {
|
||||
if(path.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;
|
||||
isServer && preloadLess();
|
||||
},
|
||||
transformIndexHtml(html) {
|
||||
let href;
|
||||
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: 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) => {
|
||||
return {
|
||||
map: needSourcemap ? this.getCombinedSourcemap() : null,
|
||||
code: content,
|
||||
};
|
||||
};
|
||||
|
||||
let processCss = '';
|
||||
const cache = codeCache.get(id);
|
||||
const isUpdate = !cache || cache.code !== code;
|
||||
|
||||
if (isUpdate) {
|
||||
const { css } = await less.render(code, {
|
||||
javascriptEnabled: true,
|
||||
modifyVars: darkModifyVars,
|
||||
filename: path.resolve(id),
|
||||
plugins: [lessPlugin(id, config)],
|
||||
});
|
||||
|
||||
const colors = css.match(colorRE);
|
||||
if (colors) {
|
||||
// The theme only extracts css related to color
|
||||
// Can effectively reduce the size
|
||||
processCss = extractVariable(css, colors.concat(['transparent']));
|
||||
}
|
||||
} else {
|
||||
processCss = cache!.css;
|
||||
}
|
||||
|
||||
if (isServer || !extractCss) {
|
||||
isUpdate && codeCache.set(id, { code, css: processCss });
|
||||
return getResult(`${getCss(processCss)}\n` + code);
|
||||
} else {
|
||||
if (!styleMap.has(id)) {
|
||||
const { css } = await less.render(getCss(processCss), {
|
||||
filename: path.resolve(id),
|
||||
plugins: [lessPlugin(id, config)],
|
||||
});
|
||||
|
||||
extCssString += `${css}\n`;
|
||||
}
|
||||
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 (error) {}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
46
build/plugin/theme/constants.ts
Normal file
46
build/plugin/theme/constants.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import path, { resolve } from 'path';
|
||||
import { normalizePath } from 'vite';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
export const VITE_CLIENT_ENTRY = '/@vite/client';
|
||||
|
||||
let clientPath = process.cwd();
|
||||
|
||||
if (
|
||||
!existsSync(
|
||||
normalizePath(path.resolve(clientPath, 'node_modules/vite-plugin-theme/es//client.js')),
|
||||
)
|
||||
) {
|
||||
clientPath = resolve(process.cwd(), '../');
|
||||
|
||||
console.log('子目录运行');
|
||||
}
|
||||
|
||||
export const VITE_PLUGIN_THEME_CLIENT_ENTRY = normalizePath(
|
||||
path.resolve(clientPath, 'node_modules/vite-plugin-theme/es/'),
|
||||
);
|
||||
|
||||
export const CLIENT_PUBLIC_ABSOLUTE_PATH = normalizePath(
|
||||
VITE_PLUGIN_THEME_CLIENT_ENTRY + '/client.js',
|
||||
);
|
||||
|
||||
export const CLIENT_PUBLIC_PATH = `/${VITE_PLUGIN_THEME_CLIENT_ENTRY}/client.js`;
|
||||
|
||||
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__';
|
||||
39
build/plugin/theme/esbuild/less.ts
Normal file
39
build/plugin/theme/esbuild/less.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import path from 'path';
|
||||
import { Plugin } from 'esbuild';
|
||||
import less from 'less';
|
||||
|
||||
/** Less-loader for esbuild */
|
||||
export const lessLoader = (content, options: Less.Options = {}): Plugin => {
|
||||
return {
|
||||
name: 'less-loader',
|
||||
setup: (build) => {
|
||||
build.onResolve({ filter: /\.less$/, namespace: 'file' }, (args) => {
|
||||
const filePath = path.resolve(
|
||||
process.cwd(),
|
||||
path.relative(process.cwd(), args.resolveDir),
|
||||
args.path
|
||||
);
|
||||
return {
|
||||
path: filePath,
|
||||
};
|
||||
});
|
||||
|
||||
// Build .less files
|
||||
build.onLoad({ filter: /\.less$/, namespace: 'file' }, async (args) => {
|
||||
const dir = path.dirname(args.path);
|
||||
try {
|
||||
const result = await less.render(content, {
|
||||
...options,
|
||||
paths: [...(options.paths || []), dir],
|
||||
});
|
||||
|
||||
return {
|
||||
contents: result.css,
|
||||
loader: 'css',
|
||||
resolveDir: dir,
|
||||
};
|
||||
} catch (e) {}
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
199
build/plugin/theme/index.ts
Normal file
199
build/plugin/theme/index.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { Plugin, ResolvedConfig } from 'vite';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { debug as Debug } from 'debug';
|
||||
import { extractVariable, minifyCSS } from './utils';
|
||||
|
||||
// export * from '../client/colorUtils';
|
||||
|
||||
export { antdDarkThemePlugin } from './antdDarkThemePlugin';
|
||||
|
||||
import { VITE_CLIENT_ENTRY, cssLangRE, cssVariableString, CLIENT_PUBLIC_PATH } from './constants';
|
||||
|
||||
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;
|
||||
isProd: boolean; // 必须传递环境标识
|
||||
}
|
||||
|
||||
import { createFileHash, formatCss } from './utils';
|
||||
import chalk from 'chalk';
|
||||
import { injectClientPlugin } from './injectClientPlugin';
|
||||
|
||||
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>();
|
||||
|
||||
let 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,
|
||||
isProd: true, // 默认为 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;
|
||||
console.log('options.isProd', options.isProd);
|
||||
return [
|
||||
injectClientPlugin('colorPlugin', {
|
||||
colorPluginCssOutputName: cssOutputName,
|
||||
colorPluginOptions: options,
|
||||
}),
|
||||
{
|
||||
...emptyPlugin,
|
||||
enforce: options.isProd ? undefined : 'post', // 生产环境不设置 enforce;开发环境设置为 post,切换主题才会都生效。
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
isServer = resolvedConfig.command === 'serve';
|
||||
clientPath = JSON.stringify(path.posix.join(config.base, CLIENT_PUBLIC_PATH));
|
||||
needSourcemap = !!resolvedConfig.build.sourcemap;
|
||||
debug('plugin config:', resolvedConfig);
|
||||
},
|
||||
|
||||
async transform(code, id) {
|
||||
if (!cssLangRE.test(id)) {
|
||||
return null;
|
||||
}
|
||||
const getResult = (content: string) => {
|
||||
return {
|
||||
map: needSourcemap ? this.getCombinedSourcemap() : null,
|
||||
code: content,
|
||||
};
|
||||
};
|
||||
|
||||
const clientCode = isServer
|
||||
? await getClientStyleString(code)
|
||||
: code.replace('export default', '').replace('"', '');
|
||||
|
||||
// Used to extract the relevant color configuration in css, you can pass in the function to override
|
||||
const extractCssCodeTemplate =
|
||||
typeof customerExtractVariable === 'function'
|
||||
? customerExtractVariable(clientCode)
|
||||
: extractVariable(clientCode, colorVariables, resolveSelectorFn);
|
||||
|
||||
debug('extractCssCodeTemplate:', id, extractCssCodeTemplate);
|
||||
|
||||
if (!extractCssCodeTemplate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// dev-server
|
||||
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.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 (error) {}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Intercept the css code embedded in js
|
||||
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;
|
||||
}
|
||||
113
build/plugin/theme/injectClientPlugin.ts
Normal file
113
build/plugin/theme/injectClientPlugin.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import path from 'path';
|
||||
import { ResolvedConfig, normalizePath, Plugin } from 'vite';
|
||||
import { ViteThemeOptions } from '.';
|
||||
import { CLIENT_PUBLIC_PATH, CLIENT_PUBLIC_ABSOLUTE_PATH } from './constants';
|
||||
import { debug as Debug } from 'debug';
|
||||
|
||||
const debug = Debug('vite:inject-vite-plugin-theme-client');
|
||||
|
||||
type PluginType = 'colorPlugin' | 'antdDarkPlugin';
|
||||
|
||||
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: boolean;
|
||||
let needSourcemap = false;
|
||||
return {
|
||||
name: 'vite:inject-vite-plugin-theme-client',
|
||||
enforce: 'pre',
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
isServer = resolvedConfig.command === 'serve';
|
||||
needSourcemap = !!resolvedConfig.build.sourcemap;
|
||||
},
|
||||
|
||||
transformIndexHtml: {
|
||||
enforce: 'pre',
|
||||
async transform(html) {
|
||||
if (html.includes(CLIENT_PUBLIC_PATH)) {
|
||||
return html;
|
||||
}
|
||||
return {
|
||||
html,
|
||||
tags: [
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: {
|
||||
type: 'module',
|
||||
src: path.posix.join(CLIENT_PUBLIC_PATH),
|
||||
},
|
||||
injectTo: 'head-prepend',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
async transform(code, id) {
|
||||
const nid = normalizePath(id);
|
||||
const path = normalizePath('vite-plugin-theme/es/client.js');
|
||||
const getMap = () => (needSourcemap ? this.getCombinedSourcemap() : null);
|
||||
|
||||
if (
|
||||
nid === CLIENT_PUBLIC_ABSOLUTE_PATH ||
|
||||
nid.includes(path) ||
|
||||
// support .vite cache
|
||||
nid.includes(path.replace(/\//gi, '_'))
|
||||
) {
|
||||
debug('transform client file:', id, code);
|
||||
|
||||
const {
|
||||
build: { assetsDir },
|
||||
} = config;
|
||||
|
||||
const getOutputFile = (name?: string) => {
|
||||
return JSON.stringify(`${config.base}${assetsDir}/${name}`);
|
||||
};
|
||||
|
||||
if (type === 'colorPlugin') {
|
||||
code = code
|
||||
.replace('__COLOR_PLUGIN_OUTPUT_FILE_NAME__', getOutputFile(colorPluginCssOutputName))
|
||||
.replace('__COLOR_PLUGIN_OPTIONS__', JSON.stringify(colorPluginOptions));
|
||||
}
|
||||
|
||||
if (type === 'antdDarkPlugin') {
|
||||
code = code.replace(
|
||||
'__ANTD_DARK_PLUGIN_OUTPUT_FILE_NAME__',
|
||||
getOutputFile(antdDarkCssOutputName)
|
||||
);
|
||||
if (typeof antdDarkExtractCss === 'boolean') {
|
||||
code = code.replace(
|
||||
'__ANTD_DARK_PLUGIN_EXTRACT_CSS__',
|
||||
JSON.stringify(antdDarkExtractCss)
|
||||
);
|
||||
}
|
||||
if (typeof antdDarkLoadLink === 'boolean') {
|
||||
code = code.replace(
|
||||
'__ANTD_DARK_PLUGIN_LOAD_LINK__',
|
||||
JSON.stringify(antdDarkExtractCss)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: code.replace('__PROD__', JSON.stringify(!isServer)),
|
||||
map: getMap(),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
174
build/plugin/theme/preprocessor/less/index.ts
Normal file
174
build/plugin/theme/preprocessor/less/index.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { Alias, normalizePath, ResolvedConfig } 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.FileManager {
|
||||
resolvers;
|
||||
rootFile;
|
||||
alias;
|
||||
constructor(rootFile: string, resolvers: 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
|
||||
): Promise<Less.FileLoadResult> {
|
||||
const resolved = await this.resolvers.less(filename, path.join(dir, '*'));
|
||||
if (resolved) {
|
||||
const result = await rebaseUrls(resolved, this.rootFile, this.alias);
|
||||
let contents;
|
||||
if (result && 'contents' in result) {
|
||||
contents = result.contents;
|
||||
} else {
|
||||
contents = fs.readFileSync(resolved, 'utf-8');
|
||||
}
|
||||
return {
|
||||
filename: path.resolve(resolved),
|
||||
contents,
|
||||
};
|
||||
} else {
|
||||
return super.loadFile(filename, dir, opts, env);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
install(_, pluginManager) {
|
||||
pluginManager.addFileManager(new ViteLessManager(rootFile, resolvers, alias));
|
||||
},
|
||||
minVersion: [3, 0, 0],
|
||||
};
|
||||
}
|
||||
|
||||
export function lessPlugin(id, config: ResolvedConfig) {
|
||||
const resolvers = createCSSResolvers(config);
|
||||
return createViteLessPlugin(id, config.resolve.alias, resolvers);
|
||||
}
|
||||
|
||||
function createCSSResolvers(config: ResolvedConfig): { less: ResolveFn } {
|
||||
let lessResolve: ResolveFn | undefined;
|
||||
return {
|
||||
get less() {
|
||||
return (
|
||||
lessResolve ||
|
||||
(lessResolve = config.createResolver({
|
||||
extensions: ['.less', '.css'],
|
||||
mainFields: ['less', 'style'],
|
||||
tryIndex: false,
|
||||
preferRelative: true,
|
||||
}))
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* relative url() inside \@imported sass and less files must be rebased to use
|
||||
* root file as base.
|
||||
*/
|
||||
async function rebaseUrls(file: string, rootFile: string, alias: Alias[]): Promise<any> {
|
||||
file = path.resolve(file); // ensure os-specific flashes
|
||||
// in the same dir, no need to rebase
|
||||
const fileDir = path.dirname(file);
|
||||
const rootDir = path.dirname(rootFile);
|
||||
if (fileDir === rootDir) {
|
||||
return { file };
|
||||
}
|
||||
// no url()
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
if (!cssUrlRE.test(content)) {
|
||||
return { file };
|
||||
}
|
||||
const rebased = await rewriteCssUrls(content, (url) => {
|
||||
if (url.startsWith('/')) return url;
|
||||
// match alias, no need to rewrite
|
||||
for (const { find } of alias) {
|
||||
const matches = typeof find === 'string' ? url.startsWith(find) : find.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})`;
|
||||
}
|
||||
128
build/plugin/theme/utils.ts
Normal file
128
build/plugin/theme/utils.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { ResolvedConfig } from 'vite';
|
||||
import { createHash } from 'crypto';
|
||||
import { ResolveSelector } from '.';
|
||||
import { commentRE, cssBlockRE, ruleRE, cssValueRE, safeEmptyRE, importSafeRE } from './constants';
|
||||
import CleanCSS from 'clean-css';
|
||||
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').digest('hex').substr(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress the generated code
|
||||
*/
|
||||
export async function minifyCSS(css: string, config: ResolvedConfig) {
|
||||
const res = new CleanCSS({
|
||||
rebase: false,
|
||||
...config.build.cleanCssOptions,
|
||||
}).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
|
||||
) {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user