@dhiwise/component-tagger
Version:
Automatically annotate JSX components and HTML elements with data attributes
297 lines (259 loc) • 10.2 kB
JavaScript
;var F=Object.create;var y=Object.defineProperty;var M=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var I=Object.getPrototypeOf,O=Object.prototype.hasOwnProperty;var $=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let l of N(t))!O.call(e,l)&&l!==r&&y(e,l,{get:()=>t[l],enumerable:!(o=M(t,l))||o.enumerable});return e};var g=(e,t,r)=>(r=e!=null?F(I(e)):{},$(t||!e||!e.__esModule?y(r,"default",{value:e,enumerable:!0}):r,e));var a=g(require("fs")),c=g(require("path")),S=g(require("fast-glob"));var w=g(require("picomatch"));var h={red:e=>`\x1B[31m${e}\x1B[0m`,green:e=>`\x1B[32m${e}\x1B[0m`,yellow:e=>`\x1B[33m${e}\x1B[0m`,blue:e=>`\x1B[34m${e}\x1B[0m`,cyan:e=>`\x1B[36m${e}\x1B[0m`};function i(e,t){if(!t.verbose)return;let r=e;e.includes("Error")||e.includes("error")?r=h.red(e):e.includes("Tagged")||e.includes("Summary")?r=h.green(e):e.includes("Processing")||e.includes("started")?r=h.blue(e):r=h.yellow(e),console.log(`[component-tagger] ${r}`)}function v(e){return`
(function() {
// Configuration
const CONFIG = {
attributePrefix: '${e.attributePrefix||"data-component"}',
includeContentAttribute: ${e.includeContentAttribute!==!1},
maxContentLength: ${e.maxContentLength||2e3},
includeLegacyAttributes: ${e.includeLegacyAttributes!==!1},
includeElements: ${JSON.stringify(e.includeElements||[])},
excludeElements: ${JSON.stringify(e.excludeElements||[])},
};
const TAG_SHOULD_EXCLUDE = [
"base",
"object",
"link",
"meta",
"noscript",
"script",
"style",
"title",
"animate",
"animateMotion",
"animateTransform",
"circle",
"clipPath",
"defs",
"desc",
"ellipse",
"feBlend",
"feColorMatrix",
"feComponentTransfer",
"feComposite",
"feConvolveMatrix",
"feDiffuseLighting",
"feDisplacementMap",
"feDistantLight",
"feDropShadow",
"feFlood",
"feFuncA",
"feFuncB",
"feFuncG",
"feFuncR",
"feGaussianBlur",
"feImage",
"feMerge",
"feMergeNode",
"feMorphology",
"feOffset",
"fePointLight",
"feSpecularLighting",
"feSpotLight",
"feTile",
"feTurbulence",
"filter",
"foreignObject",
"g",
"image",
"line",
"linearGradient",
"marker",
"mask",
"metadata",
"mpath",
"path",
"pattern",
"polygon",
"polyline",
"radialGradient",
"rect",
"set",
"stop",
"switch",
"symbol",
"text",
"textPath",
"tspan",
"use",
"view",
"body",
"param",
]
// Map to track per-class indices for unique component IDs
const classIndexMap = new Map();
/**
* Sanitizes attribute values to prevent XSS
*/
function sanitizeAttributeValue(value) {
return String(value)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
}
/**
* Creates a component ID based on class name and index
*/
function createComponentId(el) {
const path = [];
let current = el;
while (current && current.parentElement) {
const siblings = Array.from(current.parentElement.children)
.filter(child => child.tagName);
const index = siblings.indexOf(current);
path.unshift(index);
current = current.parentElement;
if (current.tagName === 'BODY') break;
}
const line = path.join('-')
return sanitizeAttributeValue(line);
}
/**
* Extracts text content from an element
*/
function extractTextContent(element) {
let text = '';
for (const node of element.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
text += node.textContent.trim() + ' ';
}
}
return text.trim();
}
/**
* Extracts attributes from an element
*/
function extractAttributes(element) {
const attributes = {};
const defaultAttrs = [
'class', 'id', 'src', 'alt', 'href', 'type', 'name', 'value'
];
// Extract default attributes
for (const attr of defaultAttrs) {
if (element.hasAttribute(attr)) {
if (attr === 'class') {
attributes['className'] = element.getAttribute(attr);
} else {
attributes[attr] = element.getAttribute(attr);
}
}
}
const textContent = extractTextContent(element);
if (textContent) {
attributes['textContent'] = textContent;
}
return attributes;
}
/**
* Determines if an element should be tagged
*/
function shouldTagElement(element) {
// Skip non-element nodes
if (element.nodeType !== Node.ELEMENT_NODE) {
return false;
}
const tagName = element.tagName.toLowerCase();
if (CONFIG.includeElements.length > 0) {
return CONFIG.includeElements.includes(tagName);
}
if (CONFIG.excludeElements.includes(tagName) || TAG_SHOULD_EXCLUDE.includes(tagName)) {
return false;
}
return true;
}
/**
* Tags an element with component metadata
*/
function tagElement(element) {
// Skip if already tagged
if (element.hasAttribute(\`\${CONFIG.attributePrefix}-id\`)) {
return;
}
// Get element name
const elementName = element.tagName.toLowerCase();
// Create component ID
const componentId = createComponentId(element);
// Set ID attribute
element.setAttribute(\`\${CONFIG.attributePrefix}-id\`, componentId);
// Add legacy attributes if enabled
if (CONFIG.includeLegacyAttributes) {
element.setAttribute(\`\${CONFIG.attributePrefix}-path\`, window.location.pathname);
element.setAttribute(\`\${CONFIG.attributePrefix}-name\`, elementName);
element.setAttribute(\`\${CONFIG.attributePrefix}-file\`, window.location.pathname.split('/').pop());
}
// Add content attribute if enabled
if (CONFIG.includeContentAttribute) {
const attributes = extractAttributes(element);
const content = {
elementName,
...attributes
};
let encodedContent = encodeURIComponent(JSON.stringify(content));
// Truncate if needed
if (encodedContent.length > CONFIG.maxContentLength) {
encodedContent = encodedContent.substring(0, CONFIG.maxContentLength) + '...';
}
element.setAttribute(\`\${CONFIG.attributePrefix}-content\`, encodedContent);
}
}
/**
* Process all elements in the document
*/
function processAllElements() {
const elements = document.querySelectorAll('*');
let taggedCount = 0;
elements.forEach((element, index) => {
if (shouldTagElement(element)) {
tagElement(element, index);
taggedCount++;
}
});
}
/**
* Initialize the component tagger
*/
function initialize() {
// Wait for DOM to be fully loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', processAllElements);
} else {
processAllElements();
}
// Also process on dynamic content changes
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (shouldTagElement(node)) {
tagElement(node);
}
// Process child elements
const childElements = node.querySelectorAll('*');
for (const child of childElements) {
if (shouldTagElement(child)) {
tagElement(child);
}
}
}
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Start the component tagger
initialize();
})();
`}function E(e={}){let t={extensions:[".html",".htm"],verbose:!1,attributePrefix:"data-component",includeContentAttribute:!0,maxContentLength:1e3,includeLegacyAttributes:!0,excludeDirectories:[],processNodeModules:!1,outputDir:"public",injectorScriptName:"dhws-data-injector.js",inspectorScriptName:"dhws-web-inspector.js",errorTrackerScriptName:"dhws-error-tracker.js",...e},r=process.cwd(),o={totalFiles:0,processedFiles:0,injectedFiles:0,scriptPath:""};i("HTML component tagger started",t);let l=v(t),p=c.resolve(r,e.outputDir||"public");a.existsSync(p)||(i(`Creating directory: ${p}`,t),a.mkdirSync(p,{recursive:!0}));let T=e.injectorScriptName||"dhws-data-injector.js",f=c.join(p,T);i(`Writing script to: ${f}`,t),a.writeFileSync(f,l),o.scriptPath=f;let C=`**/*{${t.extensions.join(",")}}`,b=S.default.sync(C,{cwd:r,ignore:["node_modules/**",...t.excludeDirectories.map(d=>`${d}/**`)]});return o.totalFiles=b.length,i(`Found ${b.length} HTML files to process`,t),b.forEach(d=>{let x=c.join(r,d),m=a.readFileSync(x,"utf8");o.processedFiles++;let G=(()=>{let u=c.relative(c.dirname(x),f).replace(/\\/g,"/");return!u.startsWith("/")&&!u.startsWith("../")?`/${u}`:u})(),L=m.includes("dhws-dataInjector"),A=`<script id="dhws-dataInjector" src="${G}"></script>`;if(L)i(`Script already injected in: ${d}`,t);else{o.injectedFiles++;let u=`${A}
`;m.includes("</body>")?m=m.replace("</body>",`${u}</body>`):m+=`
${u}`,a.writeFileSync(x,m)}}),i(`HTML component tagger completed:
- Total files scanned: ${o.totalFiles}
- Files processed: ${o.processedFiles}
- Files with script injected: ${o.injectedFiles}
- Script location: ${o.scriptPath}`,t),o}var n=process.argv.slice(2),s={};for(let e=0;e<n.length;e++)n[e]==="--verbose"||n[e]==="-v"?s.verbose=!0:n[e]==="--output-dir"&&n.length>e+1?(s.outputDir=n[e+1],e++):n[e]==="--injector-script-name"&&n.length>e+1?(s.injectorScriptName=n[e+1],e++):n[e]==="--attribute-prefix"&&n.length>e+1?(s.attributePrefix=n[e+1],e++):n[e]==="--inspector-script-name"&&n.length>e+1&&(s.inspectorScriptName=n[e+1],e++);try{let e=E(s);i("[component-tagger] Summary:",s),i(` - Total HTML files found: ${e.totalFiles}`,s),i(` - Files processed: ${e.processedFiles}`,s),i(` - Files with script injected: ${e.injectedFiles}`,s),i(` - Script location: ${e.scriptPath}`,s)}catch(e){e instanceof Error&&console.error(`[component-tagger] Error: ${e.message}`),process.exit(1)}