transform-to-unocss
Version:
🚀 Effortlessly transform CSS, inline styles, and preprocessors (Sass/Less/Stylus) to UnoCSS with smart conflict resolution and debug support
1,036 lines (1,017 loc) • 38.3 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { createGenerator, escapeRegExp } from "@unocss/core";
import presetUno from "@unocss/preset-uno";
import * as Url$2 from "node:url";
import * as Url$1 from "node:url";
import * as Url from "node:url";
import fsp from "node:fs/promises";
import { getLastName, isNot, joinWithUnderLine, toUnocssClass, transformStyleToUnocss, transformStyleToUnocssPre, trim } from "transform-to-unocss-core";
import { parse } from "node-html-parser";
import { parse as parse$1, traverse } from "@babel/core";
import vueJsxPlugin from "@vue/babel-plugin-jsx";
//#region src/utils.ts
const TRANSFER_FLAG = ".__unocss_transfer__";
function transformUnocssBack(code) {
const result = [];
return new Promise((resolve) => {
createGenerator({}, { presets: [presetUno()] }).generate(code || "").then((res) => {
const css = res.getLayers();
code.forEach((item) => {
try {
const reg = new RegExp(`${item.replace(/([!()[\]*])/g, "\\\\$1")}{(.*)}`);
const match = css.match(reg);
if (!match) return;
const matcher = match[1];
matcher.split(";").filter(Boolean).forEach((item$1) => {
const [key, v] = item$1.split(":");
result.push(key.trim());
});
} catch (error) {}
});
resolve(result);
});
});
}
function diffTemplateStyle(before, after) {
const s1 = before.match(/<style scoped>.*<\/style>/s);
const s2 = after.match(/<style scoped>.*<\/style>/s);
if (!s1 || !s2) return false;
return s1[0] === s2[0];
}
function isEmptyStyle(code) {
return /<style scoped>\s*<\/style>/.test(code);
}
function getStyleScoped(code) {
const match = code.match(/<style scoped>(.*)<\/style>/s);
if (!match) return "";
return match[1];
}
function getCssType(filename) {
const ext = filename.split(".").pop();
const result = ext === "styl" ? "stylus" : ext;
return result;
}
/**
* 动态导入 Vue Compiler SFC,避免打包时的问题
* @returns Vue Compiler SFC 中的方法
*/
async function getVueCompilerSfc() {
const { parse: parse$2 } = await import("@vue/compiler-sfc");
return { parse: parse$2 };
}
//#endregion
//#region src/prettierCode.ts
const emptyStyle = /<style[\s\w'=]*>(\s+)/;
async function prettierCode(code) {
const { parse: parse$2 } = await getVueCompilerSfc();
const { descriptor: { styles } } = parse$2(code);
if (!styles.length) return code.replace(emptyStyle, (all, v) => all.replace(v, "")).replace(/\n+/g, "\n");
const { content } = styles[0];
return code.replace(content, removeEmptyStyle(content.replace(/\n+/g, "\n")));
}
function removeEmptyStyle(code) {
return code.replace(/([\w.#:[\]="'\-\s,>~]+)\{\s*\}/g, "");
}
//#endregion
//#region src/lessCompiler.ts
async function lessCompiler(css, filepath, globalCss, alias) {
if (typeof window !== "undefined") throw new Error("lessCompiler is not supported in this browser");
const { LessPluginModuleResolver } = await import("less-plugin-module-resolver");
let result = globalCss ? `${globalCss.replace(/@(?:include|import)\s+["']([^"']*)['"]/g, (_, v) => _.replace(v, Url$2.pathToFileURL(path.resolve(process.cwd(), v))))}${css}` : css;
try {
result = (await (await import("less")).default.render(result, {
filename: filepath,
plugins: [new LessPluginModuleResolver({ alias: alias || {} })]
})).css;
return result;
} catch (error) {
console.error(`Error:\n transform-to-unocss(lessCompiler) ${error.toString()}`);
}
}
//#endregion
//#region src/sassCompiler.ts
async function sassCompiler(css, filepath, globalCss) {
if (typeof window !== "undefined") throw new Error("sassCompiler is not supported in this browser");
const baseDir = filepath ? path.dirname(filepath) : process.cwd();
const processPathInContent = (content) => {
let processed = content;
processed = processed.replace(/@(?:use|forward|include|import)\s+["']([^"']*)['"]/g, (match, filePath) => {
if (filePath.includes(":")) return match;
const resolvedPath = path.resolve(baseDir, filePath);
return match.replace(filePath, Url$1.pathToFileURL(resolvedPath));
});
processed = processed.replace(/@use\s+["']([^"']*)['"]\s+(?:as\s+\w+\*?|with\s*\([^)]*\))/g, (match, filePath) => {
if (filePath.includes(":")) return match;
const resolvedPath = path.resolve(baseDir, filePath);
return match.replace(filePath, Url$1.pathToFileURL(resolvedPath));
});
processed = processed.replace(/@forward\s+["']([^"']*)['"]\s*(?:show\s+[^;]+|hide\s+[^;]+|as\s+[^;]+)/g, (match, filePath) => {
if (filePath.includes(":")) return match;
const resolvedPath = path.resolve(baseDir, filePath);
return match.replace(filePath, Url$1.pathToFileURL(resolvedPath));
});
return processed;
};
let result = "";
if (globalCss) result += processPathInContent(globalCss);
result += processPathInContent(css);
try {
const sass = await import("sass");
result = sass.compileString(result, filepath ? {
importers: [{ findFileUrl(url) {
if (url.startsWith("~")) return new URL(url.slice(1), Url$1.pathToFileURL(baseDir));
if (!url.startsWith("/") && !url.includes("://")) return new URL(url, Url$1.pathToFileURL(baseDir));
return new URL(url, Url$1.pathToFileURL(baseDir));
} }],
syntax: "scss",
loadPaths: [baseDir, process.cwd()]
} : {
syntax: "scss",
loadPaths: [process.cwd()]
}).css;
return result;
} catch (error) {
console.error(`Error:\n transform-to-unocss(sassCompiler) ${error.toString()}`);
}
}
//#endregion
//#region src/stylusCompiler.ts
async function stylusCompiler(css, filepath, globalCss) {
if (typeof window !== "undefined") throw new Error("Stylus is not supported in this browser");
let result = globalCss ? `${globalCss.replace(/@(?:include|import)\s+["']([^"']*)['"]/g, (_, v) => _.replace(v, Url.pathToFileURL(path.resolve(process.cwd(), v))))}${css}` : css;
try {
result = (await import("stylus")).default.render(result, { filename: filepath });
return result;
} catch (error) {
console.error(`Error:\n transform-to-unocss(stylusCompiler) ${error.toString()}`);
}
}
//#endregion
//#region src/compilerCss.ts
/**
* Compiles CSS from different preprocessors (Sass, Less, Stylus) to standard CSS
* @param css - The CSS/preprocessor code to compile
* @param lang - The language type ('css' | 'scss' | 'less' | 'styl')
* @param filepath - The file path for resolving imports and relative paths.
* In plugin mode, this should always be provided by the build tool.
* @param globalCss - Global CSS configuration for preprocessors
* @returns Compiled CSS string or original CSS if no compilation needed
*/
function compilerCss(css, lang, filepath, globalCss) {
switch (lang) {
case "styl":
var _globalCss$styl;
return stylusCompiler(css, filepath, globalCss === null || globalCss === void 0 || (_globalCss$styl = globalCss.styl) === null || _globalCss$styl === void 0 ? void 0 : _globalCss$styl.additionalData);
case "less":
var _globalCss$less;
return lessCompiler(css, filepath, globalCss === null || globalCss === void 0 || (_globalCss$less = globalCss.less) === null || _globalCss$less === void 0 ? void 0 : _globalCss$less.additionalData);
case "scss":
var _globalCss$scss;
return sassCompiler(css, filepath, globalCss === null || globalCss === void 0 || (_globalCss$scss = globalCss.scss) === null || _globalCss$scss === void 0 ? void 0 : _globalCss$scss.additionalData);
default: return css;
}
}
//#endregion
//#region src/node-html-parser.ts
function nodeHtmlParser(code, selector, stack) {
const results = [];
if (!stack || !stack.length) return results;
const root = parse(code);
let elements = [];
try {
elements = root.querySelectorAll(selector);
} catch (error) {}
while (elements.length === 0 && selector.includes(":")) {
const index = selector.lastIndexOf(":");
selector = selector.slice(0, index);
try {
elements = root.querySelectorAll(selector);
} catch (error) {}
}
if (elements.length) elements.forEach((element) => {
const targetNode = getMatchNode(element.range, stack);
if (targetNode) results.push(targetNode);
});
return results;
}
function getMatchNode(range, elements) {
for (const element of elements) {
const { start, end } = element.loc;
if (range[0] === start.offset && range[1] === end.offset) return element;
else if (element.children && element.children.length) {
const match = getMatchNode(range, element.children);
if (match) return match;
}
}
}
//#endregion
//#region src/tail.ts
function tail(css) {
if (/not\(/.test(css)) return `[&:${css}]:`;
if (css.startsWith("nth")) {
if (css === "nth-child(odd)") return "odd";
if (css === "nth-child(even)") return "even";
if (css.startsWith("nth-child(")) {
const match = css.match(/nth-child\((.*)\)/);
if (match && match[1]) return `nth-[${match[1]}]`;
return "";
}
if (css.startsWith("nth-last-child(")) {
const match = css.match(/nth-last-child\((.*)\)/);
if (match && match[1]) return `nth-last-[${match[1]}]`;
return "";
}
if (!css.includes("(")) return css;
return css.split("(")[0];
}
if (css.startsWith("aria-") || css.startsWith("data-")) return css.split("=")[0];
if (css.startsWith("dir=")) return css.split("=")[1];
if (css === "file-selector-button") return "file";
if (css.endsWith("-child")) return css.split("-")[0];
if (css.startsWith("has(")) {
const match = css.match(/has\((.*)\)/);
if (match && match[1]) return `has-[${match[1]}]`;
return "";
}
if (css.startsWith("where(")) {
const match = css.match(/where\((.*)\)/);
if (match && match[1]) return `in-[${match[1]}]`;
return "";
}
if ([
"first-child",
"last-child",
"only-child"
].includes(css)) return css.split("-")[0];
const [first, ...rest] = css.split(":");
if (rest.length) return `[&:${joinWithUnderLine(first)}]:${rest.join(":")}`;
return `[&:${joinWithUnderLine(first)}]`;
}
//#endregion
//#region src/wrapperVueTemplate.ts
function wrapperVueTemplate(template, css) {
let _template = /<template>/.test(template) ? template.replace(/\n+/g, "\n") : `<template>
${template}
</template>`;
_template = /<style scoped>/.test(_template) ? _template.replace("</style>", `\n${css}<\/style>`) : `${_template}\n<style scoped>
${css}
</style>`;
return `${_template}`;
}
//#endregion
//#region src/transformCss.ts
const tailReg = /:?:(.+)/;
const emptyClass = /[,\w>.#\-+:[\]="'\s()]+\{\}\n/g;
let isRem;
async function transformCss(style, code, media = "", isJsx = true, filepath, _isRem, debug = false, globalCss) {
var _parse$descriptor$tem;
isRem = _isRem;
const allChanges = [];
const { parse: parse$2 } = await getVueCompilerSfc();
let newCode = await importCss(code, style, filepath, isJsx, debug, globalCss);
if (debug) console.log("[DEBUG] transformCss started:", JSON.stringify({
filepath,
media,
isJsx,
styleLength: style.length,
codeLength: code.length
}, null, 2));
const stack = (_parse$descriptor$tem = parse$2(newCode).descriptor.template) === null || _parse$descriptor$tem === void 0 ? void 0 : _parse$descriptor$tem.ast;
const updateOffsetMap = {};
const deferRun = [];
style.replace(/(.*)\{([#\\\s\w\-.:;,%()+'"!]*)\}/g, (all, name, value = "") => {
name = trim(name.replace(/\s+/g, " "));
const originClassName = name;
const before = trim(value.replace(/\n\s*/g, ""));
if (debug) console.log("[DEBUG] Processing CSS rule:", JSON.stringify({
originClassName,
before,
all
}, null, 2));
const [transfer, noTransfer] = transformStyleToUnocss(before, isRem);
if (debug) console.log("[DEBUG] CSS transform result:", JSON.stringify({
originClassName,
before,
transfer,
noTransfer: (noTransfer === null || noTransfer === void 0 ? void 0 : noTransfer.length) || 0
}, null, 2));
const tailMatcher = name.match(tailReg);
const prefix = tailMatcher ? (name.endsWith(tailMatcher[0]) ? "" : "group-") + tail(tailMatcher[1]) : "";
if (prefix === "group-deep") return;
if (prefix.includes(" ")) return;
const after = prefix && transfer ? `${prefix}="${transfer.replace(/="\[([^\]]*)\]"/g, (_, v) => `-[${v}]`)}"` : transfer ?? before;
if (before === after) {
if (debug) console.log("[DEBUG] CSS rule skipped - no transformation:", JSON.stringify({
originClassName,
before
}, null, 2));
return;
}
if (prefix) name = name.replace(tailMatcher[0], "");
const result = nodeHtmlParser(newCode, originClassName, stack === null || stack === void 0 ? void 0 : stack.children);
if (!result.length) {
if (debug) console.log("[DEBUG] No HTML elements found for CSS rule:", JSON.stringify({
originClassName,
name
}, null, 2));
return;
}
if (debug) console.log("[DEBUG] Found HTML elements for CSS rule:", JSON.stringify({
originClassName,
elementsCount: result.length,
elements: result.map((r) => ({
tag: r.tag,
start: r.loc.start.offset
}))
}, null, 2));
const _class = newCode.match(/<style[^>]+>(.*)<\/style>/s)[1];
let newClass = _class.replace(all, (_) => _.replace(value, noTransfer.join(";")));
newClass = newClass.replace(emptyClass, "");
newCode = newCode.replace(_class, newClass);
for (const r of result) {
const parent = r.parent;
if (prefix.startsWith("group-") && parent) {
const hasClass = parent.props.find((i) => i.name === "class");
if (hasClass) {
if (hasClass.value.content.includes("group")) return;
const index = hasClass.value.loc.start.offset;
const newIndex = hasClass.value.loc.start.offset + getCalculateOffset(updateOffsetMap, index);
const updateText = "group ";
updateOffsetMap[index] = updateText.length;
hasClass.value.content = `${hasClass.value.content} ${updateText}`;
newCode = `${newCode.slice(0, newIndex + 1)}${updateText}${newCode.slice(newIndex + 1)}`;
} else {
const index = parent.loc.start.offset + parent.tag.length + 1;
const newIndex = hasClass.value.loc.start.offset + getCalculateOffset(updateOffsetMap, index);
const updateText = "class=\"group\" ";
parent.props.push({
type: 6,
name: "class",
value: {
type: 2,
content: "group",
loc: {
start: {
column: 0,
line: 0,
offset: newIndex
},
end: {
column: 0,
line: 0,
offset: newIndex + updateText.length
}
}
},
loc: {
start: {
column: 0,
line: 0,
offset: newIndex
},
end: {
column: 0,
line: 0,
offset: newIndex + updateText.length
}
}
});
updateOffsetMap[index] = updateText.length;
newCode = `${newCode.slice(0, newIndex)}${updateText}${newCode.slice(newIndex)}`;
}
}
const { loc: { start, end }, tag, props } = r;
const attr = props.reduce((result$1, cur) => {
var _cur$value;
let item;
if (cur.name === "class" && (item = (_cur$value = cur.value) === null || _cur$value === void 0 ? void 0 : _cur$value.content)) result$1.push(item);
else if (!cur.value) result$1.push(cur.name);
return result$1;
}, []);
deferRun.push(() => {
const newIndex = getCalculateOffset(updateOffsetMap, start.offset);
const newSource = newCode.slice(start.offset + newIndex, end.offset + newIndex);
allChanges.push({
before,
after,
name: originClassName,
source: newSource,
tag,
attr,
prefix,
media,
start,
end
});
});
}
return all;
});
deferRun.forEach((run) => run());
if (debug) console.log("[DEBUG] transformCss finished, resolving conflicts:", JSON.stringify({ allChangesCount: allChanges.length }, null, 2));
return await resolveConflictClass(allChanges, newCode, isJsx, updateOffsetMap, debug);
}
async function importCss(code, style, filepath, isJsx, debug = false, globalCss) {
if (debug) console.log("[DEBUG] importCss started:", JSON.stringify({
filepath,
styleLength: style.length,
hasImports: /@import/.test(style)
}, null, 2));
const originCode = code;
for await (const match of style.matchAll(/@import (url\()?["']*([\w./\-]*)["']*\)?;/g)) {
if (!match) continue;
if (debug) console.log("[DEBUG] Processing CSS import:", JSON.stringify({ importUrl: match[2] }, null, 2));
const url = path.resolve(filepath, "..", match[2]);
const content = await fsp.readFile(path.resolve(filepath, "..", url), "utf-8");
const type = getCssType(url);
const css = await compilerCss(content, type, url, globalCss);
const [_, beforeStyle] = code.match(/<style.*>(.*)<\/style>/s);
code = code.replace(beforeStyle, "");
const vue = wrapperVueTemplate(code, css);
const transfer = await transformVue(vue, {
isJsx,
isRem,
filepath,
globalCss,
debug
});
if (diffTemplateStyle(transfer, vue)) {
code = originCode;
continue;
}
if (isEmptyStyle(transfer)) {
code = wrapperVueTemplate(transfer, beforeStyle.replace(match[0], ""));
continue;
}
const restStyle = getStyleScoped(transfer);
fsp.writeFile(url.replace(`.${type}`, `${TRANSFER_FLAG}.${type}`), restStyle, "utf-8");
code = wrapperVueTemplate(transfer.replace(/<style scoped>.*<\/style>/s, ""), beforeStyle);
continue;
}
return code;
}
async function resolveConflictClass(allChange, code, isJsx = true, updateOffset, debug = false) {
if (debug) console.log("[DEBUG] resolveConflictClass started:", JSON.stringify({
allChangesCount: allChange.length,
isJsx
}, null, 2));
const changes = findSameSource(allChange);
let result = code;
if (debug) console.log("[DEBUG] Found conflict groups:", JSON.stringify({
groupsCount: Object.keys(changes).length,
groups: Object.keys(changes).map((key) => ({
key,
changesCount: changes[key].length
}))
}, null, 2));
for await (const key of Object.keys(changes)) {
const value = changes[key];
const { tag, prefix, media, source, start: { offset }, end: { offset: offsetEnd } } = value[0];
let [after, transform] = await getConflictClass(value, debug);
if (!after) {
if (debug) console.log("[DEBUG] No conflict resolution needed for group:", key);
continue;
}
if (debug) console.log("[DEBUG] Conflict resolved for group:", JSON.stringify({
key,
after,
originalSource: source
}, null, 2));
const newResult = transform(result);
result = newResult;
const target = transform(source);
if (media) after = `${media}:${after}`;
if (prefix) if (isNot(prefix)) {
const match = target.match(/<[^>]*(class="[^"]+)[^>]*/);
if (match) after = after.replace(/class="(\[&:not\([\w\s\-.#]+\)\]:[\w\-.]+)"\s*/, (_, v) => {
const updateText = ` ${v}`;
result = result.replace(match[1], `${match[1]}${updateText}`);
return "";
});
} else after = after.replace(/="\[/g, "-\"[");
const returnValue = isJsx || after.replace(/(?:[\w\-]+|\[[^\]]+\])=("{1})(.*?)\1/g, "").includes("[") ? after.replace(/\[([^\]]+)\]/g, (all, v) => all.replace(v, joinWithUnderLine(v))).replace(/-(rgba?([^)]+))/g, "-[$1]").replace(/((?:[\w\-]+|\[[^\]]+\])(?::[\w\-]+|-\[[^\]]*\])*)=(['"]{1})(.*?)\2/g, (_all, prefix$1, _, content) => {
const splitContent = content.split(/(?<!\[[^\]]*)\s+/).filter(Boolean);
return splitContent.map((item) => `${prefix$1}-${item}`).join(` `);
}) : after;
const getUpdateOffset = getCalculateOffset(updateOffset, offset);
const startOffset = offset + getUpdateOffset;
const endOffset = offsetEnd + getUpdateOffset;
const start = result.slice(startOffset, endOffset);
if (isJsx || after.replace(/[\w\-]+=("{1})(.*?)\1/g, "").includes("[")) {
const newReg = new RegExp(`^<${tag}(?:[^\/'">]|"[^"]*"|'[^']*')*[^:]class=["']([^"']+)["']([^\/'">]|"[^"]*"|'[^']*')*\/?>`, "s");
const matcher = target.match(newReg);
if (matcher) {
const insertText$1 = ` ${returnValue}`;
result = result.slice(0, startOffset) + start.replace(`class="${matcher[1]}"`, `class="${matcher[1]}${insertText$1}"`) + result.slice(endOffset);
updateOffset[offset] = insertText$1.length;
continue;
}
const insertText = ` class="${returnValue}"`;
result = result.slice(0, startOffset) + start.replace(`<${tag}`, `<${tag}${insertText}`) + result.slice(endOffset);
updateOffset[offset] = insertText.length;
continue;
}
result = result.slice(0, startOffset) + start.replace(`<${tag}`, `<${tag} ${returnValue}`) + result.slice(endOffset);
}
return result;
}
function calculateWeight(c) {
const data = c.split(" ").filter((i) => i !== "+" && i !== ">");
let num = 0;
data.forEach((item) => {
item.replace(/#\w+/g, () => {
num += 100;
return "";
});
item.replace(/.\w+/, () => {
num += 10;
return "";
});
item.replace(/^\w+/, () => {
num += 10;
return "";
});
item.replace(/\[[\w\s='"-]+\]/g, () => {
num += 10;
return "";
});
item.replace(/:\w+/g, () => {
num += 1;
return "";
});
});
return num;
}
function findSameSource(allChange) {
const result = {};
allChange.forEach((item) => {
const { source, start, end } = item;
const key = `${source}:${start.offset}:${end.offset}`;
if (!result[key]) result[key] = [];
result[key].push(item);
});
return result;
}
const skipTransformFlag = Symbol("skipTransformFlag");
async function getConflictClass(allChange, debug = false) {
if (debug) console.log("[DEBUG] getConflictClass started:", JSON.stringify({
changesCount: allChange.length,
changes: allChange.map((c) => ({
name: c.name,
before: c.before,
after: c.after
}))
}, null, 2));
let map = {};
let transform = (code) => code;
for await (const item of allChange) {
const { before, name, source, attr, after, prefix, media } = item;
const pre = prefix ? `${prefix}|` : "";
const beforeArr = before.split(";").filter(Boolean);
const data = beforeArr.map((item$1) => {
const [key, value] = item$1.trim().split(":");
return [`${pre}${key}`, value];
});
data.forEach((item$1) => {
const [key, value] = item$1;
if (value === void 0) return;
if (!map[key]) map[key] = [calculateWeight(name), value];
else {
const [preWeight] = map[key];
if (preWeight === skipTransformFlag) return;
const curWeight = calculateWeight(name);
if (+curWeight >= +preWeight) map[key] = [+curWeight, value];
}
});
if (attr) {
const res = await transformUnocssBack(attr.map((i) => {
if (prefix) return `${prefix}="${i}"`;
if (media) return `${media}:${i}`;
return i;
}));
Object.keys(map).forEach((i) => {
const index = res.findIndex((r) => r === i);
if (index !== -1) {
const inline = item.attr[index];
if ((inline === null || inline === void 0 ? void 0 : inline.endsWith("!")) || !(after === null || after === void 0 ? void 0 : after.endsWith("!"))) return delete map[i];
else transform = (code) => code.replace(source, source.replace(` ${inline}`, ""));
}
});
}
}
const joinMap = Object.keys(map).map((key) => {
const value = map[key][1];
return `${key}:${value}`;
}).join(";");
const { transformedResult, newStyle } = transformStyleToUnocssPre(joinMap);
if (transformedResult) {
map = newStyle.split(";").reduce(
(acc, item) => {
const [key, value] = item.trim().split(":");
if (value !== void 0) acc[key] = [map[key][0], value];
return acc;
},
// 将 transformedResult 赋值给 map
// map[]
{}
);
map[transformedResult] = [1, skipTransformFlag];
}
return [Object.keys(map).reduce((result, key) => {
const keys = key.split("|");
const styleKey = keys[keys.length - 1];
let prefix = keys.length > 1 ? keys[0] : "";
let transferCss = map[key][1] === skipTransformFlag ? key : transformStyleToUnocss(`${styleKey}:${map[key][1]}`, isRem)[0];
if (debug) console.log("[DEBUG] Processing map key:", JSON.stringify({
key,
styleKey,
prefix,
transferCss,
mapValue: map[key]
}, null, 2));
const match = transferCss.match(/(\S*)="\[([^\]]*)\]"/);
if (match) {
var _match$input;
transferCss = `${(_match$input = match.input) === null || _match$input === void 0 ? void 0 : _match$input.replace(match[0], match[0].replace(/="\[([^\]]*)\]"/, (_, v) => `-[${v}]`).replace(/="([^"]*)"/, "-$1"))}`;
}
const _transferCss = prefix ? isNot(prefix) ? `class="${prefix}${transferCss.replace(/="\[([^\]]*)\]"/g, (_, v) => `-[${v}]`).replace(/="([^"]*)"/, "-$1")}"` : `${prefix}="${transferCss.replace(/="\[([^\]]*)\]"/g, (_, v) => `-[${v}]`).replace(/="([^"]*)"/, "-$1")}"` : transferCss;
if (!prefix) {
const reg = /^(\S*)="[^"]*"$/;
if (reg.test(transferCss)) prefix = transferCss.match(reg)[1];
}
if (prefix) {
const prefixReg1 = new RegExp(`(?<!\\S)${escapeRegExp(prefix)}(?!\\S)`);
if (prefixReg1.test(result)) return result.replace(prefixReg1, (all) => all.replace(prefix, _transferCss));
const prefixReg2 = new RegExp(`(?<!\\S)${escapeRegExp(prefix)}=`);
if (prefixReg2.test(result)) {
if (isNot(prefix)) {
const newPrefix = prefix.replace(/[[\]()]/g, (all) => `\\${all}`);
const reg$1 = new RegExp(`${escapeRegExp(newPrefix)}([\\w\\:\\-;\\[\\]\\/\\+%]+)`);
return result.replace(reg$1, (all) => `${all}:${transferCss}`);
}
const reg = new RegExp(`${escapeRegExp(prefix)}=(["]{1})(.*?)\\1`);
return result.replace(reg, (all, _, v) => {
const unique = [...new Set(v.split(" ").concat(_transferCss.slice(prefix.length + 2, -1).split(" ")))].join(" ");
if (v) return all.replace(v, unique);
return `${prefix}="${unique.trim()}"`;
});
}
}
return `${result}${_transferCss} `;
}, "").trim(), transform];
}
function getCalculateOffset(offsetMap, offset) {
return Object.keys(offsetMap).reduce((result, key) => {
if (+key <= offset) result += offsetMap[key];
return result;
}, 0);
}
//#endregion
//#region src/transformInlineStyle.ts
const styleReg$1 = /<([\w\-]+)[^/>]*[^:]style="([^"]+)"[^>]*>/g;
const removeStyleReg = / style=("{1})(.*?)\1/;
const templateReg = /^<template>(.*)<\/template>$/ms;
const commentReg = /<!--.*-->/gs;
function transformInlineStyle(code, isJsx, isRem$1, debug = false) {
const match = code.match(templateReg);
if (!match) return code;
let templateMatch = match[1];
const commentMap = {};
let count = 0;
const commentPrefix = "__commentMap__";
templateMatch = templateMatch.replace(commentReg, (comment) => {
count++;
commentMap[count] = comment;
return `${commentPrefix}${count}`;
});
templateMatch.replace(styleReg$1, (target, tag, inlineStyle) => {
const [after, noMap] = isJsx ? toUnocssClass(inlineStyle, isRem$1) : transformStyleToUnocss(inlineStyle, isRem$1, debug);
if (debug) console.log("[DEBUG] transformInlineStyle processing:", JSON.stringify({
tag,
inlineStyle,
after,
noMapLength: (noMap === null || noMap === void 0 ? void 0 : noMap.length) || 0
}, null, 2));
if (isJsx) {
const newReg = new RegExp(`<${tag}.*\\sclass=(["']{1})(.*?)\\1`, "s");
const matcher = target.match(newReg);
if (matcher) return templateMatch = templateMatch.replace(target, target.replace(removeStyleReg, "").replace(`class="${matcher[2]}"`, noMap.length ? `class="${matcher[2]} ${after}" style="${noMap.map((item) => item && item.trim()).join(";")}"` : `class="${matcher[2]} ${after}"`));
return templateMatch = templateMatch.replace(target, target.replace(removeStyleReg, "").replace(`<${tag}`, noMap.length ? `<${tag} class="${after}" style="${noMap.map((item) => item && item.trim()).join(";")}` : `<${tag} class="${after}"`));
}
return templateMatch = templateMatch.replace(target, target.replace(removeStyleReg, "").replace(`<${tag}`, noMap.length ? `<${tag} ${after} style="${noMap.map((item) => item && item.trim()).join(";")}"` : `<${tag} ${after}`));
});
Object.keys(commentMap).forEach((key) => {
const commentKey = `${commentPrefix}${key}`;
const value = commentMap[key];
templateMatch = templateMatch.replace(commentKey, value);
});
return code.replace(templateReg, `<template>${templateMatch}</template>`);
}
//#endregion
//#region src/transformMedia.ts
const mediaReg = /@media([\s\w]*)\(([\w-]+):\s*(\w+)\)\s*\{([\s\w.{}\-:;]*)\}/g;
const mediaSingleReg = /@media([\s\w]*)\(([\w-]+):\s*(\w+)\)\s*\{([\s\w.{}\-:;]*)\}/;
const emptyMediaReg = /@media([\s\w]*)\(([\w-]+):\s*(\w+)\)\s*\{\s*\}/g;
const valMap = {
"640px": "sm",
"768px": "md",
"1024px": "lg",
"1280px": "xl",
"1536px": "2xl"
};
/**
* Transforms CSS @media queries to UnoCSS responsive utilities
* @param code - The code containing @media queries
* @param isJsx - Whether the code is JSX/TSX format
* @param isRem - Whether to convert px values to rem
* @param filepath - The file path for resolving CSS imports within media queries
* @param debug - Whether to enable debug logging
* @param globalCss - Global CSS configuration for preprocessors
* @returns A tuple of [transformed code, restore function]
*/
async function transformMedia(code, isJsx, isRem$1, filepath, debug = false, globalCss) {
const transferBackMap = [];
let result = code;
const matcher = code.match(mediaReg);
if (!matcher) {
if (debug) console.log("[DEBUG] transformMedia: No @media queries found");
return returnValue(result);
}
if (debug) console.log("[DEBUG] transformMedia started:", JSON.stringify({
filepath,
isJsx,
isRem: isRem$1,
mediaQueriesCount: matcher.length
}, null, 2));
for await (const item of matcher) {
const [all, pre, key, val, inner] = item.match(mediaSingleReg);
const tempFlag = `/* __transformMedia${Math.random()}__ */`;
const value = valMap[val];
if (debug) console.log("[DEBUG] transformMedia processing query:", JSON.stringify({
all: `${all.substring(0, 100)}...`,
key,
val,
mappedValue: value,
hasPrefix: !!pre.trim()
}, null, 2));
if (!value) {
result = result.replace(all, tempFlag);
transferBackMap.push((r) => r.replace(tempFlag, all));
continue;
}
if (pre.trim()) {
const transfer$1 = await transformCss(inner, result, `max-${value}`, isJsx, filepath, isRem$1, debug, globalCss);
if (transfer$1 !== result) {
result = transfer$1.replace(emptyMediaReg, "");
transferBackMap.push((r) => r.replace(tempFlag, transfer$1));
continue;
}
result = result.replace(all, tempFlag);
transferBackMap.push((r) => r.replace(tempFlag, all));
continue;
}
let mapValue = value;
if (key === "prefers-reduced-motion") mapValue = `${getLastName(key)}-${val === "no-preference" ? "safe" : val}`;
const transfer = (await transformCss(inner, result, mapValue, isJsx, filepath, isRem$1, debug, globalCss)).replace(emptyMediaReg, "");
result = transfer.replace(all, tempFlag);
transferBackMap.push((r) => r.replace(tempFlag, all));
}
return returnValue(result);
function returnValue(result$1) {
return [result$1, (r) => transferBackMap.reduce((result$2, fn) => fn(result$2), r)];
}
}
//#endregion
//#region src/transformVue.ts
async function transformVue(code, options) {
const { isJsx, filepath, isRem: isRem$1, globalCss, debug } = options || {};
const { parse: parse$2 } = await getVueCompilerSfc();
if (debug) console.log("[DEBUG] transformVue started:", JSON.stringify({
filepath,
isJsx,
isRem: isRem$1,
codeLength: code.length
}, null, 2));
const { descriptor: { template, styles }, errors } = parse$2(code);
if (errors.length || !template) {
if (debug && errors.length) console.log("[DEBUG] transformVue parse errors:", errors);
return code;
}
code = transformInlineStyle(code, isJsx, isRem$1, debug);
if (debug) console.log("[DEBUG] After inline style transformation");
const [transferMediaCode, transformBack] = await transformMedia(code, isJsx, isRem$1, filepath, debug, globalCss);
code = transferMediaCode;
if (styles.length) {
if (debug) console.log("[DEBUG] Processing styles:", JSON.stringify({
stylesCount: styles.length,
firstStyle: styles[0]
}, null, 2));
const { attrs: { scoped }, content: style, lang = "css" } = styles[0];
const css = await compilerCss(style, lang, filepath, globalCss);
if (css) {
if (debug) console.log("[DEBUG] CSS compiled successfully:", JSON.stringify({
originalStyleLength: style.length,
compiledCssLength: css.length,
scoped: !!scoped
}, null, 2));
code = code.replace(style, `\n${css}\n`).replace(` lang="${lang}"`, "");
if (scoped) code = await transformCss(css, code, "", isJsx, filepath, isRem$1, debug, globalCss);
}
}
code = transformBack(code);
if (debug) console.log("[DEBUG] transformVue completed:", JSON.stringify({ finalCodeLength: code.length }, null, 2));
return prettierCode(code);
}
//#endregion
//#region src/transformAstro.ts
async function transformAstro(code, options) {
const { filepath, isRem: isRem$1, globalCss, debug = false } = options || {};
const match = code.match(/(---.*---)?(.*(?=<style>))(<style>.*<\/style>)?/s);
if (!match) return code;
const [_all, _js, template, css] = match;
const _css = css ? css.replace(/<style>(.*)<\/style>/s, "$1") : "";
const _template = wrapperVueTemplate(template, _css);
const vue = await transformVue(_template, {
isJsx: true,
isRem: isRem$1,
globalCss,
filepath,
debug
});
vue.replace(/<template>(.*)<\/template>\s*<style scoped>(.*)<\/style>/s, (_, newTemplate, newCss) => code = code.replace(template, newTemplate).replace(css, newCss));
return prettierCode(code);
}
//#endregion
//#region src/transformHtml.ts
const linkCssReg = /<link.*href="(.+css)".*>/g;
const styleReg = /\s*<style[^>]*>(.*)<\/style>\s*/s;
async function transformHtml(code, options) {
const { filepath, isRem: isRem$1, globalCss, debug = false } = options || {};
const css = await getLinkCss(code, filepath);
const style = getStyleCss(code);
const newCode = await generateNewCode(css, style, code, isRem$1, globalCss, debug);
return prettierCode(newCode);
}
async function getLinkCss(code, filepath) {
const css = [];
for (const match of code.matchAll(linkCssReg)) try {
const url = match[0];
const cssUrl = path.resolve(filepath, "../", match[1]);
css.push({
url,
content: await fsp.readFile(cssUrl, "utf-8")
});
} catch (error) {
throw new Error(error.toString());
}
return css;
}
function getStyleCss(code) {
const match = code.match(styleReg);
if (!match) return "";
return match[1];
}
function getBody(code) {
const match = code.match(/<body[^>]*>.*<\/body>/s);
if (!match) return "";
return match[0];
}
async function generateNewCode(css, style, code, isRem$1, globalCss, debug = false) {
let template = getBody(code);
const originBody = template;
if (style) {
const vue = wrapperVueTemplate(template, style);
const transferCode = await transformVue(vue, {
isJsx: true,
isRem: isRem$1,
globalCss,
debug
});
template = transferCode;
if (transferCode.includes("<style scoped></style>")) code = code.replace(styleReg, "");
}
if (css.length) for (const c of css) {
const { url, content } = c;
const vue = wrapperVueTemplate(template, content);
const transferCode = await transformVue(vue, {
isJsx: true,
isRem: isRem$1,
globalCss,
debug
});
if (diffTemplateStyle(template, transferCode)) code = code.replace(url, "");
template = transferCode;
}
return code.replace(originBody, getBody(template));
}
//#endregion
//#region src/transformJsx.ts
async function transformJsx(code, options) {
const { filepath, isRem: isRem$1, globalCss, debug = false } = options || {};
try {
const ast = parse$1(code, {
babelrc: false,
comments: true,
plugins: [[vueJsxPlugin]]
});
let container = null;
let css = "";
let cssPath = "";
traverse(ast, { enter({ node }) {
if (node.type === "JSXElement") {
if (container) return;
container = node;
}
if (node.type === "ImportDeclaration") {
const value = node.source.value;
if (value.endsWith(".css")) css += fs.readFileSync(cssPath = path.resolve(filepath, "../", value), "utf-8");
}
} });
const jsxCode = code.slice(container.start, container.end);
const isJsx = jsxCode.includes("className");
const wrapperVue = `<template>${jsxCode}</template>
<style scoped>
${css}
</style>`;
const vueTransfer = await transformVue(wrapperVue, {
isJsx,
isRem: isRem$1,
globalCss,
filepath,
debug
});
if (cssPath) {
const cssTransfer = vueTransfer.match(/<style scoped>(.*)<\/style>/s)[1];
fs.promises.writeFile(cssPath.replace(".css", ".__unocss_transfer__.css"), cssTransfer, "utf-8");
}
const jsxTransfer = vueTransfer.match(/<template>(.*)<\/template>/s)[1];
return code.replace(jsxCode, jsxTransfer);
} catch (error) {
return code;
}
}
//#endregion
//#region src/transformSvelte.ts
async function transformSvelte(code, options) {
const { filepath, isRem: isRem$1, globalCss, debug = false } = options || {};
const match = code.match(/(<script.*<\/script>)?(.*(?=<style>))(<style>.*<\/style>)?/s);
if (!match) return code;
const [_all, _js, template, css] = match;
const _css = css ? css.replace(/<style>(.*)<\/style>/s, "$1") : "";
const _template = wrapperVueTemplate(template, _css);
const vue = await transformVue(_template, {
isJsx: true,
isRem: isRem$1,
globalCss,
filepath,
debug
});
vue.replace(/<template>(.*)<\/template>\s*<style scoped>(.*)<\/style>/s, (_, newTemplate, newCss) => code = code.replace(template, newTemplate).replace(css, newCss));
return prettierCode(code);
}
//#endregion
//#region src/transformCode.ts
async function transformCode(code, options) {
const { filepath, isRem: isRem$1, type, isJsx = true, globalCss, debug } = options || {};
if (debug) console.log("[DEBUG] transformCode started:", JSON.stringify({
filepath,
type,
isJsx,
isRem: isRem$1,
codeLength: code.length
}));
if (type === "tsx") return transformJsx(code, {
filepath,
isRem: isRem$1,
globalCss,
debug
});
if (type === "html") return transformHtml(code, {
filepath,
globalCss,
debug
});
if (type === "svelte") return transformSvelte(code, {
filepath,
isRem: isRem$1,
globalCss,
debug
});
if (type === "astro") return transformAstro(code, {
filepath,
isRem: isRem$1,
globalCss,
debug
});
return transformVue(code, {
isJsx,
filepath,
isRem: isRem$1,
globalCss,
debug
});
}
//#endregion
export { TRANSFER_FLAG, transformAstro, transformCode, transformHtml, transformJsx, transformSvelte, transformVue };