UNPKG

@dhiwise/component-tagger

Version:

Automatically annotate JSX components and HTML elements with data attributes

297 lines (259 loc) 10.2 kB
#!/usr/bin/env node "use strict";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, '&amp;') .replace(/"/g, '&quot;') .replace(/'/g, '&apos;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;'); } /** * 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)}