Signed-off-by: Avior <git@avior.me>
This commit is contained in:
2025-06-30 22:41:18 +02:00
commit 86b37af716
16 changed files with 1526 additions and 0 deletions

220
src/compiler/index.ts Normal file
View File

@ -0,0 +1,220 @@
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<string, string | any>
childs?: Array<SimpleHTMLElement | string>
}
// 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<ts.Expression>): 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<ts.Expression>): Record<string, string | ts.Expression> {
const attrs: Record<string, string | ts.Expression> = {}
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<string, string | ts.Expression>
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<ts.SourceFile> = 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}`)