Files
noah-template-website/plugins/component-tagger.ts
2026-07-16 14:03:53 +05:30

104 lines
3.3 KiB
TypeScript

import { parse } from "@babel/parser";
import { walk } from "estree-walker";
import MagicString from "magic-string";
import path from "node:path";
import type { Plugin } from "vite";
const VALID_EXTENSIONS = new Set([".jsx", ".tsx"]);
/**
* Returns a Vite plugin that adds component data attributes for component selection.
*/
export default function componentTagger(): Plugin {
return {
name: "vite-plugin-component-tagger",
apply: () => true,
enforce: "pre",
async transform(code: string, id: string) {
try {
// Ignore non-jsx files and files inside node_modules
if (
!VALID_EXTENSIONS.has(path.extname(id)) ||
id.includes("node_modules")
)
return null;
const ast = parse(code, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
const ms = new MagicString(code);
const fileRelative = path.relative(process.cwd(), id);
walk(ast as any, {
enter(node: any) {
try {
if (node.type !== "JSXOpeningElement") return;
// ── 1. Extract the tag / component name ──────────────────────────────
if (node.name?.type !== "JSXIdentifier") return;
const tagName = node.name.name as string;
if (!tagName) return;
// Skip certain HTML elements that are too generic
const skipElements = new Set([
"html",
"head",
"body",
"script",
"style",
"meta",
"link",
"title",
]);
if (skipElements.has(tagName.toLowerCase())) return;
// ── 2. Check whether the tag already has data-component-id ───────────────
const alreadyTagged = node.attributes?.some(
(attr: any) =>
attr.type === "JSXAttribute" &&
attr.name?.name === "data-component-id",
);
if (alreadyTagged) return;
// ── 3. Build the id "relative/file.jsx:line:column" ─────────────────
const loc = node.loc?.start;
if (!loc) return;
const componentId = `${fileRelative}:${loc.line}:${loc.column}`;
// ── 4. Inject the attributes just after the tag name ────────────────
if (node.name.end != null) {
ms.appendLeft(
node.name.end,
` data-component-id="${componentId}" data-component-name="${tagName}"`,
);
}
} catch (error) {
console.warn(
`[component-tagger] Warning: Failed to process JSX node in ${id}:`,
error,
);
}
},
});
// If nothing changed bail out.
if (ms.toString() === code) return null;
return {
code: ms.toString(),
map: ms.generateMap({ hires: true }),
};
} catch (error) {
console.warn(
`[component-tagger] Warning: Failed to transform ${id}:`,
error,
);
return null;
}
},
};
}