UNPKG

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
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 };