104 lines
3.3 KiB
TypeScript
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;
|
|
}
|
|
},
|
|
};
|
|
}
|