UNPKG

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

Version:

Allows tailwindcss classes to be broken into multiple lines.

442 lines (418 loc) 11.1 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 }, projectRootPath, rootCssPath, srcDirPath } = state; if (id !== rootCssPath) return code; const compiler = await compile(code, { base: srcDirPath, from: id, onDependency: (path) => { this.addWatchFile(path); }, shouldRewriteUrls: true }); const scanner = new Scanner({ sources: [ { base: projectRootPath, pattern: "**/*", negated: false }, ...compiler.sources ] }); 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 { join } 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(config.root, "src", "index.css"); state.projectRootPath = config.root; state.srcDirPath = join(config.root, "src"); }, 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/generateCodeForArray.ts function generateCodeForArray(node) { const indexItems = node.indexItems.map(generateCodeForNode).join(","); return `${node.name}[${indexItems}]${node.suffix}`; } // ../../lib/generator/generateCodeForFunction.ts function generateCodeForFunction(node) { const args = node.args.map(generateCodeForNode).join(","); return `${node.name}(${args})${node.suffix}`; } // ../../lib/generator/generateCodeForNode.ts function generateCodeForNode(node) { if (node.type === "Function") return generateCodeForFunction(node); if (node.type === "Array") return generateCodeForArray(node); return node.value; } // ../../lib/generator/generateCodeForNodes.ts function generateCodeForNodes(nodes) { return nodes.map((node) => generateCodeForNode(node)).join(" "); } // ../../lib/parser/next.ts function next(state) { return state.input[state.pos++]; } // ../../lib/parser/peek.ts function peek(state) { return state.input[state.pos]; } // ../../lib/parser/parseIdentifier.ts function parseIdentifier(state) { let value = ""; while (state.pos < state.input.length) { if (/[\s,()[\]]/.test(peek(state))) break; value += next(state); } return value; } // ../../lib/parser/skipWhitespace.ts function skipWhitespace(state) { while (/\s/.test(peek(state))) next(state); } // ../../lib/parser/parseFunction.ts function parseFunction(state, name) { const args = []; next(state); while (state.pos < state.input.length) { skipWhitespace(state); if (peek(state) === ")") break; args.push(parseExpression(state)); skipWhitespace(state); if (peek(state) === ",") next(state); } next(state); const suffix = parseIdentifier(state); return { args, name, suffix, type: "Function" }; } // ../../lib/parser/parseExpression.ts function parseExpression(state) { skipWhitespace(state); const identifier = parseIdentifier(state); skipWhitespace(state); if (peek(state) === "(") return parseFunction(state, identifier); return { type: "Identifier", value: identifier }; } // ../../lib/parser/parseArrayIndex.ts function parseArrayIndex(state) { const items = []; next(state); while (state.pos < state.input.length) { skipWhitespace(state); if (peek(state) === "]") break; items.push(parseExpression(state)); skipWhitespace(state); if (peek(state) === ",") next(state); } next(state); return items; } // ../../lib/parser/parse.ts function parse(input) { const ast = []; const state = { input, pos: 0 }; while (state.pos < state.input.length) { skipWhitespace(state); const identifier = parseIdentifier(state); skipWhitespace(state); if (peek(state) === "[") { const indexItems = parseArrayIndex(state); const suffix = parseIdentifier(state); const arrayNode = { indexItems, name: identifier, suffix, type: "Array" }; ast.push(arrayNode); continue; } if (peek(state) === "(") { const functionNode = parseFunction(state, identifier); ast.push(functionNode); continue; } const identifierNode = { type: "Identifier", value: identifier }; ast.push(identifierNode); } 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); candidatesFound.push(...filtered); value.value = filtered.join(" "); } }); const { code: transformedCode, map: transformedCodeMap } = generate(ast, {}, code); return { candidatesFound, transformedCode: { code: transformedCode, map: transformedCodeMap } }; } // src/updateModule.ts var intervals = /* @__PURE__ */ new Map(); function updateModule(devServer, id) { const module = devServer.moduleGraph.getModuleById(id); if (!module) return; let interval = intervals.get(id); if (interval) clearInterval(interval); devServer.moduleGraph.invalidateModule(module); interval = setInterval(() => { if (module.invalidationState != "HARD_INVALIDATED") { clearInterval(interval); intervals.delete(id); return; } devServer.moduleGraph.invalidateModule(module); void devServer.reloadModule(module); }, 10); intervals.set(id, interval); } // 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 rawString = quasis[0].value.raw; const transformed = transformTailwindClasses(rawString); 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() { const state = { candidatesFromTransforms: { className: /* @__PURE__ */ new Map(), tagged: /* @__PURE__ */ new Map() } }; return [ initialize(state), transformTaggedStringsPlugin(state), transformJsxCssClassesPlugin(state), compileCssPlugin(state) ]; } export { multilineTailwindCss }; //# sourceMappingURL=index.js.map