@pyreon/compiler 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,7 +41,7 @@ interface CompilerWarning {
41
41
  /** Source file column number (0-based) */
42
42
  column: number;
43
43
  /** Warning code for filtering */
44
- code: "signal-call-in-jsx" | "missing-key-on-for" | "signal-in-static-prop";
44
+ code: 'signal-call-in-jsx' | 'missing-key-on-for' | 'signal-in-static-prop';
45
45
  }
46
46
  interface TransformResult {
47
47
  /** Transformed source code (JSX preserved, only expression containers modified) */
@@ -78,7 +78,7 @@ interface IslandInfo {
78
78
  hydrate: string;
79
79
  }
80
80
  interface ProjectContext {
81
- framework: "pyreon";
81
+ framework: 'pyreon';
82
82
  version: string;
83
83
  generatedAt: string;
84
84
  routes: RouteInfo[];
@@ -101,7 +101,7 @@ declare function generateContext(cwd: string): ProjectContext;
101
101
  * 2. CLI `pyreon doctor` (project-wide scanning)
102
102
  * 3. MCP server `migrate_react` / `validate` tools (AI agent integration)
103
103
  */
104
- type ReactDiagnosticCode = "react-import" | "react-dom-import" | "react-router-import" | "use-state" | "use-effect-mount" | "use-effect-deps" | "use-effect-no-deps" | "use-memo" | "use-callback" | "use-ref-dom" | "use-ref-box" | "use-reducer" | "use-layout-effect" | "memo-wrapper" | "forward-ref" | "class-name-prop" | "html-for-prop" | "on-change-input" | "dangerously-set-inner-html" | "dot-value-signal" | "array-map-jsx" | "key-on-for-child" | "create-context-import" | "use-context-import";
104
+ type ReactDiagnosticCode = 'react-import' | 'react-dom-import' | 'react-router-import' | 'use-state' | 'use-effect-mount' | 'use-effect-deps' | 'use-effect-no-deps' | 'use-memo' | 'use-callback' | 'use-ref-dom' | 'use-ref-box' | 'use-reducer' | 'use-layout-effect' | 'memo-wrapper' | 'forward-ref' | 'class-name-prop' | 'html-for-prop' | 'on-change-input' | 'dangerously-set-inner-html' | 'dot-value-signal' | 'array-map-jsx' | 'key-on-for-child' | 'create-context-import' | 'use-context-import';
105
105
  interface ReactDiagnostic {
106
106
  /** Machine-readable code for filtering and programmatic handling */
107
107
  code: ReactDiagnosticCode;
@@ -119,7 +119,7 @@ interface ReactDiagnostic {
119
119
  fixable: boolean;
120
120
  }
121
121
  interface MigrationChange {
122
- type: "replace" | "remove" | "add";
122
+ type: 'replace' | 'remove' | 'add';
123
123
  line: number;
124
124
  description: string;
125
125
  }
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@pyreon/compiler",
3
- "version": "0.11.5",
3
+ "version": "0.11.6",
4
4
  "description": "Template and JSX compiler for Pyreon",
5
+ "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/compiler#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/pyreon/pyreon/issues"
8
+ },
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "https://github.com/pyreon/pyreon.git",
9
13
  "directory": "packages/core/compiler"
10
14
  },
11
- "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/compiler#readme",
12
- "bugs": {
13
- "url": "https://github.com/pyreon/pyreon/issues"
14
- },
15
15
  "files": [
16
16
  "lib",
17
17
  "src",
18
18
  "README.md",
19
19
  "LICENSE"
20
20
  ],
21
- "sideEffects": false,
22
21
  "type": "module",
22
+ "sideEffects": false,
23
23
  "main": "./lib/index.js",
24
24
  "module": "./lib/index.js",
25
25
  "types": "./lib/types/index.d.ts",
@@ -30,18 +30,18 @@
30
30
  "types": "./lib/types/index.d.ts"
31
31
  }
32
32
  },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
33
36
  "scripts": {
34
37
  "build": "vl_rolldown_build",
35
38
  "dev": "vl_rolldown_build-watch",
36
39
  "test": "vitest run",
37
40
  "typecheck": "tsc --noEmit",
38
- "lint": "biome check .",
41
+ "lint": "oxlint .",
39
42
  "prepublishOnly": "bun run build"
40
43
  },
41
44
  "peerDependencies": {
42
45
  "typescript": ">=5.0.0"
43
- },
44
- "publishConfig": {
45
- "access": "public"
46
46
  }
47
47
  }
package/src/index.ts CHANGED
@@ -1,24 +1,19 @@
1
1
  // @pyreon/compiler — JSX reactive transform for Pyreon
2
2
 
3
- export type { CompilerWarning, TransformResult } from "./jsx"
4
- export { transformJSX } from "./jsx"
5
- export type {
6
- ComponentInfo,
7
- IslandInfo,
8
- ProjectContext,
9
- RouteInfo,
10
- } from "./project-scanner"
11
- export { generateContext } from "./project-scanner"
3
+ export type { CompilerWarning, TransformResult } from './jsx'
4
+ export { transformJSX } from './jsx'
5
+ export type { ComponentInfo, IslandInfo, ProjectContext, RouteInfo } from './project-scanner'
6
+ export { generateContext } from './project-scanner'
12
7
  export type {
13
8
  ErrorDiagnosis,
14
9
  MigrationChange,
15
10
  MigrationResult,
16
11
  ReactDiagnostic,
17
12
  ReactDiagnosticCode,
18
- } from "./react-intercept"
13
+ } from './react-intercept'
19
14
  export {
20
15
  detectReactPatterns,
21
16
  diagnoseError,
22
17
  hasReactPatterns,
23
18
  migrateReactCode,
24
- } from "./react-intercept"
19
+ } from './react-intercept'
package/src/jsx.ts CHANGED
@@ -33,7 +33,7 @@
33
33
  * granularity. Fine-grained nested wrapping is planned for a future pass.
34
34
  */
35
35
 
36
- import ts from "typescript"
36
+ import ts from 'typescript'
37
37
 
38
38
  export interface CompilerWarning {
39
39
  /** Warning message */
@@ -43,7 +43,7 @@ export interface CompilerWarning {
43
43
  /** Source file column number (0-based) */
44
44
  column: number
45
45
  /** Warning code for filtering */
46
- code: "signal-call-in-jsx" | "missing-key-on-for" | "signal-in-static-prop"
46
+ code: 'signal-call-in-jsx' | 'missing-key-on-for' | 'signal-in-static-prop'
47
47
  }
48
48
 
49
49
  export interface TransformResult {
@@ -56,39 +56,39 @@ export interface TransformResult {
56
56
  }
57
57
 
58
58
  // Props that should never be wrapped in a reactive getter
59
- const SKIP_PROPS = new Set(["key", "ref"])
59
+ const SKIP_PROPS = new Set(['key', 'ref'])
60
60
  // Event handler pattern: onClick, onInput, onMouseEnter, …
61
61
  const EVENT_RE = /^on[A-Z]/
62
62
  // Events delegated to the container — must match runtime DELEGATED_EVENTS set
63
63
  const DELEGATED_EVENTS = new Set([
64
- "click",
65
- "dblclick",
66
- "contextmenu",
67
- "focusin",
68
- "focusout",
69
- "input",
70
- "change",
71
- "keydown",
72
- "keyup",
73
- "mousedown",
74
- "mouseup",
75
- "mousemove",
76
- "mouseover",
77
- "mouseout",
78
- "pointerdown",
79
- "pointerup",
80
- "pointermove",
81
- "pointerover",
82
- "pointerout",
83
- "touchstart",
84
- "touchend",
85
- "touchmove",
86
- "submit",
64
+ 'click',
65
+ 'dblclick',
66
+ 'contextmenu',
67
+ 'focusin',
68
+ 'focusout',
69
+ 'input',
70
+ 'change',
71
+ 'keydown',
72
+ 'keyup',
73
+ 'mousedown',
74
+ 'mouseup',
75
+ 'mousemove',
76
+ 'mouseover',
77
+ 'mouseout',
78
+ 'pointerdown',
79
+ 'pointerup',
80
+ 'pointermove',
81
+ 'pointerover',
82
+ 'pointerout',
83
+ 'touchstart',
84
+ 'touchend',
85
+ 'touchmove',
86
+ 'submit',
87
87
  ])
88
88
 
89
- export function transformJSX(code: string, filename = "input.tsx"): TransformResult {
89
+ export function transformJSX(code: string, filename = 'input.tsx'): TransformResult {
90
90
  const scriptKind =
91
- filename.endsWith(".tsx") || filename.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TSX // default to TSX so JSX is always parsed
91
+ filename.endsWith('.tsx') || filename.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TSX // default to TSX so JSX is always parsed
92
92
 
93
93
  const sf = ts.createSourceFile(
94
94
  filename,
@@ -102,7 +102,7 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
102
102
  const replacements: Replacement[] = []
103
103
  const warnings: CompilerWarning[] = []
104
104
 
105
- function warn(node: ts.Node, message: string, warnCode: CompilerWarning["code"]): void {
105
+ function warn(node: ts.Node, message: string, warnCode: CompilerWarning['code']): void {
106
106
  const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf))
107
107
  warnings.push({ message, line: line + 1, column: character, code: warnCode })
108
108
  }
@@ -169,25 +169,25 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
169
169
  /** Emit warnings for common JSX mistakes (e.g. <For> without by). */
170
170
  function checkForWarnings(node: ts.JsxElement | ts.JsxSelfClosingElement): void {
171
171
  const opening = ts.isJsxElement(node) ? node.openingElement : node
172
- const tagName = ts.isIdentifier(opening.tagName) ? opening.tagName.text : ""
173
- if (tagName !== "For") return
172
+ const tagName = ts.isIdentifier(opening.tagName) ? opening.tagName.text : ''
173
+ if (tagName !== 'For') return
174
174
  const hasBy = opening.attributes.properties.some(
175
- (p) => ts.isJsxAttribute(p) && ts.isIdentifier(p.name) && p.name.text === "by",
175
+ (p) => ts.isJsxAttribute(p) && ts.isIdentifier(p.name) && p.name.text === 'by',
176
176
  )
177
177
  if (!hasBy) {
178
178
  warn(
179
179
  opening.tagName,
180
180
  `<For> without a "by" prop will use index-based diffing, which is slower and may cause bugs with stateful children. Add by={(item) => item.id} for efficient keyed reconciliation.`,
181
- "missing-key-on-for",
181
+ 'missing-key-on-for',
182
182
  )
183
183
  }
184
184
  }
185
185
 
186
186
  /** Handle a JSX attribute node — wrap or hoist its value if needed. */
187
187
  function handleJsxAttribute(node: ts.JsxAttribute): void {
188
- const name = ts.isIdentifier(node.name) ? node.name.text : ""
188
+ const name = ts.isIdentifier(node.name) ? node.name.text : ''
189
189
  const openingEl = node.parent.parent as ts.JsxOpeningElement | ts.JsxSelfClosingElement
190
- const tagName = ts.isIdentifier(openingEl.tagName) ? openingEl.tagName.text : ""
190
+ const tagName = ts.isIdentifier(openingEl.tagName) ? openingEl.tagName.text : ''
191
191
  const isComponentElement =
192
192
  tagName.length > 0 && tagName.charAt(0) !== tagName.charAt(0).toLowerCase()
193
193
  if (isComponentElement) return
@@ -245,24 +245,24 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
245
245
  lastPos = r.end
246
246
  }
247
247
  parts.push(code.slice(lastPos))
248
- let result = parts.join("")
248
+ let result = parts.join('')
249
249
 
250
250
  // Prepend module-scope hoisted static VNode declarations
251
251
  if (hoists.length > 0) {
252
- const preamble = hoists.map((h) => `const ${h.name} = /*@__PURE__*/ ${h.text}\n`).join("")
252
+ const preamble = hoists.map((h) => `const ${h.name} = /*@__PURE__*/ ${h.text}\n`).join('')
253
253
  result = preamble + result
254
254
  }
255
255
 
256
256
  // Prepend template imports if _tpl() was emitted
257
257
  if (needsTplImport) {
258
- const runtimeDomImports = ["_tpl"]
259
- if (needsBindDirectImportGlobal) runtimeDomImports.push("_bindDirect")
260
- if (needsBindTextImportGlobal) runtimeDomImports.push("_bindText")
258
+ const runtimeDomImports = ['_tpl']
259
+ if (needsBindDirectImportGlobal) runtimeDomImports.push('_bindDirect')
260
+ if (needsBindTextImportGlobal) runtimeDomImports.push('_bindText')
261
261
  const reactivityImports = needsBindImportGlobal
262
262
  ? `\nimport { _bind } from "@pyreon/reactivity";`
263
- : ""
263
+ : ''
264
264
  result =
265
- `import { ${runtimeDomImports.join(", ")} } from "@pyreon/runtime-dom";${reactivityImports}\n` +
265
+ `import { ${runtimeDomImports.join(', ')} } from "@pyreon/runtime-dom";${reactivityImports}\n` +
266
266
  result
267
267
  }
268
268
 
@@ -274,7 +274,7 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
274
274
  function hasBailAttr(node: ts.JsxElement | ts.JsxSelfClosingElement): boolean {
275
275
  for (const attr of jsxAttrs(node)) {
276
276
  if (ts.isJsxSpreadAttribute(attr)) return true
277
- if (ts.isJsxAttribute(attr) && ts.isIdentifier(attr.name) && attr.name.text === "key")
277
+ if (ts.isJsxAttribute(attr) && ts.isIdentifier(attr.name) && attr.name.text === 'key')
278
278
  return true
279
279
  }
280
280
  return false
@@ -354,7 +354,7 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
354
354
 
355
355
  /** Resolve the variable name for an element given its accessor path. */
356
356
  function resolveElementVar(accessor: string, hasDynamic: boolean): string {
357
- if (accessor === "__root") return "__root"
357
+ if (accessor === '__root') return '__root'
358
358
  if (hasDynamic) {
359
359
  const v = nextVar()
360
360
  bindLines.push(`const ${v} = ${accessor}`)
@@ -372,7 +372,7 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
372
372
 
373
373
  /** Emit event handler bind line — delegated (expando) or addEventListener. */
374
374
  function emitEventListener(attr: ts.JsxAttribute, attrName: string, varName: string): void {
375
- const eventName = (attrName[2] ?? "").toLowerCase() + attrName.slice(3)
375
+ const eventName = (attrName[2] ?? '').toLowerCase() + attrName.slice(3)
376
376
  if (!attr.initializer || !ts.isJsxExpression(attr.initializer)) return
377
377
  if (!attr.initializer.expression) return
378
378
  const handler = sliceExpr(attr.initializer.expression)
@@ -390,7 +390,7 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
390
390
  if (ts.isStringLiteral(exprNode)) return ` ${htmlAttrName}="${escapeHtmlAttr(exprNode.text)}"`
391
391
  if (ts.isNumericLiteral(exprNode)) return ` ${htmlAttrName}="${exprNode.text}"`
392
392
  if (exprNode.kind === ts.SyntaxKind.TrueKeyword) return ` ${htmlAttrName}`
393
- return "" // false/null/undefined → omit
393
+ return '' // false/null/undefined → omit
394
394
  }
395
395
 
396
396
  /**
@@ -431,8 +431,8 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
431
431
 
432
432
  /** Build a setter expression for an attribute. */
433
433
  function attrSetter(htmlAttrName: string, varName: string, expr: string): string {
434
- if (htmlAttrName === "class") return `${varName}.className = ${expr}`
435
- if (htmlAttrName === "style") return `${varName}.style.cssText = ${expr}`
434
+ if (htmlAttrName === 'class') return `${varName}.className = ${expr}`
435
+ if (htmlAttrName === 'style') return `${varName}.style.cssText = ${expr}`
436
436
  return `${varName}.setAttribute("${htmlAttrName}", ${expr})`
437
437
  }
438
438
 
@@ -456,9 +456,9 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
456
456
  needsBindDirectImport = true
457
457
  const d = nextDisp()
458
458
  const updater =
459
- htmlAttrName === "class"
459
+ htmlAttrName === 'class'
460
460
  ? `(v) => { ${varName}.className = v == null ? "" : String(v) }`
461
- : htmlAttrName === "style"
461
+ : htmlAttrName === 'style'
462
462
  ? `(v) => { if (typeof v === "string") ${varName}.style.cssText = v; else if (v) Object.assign(${varName}.style, v) }`
463
463
  : `(v) => { ${varName}.setAttribute("${htmlAttrName}", v == null ? "" : String(v)) }`
464
464
  bindLines.push(`const ${d} = _bindDirect(${directRef}, ${updater})`)
@@ -478,18 +478,18 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
478
478
  if (staticHtml !== null) return staticHtml
479
479
 
480
480
  // style={{...}} → Object.assign(el.style, {...}) for object expressions
481
- if (htmlAttrName === "style" && ts.isObjectLiteralExpression(exprNode)) {
481
+ if (htmlAttrName === 'style' && ts.isObjectLiteralExpression(exprNode)) {
482
482
  bindLines.push(`Object.assign(${varName}.style, ${sliceExpr(exprNode)})`)
483
- return ""
483
+ return ''
484
484
  }
485
485
 
486
486
  emitDynamicAttr(sliceExpr(exprNode), exprNode, htmlAttrName, varName)
487
- return ""
487
+ return ''
488
488
  }
489
489
 
490
490
  /** Emit side-effects for special attrs (ref, event). Returns true if handled. */
491
491
  function tryEmitSpecialAttr(attr: ts.JsxAttribute, attrName: string, varName: string): boolean {
492
- if (attrName === "ref") {
492
+ if (attrName === 'ref') {
493
493
  emitRef(attr, varName)
494
494
  return true
495
495
  }
@@ -511,21 +511,21 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
511
511
  return ` ${htmlAttrName}="${escapeHtmlAttr(attr.initializer.text)}"`
512
512
  if (ts.isJsxExpression(attr.initializer) && attr.initializer.expression)
513
513
  return emitAttrExpression(attr.initializer.expression, htmlAttrName, varName)
514
- return ""
514
+ return ''
515
515
  }
516
516
 
517
517
  /** Process a single attribute, returning HTML to append. */
518
518
  function processOneAttr(attr: ts.JsxAttributeLike, varName: string): string {
519
- if (!ts.isJsxAttribute(attr)) return ""
520
- const attrName = ts.isIdentifier(attr.name) ? attr.name.text : ""
521
- if (attrName === "key") return ""
522
- if (tryEmitSpecialAttr(attr, attrName, varName)) return ""
519
+ if (!ts.isJsxAttribute(attr)) return ''
520
+ const attrName = ts.isIdentifier(attr.name) ? attr.name.text : ''
521
+ if (attrName === 'key') return ''
522
+ if (tryEmitSpecialAttr(attr, attrName, varName)) return ''
523
523
  return attrInitializerToHtml(attr, JSX_TO_HTML_ATTR[attrName] ?? attrName, varName)
524
524
  }
525
525
 
526
526
  /** Process all attributes on an element, returning the HTML attribute string. */
527
527
  function processAttrs(el: ts.JsxElement | ts.JsxSelfClosingElement, varName: string): string {
528
- let htmlAttrs = ""
528
+ let htmlAttrs = ''
529
529
  for (const attr of jsxAttrs(el)) htmlAttrs += processOneAttr(attr, varName)
530
530
  return htmlAttrs
531
531
  }
@@ -558,7 +558,7 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
558
558
  // Collected into the combined _bind at the end
559
559
  reactiveBindExprs.push(`${tVar}.data = ${expr}`)
560
560
  }
561
- return needsPlaceholder ? "<!>" : ""
561
+ return needsPlaceholder ? '<!>' : ''
562
562
  }
563
563
 
564
564
  /** Emit bind lines for a static text expression child. */
@@ -575,10 +575,10 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
575
575
  bindLines.push(
576
576
  `${parentRef}.replaceChild(${tVar}, ${parentRef}.childNodes[${childNodeIdx}])`,
577
577
  )
578
- return "<!>"
578
+ return '<!>'
579
579
  }
580
580
  bindLines.push(`${varName}.textContent = ${expr}`)
581
- return ""
581
+ return ''
582
582
  }
583
583
 
584
584
  /** Process a single flat child, returning the HTML contribution or null on failure. */
@@ -590,8 +590,8 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
590
590
  useMultiExpr: boolean,
591
591
  childNodeIdx: number,
592
592
  ): string | null {
593
- if (child.kind === "text") return escapeHtmlText(child.text)
594
- if (child.kind === "element") {
593
+ if (child.kind === 'text') return escapeHtmlText(child.text)
594
+ if (child.kind === 'element') {
595
595
  const childAccessor = useMixed
596
596
  ? `${parentRef}.childNodes[${childNodeIdx}]`
597
597
  : `${parentRef}.children[${child.elemIdx}]`
@@ -617,9 +617,9 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
617
617
  function processChildren(el: ts.JsxElement, varName: string, accessor: string): string | null {
618
618
  const flatChildren = flattenChildren(el.children)
619
619
  const { useMixed, useMultiExpr } = analyzeChildren(flatChildren)
620
- const parentRef = accessor === "__root" ? "__root" : varName
620
+ const parentRef = accessor === '__root' ? '__root' : varName
621
621
 
622
- let html = ""
622
+ let html = ''
623
623
  let childNodeIdx = 0
624
624
 
625
625
  for (const child of flatChildren) {
@@ -661,14 +661,14 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
661
661
  return html
662
662
  }
663
663
 
664
- const html = processElement(node, "__root")
664
+ const html = processElement(node, '__root')
665
665
  if (html === null) return null
666
666
 
667
667
  if (needsBindTextImport) needsBindTextImportGlobal = true
668
668
  if (needsBindDirectImport) needsBindDirectImportGlobal = true
669
669
 
670
670
  // Build bind function body
671
- const escaped = html.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
671
+ const escaped = html.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
672
672
 
673
673
  // Emit combined _bind for reactive attribute/text expressions that
674
674
  // weren't handled by _bindText. This merges N separate _bind calls into
@@ -679,7 +679,7 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
679
679
  if (reactiveBindExprs.length > 0) {
680
680
  needsBindImportGlobal = true
681
681
  const combinedName = nextDisp()
682
- const combinedBody = reactiveBindExprs.join("; ")
682
+ const combinedBody = reactiveBindExprs.join('; ')
683
683
  bindLines.push(`const ${combinedName} = _bind(() => { ${combinedBody} })`)
684
684
  }
685
685
 
@@ -687,11 +687,11 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
687
687
  return `_tpl("${escaped}", () => null)`
688
688
  }
689
689
 
690
- let body = bindLines.map((l) => ` ${l}`).join("\n")
690
+ let body = bindLines.map((l) => ` ${l}`).join('\n')
691
691
  if (disposerNames.length > 0) {
692
- body += `\n return () => { ${disposerNames.map((d) => `${d}()`).join("; ")} }`
692
+ body += `\n return () => { ${disposerNames.map((d) => `${d}()`).join('; ')} }`
693
693
  } else {
694
- body += "\n return null"
694
+ body += '\n return null'
695
695
  }
696
696
 
697
697
  return `_tpl("${escaped}", (__root) => {\n${body}\n})`
@@ -699,9 +699,9 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
699
699
 
700
700
  /** Flat child descriptor for template children processing */
701
701
  type FlatChild =
702
- | { kind: "text"; text: string }
703
- | { kind: "element"; node: ts.JsxElement | ts.JsxSelfClosingElement; elemIdx: number }
704
- | { kind: "expression"; expression: ts.Expression }
702
+ | { kind: 'text'; text: string }
703
+ | { kind: 'element'; node: ts.JsxElement | ts.JsxSelfClosingElement; elemIdx: number }
704
+ | { kind: 'expression'; expression: ts.Expression }
705
705
 
706
706
  /** Classify a single JSX child into a FlatChild descriptor. */
707
707
  function classifyJsxChild(
@@ -711,16 +711,16 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
711
711
  recurse: (kids: ts.NodeArray<ts.JsxChild>) => void,
712
712
  ): void {
713
713
  if (ts.isJsxText(child)) {
714
- const trimmed = child.text.replace(/\n\s*/g, "").trim()
715
- if (trimmed) out.push({ kind: "text", text: trimmed })
714
+ const trimmed = child.text.replace(/\n\s*/g, '').trim()
715
+ if (trimmed) out.push({ kind: 'text', text: trimmed })
716
716
  return
717
717
  }
718
718
  if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
719
- out.push({ kind: "element", node: child, elemIdx: elemIdxRef.value++ })
719
+ out.push({ kind: 'element', node: child, elemIdx: elemIdxRef.value++ })
720
720
  return
721
721
  }
722
722
  if (ts.isJsxExpression(child)) {
723
- if (child.expression) out.push({ kind: "expression", expression: child.expression })
723
+ if (child.expression) out.push({ kind: 'expression', expression: child.expression })
724
724
  return
725
725
  }
726
726
  if (ts.isJsxFragment(child)) recurse(child.children)
@@ -747,17 +747,17 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
747
747
  useMixed: boolean
748
748
  useMultiExpr: boolean
749
749
  } {
750
- const hasElem = flatChildren.some((c) => c.kind === "element")
751
- const hasNonElem = flatChildren.some((c) => c.kind !== "element")
752
- const exprCount = flatChildren.filter((c) => c.kind === "expression").length
750
+ const hasElem = flatChildren.some((c) => c.kind === 'element')
751
+ const hasNonElem = flatChildren.some((c) => c.kind !== 'element')
752
+ const exprCount = flatChildren.filter((c) => c.kind === 'expression').length
753
753
  return { useMixed: hasElem && hasNonElem, useMultiExpr: exprCount > 1 }
754
754
  }
755
755
 
756
756
  /** Check if a single attribute is dynamic (has ref, event, or non-static expression). */
757
757
  function attrIsDynamic(attr: ts.JsxAttributeLike): boolean {
758
758
  if (!ts.isJsxAttribute(attr)) return false
759
- const name = ts.isIdentifier(attr.name) ? attr.name.text : ""
760
- if (name === "ref") return true
759
+ const name = ts.isIdentifier(attr.name) ? attr.name.text : ''
760
+ if (name === 'ref') return true
761
761
  if (EVENT_RE.test(name)) return true
762
762
  if (!attr.initializer || !ts.isJsxExpression(attr.initializer)) return false
763
763
  const expr = attr.initializer.expression
@@ -781,7 +781,7 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
781
781
  /** Get tag name string */
782
782
  function jsxTagName(node: ts.JsxElement | ts.JsxSelfClosingElement): string {
783
783
  const tag = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName
784
- return ts.isIdentifier(tag) ? tag.text : ""
784
+ return ts.isIdentifier(tag) ? tag.text : ''
785
785
  }
786
786
 
787
787
  /** Get attribute list */
@@ -797,25 +797,25 @@ export function transformJSX(code: string, filename = "input.tsx"): TransformRes
797
797
  // ─── Template constants ──────────────────────────────────────────────────────
798
798
 
799
799
  const VOID_ELEMENTS = new Set([
800
- "area",
801
- "base",
802
- "br",
803
- "col",
804
- "embed",
805
- "hr",
806
- "img",
807
- "input",
808
- "link",
809
- "meta",
810
- "param",
811
- "source",
812
- "track",
813
- "wbr",
800
+ 'area',
801
+ 'base',
802
+ 'br',
803
+ 'col',
804
+ 'embed',
805
+ 'hr',
806
+ 'img',
807
+ 'input',
808
+ 'link',
809
+ 'meta',
810
+ 'param',
811
+ 'source',
812
+ 'track',
813
+ 'wbr',
814
814
  ])
815
815
 
816
816
  const JSX_TO_HTML_ATTR: Record<string, string> = {
817
- className: "class",
818
- htmlFor: "for",
817
+ className: 'class',
818
+ htmlFor: 'for',
819
819
  }
820
820
 
821
821
  function isLowerCase(s: string): boolean {
@@ -830,11 +830,11 @@ function containsJSXInExpr(node: ts.Node): boolean {
830
830
  }
831
831
 
832
832
  function escapeHtmlAttr(s: string): string {
833
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;")
833
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;')
834
834
  }
835
835
 
836
836
  function escapeHtmlText(s: string): string {
837
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;")
837
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;')
838
838
  }
839
839
 
840
840
  // ─── Static JSX analysis ──────────────────────────────────────────────────────