update solid compiler

This commit is contained in:
2026-07-15 11:28:24 -04:00
parent f1745b8a1b
commit 4e378842e4
8 changed files with 255 additions and 38 deletions

View File

@@ -4,8 +4,9 @@ package jsbundler
//
// Strategy (behavior-parity with babel-preset-solid, not byte-parity):
// - Static element structure is baked into an HTML template string cloned at
// runtime via _$template(); we always quote attrs and emit closing tags
// (valid HTML the browser parses to the same DOM babel's terser template does).
// runtime via _$template(); we always quote attrs and emit closing tags,
// except for void elements (br, img, input, ...) which never get one —
// see voidElements.
// - Dynamic children -> _$insert(parent, () => expr, marker). Every dynamic
// expression is wrapped in a thunk: correct and (for static exprs) merely an
// extra no-op effect. babel unwraps `f()` -> `f` as an optimization; we skip
@@ -461,11 +462,16 @@ func (g *solidGen) genElement(node *jsxNode) string {
return tv + "()"
}
var b strings.Builder
b.WriteString("(() => { var " + strings.Join(c.decls, ", ") + "; ")
b.WriteString("(() => { var ")
b.WriteString(strings.Join(c.decls, ", "))
b.WriteString("; ")
for _, op := range c.ops {
b.WriteString(op + "; ")
b.WriteString(op)
b.WriteString("; ")
}
b.WriteString("return " + root + "; })()")
b.WriteString("return ")
b.WriteString(root)
b.WriteString("; })()")
return b.String()
}
@@ -566,7 +572,7 @@ func exprIsEmpty(expr string) bool {
func (c *iife) assignChildRefs(node *jsxNode, parentRef string) map[int]string {
refs := map[int]string{}
// Which children need a ref?
need := func(i int, ch *jsxNode) bool {
need := func(_ int, ch *jsxNode) bool {
switch ch.kind {
case jsxElement:
return elementNeedsRef(ch)
@@ -644,6 +650,14 @@ func (c *iife) attr(ref string, a jsxAttr) {
// innerHTML icon content would never appear). Assign the property.
c.ops = append(c.ops, fmt.Sprintf("%s(() => %s.%s = %s)",
c.g.helper("effect"), ref, a.name, expr))
case booleanAttrs[a.name]:
// HTML boolean attributes are presence-based: setAttribute(name,
// String(value)) would emit `checked="false"`, which the browser
// parses as present (any string value) -> checked stays true. Use
// _$setBoolAttribute, which adds the attribute (empty) when truthy
// and removes it when falsy.
c.ops = append(c.ops, fmt.Sprintf("%s(() => %s(%s, %q, %s))",
c.g.helper("effect"), c.g.helper("setBoolAttribute"), ref, a.name, expr))
default:
c.ops = append(c.ops, fmt.Sprintf("%s(() => %s(%s, %q, %s))",
c.g.helper("effect"), c.g.helper("setAttribute"), ref, a.name, expr))
@@ -725,30 +739,44 @@ func (g *solidGen) genComponent(node *jsxNode) string {
}
// componentProps builds the props object: static attrs as plain values, dynamic
// attrs as reactive getters, and children as a `children` prop.
// attrs as reactive getters, children as a `children` prop, and any {...spread}
// threaded through the mergeProps helper (mirroring spread(), preserving source
// order so later props override earlier ones, matching babel).
func (g *solidGen) componentProps(node *jsxNode) string {
var parts []string
var args []string
var obj []string
flush := func() {
if len(obj) > 0 {
args = append(args, "{ "+strings.Join(obj, ", ")+" }")
obj = nil
}
}
for _, a := range node.attrs {
switch a.kind {
case attrStatic:
if a.boolt {
parts = append(parts, fmt.Sprintf("%s: true", propKey(a.name)))
obj = append(obj, fmt.Sprintf("%s: true", propKey(a.name)))
} else {
parts = append(parts, fmt.Sprintf("%s: %s", propKey(a.name), jsStringLit(a.value)))
obj = append(obj, fmt.Sprintf("%s: %s", propKey(a.name), jsStringLit(a.value)))
}
case attrExpr:
parts = append(parts, fmt.Sprintf("get %s() { return %s; }", propKey(a.name), g.compileExpr(a.expr)))
obj = append(obj, fmt.Sprintf("get %s() { return %s; }", propKey(a.name), g.compileExpr(a.expr)))
case attrSpread:
// milestone 3 (mergeProps)
flush()
args = append(args, g.compileExpr(a.expr))
}
}
if ch := g.childrenProp(node); ch != "" {
parts = append(parts, ch)
obj = append(obj, ch)
}
if len(parts) == 0 {
flush()
if len(args) == 0 {
return "{}"
}
return "{ " + strings.Join(parts, ", ") + " }"
if len(args) == 1 {
return args[0]
}
return fmt.Sprintf("%s(%s)", g.helper("mergeProps"), strings.Join(args, ", "))
}
// childrenProp builds a component's `children` prop entry. JSX children are
@@ -786,8 +814,8 @@ func (g *solidGen) genFragment(node *jsxNode) string {
var kids []*jsxNode
for i := range node.children {
ch := &node.children[i]
if ch.kind == jsxText && strings.TrimSpace(ch.text) == "" {
continue
if !renderedChild(ch) {
continue // drop whitespace-only text and comment-only expressions ({/* ... */})
}
kids = append(kids, ch)
}
@@ -807,6 +835,21 @@ func (g *solidGen) genFragment(node *jsxNode) string {
// ---- template building -----------------------------------------------------
// voidElements are HTML elements that can never have a closing tag or
// children. Writing one out as `<tag></tag>` isn't just redundant — for `br`
// specifically it's actively wrong: the HTML parser doesn't ignore a stray
// `</br>` like it does for other unmatched end tags, it's spec'd to act as
// *another* `<br>` start tag. That silently inserts an extra DOM node,
// shifting every subsequent .firstChild/.nextSibling in the compiled
// navigation chain by one and crashing template hydration. Matches the list
// solid-js/html's own VOID_ELEMENTS uses (frontend/vendor/solid-js/html).
var voidElements = map[string]bool{
"area": true, "base": true, "br": true, "col": true, "embed": true,
"hr": true, "img": true, "input": true, "keygen": true, "link": true,
"menuitem": true, "meta": true, "param": true, "source": true,
"track": true, "wbr": true,
}
func buildTemplate(node *jsxNode) string {
var sb strings.Builder
writeTemplate(node, &sb)
@@ -814,19 +857,28 @@ func buildTemplate(node *jsxNode) string {
}
func writeTemplate(node *jsxNode, sb *strings.Builder) {
sb.WriteString("<" + node.tag)
sb.WriteString("<")
sb.WriteString(node.tag)
if !hasSpread(node) { // with a spread, all attrs are applied at runtime
for _, a := range node.attrs {
if a.kind == attrStatic {
if a.boolt {
sb.WriteString(" " + a.name)
sb.WriteString(" ")
sb.WriteString(a.name)
} else {
sb.WriteString(" " + a.name + `="` + a.value + `"`)
sb.WriteString(" ")
sb.WriteString(a.name)
sb.WriteString(`="`)
sb.WriteString(a.value)
sb.WriteString(`"`)
}
}
}
}
sb.WriteString(">")
if voidElements[strings.ToLower(node.tag)] {
return // no children, no closing tag
}
for i := range node.children {
ch := &node.children[i]
switch ch.kind {
@@ -840,7 +892,9 @@ func writeTemplate(node *jsxNode, sb *strings.Builder) {
}
}
}
sb.WriteString("</" + node.tag + ">")
sb.WriteString("</")
sb.WriteString(node.tag)
sb.WriteString(">")
}
// annotateMarkers sets node.marker on dynamic children that need a `<!>` anchor:
@@ -904,6 +958,21 @@ func isStaticLiteral(e string) bool {
// direct property assignment rather than setAttribute.
var contentProps = map[string]bool{"innerHTML": true, "textContent": true, "innerText": true}
// booleanAttrs are HTML boolean attributes: presence means true regardless of
// the attribute's string value, so a naive setAttribute(name, String(false))
// (which emits e.g. checked="false") leaves the attribute present and the
// browser reads it as true. These must go through setBoolAttribute instead,
// which adds/removes the attribute based on the JS value's truthiness.
var booleanAttrs = map[string]bool{
"allowfullscreen": true, "async": true, "autofocus": true, "autoplay": true,
"checked": true, "controls": true, "default": true, "defer": true,
"disabled": true, "formnovalidate": true, "hidden": true, "indeterminate": true,
"ismap": true, "loop": true, "multiple": true, "muted": true,
"nomodule": true, "novalidate": true, "open": true, "playsinline": true,
"readonly": true, "required": true, "reversed": true, "seamless": true,
"selected": true,
}
func isEventAttr(name string) bool {
// An event handler is `on` followed by the event name; casing after `on` is
// irrelevant (onClick and onclick both mean click) — the name is lowercased