import ts from 'typescript' import fs from 'fs' const input = 'src/components/button/index.ts' // Input file path const output = 'src/components/button/Button.transformed.ts' // Output file path for transformed code interface SimpleHTMLElement { name: string attrs?: Record childs?: Array } // Read input source code const src = fs.readFileSync(input, 'utf-8') // Create a TypeScript source file from input source code string const sourceFile = ts.createSourceFile(input, src, ts.ScriptTarget.Latest, true) function parseName(name: string, values: Array): string | ts.Expression { const match = /^\u0000(\d+)\u0000$/.exec(name) if (match) { const idx = Number(match[1]) return values[idx] } return name } function parseAttrs(attrString: string, values: Array): Record { const attrs: Record = {} console.log(attrString) // \u00001\u0000 const parts = attrString.match(/(?:[^.*=]+=(?:"[^"]*"|'[^']*'|[^.*"']+))/g) ?? [] for (const part of parts) { if (!part) { continue } const [key, raw] = part.trim().split('=') as [string, string] if (!raw) { continue } const val = raw.replace(/^['"]?|['"]?$/g, '') const match = /^\u0000(\d+)\u0000$/.exec(val) if (match) { const idx = Number(match[1]) attrs[key] = values[idx] } else { attrs[key] = val } } return attrs } /** * Naively parse a TemplateLiteral assumed to contain a simple html tagged template string. * Extracts: * - tagName (e.g. 'button') * - attrs as key-value pairs (only "class" attribute parsed here) * - children: a mix of static strings and interpolated expressions * @param template TemplateLiteral node from the AST (expects TemplateExpression) * @returns parsed SimpleHTMLElement parts or null if parsing fails */ function extractHtmlData(template: ts.TemplateLiteral): { tagName: string | ts.Expression attrs: Record children: (ts.Expression | ts.StringLiteral)[] } | null { // We only handle TemplateExpression (with interpolations) here if (!ts.isTemplateExpression(template)) return null // Collect all template literal strings and expressions separately const fullStrings: string[] = [template.head.text] const exprs: ts.Expression[] = [] // Each template span has an expression and a literal text after it let idx = 0 for (const span of template.templateSpans) { exprs.push(span.expression) // interpolation `${...}` fullStrings.push(`\u0000${idx++}\u0000`) // literal after interpolation fullStrings.push(span.literal.text) // literal after interpolation } console.log('aaa', exprs, fullStrings.join('').replaceAll(/\s*\n\s*/g, '').trim()) // Join all literal parts to parse the opening tag naively with regex const fullHtml = fullStrings.join('').replaceAll(/\s*\n\s*/g, '').trim() const selfClose = /^<([a-zA-Z0-9-\u0000]+)([^>]*)\/>$/.exec(fullHtml) if (selfClose) { const [, tagName, rawAttrs] = selfClose const attrs = parseAttrs(rawAttrs, exprs) return { tagName: parseName(tagName, exprs), attrs: Object.keys(attrs).length ? attrs : {}, children: [] } } const fullMatch = /^<([a-zA-Z0-9-\u0000]+)([^>]*)>([\s\S]*)<\/.*>$/.exec(fullHtml) if (!fullMatch) { throw new Error(`Invalid HTML: ${fullHtml}`) } const [, tagName, rawAttrs, inner] = fullMatch const attrs = parseAttrs(rawAttrs, exprs) const children: (ts.Expression | ts.StringLiteral)[] = [] const startsWithShit = inner.startsWith('\u0000') inner.split(/\u0000/).forEach((part, i) => { if (i % 2 === (startsWithShit ? 1 : 0) && exprs[Number.parseInt(part)]) { children.push(exprs[Number.parseInt(part)]) } else if (part) { children.push(ts.factory.createStringLiteral(part)) } }) // // Build children array alternating strings and expressions from template parts // const children: (ts.Expression | ts.StringLiteral)[] = [] // for (let i = 0; i < fullStrings.length; i++) { // console.log(fullStrings[i], exprs[i]) // // Add string chunk if not empty // if (inner[i]) { // children.push(ts.factory.createStringLiteral(fullStrings[i])) // } // // Add expression if exists (one less than strings length) // if (exprs[i]) { // children.push(exprs[i]) // } // } return { tagName: parseName(tagName, exprs), attrs, children, } } /** * TypeScript Transformer Factory * Transforms `html` tagged template literals into SimpleHTMLElement object literals. * Only handles simple cases with static tags and attributes and interpolated children. */ const transformer: ts.TransformerFactory = context => { return rootNode => { // Recursive AST visitor function function visit(node: ts.Node): ts.Node { // Check if node is a tagged template expression with tag name 'html' if ( ts.isTaggedTemplateExpression(node) && node.tag.getText() === 'html' && ts.isTemplateExpression(node.template) ) { // Extract html data from template literal const parsed = extractHtmlData(node.template) // console.log(parsed) if (!parsed) return node // fallback: no transform if parsing failed // Create AST properties for the SimpleHTMLElement object literal // console.log(parsed.tagName) const props: ts.ObjectLiteralElementLike[] = [ // name property: tag name as string literal ts.factory.createPropertyAssignment( 'name', typeof parsed.tagName === 'string' ? ts.factory.createStringLiteral(parsed.tagName) : parsed.tagName ), ] // Add attrs property if any attributes found if (Object.keys(parsed.attrs).length > 0) { props.push( ts.factory.createPropertyAssignment( 'attrs', ts.factory.createObjectLiteralExpression( // Create key-value pairs for each attribute Object.entries(parsed.attrs).map(([k, v]) => ts.factory.createPropertyAssignment(k, typeof v === 'string' ? ts.factory.createStringLiteral(v) : v) ), true // multiline formatting ) ) ) } // Add childs property if any children (strings or expressions) if (parsed.children.length > 0) { props.push( ts.factory.createPropertyAssignment( 'childs', ts.factory.createArrayLiteralExpression(parsed.children, true) ) ) } // Return the object literal AST node that replaces the `html` tagged template call return ts.factory.createObjectLiteralExpression(props, true) } // Recursively visit children nodes return ts.visitEachChild(node, visit, context) } // Start AST traversal from root return ts.visitNode(rootNode, visit) as ts.SourceFile } } // Run the transform on the source file with our custom transformer const result = ts.transform(sourceFile, [transformer]) // Prepare printer to output transformed AST back to TypeScript code string const printer = ts.createPrinter() // Get transformed source code text const outputText = printer.printFile(result.transformed[0]) // Write the transformed source code to output file fs.writeFileSync(output, outputText) console.log(`✅ Wrote transformed file to: ${output}`)