UNPKG

kulp-ai-tagger

Version:

Vite plugin for tagging React components

184 lines (183 loc) 8.48 kB
import { parse } from "@babel/parser"; import * as esbuild from "esbuild"; import fs from "fs/promises"; import MagicString from "magic-string"; import * as path2 from "path"; import resolveConfig from "tailwindcss/resolveConfig.js"; import { findProjectRoot, shouldTagElement } from "./util.js"; const validExtensions = new Set([".jsx", ".tsx"]); const projectRoot = findProjectRoot(); const tailwindInputFile = path2.resolve(projectRoot, "./tailwind.config.ts"); const tailwindJsonOutfile = path2.resolve(projectRoot, "./src/tailwind.config.kulp.json"); const tailwindIntermediateFile = path2.resolve(projectRoot, "./.kulp.tailwind.config.js"); const isSandbox = process.env.KULP_DEV_SERVER === "true"; export function componentTagger() { const cwd = process.cwd(); const stats = { totalFiles: 0, processedFiles: 0, totalElements: 0 }; return { name: "vite-plugin-component-tagger", enforce: "pre", async transform(code, id) { if (!validExtensions.has(path2.extname(id)) || id.includes("node_modules")) { return null; } stats.totalFiles++; const relativePath = path2.relative(cwd, id); try { const parserOptions = { sourceType: "module", plugins: ["jsx", "typescript"] }; const ast = parse(code, parserOptions); const magicString = new MagicString(code); let changedElementsCount = 0; let currentElement = null; const { walk } = await import("estree-walker"); walk(ast, { enter(node) { if (node.type === "JSXElement") { currentElement = node; } if (node.type === "JSXOpeningElement") { const jsxNode = node; let elementName; if (jsxNode.name.type === "JSXIdentifier") { elementName = jsxNode.name.name; } else if (jsxNode.name.type === "JSXMemberExpression") { const memberExpr = jsxNode.name; elementName = `${memberExpr.object.name}.${memberExpr.property.name}`; } else { return; } if (elementName === "Fragment" || elementName === "React.Fragment") { return; } const attributes = jsxNode.attributes.reduce((acc, attr) => { if (attr.type === "JSXAttribute") { if (attr.value?.type === "StringLiteral") { acc[attr.name.name] = attr.value.value; } else if (attr.value?.type === "JSXExpressionContainer" && attr.value.expression.type === "StringLiteral") { acc[attr.name.name] = attr.value.expression.value; } } return acc; }, {}); let textContent = ""; if (currentElement && currentElement.children) { textContent = currentElement.children.map((child) => { if (child.type === "JSXText") { return child.value.trim(); } else if (child.type === "JSXExpressionContainer") { if (child.expression.type === "StringLiteral") { return child.expression.value; } } return ""; }).filter(Boolean).join(" ").trim(); } const content = {}; if (textContent) { content.text = textContent; } if (attributes.placeholder) { content.placeholder = attributes.placeholder; } if (attributes.className) { content.className = attributes.className; } const line = jsxNode.loc?.start?.line ?? 0; const col = jsxNode.loc?.start?.column ?? 0; const dataComponentId = `${relativePath}:${line}:${col}`; const fileName = path2.basename(id); const shouldTag = shouldTagElement(elementName); if (shouldTag) { const legacyIds = ` data-component-path="${relativePath}" data-component-line="${line}" data-component-file="${fileName}" data-component-name="${elementName}" data-component-content="${encodeURIComponent(JSON.stringify(content))}"`; magicString.appendLeft(jsxNode.name.end ?? 0, ` data-kulp-id="${dataComponentId}" data-kulp-name="${elementName}" ${legacyIds}`); changedElementsCount++; } } } }); stats.processedFiles++; stats.totalElements += changedElementsCount; return { code: magicString.toString(), map: magicString.generateMap({ hires: true }) }; } catch (error) { console.error(`Error processing file ${relativePath}:`, error); stats.processedFiles++; return null; } }, async buildStart() { if (!isSandbox) return; try { await generateConfig(); } catch (error) { console.error("Error generating tailwind.config.kulp.json:", error); } }, configureServer(server) { if (!isSandbox) return; try { server.watcher.add(tailwindInputFile); server.watcher.on("change", async (changedPath) => { if (path2.normalize(changedPath) === path2.normalize(tailwindInputFile)) { await generateConfig(); } }); } catch (error) { console.error("Error adding watcher:", error); } } }; } async function generateConfig() { try { await esbuild.build({ entryPoints: [tailwindInputFile], outfile: tailwindIntermediateFile, bundle: true, format: "esm", banner: { js: 'import { createRequire } from "module"; const require = createRequire(import.meta.url);' } }); try { const userConfig = await import(tailwindIntermediateFile + "?update=" + Date.now() // cache buster ); if (!userConfig || !userConfig.default) { console.error("Invalid Tailwind config structure:", userConfig); throw new Error("Invalid Tailwind config structure"); } const resolvedConfig = resolveConfig(userConfig.default); await fs.writeFile(tailwindJsonOutfile, JSON.stringify(resolvedConfig, null, 2)); await fs.unlink(tailwindIntermediateFile).catch(() => { // Ignore errors when unlinking intermediate file }); } catch (error) { console.error("Error processing config:", error); throw error; } } catch (error) { console.error("Error in generateConfig:", error); throw error; } }