vite-plugin-antd-style-px-to-rem
Version:
A Vite plugin that automatically converts px values to rem units in antd-style CSS template literals, createStyles functions, and JSX attributes during build time
619 lines (613 loc) • 22.1 kB
JavaScript
// index.ts
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
// constants.ts
var defaultOptions = {
rootValue: 16,
unitPrecision: 5,
minPixelValue: 0,
propList: ["*"],
selectorBlackList: [],
replace: true,
mediaQuery: false,
include: void 0,
exclude: void 0,
cssTemplateFunctions: ["css"],
enableJSXTransform: true,
jsxAttributeMapping: {}
};
var lengthProperties = /* @__PURE__ */ new Set([
"width",
"height",
"minWidth",
"minHeight",
"maxWidth",
"maxHeight",
"padding",
"paddingTop",
"paddingRight",
"paddingBottom",
"paddingLeft",
"margin",
"marginTop",
"marginRight",
"marginBottom",
"marginLeft",
"left",
"right",
"top",
"bottom",
"fontSize",
"lineHeight",
"borderRadius",
"borderTopLeftRadius",
"borderTopRightRadius",
"borderBottomLeftRadius",
"borderBottomRightRadius",
"borderWidth",
"borderTopWidth",
"borderRightWidth",
"borderBottomWidth",
"borderLeftWidth",
"gap",
"rowGap",
"columnGap",
"flexBasis",
"outlineWidth",
"letterSpacing",
"wordSpacing"
]);
// utils.ts
function shouldProcess(id, include, exclude) {
if (exclude) {
const excludePatterns = Array.isArray(exclude) ? exclude : [exclude];
for (const pattern of excludePatterns) {
if (typeof pattern === "string" && id.includes(pattern)) return false;
if (pattern instanceof RegExp && pattern.test(id)) return false;
}
}
if (include) {
const includePatterns = Array.isArray(include) ? include : [include];
return includePatterns.some((pattern) => {
if (typeof pattern === "string") return id.includes(pattern);
if (pattern instanceof RegExp) return pattern.test(id);
return false;
});
}
return /\.(tsx?|jsx?)$/.test(id);
}
function shouldConvertProperty(propName, propList) {
if (propList.length === 0) {
return false;
}
const hasWildcard = propList.includes("*");
const exclusions = propList.filter((prop) => prop.startsWith("!")).map((prop) => prop.substring(1));
const inclusions = propList.filter((prop) => !prop.startsWith("!"));
if (exclusions.includes(propName)) {
return false;
}
if (hasWildcard) {
return true;
}
return inclusions.includes(propName);
}
function isLengthProperty(propName) {
return lengthProperties.has(propName);
}
function hasVariableExpressions(template) {
return template.expressions.length > 0;
}
function createPxToRemConverter(options) {
return function pxtorem(value) {
const num = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(num) || Math.abs(num) <= options.minPixelValue) {
return String(value);
}
const rem = num / options.rootValue;
const remValue = parseFloat(rem.toFixed(options.unitPrecision));
if (remValue === 0) {
return "0";
}
return `${remValue}rem`;
};
}
// processors/css-processor.ts
function processCssTemplate(cssContent, options) {
try {
if (!cssContent || typeof cssContent !== "string" || cssContent.trim() === "") {
return cssContent;
}
const { rootValue, unitPrecision, minPixelValue, propList } = options;
let hasChanges = false;
const pxToRem = createPxToRemConverter({
rootValue,
unitPrecision,
minPixelValue
});
const pxRegex = /(-?\d*\.?\d+)px/g;
const lines = cssContent.split("\n");
let inBlockComment = false;
const processedLines = lines.map((line, index) => {
const trimmedLine = line.trim();
if (inBlockComment) {
if (trimmedLine.includes("*/")) {
inBlockComment = false;
}
return line;
}
if (trimmedLine.startsWith("/*")) {
if (!trimmedLine.includes("*/")) {
inBlockComment = true;
}
return line;
}
if (trimmedLine.startsWith("//")) {
return line;
}
const currentLineHasIgnore = line.includes("antd-style-px-to-rem ignore") || line.includes("antd-style-px-to-rem ignore");
const previousLineIsIgnoreComment = index > 0 && lines[index - 1] && (lines[index - 1].trim().includes("antd-style-px-to-rem ignore") || lines[index - 1].trim().includes("antd-style-px-to-rem ignore")) && !lines[index - 1].includes(":");
if (previousLineIsIgnoreComment) {
return line;
}
const propertyMatch = line.match(/^(\s*)(-{0,2}[a-zA-Z_][a-zA-Z0-9_-]*)(\s*:\s*)(.*)$/);
if (propertyMatch) {
const indentation = propertyMatch[1] || "";
const propName = propertyMatch[2] || "";
const colonPart = propertyMatch[3] || "";
let value = propertyMatch[4] || "";
if (shouldConvertProperty(propName, propList) && !currentLineHasIgnore) {
const processedValue = value.replace(pxRegex, (match, numStr) => {
const convertedValue = pxToRem(numStr);
if (convertedValue !== match) {
hasChanges = true;
}
return convertedValue;
});
value = processedValue;
}
return `${indentation}${propName}${colonPart}${value}`;
} else if (propList.includes("*") && !propList.some((p) => p.startsWith("!"))) {
if (!currentLineHasIgnore) {
const processedLine = line.replace(pxRegex, (match, numStr) => {
const convertedValue = pxToRem(numStr);
if (convertedValue !== match) {
hasChanges = true;
}
return convertedValue;
});
return processedLine;
}
}
return line;
});
const processedContent = processedLines.join("\n");
return hasChanges ? processedContent : cssContent;
} catch (error) {
console.warn("Failed to process CSS template with px to rem conversion:", error);
return cssContent;
}
}
function processTemplateQuasis(template, options) {
let hasChanges = false;
for (const quasi of template.quasis) {
if (quasi.value && quasi.value.raw) {
const originalCss = quasi.value.raw;
const processedCss = processCssTemplate(originalCss, options);
if (processedCss !== originalCss) {
quasi.value.raw = processedCss;
quasi.value.cooked = processedCss;
hasChanges = true;
}
}
}
return hasChanges;
}
// processors/jsx-processor.ts
function processStyleObjectExpression(objectExpression, options) {
let hasChanges = false;
const pxtorem = createPxToRemConverter({
rootValue: options.rootValue,
unitPrecision: options.unitPrecision,
minPixelValue: options.minPixelValue
});
for (const property of objectExpression.properties) {
if (property.type === "ObjectProperty") {
let propName = "";
if (property.key.type === "Identifier") {
propName = property.key.name;
} else if (property.key.type === "StringLiteral") {
propName = property.key.value;
}
if (!propName) continue;
const shouldConvert = shouldConvertProperty(propName, options.propList);
if (property.value.type === "StringLiteral") {
if (!shouldConvert) continue;
const originalValue = property.value.value;
const pxRegex = /(-?\d*\.?\d+)px/g;
if (pxRegex.test(originalValue)) {
const newValue = originalValue.replace(pxRegex, (_, numStr) => pxtorem(numStr));
if (newValue !== originalValue) {
property.value.value = newValue;
hasChanges = true;
}
}
} else if (property.value.type === "NumericLiteral") {
if (!shouldConvert) continue;
if (isLengthProperty(propName)) {
const newValue = pxtorem(property.value.value);
property.value = {
type: "StringLiteral",
value: newValue
};
hasChanges = true;
}
} else if (property.value.type === "TemplateLiteral") {
if (!shouldConvert || hasVariableExpressions(property.value)) continue;
if (processTemplateQuasis(property.value, options)) {
hasChanges = true;
}
} else if (property.value.type === "ConditionalExpression") {
if (processConditionalPropertyValue(property.value, propName, options)) {
hasChanges = true;
}
}
}
}
return hasChanges;
}
function processConditionalExpression(conditionalExpression, options) {
let hasChanges = false;
if (conditionalExpression.consequent.type === "ObjectExpression") {
if (processStyleObjectExpression(conditionalExpression.consequent, options)) {
hasChanges = true;
}
} else if (conditionalExpression.consequent.type === "ConditionalExpression") {
if (processConditionalExpression(conditionalExpression.consequent, options)) {
hasChanges = true;
}
}
if (conditionalExpression.alternate.type === "ObjectExpression") {
if (processStyleObjectExpression(conditionalExpression.alternate, options)) {
hasChanges = true;
}
} else if (conditionalExpression.alternate.type === "ConditionalExpression") {
if (processConditionalExpression(conditionalExpression.alternate, options)) {
hasChanges = true;
}
}
return hasChanges;
}
function processConditionalPropertyValue(conditionalExpression, propName, options) {
let hasChanges = false;
const shouldConvert = shouldConvertProperty(propName, options.propList);
if (!shouldConvert) return false;
const pxtorem = createPxToRemConverter({
rootValue: options.rootValue,
unitPrecision: options.unitPrecision,
minPixelValue: options.minPixelValue
});
if (conditionalExpression.consequent.type === "NumericLiteral") {
if (isLengthProperty(propName)) {
const newValue = pxtorem(conditionalExpression.consequent.value);
conditionalExpression.consequent = {
type: "StringLiteral",
value: newValue
};
hasChanges = true;
}
} else if (conditionalExpression.consequent.type === "StringLiteral") {
const originalValue = conditionalExpression.consequent.value;
const pxRegex = /(-?\d*\.?\d+)px/g;
if (pxRegex.test(originalValue)) {
const newValue = originalValue.replace(pxRegex, (_, numStr) => pxtorem(numStr));
if (newValue !== originalValue) {
conditionalExpression.consequent.value = newValue;
hasChanges = true;
}
}
} else if (conditionalExpression.consequent.type === "ConditionalExpression") {
if (processConditionalPropertyValue(conditionalExpression.consequent, propName, options)) {
hasChanges = true;
}
}
if (conditionalExpression.alternate.type === "NumericLiteral") {
if (isLengthProperty(propName)) {
const newValue = pxtorem(conditionalExpression.alternate.value);
conditionalExpression.alternate = {
type: "StringLiteral",
value: newValue
};
hasChanges = true;
}
} else if (conditionalExpression.alternate.type === "StringLiteral") {
const originalValue = conditionalExpression.alternate.value;
const pxRegex = /(-?\d*\.?\d+)px/g;
if (pxRegex.test(originalValue)) {
const newValue = originalValue.replace(pxRegex, (_, numStr) => pxtorem(numStr));
if (newValue !== originalValue) {
conditionalExpression.alternate.value = newValue;
hasChanges = true;
}
}
} else if (conditionalExpression.alternate.type === "ConditionalExpression") {
if (processConditionalPropertyValue(conditionalExpression.alternate, propName, options)) {
hasChanges = true;
}
}
return hasChanges;
}
// processors/ast-processor.ts
function isTargetTemplateExpression(node, targetFunctions) {
if (node.type !== "TaggedTemplateExpression") return false;
const tag = node.tag;
if (tag.type === "Identifier") {
return targetFunctions.includes(tag.name);
}
if (tag.type === "MemberExpression" && tag.property.type === "Identifier") {
return targetFunctions.includes(tag.property.name);
}
return false;
}
function processTemplateExpressions(template, options) {
let hasChanges = false;
const pxtorem = createPxToRemConverter({
rootValue: options.rootValue,
unitPrecision: options.unitPrecision,
minPixelValue: options.minPixelValue
});
const processExpression = (expr) => {
let changed = false;
if (expr.type === "StringLiteral") {
const originalValue = expr.value;
const pxRegex = /(-?\d*\.?\d+)px/g;
const newValue = originalValue.replace(pxRegex, (match, numStr) => {
const pxValue = parseFloat(numStr);
if (isNaN(pxValue) || Math.abs(pxValue) < options.minPixelValue) {
return match;
}
return pxtorem(pxValue);
});
if (newValue !== originalValue) {
expr.value = newValue;
changed = true;
}
} else if (expr.type === "ConditionalExpression") {
if (processExpression(expr.consequent)) changed = true;
if (processExpression(expr.alternate)) changed = true;
}
return changed;
};
for (const expression of template.expressions) {
if (expression && typeof expression === "object" && expression.type) {
if (processExpression(expression)) {
hasChanges = true;
}
}
}
return hasChanges;
}
function processCompleteTemplate(template, options) {
let hasChanges = false;
if (processTemplateQuasis(template, options)) {
hasChanges = true;
}
if (processTemplateExpressions(template, options)) {
hasChanges = true;
}
return hasChanges;
}
// index.ts
var babelTraverse = (() => {
if (typeof traverse === "function") return traverse;
if (traverse && typeof traverse.default === "function") return traverse.default;
throw new Error("@babel/traverse import failed");
})();
var babelGenerate = (() => {
if (typeof generate === "function") return generate;
if (generate && typeof generate.default === "function") return generate.default;
throw new Error("@babel/generator import failed");
})();
function antdStylePxToRem(options = {}) {
const mergedOptions = {
...defaultOptions,
...options
};
return {
name: "antd-style-px-to-rem",
enforce: "pre",
// Run before other plugins to process JSX before React transforms it
transform(code, id) {
if (!shouldProcess(id, mergedOptions.include, mergedOptions.exclude)) {
return null;
}
const hasTargetFunctions = mergedOptions.cssTemplateFunctions.some((fn) => {
const patterns = [
fn + "`",
// css`...`
"." + fn + "`",
// styled.css`...`
" " + fn + "`",
// { css }`...`
"(" + fn + "`",
// (css)`...`
"{" + fn + "`"
// {css}`...`
];
return patterns.some((pattern) => code.includes(pattern));
});
const hasCreateStyles = code.includes("createStyles");
const hasJSXAttributes = mergedOptions.enableJSXTransform && (code.includes("style=") || Object.values(mergedOptions.jsxAttributeMapping).flat().some(
(attr) => code.includes(`${attr}=`)
));
if (!hasTargetFunctions && !hasCreateStyles && !hasJSXAttributes) {
return null;
}
try {
const ast = parse(code, {
sourceType: "module",
plugins: [
"typescript",
"jsx",
"decorators-legacy",
"classProperties",
"objectRestSpread"
]
});
let hasChanges = false;
const codeLines = code.split("\n");
const processOptions = {
rootValue: mergedOptions.rootValue,
unitPrecision: mergedOptions.unitPrecision,
minPixelValue: mergedOptions.minPixelValue,
propList: mergedOptions.propList,
replace: mergedOptions.replace,
mediaQuery: mergedOptions.mediaQuery
};
const pxToRem = createPxToRemConverter({
rootValue: processOptions.rootValue,
unitPrecision: processOptions.unitPrecision,
minPixelValue: processOptions.minPixelValue
});
babelTraverse(ast, {
TaggedTemplateExpression(path) {
const node = path.node;
if (isTargetTemplateExpression(node, mergedOptions.cssTemplateFunctions)) {
const templateChanged = processCompleteTemplate(
node.quasi,
processOptions
);
if (templateChanged) {
hasChanges = true;
}
}
},
CallExpression(path) {
const node = path.node;
const callee = node.callee;
if (callee.type !== "Identifier" || callee.name !== "createStyles") {
return;
}
path.traverse({
ObjectProperty(propPath) {
const valueNode = propPath.node.value;
const keyNode = propPath.node.key;
let propName = "";
if (keyNode.type === "Identifier") {
propName = keyNode.name;
} else if (keyNode.type === "StringLiteral") {
propName = keyNode.value;
}
if (!propName) return;
const shouldConvert = shouldConvertProperty(
propName,
processOptions.propList
);
if (valueNode.type === "StringLiteral") {
if (!shouldConvert) return;
const originalValue = valueNode.value;
const pxRegex = /(-?\d*\.?\d+)px/g;
if (pxRegex.test(originalValue)) {
const newValue = originalValue.replace(
pxRegex,
(_, numStr) => pxToRem(numStr)
);
if (newValue !== originalValue) {
propPath.get("value").replaceWith({
type: "StringLiteral",
value: newValue
});
hasChanges = true;
}
}
} else if (valueNode.type === "NumericLiteral") {
if (!shouldConvert) return;
if (isLengthProperty(propName)) {
const newValue = pxToRem(valueNode.value);
propPath.get("value").replaceWith({ type: "StringLiteral", value: newValue });
hasChanges = true;
}
} else if (valueNode.type === "TemplateLiteral") {
if (processCompleteTemplate(valueNode, processOptions)) {
hasChanges = true;
}
}
}
});
},
JSXElement(path) {
var _a, _b, _c, _d;
const node = path.node;
const openingElement = node.openingElement;
const nodeStart = (_a = node.loc) == null ? void 0 : _a.start.line;
let hasIgnoreComment = false;
if (nodeStart && nodeStart > 0 && nodeStart <= codeLines.length && codeLines.length > 0) {
const currentLine = codeLines[nodeStart - 1] || "";
const previousLine = nodeStart > 1 ? codeLines[nodeStart - 2] || "" : "";
hasIgnoreComment = currentLine.includes("antd-style-px-to-rem ignore") || currentLine.includes("antd-style-px-to-rem ignore") || previousLine.includes("antd-style-px-to-rem ignore") || previousLine.includes("antd-style-px-to-rem ignore");
}
if (hasIgnoreComment) {
return;
}
if (mergedOptions.enableJSXTransform && openingElement.name.type === "JSXIdentifier") {
const componentName = openingElement.name.name;
for (const attr of openingElement.attributes) {
if (attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier") {
const attrName = attr.name.name;
if (attrName === "style" && ((_b = attr.value) == null ? void 0 : _b.type) === "JSXExpressionContainer") {
if (attr.value.expression.type === "ObjectExpression") {
if (processStyleObjectExpression(
attr.value.expression,
processOptions
)) {
hasChanges = true;
}
} else if (attr.value.expression.type === "ConditionalExpression") {
if (processConditionalExpression(
attr.value.expression,
processOptions
)) {
hasChanges = true;
}
}
} else if ((_c = mergedOptions.jsxAttributeMapping[componentName]) == null ? void 0 : _c.includes(attrName)) {
if (((_d = attr.value) == null ? void 0 : _d.type) === "JSXExpressionContainer" && attr.value.expression.type === "NumericLiteral") {
const numValue = attr.value.expression.value;
if (typeof numValue === "number" && !isNaN(numValue) && Math.abs(numValue) > processOptions.minPixelValue) {
const remValue = pxToRem(numValue);
attr.value.expression = {
type: "StringLiteral",
value: remValue
};
hasChanges = true;
}
}
}
}
}
}
}
});
if (hasChanges) {
const output = babelGenerate(ast, {
retainLines: true,
compact: false
});
return {
code: output.code,
map: output.map
};
}
return null;
} catch (error) {
console.error(`Failed to process file ${id}:`, error);
return null;
}
}
};
}
export {
antdStylePxToRem,
defaultOptions
};
//# sourceMappingURL=index.mjs.map