vite-plugin-vue-css-module
Version:
css-module syntactic sugar for vue3
520 lines (515 loc) • 19.6 kB
JavaScript
// src/index.ts
import { MagicString } from "vue/compiler-sfc";
// src/utils/parseVue.ts
import { parse } from "vue/compiler-sfc";
function parseVue(code) {
const descriptor = parse(code).descriptor;
const cssModule = descriptor.styles.find((style) => style.module !== void 0)?.module;
return {
cssModule,
template: descriptor.template
};
}
// src/utils/tool.ts
function isObjectExp(code) {
return code.startsWith("{") && code.endsWith("}");
}
function isArrayExp(code) {
return code.startsWith("[") && code.endsWith("]");
}
function getObjectOrArrayExpressionContent(code) {
let str = trimString(code.substring(1, code.length - 1));
return str.endsWith(",") ? str.substring(0, str.length - 1) : str;
}
function trimString(code) {
return code.trim();
}
function transformString2Array(code) {
return code.split(" ").filter((str) => str);
}
function transformString2ObjectString(code, quote) {
if (!code)
return "";
return splitStr2Arr(code, quote).map((val) => {
const _val = trimString(val);
if (isObjectExp(_val)) {
return _val.substring(1, _val.length - 1);
} else if (isArrayExp(_val)) {
return transformString2ObjectString(_val.substring(1, _val.length - 1), quote);
} else {
return `[${_val}]:${_val}`;
}
}).join(",");
}
function transformExp(code, cssModuleName, quote, keepReturnType = false) {
if (isObjectExp(code)) {
return transformObject(code, cssModuleName, quote, keepReturnType);
} else if (isArrayExp(code)) {
return transformArray(code, cssModuleName, quote, keepReturnType);
} else {
return transformString(code, cssModuleName);
}
}
function splitStr2Arr(code, quote) {
const result = [];
let buffer = "";
let stack = [];
for (let i = 0; i < code.length; i++) {
const char = code[i];
if (char === "{" || char === "[") {
if (stack.length > 0 && stack[stack.length - 1] === quote) {
buffer += char;
} else {
stack.push(char);
buffer += char;
}
} else if (char === "}") {
if (stack.length > 0 && stack[stack.length - 1] === "{") {
stack.pop();
buffer += char;
} else {
buffer += char;
}
} else if (char === "]") {
if (stack.length > 0 && stack[stack.length - 1] === "[") {
stack.pop();
buffer += char;
} else {
buffer += char;
}
} else if (char === quote) {
if (stack.length > 0 && stack[stack.length - 1] === quote) {
stack.pop();
buffer += char;
} else {
stack.push(quote);
buffer += char;
}
} else if (char === ",") {
if (stack.length === 0) {
result.push(trimString(buffer));
buffer = "";
} else {
buffer += char;
}
} else {
buffer += char;
}
}
if (trimString(buffer) !== "") {
result.push(trimString(buffer));
}
return result;
}
function transformObject(code, cssModuleName, quote, keepReturnType) {
const content = getObjectOrArrayExpressionContent(code);
if (!content)
return "";
const contentArr = splitStr2Arr(content, quote);
const result = contentArr.map((item) => {
let key = item, value = item;
let firstColonIndex = item.indexOf(":");
if (firstColonIndex !== -1) {
key = item.slice(0, firstColonIndex);
value = item.slice(firstColonIndex + 1);
}
let _key = trimString(key);
if (isLegalVariate(_key)) {
_key = `${quote}${_key}${quote}`;
}
return `[${cssModuleName}[${_key}]]:${trimString(value)}`;
}).join(",");
return keepReturnType ? "{" + result + "}" : result;
}
function transformArray(code, cssModuleName, quote, keepReturnType) {
const content = getObjectOrArrayExpressionContent(code);
if (!content)
return "";
const contentArr = splitStr2Arr(content, quote);
const result = contentArr.map((item) => {
return transformExp(item, cssModuleName, quote, true);
}).join(",");
return keepReturnType ? "[" + result + "]" : result;
}
function transformString(code, cssModuleName) {
if (!code)
return "";
return `${cssModuleName}[${code}]`;
}
function isLegalVariate(variableName) {
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(variableName);
}
function getQuote(code) {
for (let i = 0; i < code.length; i++) {
if (code[i] === '"') {
return '"';
}
if (code[i] === "'") {
return "'";
}
if (code[i] === "`") {
return "`";
}
}
return "";
}
function swapQuotes(code) {
let result;
const placeholder = "__QUOTE__";
result = code.replace(/'/g, placeholder);
result = result.replace(/"/g, "'");
result = result.replace(new RegExp(placeholder, "g"), '"');
return result;
}
function transform2SingleQuotes(code) {
return code.replace(/"/g, "'");
}
function getPugVal(code) {
if (typeof code === "boolean")
return "";
return trimString(code.substring(1, code.length - 1));
}
// src/utils/parseHtml.ts
function parseHtml(childNode, s, attrName, cssModuleName) {
childNode.forEach((node) => {
if ("props" in node) {
let bindClassNode, attrNameNode, bindAttrNameNode;
node.props.forEach((prop) => {
if (prop.name === "bind" && "arg" in prop && prop.arg && "content" in prop.arg) {
if (prop.arg.content === "class") {
bindClassNode = prop;
} else if (prop.arg.content === attrName) {
bindAttrNameNode = prop;
}
} else if (prop.name === attrName && "value" in prop) {
attrNameNode = prop;
}
});
if (attrNameNode) {
const attrNameQuote = getQuote(attrNameNode.loc.source);
let attrNameArr = transformString2Array(attrNameNode.value?.content || "");
if (attrNameArr.length === 0) {
s.update(attrNameNode.loc.start.offset, attrNameNode.loc.end.offset, "");
return;
}
if (bindClassNode && bindClassNode.exp && "content" in bindClassNode.exp) {
const bindClassQuote = getQuote(bindClassNode.loc.source);
const strQuote = bindClassQuote === "'" ? '"' : "'";
const bindClassContent = trimString(bindClassNode.exp.content);
let result;
if (isObjectExp(bindClassContent)) {
let objectContent = getObjectOrArrayExpressionContent(bindClassContent);
if (objectContent) {
objectContent += ",";
}
result = `:class=${bindClassQuote}{${objectContent}${attrNameArr.map((val) => `[${cssModuleName}[${strQuote}${val}${strQuote}]]:true`).join(",")}}${bindClassQuote}`;
} else if (isArrayExp(bindClassContent)) {
let arrayContent = getObjectOrArrayExpressionContent(bindClassContent);
if (arrayContent) {
arrayContent += ",";
}
result = `:class=${bindClassQuote}[${arrayContent}${attrNameArr.map((val) => `${cssModuleName}[${strQuote}${val}${strQuote}]`).join(",")}]${bindClassQuote}`;
} else {
result = `:class=${bindClassQuote}[${bindClassContent},${attrNameArr.map((val) => `${cssModuleName}[${strQuote}${val}${strQuote}]`).join(",")}]${bindClassQuote}`;
}
s.update(bindClassNode.loc.start.offset, bindClassNode.loc.end.offset, result);
s.update(attrNameNode.loc.start.offset, attrNameNode.loc.end.offset, "");
} else {
const strQuote = attrNameQuote === "'" ? '"' : "'";
s.update(
attrNameNode.loc.start.offset,
attrNameNode.loc.end.offset,
`:class=${attrNameQuote}[${attrNameArr.map((val) => `${cssModuleName}[${strQuote}${val}${strQuote}]`).join(",")}]${attrNameQuote}`
);
}
}
if (bindAttrNameNode && bindAttrNameNode.exp && "content" in bindAttrNameNode.exp) {
const bindAttrNameQuote = getQuote(bindAttrNameNode.loc.source);
const bindAttrNameContent = trimString(bindAttrNameNode.exp.content);
let bindAttrNameContent2CssModuleNameStr = transformExp(
bindAttrNameContent,
cssModuleName,
bindAttrNameQuote === "'" ? '"' : "'"
);
if (!bindAttrNameContent2CssModuleNameStr) {
s.update(bindAttrNameNode.loc.start.offset, bindAttrNameNode.loc.end.offset, "");
return;
}
if (bindClassNode && bindClassNode.exp && "content" in bindClassNode.exp) {
const bindClassQuote = getQuote(bindClassNode.loc.source);
const bindClassContent = trimString(bindClassNode.exp.content);
if (bindAttrNameQuote !== bindClassQuote) {
bindAttrNameContent2CssModuleNameStr = swapQuotes(bindAttrNameContent2CssModuleNameStr);
}
let result;
if (isObjectExp(bindClassContent)) {
let objectContent = getObjectOrArrayExpressionContent(bindClassContent);
if (objectContent) {
objectContent += ",";
}
if (isObjectExp(bindAttrNameContent)) {
result = `:class=${bindClassQuote}{${objectContent}${bindAttrNameContent2CssModuleNameStr}}${bindClassQuote}`;
} else {
result = `:class=${bindClassQuote}{${objectContent}${transformString2ObjectString(
bindAttrNameContent2CssModuleNameStr,
bindClassQuote === '"' ? "'" : '"'
)}}${bindClassQuote}`;
}
} else if (isArrayExp(bindClassContent)) {
let arrayContent = getObjectOrArrayExpressionContent(bindClassContent);
if (isObjectExp(bindAttrNameContent)) {
arrayContent = transformString2ObjectString(
arrayContent,
bindClassQuote === '"' ? "'" : '"'
);
if (arrayContent) {
arrayContent += ",";
}
result = `:class=${bindClassQuote}{${arrayContent}${bindAttrNameContent2CssModuleNameStr}}${bindClassQuote}`;
} else {
result = `:class=${bindClassQuote}[${arrayContent},${bindAttrNameContent2CssModuleNameStr}]${bindClassQuote}`;
}
} else {
if (isObjectExp(bindAttrNameContent)) {
result = `:class=${bindClassQuote}{${transformString2ObjectString(
bindClassContent,
bindClassQuote === '"' ? "'" : '"'
)},${bindAttrNameContent2CssModuleNameStr}}${bindClassQuote}`;
} else {
result = `:class=${bindClassQuote}[${bindClassContent},${bindAttrNameContent2CssModuleNameStr}]${bindClassQuote}`;
}
}
s.update(bindClassNode.loc.start.offset, bindClassNode.loc.end.offset, result);
s.update(bindAttrNameNode.loc.start.offset, bindAttrNameNode.loc.end.offset, "");
} else {
if (isObjectExp(bindAttrNameContent)) {
s.update(
bindAttrNameNode.loc.start.offset,
bindAttrNameNode.loc.end.offset,
`:class=${bindAttrNameQuote}{${bindAttrNameContent2CssModuleNameStr}}${bindAttrNameQuote}`
);
} else {
s.update(
bindAttrNameNode.loc.start.offset,
bindAttrNameNode.loc.end.offset,
`:class=${bindAttrNameQuote}[${bindAttrNameContent2CssModuleNameStr}]${bindAttrNameQuote}`
);
}
}
}
node.children && parseHtml(node.children, s, attrName, cssModuleName);
}
});
}
// src/utils/parsePug.ts
var pugPackage;
var setPugPackage = async () => {
pugPackage = {
parse: (await import("pug-parser")).default,
lexer: (await import("pug-lexer")).default,
walk: (await import("pug-walk")).default,
generate: (await import("pug-source-gen")).default
};
};
async function parsePug(source, options, cssModuleName) {
const { attrName, pugClassLiterals } = options;
if (!pugPackage)
await setPugPackage();
const { parse: parse2, lexer, walk, generate } = pugPackage;
const ast = parse2(lexer(source));
walk(ast, (node) => {
if (node.attrs?.length) {
let bindClassNode, bindAttrNameNode, attrNameNodes = [];
node.attrs.forEach((attr) => {
switch (attr.name) {
case ":class":
case "v-bind:class":
bindClassNode = attr;
break;
case `:${attrName}`:
case `v-bind:${attrName}`:
bindAttrNameNode = attr;
break;
case attrName:
attrNameNodes.push(attr);
break;
case "class":
if (pugClassLiterals && attr.mustEscape === false) {
attrNameNodes.push(attr);
}
}
});
if (attrNameNodes.length) {
const attrNameArr = transformString2Array(
attrNameNodes.map((node2) => getPugVal(node2.val)).join(" ")
);
if (attrNameArr.length) {
if (bindClassNode) {
let result;
const bindClassContent = transform2SingleQuotes(getPugVal(bindClassNode.val));
if (isObjectExp(bindClassContent)) {
let objectContent = getObjectOrArrayExpressionContent(bindClassContent);
if (objectContent) {
objectContent += ",";
}
result = `"{${objectContent}${attrNameArr.map((cls) => `[${cssModuleName}['${cls}']]:true`).join(",")}}"`;
} else if (isArrayExp(bindClassContent)) {
const arrayContent = getObjectOrArrayExpressionContent(bindClassContent);
result = `"[${arrayContent}, ${attrNameArr.map((cls) => `${cssModuleName}['${cls}']`).join(",")}]"`;
} else {
result = `"[${bindClassContent},${attrNameArr.map((cls) => `${cssModuleName}['${cls}']`).join(",")}]"`;
}
bindClassNode.val = result;
} else {
const attrNameNode = attrNameNodes.shift();
attrNameNode.mustEscape = true;
attrNameNode.name = `:class`;
attrNameNode.val = `"[${attrNameArr.map((cls) => `${cssModuleName}['${cls}']`).join(",")}]"`;
bindClassNode = attrNameNode;
}
}
if (attrNameNodes.length) {
node.attrs = node.attrs.filter((attr) => !attrNameNodes.includes(attr));
}
}
if (bindAttrNameNode) {
const bindAttrNameContent = transform2SingleQuotes(getPugVal(bindAttrNameNode.val));
const bindAttrNameContent2CssModuleNameStr = transformExp(
bindAttrNameContent,
cssModuleName,
"'"
);
if (!bindAttrNameContent2CssModuleNameStr) {
node.attrs = node.attrs.filter((attr) => attr !== bindAttrNameNode);
return;
}
if (bindClassNode) {
const bindClassContent = transform2SingleQuotes(getPugVal(bindClassNode.val));
let result;
if (isObjectExp(bindClassContent)) {
let objectContent = getObjectOrArrayExpressionContent(bindClassContent);
if (objectContent) {
objectContent += ",";
}
if (isObjectExp(bindAttrNameContent)) {
result = `"{${objectContent}${bindAttrNameContent2CssModuleNameStr}}"`;
} else {
result = `"{${objectContent}${transformString2ObjectString(
bindAttrNameContent2CssModuleNameStr,
"'"
)}}"`;
}
} else if (isArrayExp(bindClassContent)) {
let arrayContent = getObjectOrArrayExpressionContent(bindClassContent);
if (isObjectExp(bindAttrNameContent)) {
arrayContent = transformString2ObjectString(arrayContent, "'");
if (arrayContent) {
arrayContent += ",";
}
result = `"{${arrayContent}${bindAttrNameContent2CssModuleNameStr}}"`;
} else {
result = `"[${arrayContent},${bindAttrNameContent2CssModuleNameStr}]"`;
}
} else {
if (isObjectExp(bindAttrNameContent)) {
result = `"{${transformString2ObjectString(
bindClassContent,
"'"
)},${bindAttrNameContent2CssModuleNameStr}}"`;
} else {
result = `"[${bindClassContent},${bindAttrNameContent2CssModuleNameStr}]"`;
}
}
bindClassNode.val = result;
node.attrs = node.attrs.filter((attr) => attr !== bindAttrNameNode);
} else {
bindAttrNameNode.name = ":class";
if (isObjectExp(bindAttrNameContent)) {
bindAttrNameNode.val = `"{${bindAttrNameContent2CssModuleNameStr}}"`;
} else {
bindAttrNameNode.val = `"[${bindAttrNameContent2CssModuleNameStr}]"`;
}
}
}
}
});
return generate(ast);
}
// src/index.ts
function vueCssModule(userOptions = {}) {
const options = {
attrName: "cls",
// 默认属性名
pugClassLiterals: false,
...userOptions
};
const cache = /* @__PURE__ */ new Map();
async function transformTemplate({
code,
template,
cssModule
}) {
const cssModuleName = cssModule === true ? "$style" : cssModule;
const s = new MagicString(code);
if (template.lang === "pug") {
const result = await parsePug(template.content, options, cssModuleName);
s.update(template.loc.start.offset, template.loc.end.offset, result);
return s;
} else if (template.ast?.children.length) {
parseHtml(template.ast.children, s, options.attrName, cssModuleName);
return s;
}
}
return {
name: "vite-plugin-vue-css-module",
enforce: "pre",
async transform(code, id) {
const [file, queryString] = id.split("?");
if (file.endsWith(".vue")) {
const params = new URLSearchParams(queryString);
const type = params.get("type");
if (id === file) {
const cached = cache.get(file);
const { template, cssModule } = cached?.code === code ? cached : parseVue(code);
cache.set(file, { code, template, cssModule });
if (template && cssModule) {
const s = await transformTemplate({ code, template, cssModule });
if (s) {
return { code: s.toString(), map: s.generateMap() };
}
}
} else if (type === "template") {
const cached = cache.get(file);
if (cached?.mustTransform) {
cached.mustTransform = false;
const { code: code2, template, cssModule } = cached;
if (template && cssModule) {
const s0 = await transformTemplate({ code: code2, template, cssModule });
if (s0) {
const s = s0.snip(template.loc.start.offset, template.loc.end.offset);
return { code: s.toString(), map: s.generateMap() };
}
}
}
}
}
},
async handleHotUpdate({ file, read }) {
if (file.endsWith(".vue")) {
const cached = cache.get(file);
if (cached) {
const code = await read();
const { template, cssModule } = parseVue(code);
if (template) {
const mustTransform = !(cached.template && cached.template.content === template.content && cached.template.lang === template.lang && cached.cssModule === cssModule);
cache.set(file, { code, template, cssModule, mustTransform });
}
}
}
}
};
}
export {
vueCssModule as default
};
//# sourceMappingURL=index.js.map