update solid compiler
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user