transform-to-unocss
Version:
🚀 Effortlessly transform CSS, inline styles, and preprocessors (Sass/Less/Stylus) to UnoCSS with smart conflict resolution and debug support
1,216 lines (1,196 loc) • 46 kB
JavaScript
import { createRequire } from "module";
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 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 rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
var __require = /* @__PURE__ */ createRequire(import.meta.url);
//#endregion
//#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 };
}
/**
* 检查是否在 Node.js 环境中运行
* @returns {boolean} 如果在 Node.js 环境中返回 true,在浏览器环境中返回 false
*/
function isNodeEnvironment() {
try {
var _process$versions;
const process$1 = __require("node:process");
return typeof window === "undefined" && typeof process$1 !== "undefined" && Boolean((_process$versions = process$1.versions) === null || _process$versions === void 0 ? void 0 : _process$versions.node);
} catch {
return false;
}
}
//#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, debug) {
if (typeof window !== "undefined") throw new Error("lessCompiler is not supported in this browser");
if (debug) console.log(`[transform-to-tailwindcss] Compiling LESS file: ${filepath || "unknown file"}`);
let result = globalCss ? `${globalCss}${css}` : css;
try {
const less = await import("less");
const { LessPluginModuleResolver } = await import("less-plugin-module-resolver");
result = (await less.default.render(result, {
filename: filepath,
plugins: [new LessPluginModuleResolver({ alias: {} })]
})).css;
return result;
} catch (error) {
if (error.code === "MODULE_NOT_FOUND" || error.message.includes("Cannot resolve module")) {
const missingModule = error.message.includes("less-plugin-module-resolver") ? "less-plugin-module-resolver" : "less";
throw new Error(`${missingModule} not found. Please install it in your project:\nnpm install ${missingModule}\nor\nyarn add ${missingModule}\nor\npnpm add ${missingModule}`);
}
console.error(`Error:\n transform-to-unocss(lessCompiler) ${error.toString()}`);
}
}
//#endregion
//#region src/sassCompiler.ts
/**
* Sass 编译器 - 处理 SCSS/Sass 文件编译
*
* 解决了以下问题:
* 1. Sass 新版本的 mixed-decls 弃用警告
* 2. legacy-js-api 弃用警告(优先使用新 API)
* 3. @import 规则弃用警告(Dart Sass 3.0.0 将移除)
* 4. 嵌套规则后声明的兼容性问题
* 5. 支持用户项目中的 Sass 版本
* 6. 自动检测并使用最合适的 Sass API
* 7. 多层次的警告抑制机制(console.warn 拦截 + 自定义 logger + silenceDeprecations)
*
* @param css 要编译的 CSS 内容
* @param filepath 文件路径,用于解析 @import 等
* @param globalCss 全局 CSS 内容(字符串)或包含 CSS 的对象(如 {css: string})
* @param debug 是否开启调试模式
* @returns 编译后的 CSS 字符串
*/
async function sassCompiler(css, filepath, globalCss, debug) {
if (typeof window !== "undefined") throw new Error("sassCompiler is not supported in this browser");
if (typeof css !== "string") {
if (debug) console.warn(`[transform-to-unocss] sassCompiler received non-string CSS input: ${typeof css}`);
return String(css || "");
}
if (filepath) {
const isValidSassFile = filepath.endsWith(".scss") || filepath.endsWith(".sass") || filepath.includes(".vue") || filepath.includes(".svelte") || filepath.includes(".astro") || filepath.includes(".tsx") || filepath.includes(".jsx");
if (!isValidSassFile && debug) console.warn(`[transform-to-unocss] sassCompiler called for unexpected file type: ${filepath}`);
}
if (debug) console.log(`[transform-to-unocss] Compiling SCSS file: ${filepath || "unknown file"}`);
const baseDir = path.dirname(filepath);
let result = "";
if (globalCss) {
if (typeof globalCss === "string") result += globalCss;
else if (typeof globalCss === "object" && globalCss !== null) {
const globalCssObj = globalCss;
if ("css" in globalCssObj && typeof globalCssObj.css === "string") result += globalCssObj.css;
else if (debug) console.warn(`[transform-to-unocss] Unexpected globalCss object format:`, globalCss);
} else if (debug) console.warn(`[transform-to-unocss] globalCss is not a string or valid object: ${typeof globalCss}`, globalCss);
}
result += css;
try {
const sass = await import("sass");
const originalWarn = console.warn;
const filteredWarn = (message, ...args) => {
const messageStr = String(message);
const deprecationPatterns = [
"Deprecation Warning",
"mixed-decls",
"legacy-js-api",
"import",
"Sass @import rules are deprecated",
"will be removed in Dart Sass",
"More info and automated migrator"
];
const shouldIgnore = deprecationPatterns.some((pattern) => messageStr.includes(pattern));
if (!shouldIgnore) originalWarn(message, ...args);
};
try {
const sassInfo = sass.info || "";
const isModernSass = sassInfo.includes("dart-sass") || sassInfo.includes("1.");
const compileOptions = {
syntax: "scss",
loadPaths: [baseDir]
};
if (isModernSass) {
compileOptions.quietDeps = true;
compileOptions.verbose = false;
try {
compileOptions.silenceDeprecations = [
"mixed-decls",
"import",
"legacy-js-api"
];
} catch (e) {}
compileOptions.logger = { warn: (message, options) => {
const deprecationPatterns = [
"mixed-decls",
"legacy-js-api",
"import",
"Deprecation Warning",
"behavior for declarations that appear after nested",
"will be changing to match the behavior specified by CSS",
"The legacy JS API is deprecated",
"Sass @import rules are deprecated",
"will be removed in Dart Sass 3.0.0",
"More info and automated migrator"
];
const shouldIgnore = deprecationPatterns.some((pattern) => message.includes(pattern));
if (shouldIgnore) return;
if (debug) console.warn(`[transform-to-unocss] Sass warning: ${message}`);
} };
}
let compiledResult;
console.warn = filteredWarn;
if (sass.compile && typeof sass.compile === "function") {
const fs$1 = await import("node:fs");
const os = await import("node:os");
const tempFilePath = `${os.tmpdir()}/transform-to-unocss-${Date.now()}.scss`;
try {
fs$1.writeFileSync(tempFilePath, result);
compiledResult = sass.compile(tempFilePath, compileOptions);
} finally {
try {
fs$1.unlinkSync(tempFilePath);
} catch (e) {}
}
} else compiledResult = sass.compileString(result, compileOptions);
result = compiledResult.css;
return result;
} finally {
console.warn = originalWarn;
}
} catch (error) {
if (error.code === "MODULE_NOT_FOUND" || error.message.includes("Cannot resolve module")) throw new Error("Sass compiler not found. Please install sass in your project:\nnpm install sass\nor\nyarn add sass\nor\npnpm add sass");
console.error(`Error:\n transform-to-unocss(sassCompiler) ${error.toString()}`);
return css;
}
}
//#endregion
//#region src/stylusCompiler.ts
async function stylusCompiler(css, filepath, globalCss, debug) {
if (typeof window !== "undefined") throw new Error("Stylus is not supported in this browser");
if (debug) console.log(`[transform-to-tailwindcss] Compiling Stylus file: ${filepath || "unknown file"}`);
let result = globalCss ? `${globalCss}${css}` : css;
try {
const stylus = await import("stylus");
result = stylus.default.render(result, { filename: filepath });
return result;
} catch (error) {
if (error.code === "MODULE_NOT_FOUND" || error.message.includes("Cannot resolve module")) throw new Error("Stylus compiler not found. Please install stylus in your project:\nnpm install stylus\nor\nyarn add stylus\nor\npnpm add stylus");
console.error(`Error:\n transform-to-unocss(stylusCompiler) ${error.toString()}`);
}
}
//#endregion
//#region src/compilerCss.ts
function compilerCss(css, lang, filepath = isNodeEnvironment() ? process.cwd() : "", globalCss, debug) {
if (typeof css !== "string") {
if (debug) console.warn(`[transform-to-unocss] compilerCss received non-string CSS input: ${typeof css}, filepath: ${filepath}`);
return String(css || "");
}
if (![
"stylus",
"less",
"scss",
"css"
].includes(lang)) {
if (debug) console.warn(`[transform-to-unocss] compilerCss received unknown language: ${lang}, filepath: ${filepath}`);
return css;
}
switch (lang) {
case "stylus": return stylusCompiler(css, filepath, globalCss, debug);
case "less": return lessCompiler(css, filepath, globalCss, debug);
case "scss": return sassCompiler(css, filepath, globalCss, debug);
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(/([.#\w](?:[^{}]|\n)*)\{([#\\\s\w\-.:;,%@/()+'"!]*)\}/g, (all, name, value = "") => {
name = trim(name.replace(/\s+/g, " "));
if (name.includes(":deep(") || name.includes(">>>") || name.includes("/deep/") || name.includes("::v-deep") || name.includes(":global(") || name.includes("@")) return;
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;
let _class$1 = "";
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)) _class$1 = 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,
class: _class$1,
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, debug);
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 getMatchingWeight(name, currentClass) {
if (name.includes(",")) {
const selectors = name.split(",").map((s) => s.trim());
const currentClasses = currentClass.split(" ").filter(Boolean);
let bestMatch = "";
let maxMatchCount = 0;
for (const selector of selectors) {
let matchCount = 0;
const selectorClasses = selector.match(/\.[A-Z][\w-]*/gi) || [];
for (const selectorClass of selectorClasses) {
const className = selectorClass.substring(1);
if (currentClasses.includes(className)) matchCount++;
}
if (matchCount > maxMatchCount) {
maxMatchCount = matchCount;
bestMatch = selector;
}
}
return calculateWeight(bestMatch || selectors[0]);
}
return calculateWeight(name);
}
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, class: _class } = 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];
});
const currentWeight = getMatchingWeight(name, _class);
data.forEach((item$1) => {
const [key, value] = item$1;
if (value === void 0) return;
if (!map[key]) map[key] = [currentWeight, value];
else {
const [preWeight] = map[key];
if (preWeight === skipTransformFlag) return;
if (+currentWeight >= +preWeight) map[key] = [+currentWeight, 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 match = item.trim().match(/(^(?:[^:[]|\[[^\]]*\])+):\s*(.*)$/);
if (match) {
const key = match[1].trim();
const value = match[2];
if (value !== void 0) {
var _map$key;
acc[key] = [((_map$key = map[key]) === null || _map$key === void 0 ? void 0 : _map$key[0]) ?? 1, value];
}
}
return acc;
}, {});
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"))}`;
}
transferCss = transferCss.replace(/"/g, "'");
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 || {};
if (typeof code !== "string") {
if (debug) console.warn(`[transform-to-unocss] transformVue received non-string code: ${typeof code}, filepath: ${filepath}`);
return String(code || "");
}
if (filepath && !filepath.endsWith(".vue") && !code.includes("<template>") && !code.includes("<script>") && !code.includes("<style>")) {
if (debug) console.warn(`[transform-to-unocss] transformVue called for non-Vue file: ${filepath}`);
return code;
}
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, debug);
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 (typeof code !== "string") {
if (debug) console.warn(`[transform-to-unocss] transformCode received non-string code: ${typeof code}, filepath: ${filepath}`);
return String(code || "");
}
if (debug) console.log("[DEBUG] transformCode started:", JSON.stringify({
filepath,
type,
isJsx,
isRem: isRem$1,
codeLength: code.length
}));
let detectedType = type;
if (!detectedType && filepath) {
if (filepath.endsWith(".tsx") || filepath.endsWith(".jsx")) detectedType = "tsx";
else if (filepath.endsWith(".html")) detectedType = "html";
else if (filepath.endsWith(".svelte")) detectedType = "svelte";
else if (filepath.endsWith(".astro")) detectedType = "astro";
else if (filepath.endsWith(".vue")) detectedType = "vue";
}
if (debug) console.log(`[DEBUG] transformCode detected type: ${detectedType}, original type: ${type}, filepath: ${filepath}`);
if (!detectedType && filepath && !filepath.endsWith(".vue") && !code.includes("<template>")) {
if (debug) console.warn(`[transform-to-unocss] transformCode: Unknown file type for ${filepath}, skipping transformation`);
return code;
}
if (detectedType === "tsx") {
if (debug) console.log(`[DEBUG] transformCode: Processing as TSX file`);
return transformJsx(code, {
filepath,
isRem: isRem$1,
globalCss,
debug
});
}
if (detectedType === "html") return transformHtml(code, {
filepath,
globalCss,
debug
});
if (detectedType === "svelte") return transformSvelte(code, {
filepath,
isRem: isRem$1,
globalCss,
debug
});
if (detectedType === "astro") return transformAstro(code, {
filepath,
isRem: isRem$1,
globalCss,
debug
});
if (detectedType === "vue" || code.includes("<template>") || code.includes("<script>") || code.includes("<style>")) {
if (debug) console.log(`[DEBUG] transformCode: Processing as Vue file`);
return transformVue(code, {
isJsx,
filepath,
isRem: isRem$1,
globalCss,
debug
});
}
if (debug) console.warn(`[transform-to-unocss] transformCode: No suitable transformer found for ${filepath}, returning original code`);
return code;
}
//#endregion
export { TRANSFER_FLAG, __commonJS, __toESM, transformAstro, transformCode, transformHtml, transformJsx, transformSvelte, transformVue };