UNPKG

@borela-tech/vite-plugin-multiline-tailwindcss

Version:

Allows tailwindcss classes to be broken into multiple lines.

673 lines (639 loc) 16.9 kB
// 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, "&quot;")); 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