221 lines
7.1 KiB
TypeScript
221 lines
7.1 KiB
TypeScript
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}`)
|