antd-theme
Version:
Provide runtime dynamic theme for ant design.
266 lines (233 loc) • 8.05 kB
JavaScript
// eslint-disable-next-line max-classes-per-file
const less = require('less');
const path = require('path');
const { RawSource } = require('webpack-sources');
const { SyncWaterfallHook } = require('tapable');
const { version } = require('less-loader/package.json');
const semver = require('semver');
const ColorPalettePlugin = require('./colorPalettePlugin');
const extractVariables = require('./extractVariables');
const { pluginName, lessLoaderOptions } = require('./constants');
const RuntimeVariableVisitor = require('./visitors/runtimeVariableVisitor');
const serialize = require('./serialize');
const ResolveVisitor = require('./visitors/resolveVisitor');
const sha1 = require('./sha1');
const LessThemePlugin = require('./lessThemePlugin');
const themesModulePath = path.resolve(__dirname, '../lib/themes.js');
const defaultThemeName = 'default';
class LessLoaderVersionError extends Error {
constructor(version) {
super(`less-loader@${version} is not suported now, please use less-loader@5 or less-loader@6`);
}
}
const getLessLoaderOptions = (loaderOptions, plugin) => {
if (semver.satisfies(version, '^5')) {
const lessOptions = {
plugins: [],
...loaderOptions,
};
lessOptions.plugins.unshift(plugin);
return lessOptions;
}
if (semver.satisfies(version, '^6')) {
const lessOptions = {
plugins: [],
...loaderOptions.lessOptions,
};
lessOptions.plugins.unshift(plugin);
return {
...loaderOptions,
lessOptions,
implementation: less,
};
}
throw new LessLoaderVersionError(version);
};
class AntdThemePlugin {
constructor(options) {
if (!semver.satisfies(version, '^5 || ^6')) {
throw new LessLoaderVersionError(version);
}
this.options = options;
}
apply(compiler) {
const { options } = this;
const extractedExprs = {};
const mergedVariableGroups = {};
const changedVariableNames = new Map();
const themes = options.themes || [];
const runtimeVariableNames = options.variables || [];
const runtimeVariableVisitors = [];
const resolveVisitors = [];
const isRuntimeVariable = (i, node, variableName) => {
if (runtimeVariableNames.length === 0) {
return false;
}
return runtimeVariableVisitors[i].include(node, variableName);
};
compiler.hooks[lessLoaderOptions] = new SyncWaterfallHook(['options']);
const lessThemePlugin = new LessThemePlugin(
(expr) => {
extractedExprs[expr.name] = expr;
},
{
has: (key) => changedVariableNames.has(key),
get: (key) => changedVariableNames.get(key),
}
);
compiler.hooks[lessLoaderOptions].tap(
pluginName,
(options) => (
getLessLoaderOptions(
options,
lessThemePlugin
)
)
);
compiler.hooks.beforeCompile.tapPromise(pluginName, async () => {
const lessOptions = {
javascriptEnabled: true,
plugins: [
new ColorPalettePlugin(),
],
};
const filenames = [
require.resolve('antd/lib/style/themes/default.less'),
...themes.map(({ filename }) => filename),
];
const [defaultVariables, ...themeVariableGroups] = await Promise.all(
filenames.map(
(filename) => extractVariables(`@import "${filename}";`, lessOptions)
)
).then(
(variableGroups) => variableGroups.map(
(variables) => variables.reduce(
(acc, variable) => ({
...acc,
[variable.name]: variable,
}),
{}
)
)
);
mergedVariableGroups[defaultThemeName] = defaultVariables;
this.options.themes.forEach(
(theme, i) => {
mergedVariableGroups[theme.name] = {
...defaultVariables,
...themeVariableGroups[i],
};
}
);
Object.keys(mergedVariableGroups).forEach(
(name, i) => {
runtimeVariableVisitors[i] = new RuntimeVariableVisitor(
runtimeVariableNames,
mergedVariableGroups[name]
);
resolveVisitors[i] = new ResolveVisitor(
runtimeVariableNames,
mergedVariableGroups[name],
runtimeVariableVisitors[i]
);
}
);
Object.keys(mergedVariableGroups).forEach(
(themeName, themeIndex) => {
const themeVariables = mergedVariableGroups[themeName];
Object.keys(themeVariables).forEach(
(name) => {
const themeVariable = themeVariables[name];
themeVariables[name] = {
...themeVariable,
runtime: isRuntimeVariable(themeIndex, themeVariable.expr, themeVariable.name),
};
}
);
}
);
Object.values(mergedVariableGroups).forEach(
(themeVariables, i) => {
if (i === 0) {
return;
}
Object.values(themeVariables).forEach(
(themeVariable) => {
const { name } = themeVariable;
const defaultVariable = defaultVariables[name];
if (!defaultVariable) {
return;
}
if (themeVariable.runtime || defaultVariable.value !== themeVariable.value) {
changedVariableNames.set(name, defaultVariable.node);
}
}
);
}
);
});
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
compilation.hooks.finishModules.tapPromise(pluginName, async (modules) => {
const resolvedExprGroups = {};
const defineRuntimeExprs = new Map();
Object.keys(mergedVariableGroups).forEach((themeName, themeIndex) => {
const resolvedExprs = {};
Object.keys(extractedExprs).forEach(
(name) => {
const expr = extractedExprs[name];
const resolved = resolveVisitors[themeIndex].run(expr.node);
if (resolved.runtime) {
const code = JSON.stringify(serialize(resolved.expr));
const hash = sha1(code);
resolved.hash = hash;
defineRuntimeExprs.set(hash, code);
}
resolvedExprs[name] = resolved;
}
);
resolvedExprGroups[themeName] = resolvedExprs;
});
const source = [];
defineRuntimeExprs.forEach(
(value, key) => {
source.push(`var _${key} = ${value};`);
}
);
source.push('var themes = {');
const themeSource = Object.keys(resolvedExprGroups).map(
(themeName) => {
const source = [];
source.push(`${JSON.stringify(themeName)}: {`);
const resolvedExprs = resolvedExprGroups[themeName];
const outputVariables = Object.keys(resolvedExprs).map(
(name) => {
const value = JSON.stringify(resolvedExprs[name].value);
const resolvedExpr = resolvedExprs[name];
if (resolvedExpr.runtime) {
return (`${JSON.stringify(name)}: { expr: _${resolvedExpr.hash}, default: ${value} }`);
}
return (`${JSON.stringify(name)}: ${value}`);
}
);
source.push(outputVariables.join(',\n'));
source.push('}');
return source.join('\n');
}
);
source.push(themeSource.join(', '));
source.push('};');
source.push('module.exports = themes;');
const content = source.join('\n');
modules.forEach(
(module) => {
if (themesModulePath && module.resource === themesModulePath) {
module._source = new RawSource(content);
}
}
);
});
});
}
}
AntdThemePlugin.loader = path.resolve(__dirname, './loader.js');
module.exports = AntdThemePlugin;