220
src/compiler/index.ts
Normal file
220
src/compiler/index.ts
Normal 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}`)
|
Reference in New Issue
Block a user