@borela-tech/vite-plugin-multiline-tailwindcss
Version:
Allows tailwindcss classes to be broken into multiple lines.
673 lines (639 loc) • 16.9 kB
JavaScript
// src/compileCssPlugin.ts
import { Scanner } from "@tailwindcss/oxide";
import {
compile,
toSourceMap
} from "@tailwindcss/node";
function compileCssPlugin(state) {
return {
name: "@borela-tech/vite-plugin-multiline-tailwindcss:compile-css",
enforce: "pre",
async transform(code, id) {
const {
candidatesFromTransforms: {
className: classNameCandidatesPerId,
tagged: taggedCandidatesPerId
},
rootCssDirPath,
rootCssPath
} = state;
if (id !== rootCssPath)
return code;
const compiler = await compile(code, {
base: rootCssDirPath,
from: id,
onDependency: (path) => {
this.addWatchFile(path);
},
shouldRewriteUrls: true
});
const scanner = new Scanner({
sources: [
{
base: rootCssDirPath,
pattern: "**/*",
negated: false
},
...compiler.sources
]
});
for (const path of scanner.files)
this.addWatchFile(path);
const ALL_CANDIDATES = scanner.scan();
for (const [, candidates] of classNameCandidatesPerId)
ALL_CANDIDATES.push(...candidates);
for (const [, candidates] of taggedCandidatesPerId)
ALL_CANDIDATES.push(...candidates);
const MAP = compiler.buildSourceMap();
const GENERATED_CODE = compiler.build(ALL_CANDIDATES);
const GENERATED_MAP = toSourceMap(MAP).raw;
return {
code: GENERATED_CODE,
map: GENERATED_MAP
};
}
};
}
// src/initialize.ts
import {
dirname,
join,
resolve
} from "node:path";
function initialize(state) {
return {
name: "@borela-tech/vite-plugin-multiline-tailwindcss:initialize",
enforce: "pre",
configResolved(config) {
if (!config.root)
throw new Error("root is not defined");
state.rootCssPath ||= join("src", "index.css");
state.rootCssPath = resolve(
config.root,
state.rootCssPath
);
state.rootCssDirPath = dirname(state.rootCssPath);
},
configureServer(server) {
state.devServer = server;
}
};
}
// ../../lib/transformJsxCssClasses.ts
import * as t from "@babel/types";
// ../../lib/babel/generate.ts
import generatorModuleOrFunction from "@babel/generator";
var generate = generatorModuleOrFunction;
if (typeof generatorModuleOrFunction != "function")
generate = generatorModuleOrFunction.default;
// ../../lib/transformJsxCssClasses.ts
import { parse as parse2 } from "@babel/parser";
// ../../lib/generator/generateCodeForBracketedExpression.ts
function generateCodeForBracketedExpression(node) {
const prefix = node.prefix ? generateCodeForNode(node.prefix) : "";
const expressions = node.value.map(generateCodeForNode).join(",");
let separator = "";
if (prefix) {
if (!prefix.endsWith("-"))
separator = ":";
}
return `${prefix}${separator}[${expressions}]`;
}
// ../../lib/generator/generateCodeForCssProperty.ts
function generateCodeForCssProperty(node) {
const name = node.name || "";
const value = generateCodeForNode(node.value);
const base = `${name}:${value}`;
if (node.prefix) {
const prefix = generateCodeForNode(node.prefix);
return `${prefix}:${base}`;
}
return base;
}
// ../../lib/generator/generateCodeForExpressionNode.ts
function generateCodeForExpressionNode(node) {
return node.items.map(generateCodeForNode).join("_").trim();
}
// ../../lib/generator/generateCodeForFunction.ts
function generateCodeForFunction(node) {
let fullName = node.name;
if (node.prefix) {
const prefix = generateCodeForNode(node.prefix);
fullName = `${prefix}:${fullName}`;
}
const args = node.args.map(generateCodeForNode).join(",");
return `${fullName}(${args})`;
}
// ../../lib/generator/generateCodeForQuotedString.ts
function generateCodeForQuotedString(node) {
const value = node.value.replaceAll(node.quote, `\\${node.quote}`);
return `${node.quote}${value}${node.quote}`;
}
// ../../lib/generator/generateCodeForNode.ts
function generateCodeForNode(node) {
switch (node.type) {
case "BracketedExpression":
return generateCodeForBracketedExpression(node);
case "Expression":
return generateCodeForExpressionNode(node);
case "Function":
return generateCodeForFunction(node);
case "CssProperty":
return generateCodeForCssProperty(node);
case "QuotedString":
return generateCodeForQuotedString(node);
case "Identifier":
if (node.prefix) {
const prefix = generateCodeForNode(node.prefix);
return `${prefix}:${node.value}`;
}
return node.value;
}
}
// ../../lib/generator/generateCodeForNodes.ts
function generateCodeForNodes(nodes) {
return nodes.map((node) => generateCodeForNode(node)).join(" ").trim();
}
// ../../lib/parser/next.ts
function next(state) {
return state.input[state.pos++];
}
// ../../lib/parser/parseCssProperty.ts
function parseCssProperty(state, name, prefix) {
next(state);
const value = parseExpression(state);
return {
name,
prefix,
type: "CssProperty",
value
};
}
// ../../lib/parser/peek.ts
function peek(state, length = 1) {
return state.input.slice(
state.pos,
state.pos + length
);
}
// ../../lib/parser/formatContext.ts
function formatContext(state, message) {
const lines = state.input.split("\n");
let currentLine = 0;
let currentPos = 0;
let lineStart = 0;
for (let i = 0; i < lines.length; i++) {
const lineLength = lines[i].length + 1;
if (currentPos + lineLength > state.pos) {
currentLine = i;
lineStart = currentPos;
break;
}
currentPos += lineLength;
}
const column = state.pos - lineStart;
const startLine = Math.max(0, currentLine - 5);
const endLine = Math.min(lines.length - 1, currentLine + 5);
let result = "";
for (let i = startLine; i <= endLine; i++) {
const lineNumberGutter = `${i + 1}| `;
const lineNumberGutterLength = lineNumberGutter.length;
const line = `${lineNumberGutter}${lines[i]}
`;
result += line;
if (i === currentLine) {
result += " ".repeat(lineNumberGutterLength + column) + "^\n";
result += " ".repeat(lineNumberGutterLength) + message + "\n";
}
}
return result.trim();
}
// ../../lib/parser/throwError.ts
function throwError(state, message) {
throw new Error(formatContext(state, message));
}
// ../../lib/parser/skipComments.ts
function skipLineComment(state) {
next(state);
next(state);
while (state.pos < state.input.length && peek(state) !== "\n")
next(state);
if (peek(state) == "\n")
next(state);
}
function skipBlockComment(state) {
next(state);
next(state);
let closed = false;
while (state.pos < state.input.length) {
if (peek(state, 2) === "*/") {
next(state);
next(state);
closed = true;
break;
}
next(state);
}
if (!closed)
throwError(state, "Unclosed comment");
}
function skipHashComment(state) {
next(state);
while (state.pos < state.input.length && peek(state) !== "\n")
next(state);
if (peek(state) == "\n")
next(state);
}
function skipComments(state) {
if (peek(state, 2) === "//")
skipLineComment(state);
else if (peek(state, 2) === "/*")
skipBlockComment(state);
else if (peek(state) === "#")
skipHashComment(state);
}
// ../../lib/parser/skipWhitespace.ts
function skipWhitespace(state) {
while (/\s/.test(peek(state)))
next(state);
}
// ../../lib/parser/skipWhitespaceAndComments.ts
function skipWhitespaceAndComments(state) {
const initialStatePosition = state.pos;
let previousPosition;
do {
previousPosition = state.pos;
skipWhitespace(state);
skipComments(state);
} while (state.pos !== previousPosition);
return state.pos != initialStatePosition;
}
// ../../lib/parser/parseFunction.ts
function parseFunction(state, name, prefix) {
next(state);
const args = [];
while (state.pos < state.input.length) {
if (skipWhitespaceAndComments(state))
continue;
if (peek(state) === ",") {
next(state);
continue;
}
if (peek(state) === ")")
break;
args.push(parseExpression(state));
}
next(state);
return {
args,
name,
prefix,
type: "Function"
};
}
// ../../lib/parser/parseIdentifier.ts
function parseIdentifier(state) {
let value = "";
while (state.pos < state.input.length) {
if (/\\/.test(peek(state))) {
value += next(state);
value += next(state);
continue;
}
if (/[\s'"_,:()[\]`]/.test(peek(state)))
break;
value += next(state);
}
return value;
}
// ../../lib/parser/parseQuotedString.ts
function parseQuotedString(state) {
const quote = next(state);
let closed = false;
let value = "";
while (state.pos < state.input.length) {
const ch = next(state);
if (ch === "\\") {
if (peek(state) === quote) {
value += next(state);
continue;
}
value += ch + next(state);
continue;
}
if (ch === quote) {
closed = true;
break;
}
if (ch === " ")
value += "_";
else
value += ch;
}
if (!closed)
throwError(state, "Unclosed quoted string");
return {
type: "QuotedString",
value,
quote
};
}
// ../../lib/parser/parseExpression.ts
function parseExpression(state) {
const items = [];
while (state.pos < state.input.length) {
if (skipWhitespaceAndComments(state))
continue;
if (/[,)\]]/.test(peek(state)))
break;
if (/['"]/.test(peek(state))) {
items.push(parseQuotedString(state));
continue;
}
if (peek(state) === "_") {
next(state);
continue;
}
const identifier = parseIdentifier(state);
if (peek(state) === ":") {
items.push(parseCssProperty(state, identifier));
continue;
}
if (peek(state) === "(") {
items.push(parseFunction(state, identifier));
continue;
}
items.push({
type: "Identifier",
value: identifier
});
}
return {
type: "Expression",
items
};
}
// ../../lib/parser/parseBracketedExpression.ts
function parseBracketedExpression(state, prefix) {
next(state);
const expressions = [];
while (state.pos < state.input.length) {
if (skipWhitespaceAndComments(state))
continue;
if (peek(state) === ",") {
next(state);
continue;
}
if (peek(state) === "]")
break;
const expression = parseExpression(state);
expressions.push(expression);
}
const node = {
prefix,
type: "BracketedExpression",
value: expressions
};
next(state);
return node;
}
// ../../lib/parser/parseIdentifierNode.ts
function parseIdentifierNode(state) {
const identifier = parseIdentifier(state);
return {
type: "Identifier",
value: identifier
};
}
// ../../lib/parser/parse.ts
function parse(input) {
const ast = [];
const state = { input, pos: 0 };
let node = void 0;
let prefix = void 0;
while (state.pos < state.input.length) {
if (skipWhitespaceAndComments(state))
continue;
if (peek(state) === "_") {
next(state);
continue;
}
if (peek(state) === ",") {
next(state);
continue;
}
node = peek(state) === "[" ? parseBracketedExpression(state) : parseIdentifierNode(state);
if (prefix)
node.prefix = prefix;
if (node.type === "Identifier") {
if (peek(state) === "(")
node = parseFunction(state, node.value, node.prefix);
else if (peek(state) === "[")
node = parseBracketedExpression(state, node);
}
if (peek(state) === ":") {
next(state);
prefix = node;
node = void 0;
continue;
}
ast.push(node);
node = void 0;
prefix = void 0;
}
return ast;
}
// ../../lib/transformTailwindClasses.ts
function transformTailwindClasses(tailwindClasses) {
const nodes = parse(tailwindClasses);
return generateCodeForNodes(nodes);
}
// ../../lib/babel/traverse.ts
import traverseModuleOrFunction from "@babel/traverse";
var traverse = traverseModuleOrFunction;
if (typeof traverseModuleOrFunction != "function")
traverse = traverseModuleOrFunction.default;
// ../../lib/transformJsxCssClasses.ts
function transformJsxCssClasses(code) {
const candidatesFound = [];
const ast = parse2(code, {
sourceType: "module",
plugins: [
"jsx",
"typescript"
]
});
traverse(ast, {
JSXAttribute(path) {
const {
node: {
name,
value
}
} = path;
if (!t.isJSXIdentifier(name))
return;
if (name.name != "className")
return;
if (!t.isStringLiteral(value))
return;
const transformed = transformTailwindClasses(value.value);
const filtered = transformed.split(" ").filter(Boolean).map((x) => x.replace(/"/g, """));
candidatesFound.push(...filtered);
value.extra ||= {};
value.extra.raw = `"${filtered.join(" ")}"`;
}
});
const {
code: transformedCode,
map: transformedCodeMap
} = generate(ast, {}, code);
return {
candidatesFound,
transformedCode: {
code: transformedCode,
map: transformedCodeMap
}
};
}
// src/updateModule.ts
function updateModule(devServer, id) {
devServer.hot.send("vite:invalidate", {
path: id,
message: "New Tailwind candidates found.",
firstInvalidatedBy: "@borela-tech/vite-plugin-multiline-tailwindcss"
});
}
// src/transformJsxCssClassesPlugin.ts
function transformJsxCssClassesPlugin(state) {
return {
name: "@borela-tech/vite-plugin-multiline-tailwindcss:transform-jsx-css-classes",
enforce: "pre",
transform(code, id) {
if (!/\.[jt]sx$/.test(id))
return code;
const {
candidatesFromTransforms: {
className: candidatesPerId
},
devServer,
rootCssPath
} = state;
const {
candidatesFound,
transformedCode: {
code: transformedCode,
map: transformedCodeMap
}
} = transformJsxCssClasses(code);
candidatesPerId.set(id, candidatesFound);
if (candidatesFound.length > 0) {
if (devServer)
updateModule(devServer, rootCssPath);
}
return {
code: transformedCode,
map: transformedCodeMap
};
}
};
}
// ../../lib/transformTaggedStrings.ts
import * as t2 from "@babel/types";
import { parse as parse3 } from "@babel/parser";
function transformTaggedStrings(code) {
const candidatesFound = [];
const ast = parse3(code, {
sourceType: "module",
plugins: [
"jsx",
"typescript"
]
});
traverse(ast, {
TaggedTemplateExpression(path) {
const {
tag,
quasi: { quasis }
} = path.node;
if (!t2.isIdentifier(tag))
return;
if (tag.name !== "tailwindcss")
return;
if (quasis.length !== 1)
throw new Error("Tailwind tagged template should have exactly one argument.");
const value = quasis[0].value.cooked || "";
const transformed = transformTailwindClasses(value);
const filtered = transformed.split(" ").filter(Boolean);
candidatesFound.push(...filtered);
path.replaceWith(
t2.stringLiteral(transformed)
);
}
});
const {
code: transformedCode,
map: transformedCodeMap
} = generate(ast, {}, code);
return {
candidatesFound,
transformedCode: {
code: transformedCode,
map: transformedCodeMap
}
};
}
// src/transformTaggedStringsPlugin.ts
function transformTaggedStringsPlugin(state) {
return {
name: "@borela-tech/vite-plugin-multiline-tailwindcss:transform-tagged-strings",
enforce: "pre",
transform(code, id) {
if (!/\.[jt]sx?$/.test(id))
return code;
const {
candidatesFromTransforms: {
tagged: candidatesPerId
},
devServer,
rootCssPath
} = state;
const {
candidatesFound,
transformedCode: {
code: transformedCode,
map: transformedCodeMap
}
} = transformTaggedStrings(code);
candidatesPerId.set(id, candidatesFound);
if (candidatesFound.length > 0) {
if (devServer)
updateModule(devServer, rootCssPath);
}
return {
code: transformedCode,
map: transformedCodeMap
};
}
};
}
// src/index.ts
function multilineTailwindCss(config) {
const state = {
candidatesFromTransforms: {
className: /* @__PURE__ */ new Map(),
tagged: /* @__PURE__ */ new Map()
},
...config
};
return [
initialize(state),
transformTaggedStringsPlugin(state),
transformJsxCssClassesPlugin(state),
compileCssPlugin(state)
];
}
export {
multilineTailwindCss
};
//# sourceMappingURL=index.js.map