@zougt/vite-plugin-theme-preprocessor
Version:
css theme preprocessor plugin for vite
492 lines (477 loc) • 19.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.themePreprocessorPlugin = exports.default = themePreprocessorPlugin;
Object.defineProperty(exports, "resetStylePreprocessor", {
enumerable: true,
get: function () {
return _resetStylePreprocessor.resetStylePreprocessor;
}
});
exports.themePreprocessorHmrPlugin = themePreprocessorHmrPlugin;
var _path = _interopRequireDefault(require("path"));
var _fsExtra = _interopRequireDefault(require("fs-extra"));
var _someLoaderUtils = require("@zougt/some-loader-utils");
var _package = _interopRequireDefault(require("../package.json"));
var _addExtractThemeLinkTag = require("./common/addExtractThemeLinkTag");
var _createSetCustomTheme = require("./common/createSetCustomTheme");
var _getModulesScopeGenerater = require("./common/getModulesScopeGenerater");
var _resetStylePreprocessor = require("./common/resetStylePreprocessor");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// eslint-disable-next-line import/no-unresolved
let preCustomThemeOutputPath = "";
/**
* lang : "less" | "scss" | "sass"
* @param {object} options : { [lang]:{ multipleScopeVars: [{scopeName:"theme-1",path: path.resolve('./vars.less')}], outputDir, defaultScopeName ,extract,removeCssScopeName,customThemeCssFileName,themeLinkTagId,themeLinkTagInjectTo } }
* @param {object} [options.less]
* @param {object} [options.scss]
* @param {object} [options.sass]
* @returns {object}
*/
function themePreprocessorPlugin(options = {}) {
let config = {
root: process.cwd()
};
// @zougt/vite-plugin-theme-preprocessor 被 require() 时的实际路径
const targetResolved = require.resolve(_package.default.name).replace(/[\\/]dist[\\/]index\.js$/, "").replace(/\\/g, "/");
const customThemeOutputPath = `${targetResolved}/setCustomTheme.js`;
let buildCommand;
const processorNames = Object.keys(options);
let defaultOptions = {
outputDir: "",
// multipleScopeVars:[{scopeName:"theme-default",path:""}],
// 默认取 multipleScopeVars[0].scopeName
defaultScopeName: "",
// 强制将一些颜色值的样式作为主题样式
includeStyleWithColors: [
// {color:"#ffffff",inGradient:false}
],
extract: true,
themeLinkTagId: "theme-link-tag",
// "head"||"head-prepend" || "body" ||"body-prepend"
themeLinkTagInjectTo: "head",
removeCssScopeName: false,
customThemeCssFileName: null,
// 以下是任意主题模式的参数 arbitraryMode:true 有效
arbitraryMode: false,
// 默认主题色,必填
defaultPrimaryColor: "",
// 输出切换主题色的方法文件
customThemeOutputPath,
// style标签的id
styleTagId: "custom-theme-tagid",
// boolean || "head" || "body"
InjectDefaultStyleTagToHtml: true,
// 调整色相对比的范围值,low:往低减去的数值,high:往高加的数值
hueDiffControls: {
low: 0,
high: 0
}
};
const allmultipleScopeVars = [];
let cacheThemeStyleContent = "";
const setCustomThemeCodeReplacer = "const setCustomTheme=function(options){window._setCustomTheme_=options;}";
return {
name: "vite-plugin-theme-preprocessor",
enforce: "pre",
api: {
getOptions() {
return defaultOptions;
},
getProcessorNames() {
return processorNames;
},
getMultipleScopeVars() {
return allmultipleScopeVars;
}
},
config(conf, {
command
}) {
buildCommand = command;
// 在对应的预处理器配置添加 multipleScopeVars 属性
const css = conf.css || {};
const preprocessorOptions = css.preprocessorOptions || {};
processorNames.forEach(lang => {
const langOptions = options[lang] || {};
// 合并参数
defaultOptions = {
...defaultOptions,
...langOptions
};
if (Array.isArray(langOptions.multipleScopeVars) && langOptions.multipleScopeVars.length) {
preprocessorOptions[lang] = {
...(preprocessorOptions[lang] || {}),
multipleScopeVars: langOptions.multipleScopeVars
};
langOptions.multipleScopeVars.forEach(item => {
const founded = allmultipleScopeVars.find(f => f.scopeName === item.scopeName);
if (!founded) {
allmultipleScopeVars.push({
...item
});
} else if (item.path) {
founded.path = Array.isArray(founded.path) ? founded.path : [founded.path];
const itemPath = Array.isArray(item.path) ? item.path : [item.path];
founded.path = [...new Set(founded.path.concat(itemPath))];
}
});
}
});
css.preprocessorOptions = preprocessorOptions;
const modulesOptions = css.modules !== false ? css.modules || {} : null;
if (modulesOptions && !defaultOptions.arbitraryMode) {
modulesOptions.generateScopedName = (0, _getModulesScopeGenerater.getModulesScopeGenerater)({
multipleScopeVars: allmultipleScopeVars,
generateScopedName: modulesOptions.generateScopedName
});
}
css.modules = modulesOptions;
const server = conf.server || {};
const watch = server.watch || {};
server.watch = {
...watch,
// 热更新时必需的,希望监听setCustomTheme.js
ignored: ["!**/node_modules/**/setCustomTheme.js"].concat(Array.isArray(watch.ignored) ? watch.ignored : watch.ignored ? [watch.ignored] : [])
};
const optimizeDeps = conf.optimizeDeps || {};
optimizeDeps.exclude = ["@zougt/vite-plugin-theme-preprocessor/dist/browser-utils", "@zougt/vite-plugin-theme-preprocessor/dist/browser-utils.js"].concat(Array.isArray(optimizeDeps.exclude) ? optimizeDeps.exclude : optimizeDeps.exclude ? [optimizeDeps.exclude] : []);
return {
...conf,
css,
optimizeDeps,
server
};
},
configResolved(resolvedConfig) {
// 存储最终解析的配置
config = resolvedConfig;
(0, _someLoaderUtils.createPulignParamsFile)({
extract: buildCommand !== "build" ? false : defaultOptions.extract
});
if (!defaultOptions.arbitraryMode) {
// 预设主题模式,提供 brower-utils.js 需要的参数
const browerPreprocessorOptions = {
...defaultOptions,
multipleScopeVars: allmultipleScopeVars
};
const packRoot = require.resolve(_package.default.name).replace(/[\\/]index\.js$/, "").replace(/\\/g, "/");
// 将一些参数打入到 toBrowerEnvs.js , 由brower-utils.js 获取
_fsExtra.default.writeFileSync(`${packRoot}/toBrowerEnvs.js`, `export const browerPreprocessorOptions = ${JSON.stringify(browerPreprocessorOptions)};\nexport const basePath="${config.base || ""}";\nexport const assetsDir="${config.build.assetsDir || ""}";\nexport const buildCommand="${buildCommand}";
`);
}
if (defaultOptions.arbitraryMode && preCustomThemeOutputPath !== defaultOptions.customThemeOutputPath) {
preCustomThemeOutputPath = defaultOptions.customThemeOutputPath;
return (0, _createSetCustomTheme.createSetCustomTheme)({
...defaultOptions,
buildCommand,
cacheThemeStyleContent
}).then(result => {
if (result) {
cacheThemeStyleContent = result.styleContent;
}
});
}
return null;
},
buildStart() {
return Promise.all(processorNames.map(lang => {
const langName = lang === "scss" ? "sass" : lang;
// 得到 require('less') 时的绝对路径
const resolved = require.resolve(langName).replace(/\\/g, "/");
const pathnames = resolved.split("/");
// 存在类似 _less@ 开头的,兼容cnpm install
const index = pathnames.findIndex(str => new RegExp(`^_${langName}@`).test(str) || str === langName);
// 真正 less 执行的目录名称,通常情况下就是 "less" , 但cnpm install的可能就是 "_less@4.1.2@less"
const resolveName = pathnames[index];
// 完整的 less 所在的路径
const resolveDir = `${pathnames.slice(0, index).join("/")}/${resolveName}`;
const originalDir = _path.default.resolve("node_modules/.zougtTheme/original").replace(/\\/g, "/");
if (!_fsExtra.default.existsSync(resolveDir) && !_fsExtra.default.existsSync(`${originalDir}/${resolveName}`)) {
throw new Error(`Preprocessor dependency "${langName}" not found. Did you install it?`);
}
// substitute:替代品的源位置
const substituteDir = `${targetResolved}/dist/substitute`;
const substitutePreprocessorDir = `${substituteDir}/${resolveName}`;
return (0, _resetStylePreprocessor.resetStylePreprocessor)({
langs: [langName]
}).then(() => {
// "getLess" || "getSass"
const funName = `get${langName.slice(0, 1).toUpperCase() + langName.slice(1)}`;
// 在substitute生成替代包
const copyPreFiles = _fsExtra.default.readdirSync(resolveDir) || [];
copyPreFiles.forEach(name => {
if (name !== "node_modules" && name !== "bin") {
_fsExtra.default.copySync(`${resolveDir}/${name}`, `${substitutePreprocessorDir}/${name}`);
}
});
_fsExtra.default.copySync(`${substituteDir}/preprocessor-substitute-options.js`, `${substitutePreprocessorDir}/preprocessor-substitute-options.js`);
// require('less')时的文件名,如 "index.js"
const mainFile = resolved.replace(resolveDir, "").replace(/^\/+/g, "");
// @zougt/some-loader-utils 被 require() 时的实际路径
const loaderUtilsResolved = require.resolve("@zougt/some-loader-utils").replace(/[\\/]index\.js$/, "").replace(/\\/g, "/");
// 向 "index.js" 中写上如 "getLess" 的调用
_fsExtra.default.writeFileSync(`${substitutePreprocessorDir}/${mainFile}`, `const nodePreprocessor = require("${originalDir}/${resolveName}/${mainFile}");
const { ${funName} } = require("${loaderUtilsResolved}");
module.exports = ${funName}({
arbitraryMode:${defaultOptions.arbitraryMode},
includeStyleWithColors:${JSON.stringify(defaultOptions.includeStyleWithColors)},
implementation: nodePreprocessor,
});
`);
// 替换了处理器的标识
const isSubstitute = _fsExtra.default.existsSync(`${resolveDir}/preprocessor-substitute-options.js`);
if (!isSubstitute) {
// 用less的替代品替换 源 less
const moveFiles = _fsExtra.default.readdirSync(resolveDir) || [];
moveFiles.forEach(name => {
if (name !== "node_modules" && name !== "bin") {
_fsExtra.default.copySync(`${resolveDir}/${name}`, `${originalDir}/${resolveName}/${name}`);
}
});
const copyFiles = _fsExtra.default.readdirSync(substitutePreprocessorDir);
copyFiles.forEach(name => {
if (name !== "node_modules" && name !== "bin") {
_fsExtra.default.copySync(`${substitutePreprocessorDir}/${name}`, `${resolveDir}/${name}`);
}
});
}
return Promise.resolve();
});
}));
},
resolveId(id) {
if (id === "@setCustomTheme") {
return id;
}
return null;
},
load(id) {
// 动态主题模式下 加载虚拟模块 "@setCustomTheme"
if (id === "@setCustomTheme" && defaultOptions.arbitraryMode && defaultOptions.customThemeOutputPath) {
if (buildCommand !== "build") {
// 开发模式
return `import { default as setCustomTheme } from "${defaultOptions.customThemeOutputPath}";
export default setCustomTheme;
import Color from "color";
import.meta.hot.on('custom-theme-update', (data) => {
setCustomTheme({...data,Color});
})
`;
}
// 打包时"@setCustomTheme"模块的内容,会在 renderChunk 进行源码替换
return `${setCustomThemeCodeReplacer};export default setCustomTheme;`;
}
return null;
},
renderChunk(code) {
// 打包才会进入这个钩子
if (defaultOptions.arbitraryMode && code.includes(setCustomThemeCodeReplacer)) {
return (0, _createSetCustomTheme.createSetCustomTheme)({
...defaultOptions,
buildCommand,
customThemeOutputPath: null,
cacheThemeStyleContent: null
}).then(result => {
if (result) {
return code.replace(setCustomThemeCodeReplacer, `\n${result.setCustomThemeConent}\n`);
}
return null;
});
}
return null;
},
transformIndexHtml(html) {
const {
arbitraryMode,
styleTagId,
InjectDefaultStyleTagToHtml
} = defaultOptions;
if (arbitraryMode) {
// 任意模式下,获取主题css生成一个setCustomTheme.js,并添加style tag到html
const loaderRsoleved = (0, _someLoaderUtils.getCurrentPackRequirePath)();
const dirName = "extractTheme";
if (!_fsExtra.default.existsSync(`${loaderRsoleved}/${dirName}`)) {
return null;
}
const themeResult = buildCommand !== "build" ? (0, _createSetCustomTheme.createSetCustomTheme)({
...defaultOptions,
buildCommand,
cacheThemeStyleContent
}) : (0, _someLoaderUtils.getThemeStyleContent)();
return themeResult.then(result => {
let styleContent = cacheThemeStyleContent || "";
if (result) {
styleContent = result.styleContent;
cacheThemeStyleContent = styleContent;
}
if (styleContent) {
let injectTo = "body";
if (InjectDefaultStyleTagToHtml === "head" && buildCommand === "build") {
injectTo = "head-prepend";
}
const tag = {
tag: "style",
attrs: {
id: styleTagId,
type: "text/css"
},
injectTo,
children: styleContent
};
return {
html,
tags: InjectDefaultStyleTagToHtml ? [tag] : []
};
}
return null;
});
}
// 非任意模式,添加默认的抽取的主题css的link
return (0, _addExtractThemeLinkTag.addExtractThemeLinkTag)({
html,
defaultOptions,
allmultipleScopeVars,
buildCommand,
config
});
},
generateBundle() {
if (buildCommand !== "build") {
return Promise.resolve();
}
// 在资产生成文件之前,抽取multipleScopeVars对应的内容
const {
extract,
arbitraryMode,
removeCssScopeName,
outputDir,
customThemeCssFileName
} = defaultOptions;
if (extract && !arbitraryMode) {
// 生产时,非任意模式下抽取对应的主题css
return (0, _someLoaderUtils.extractThemeCss)({
removeCssScopeName
}).then(({
themeCss
}) => {
Object.keys(themeCss).forEach(scopeName => {
const name = (typeof customThemeCssFileName === "function" ? customThemeCssFileName(scopeName) : "") || scopeName;
const fileName = _path.default.posix.join(outputDir || config.build.assetsDir, `${name}.css`).replace(/^[\\/]+/g, "");
this.emitFile({
type: "asset",
fileName,
source: themeCss[scopeName]
});
});
});
}
return Promise.resolve();
}
};
}
/**
* 动态主题模式的热更新插件
* @returns object
*/
function themePreprocessorHmrPlugin() {
let parentApi = null;
let cacheThemeStyleContent = "";
let buildCommand = "";
// 触发热更新时的 样式文件
const hotUpdateStyleFiles = new Set();
// 进入transform的样式文件
const transformStyleFiles = new Set();
let hotServer = null;
let config = {};
return {
// 插件顺序必须post
enforce: "post",
name: "vite-plugin-theme-preprocessor-hmr",
config(conf, {
command
}) {
buildCommand = command;
},
configResolved(resolvedConfig) {
// 存储最终解析的配置
config = resolvedConfig;
},
buildStart() {
// 获取依赖插件提供的 方法
const parentName = "vite-plugin-theme-preprocessor";
const parentPlugin = config.plugins.find(plugin => plugin.name === parentName);
if (!parentPlugin) {
throw new Error(`This plugin depends on the "${parentName}" plugin.`);
}
parentApi = parentPlugin.api;
},
transform(code, id) {
// vite:css插件内的transform使用less/sass,需要在less/sass编译完后调用 getThemeStyleContent
const defaultOptions = parentApi.getOptions();
if (defaultOptions.arbitraryMode && /\.(less|scss|sass)(\?.+)?/.test(id)) {
transformStyleFiles.add(id);
// 当transform的的样式文件数量 到达 触发热更新的样式文件数量时,就获取主题css,并触发热更新事件 import.meta.hot.on('custom-theme-update',()=>{})
if (hotUpdateStyleFiles.size && hotUpdateStyleFiles.size === transformStyleFiles.size) {
(0, _someLoaderUtils.getThemeStyleContent)();
(0, _createSetCustomTheme.createSetCustomTheme)({
...defaultOptions,
buildCommand,
cacheThemeStyleContent
}).then(result => {
if (result) {
cacheThemeStyleContent = result.styleContent;
hotServer.ws.send({
type: "custom",
event: "custom-theme-update",
data: {
sourceThemeStyle: result.styleContent,
hybridValueMap: result.hybridValueMap,
otherValues: result.otherValues,
sourceColorMap: result.sourceColorMap
}
});
}
});
}
}
},
handleHotUpdate({
file,
server,
modules
}) {
hotServer = server;
const defaultOptions = parentApi.getOptions();
const {
arbitraryMode,
customThemeOutputPath
} = defaultOptions;
if (!arbitraryMode) {
return Promise.resolve();
}
hotUpdateStyleFiles.clear();
transformStyleFiles.clear();
if (parentApi.getMultipleScopeVars().some(item => typeof item.path === "string" && file === item.path || Array.isArray(item.path) && item.path.some(p => p === file))) {
(0, _someLoaderUtils.removeThemeFiles)();
modules[0].importers.forEach(item => {
// console.log(item)
if (item.id && /\.(less|scss|sass)(\?.+)?/.test(item.id)) {
hotUpdateStyleFiles.add(item.id);
}
});
} else {
modules.forEach(item => {
if (item.id && /\.(less|scss|sass)(\?.+)?/.test(item.id)) {
hotUpdateStyleFiles.add(item.id);
}
});
}
if (file === customThemeOutputPath) {
return Promise.resolve([]);
}
return Promise.resolve();
}
};
}