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(); let extCssSet = new Set(); 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; }