UNPKG

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
// 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