Initial add backend stuff
This commit is contained in:
399
bundler/compile_solid.go
Normal file
399
bundler/compile_solid.go
Normal file
@@ -0,0 +1,399 @@
|
||||
package bundler
|
||||
|
||||
// Go-native Solid JSX compiler (replaces babel-preset-solid running in goja).
|
||||
//
|
||||
// Pipeline: esbuild strips TS types (JSX preserved), then this package parses the
|
||||
// JSX trees out of the JS and rewrites each into Solid's dom-expressions runtime
|
||||
// output (template cloning + fine-grained _$insert/_$effect/_$createComponent).
|
||||
// JS expressions inside `{...}` are captured as opaque strings and, where they
|
||||
// may contain nested JSX, recompiled recursively — so we never need a full JS
|
||||
// parser, only a JSX-aware scanner.
|
||||
//
|
||||
// This file is the PARSER (JSX text -> tree). Codegen lives in compile_solid_gen.go.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type jsxKind int
|
||||
|
||||
const (
|
||||
jsxElement jsxKind = iota // lowercase tag -> real DOM element (templated)
|
||||
jsxComponent // Capitalized/dotted tag -> _$createComponent
|
||||
jsxFragment // <>...</>
|
||||
jsxText // literal character data between tags
|
||||
jsxExpr // {expr} — raw JS, may itself contain JSX
|
||||
)
|
||||
|
||||
type attrKind int
|
||||
|
||||
const (
|
||||
attrStatic attrKind = iota // name="literal" or bare boolean
|
||||
attrExpr // name={expr}
|
||||
attrSpread // {...expr}
|
||||
)
|
||||
|
||||
type jsxAttr struct {
|
||||
kind attrKind
|
||||
name string
|
||||
value string // literal string value (attrStatic with a value)
|
||||
expr string // JS expression (attrExpr) or spread source (attrSpread)
|
||||
boolt bool // bare boolean attribute (attrStatic, no `=`)
|
||||
}
|
||||
|
||||
type jsxNode struct {
|
||||
kind jsxKind
|
||||
tag string
|
||||
attrs []jsxAttr
|
||||
children []jsxNode
|
||||
text string // jsxText
|
||||
expr string // jsxExpr (raw, may contain nested JSX)
|
||||
|
||||
marker bool // codegen: this dynamic child needs a `<!>` insert anchor
|
||||
}
|
||||
|
||||
// parseJSX parses a JSX element/fragment beginning at src[i] == '<'. It returns
|
||||
// the node and the index just past the element's closing `>`.
|
||||
func parseJSX(src string, i int) (jsxNode, int, error) {
|
||||
n := len(src)
|
||||
if i >= n || src[i] != '<' {
|
||||
return jsxNode{}, 0, fmt.Errorf("parseJSX: expected '<' at %d", i)
|
||||
}
|
||||
i++ // consume '<'
|
||||
|
||||
// Fragment: <> ... </>
|
||||
if i < n && src[i] == '>' {
|
||||
i++
|
||||
children, ci, err := parseChildren(src, i)
|
||||
if err != nil {
|
||||
return jsxNode{}, 0, err
|
||||
}
|
||||
i, err = consumeCloseTag(src, ci)
|
||||
if err != nil {
|
||||
return jsxNode{}, 0, err
|
||||
}
|
||||
return jsxNode{kind: jsxFragment, children: children}, i, nil
|
||||
}
|
||||
|
||||
// Tag name.
|
||||
start := i
|
||||
for i < n && isTagChar(src[i]) {
|
||||
i++
|
||||
}
|
||||
if i == start {
|
||||
return jsxNode{}, 0, fmt.Errorf("parseJSX: empty tag name at %d", start)
|
||||
}
|
||||
node := jsxNode{tag: src[start:i]}
|
||||
if isComponentTag(node.tag) {
|
||||
node.kind = jsxComponent
|
||||
} else {
|
||||
node.kind = jsxElement
|
||||
}
|
||||
|
||||
attrs, ai, selfClose, err := parseAttrs(src, i)
|
||||
if err != nil {
|
||||
return jsxNode{}, 0, err
|
||||
}
|
||||
node.attrs = attrs
|
||||
i = ai
|
||||
if selfClose {
|
||||
return node, i, nil
|
||||
}
|
||||
|
||||
children, ci, err := parseChildren(src, i)
|
||||
if err != nil {
|
||||
return jsxNode{}, 0, err
|
||||
}
|
||||
node.children = children
|
||||
i, err = consumeCloseTag(src, ci)
|
||||
if err != nil {
|
||||
return jsxNode{}, 0, err
|
||||
}
|
||||
return node, i, nil
|
||||
}
|
||||
|
||||
// parseAttrs parses attributes after the tag name until `>` or `/>`. It returns
|
||||
// the attrs, the index past the terminator, and whether the tag self-closed.
|
||||
func parseAttrs(src string, i int) ([]jsxAttr, int, bool, error) {
|
||||
n := len(src)
|
||||
var attrs []jsxAttr
|
||||
for i < n {
|
||||
for i < n && isSpace(src[i]) {
|
||||
i++
|
||||
}
|
||||
if i >= n {
|
||||
break
|
||||
}
|
||||
switch {
|
||||
case src[i] == '>':
|
||||
return attrs, i + 1, false, nil
|
||||
case src[i] == '/' && i+1 < n && src[i+1] == '>':
|
||||
return attrs, i + 2, true, nil
|
||||
case src[i] == '{': // {...spread}
|
||||
expr, ni := captureBraces(src, i)
|
||||
e := strings.TrimSpace(expr)
|
||||
e = strings.TrimSpace(strings.TrimPrefix(e, "..."))
|
||||
attrs = append(attrs, jsxAttr{kind: attrSpread, expr: e})
|
||||
i = ni
|
||||
default:
|
||||
ns := i
|
||||
for i < n && isAttrNameChar(src[i]) {
|
||||
i++
|
||||
}
|
||||
if i == ns {
|
||||
return nil, 0, false, fmt.Errorf("parseAttrs: unexpected %q at %d", src[i], i)
|
||||
}
|
||||
name := src[ns:i]
|
||||
for i < n && isSpace(src[i]) {
|
||||
i++
|
||||
}
|
||||
if i < n && src[i] == '=' {
|
||||
i++
|
||||
for i < n && isSpace(src[i]) {
|
||||
i++
|
||||
}
|
||||
if i >= n {
|
||||
return nil, 0, false, fmt.Errorf("parseAttrs: attr value expected")
|
||||
}
|
||||
if src[i] == '{' {
|
||||
expr, ni := captureBraces(src, i)
|
||||
attrs = append(attrs, jsxAttr{kind: attrExpr, name: name, expr: strings.TrimSpace(expr)})
|
||||
i = ni
|
||||
} else if src[i] == '"' || src[i] == '\'' {
|
||||
q := src[i]
|
||||
i++
|
||||
vs := i
|
||||
for i < n && src[i] != q {
|
||||
i++
|
||||
}
|
||||
attrs = append(attrs, jsxAttr{kind: attrStatic, name: name, value: src[vs:i]})
|
||||
if i < n {
|
||||
i++ // closing quote
|
||||
}
|
||||
} else {
|
||||
return nil, 0, false, fmt.Errorf("parseAttrs: bad attr value at %d", i)
|
||||
}
|
||||
} else {
|
||||
attrs = append(attrs, jsxAttr{kind: attrStatic, name: name, boolt: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, 0, false, fmt.Errorf("parseAttrs: unterminated tag")
|
||||
}
|
||||
|
||||
// parseChildren parses child nodes until the matching `</`. It returns the nodes
|
||||
// and the index at the `<` of the closing tag. Adjacent character data becomes a
|
||||
// single jsxText node (JSX whitespace normalization happens in codegen).
|
||||
func parseChildren(src string, i int) ([]jsxNode, int, error) {
|
||||
n := len(src)
|
||||
var nodes []jsxNode
|
||||
var text strings.Builder
|
||||
flush := func() {
|
||||
if text.Len() > 0 {
|
||||
nodes = append(nodes, jsxNode{kind: jsxText, text: text.String()})
|
||||
text.Reset()
|
||||
}
|
||||
}
|
||||
for i < n {
|
||||
c := src[i]
|
||||
switch {
|
||||
case c == '<' && i+1 < n && src[i+1] == '/':
|
||||
flush()
|
||||
return nodes, i, nil
|
||||
case c == '<':
|
||||
flush()
|
||||
child, ni, err := parseJSX(src, i)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
nodes = append(nodes, child)
|
||||
i = ni
|
||||
case c == '{':
|
||||
flush()
|
||||
expr, ni := captureBraces(src, i)
|
||||
nodes = append(nodes, jsxNode{kind: jsxExpr, expr: strings.TrimSpace(expr)})
|
||||
i = ni
|
||||
default:
|
||||
text.WriteByte(c)
|
||||
i++
|
||||
}
|
||||
}
|
||||
return nil, 0, fmt.Errorf("parseChildren: unterminated (missing close tag)")
|
||||
}
|
||||
|
||||
// consumeCloseTag consumes `</name>` (or `</>`) starting at src[i] == '<'.
|
||||
func consumeCloseTag(src string, i int) (int, error) {
|
||||
n := len(src)
|
||||
if i+1 >= n || src[i] != '<' || src[i+1] != '/' {
|
||||
return 0, fmt.Errorf("consumeCloseTag: expected '</' at %d", i)
|
||||
}
|
||||
i += 2
|
||||
for i < n && src[i] != '>' {
|
||||
i++
|
||||
}
|
||||
if i >= n {
|
||||
return 0, fmt.Errorf("consumeCloseTag: unterminated close tag")
|
||||
}
|
||||
return i + 1, nil // past '>'
|
||||
}
|
||||
|
||||
// captureBraces returns the text between a `{` at src[i] and its matching `}`
|
||||
// (exclusive), and the index just past that `}`. It tracks strings, template
|
||||
// literals (with `${}` interpolation), comments, and regex literals so braces
|
||||
// inside them don't miscount — the same lexer the segmenter uses.
|
||||
func captureBraces(src string, i int) (inner string, next int) {
|
||||
n := len(src)
|
||||
start := i + 1
|
||||
i++ // skip opening '{'
|
||||
depth := 0
|
||||
state := stNormal
|
||||
var tmplStack []int
|
||||
var prevSig byte
|
||||
for ; i < n; i++ {
|
||||
c := src[i]
|
||||
switch state {
|
||||
case stNormal:
|
||||
switch c {
|
||||
case '/':
|
||||
if i+1 < n && src[i+1] == '/' {
|
||||
state = stLineComment
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if i+1 < n && src[i+1] == '*' {
|
||||
state = stBlockComment
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if regexAllowed(src, i, prevSig) {
|
||||
state = stRegex
|
||||
prevSig = c
|
||||
continue
|
||||
}
|
||||
prevSig = c
|
||||
case '\'':
|
||||
state = stSingle
|
||||
prevSig = c
|
||||
case '"':
|
||||
state = stDouble
|
||||
prevSig = c
|
||||
case '`':
|
||||
state = stTemplate
|
||||
prevSig = c
|
||||
case '{', '(', '[':
|
||||
depth++
|
||||
prevSig = c
|
||||
case '}':
|
||||
if len(tmplStack) > 0 && depth == tmplStack[len(tmplStack)-1] {
|
||||
tmplStack = tmplStack[:len(tmplStack)-1]
|
||||
depth--
|
||||
state = stTemplate
|
||||
} else if depth > 0 {
|
||||
depth--
|
||||
prevSig = c
|
||||
} else {
|
||||
return src[start:i], i + 1 // matching close of the outer '{'
|
||||
}
|
||||
case ')', ']':
|
||||
if depth > 0 {
|
||||
depth--
|
||||
}
|
||||
prevSig = c
|
||||
case '<':
|
||||
// A `<` in expression position (and followed by a tag/fragment
|
||||
// start) opens nested JSX, not a less-than: skip the whole element
|
||||
// via parseJSX so its `</tag>` slashes and `{}` don't desync the JS
|
||||
// lexer. Otherwise it's the comparison operator.
|
||||
if regexAllowed(src, i, prevSig) && i+1 < n && (isASCIILetter(src[i+1]) || src[i+1] == '>') {
|
||||
if _, ni, err := parseJSX(src, i); err == nil {
|
||||
i = ni - 1 // loop's i++ lands just past the element
|
||||
prevSig = '>'
|
||||
continue
|
||||
}
|
||||
}
|
||||
prevSig = c
|
||||
default:
|
||||
if !isSpace(c) {
|
||||
prevSig = c
|
||||
}
|
||||
}
|
||||
case stLineComment:
|
||||
if c == '\n' {
|
||||
state = stNormal
|
||||
}
|
||||
case stBlockComment:
|
||||
if c == '*' && i+1 < n && src[i+1] == '/' {
|
||||
state = stNormal
|
||||
i++
|
||||
}
|
||||
case stSingle:
|
||||
if c == '\\' {
|
||||
i++
|
||||
} else if c == '\'' {
|
||||
state = stNormal
|
||||
prevSig = c
|
||||
}
|
||||
case stDouble:
|
||||
if c == '\\' {
|
||||
i++
|
||||
} else if c == '"' {
|
||||
state = stNormal
|
||||
prevSig = c
|
||||
}
|
||||
case stTemplate:
|
||||
if c == '\\' {
|
||||
i++
|
||||
} else if c == '`' {
|
||||
state = stNormal
|
||||
prevSig = c
|
||||
} else if c == '$' && i+1 < n && src[i+1] == '{' {
|
||||
depth++
|
||||
tmplStack = append(tmplStack, depth)
|
||||
state = stNormal
|
||||
i++
|
||||
}
|
||||
case stRegex:
|
||||
if c == '\\' {
|
||||
i++
|
||||
} else if c == '[' {
|
||||
for i++; i < n; i++ {
|
||||
if src[i] == '\\' {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if src[i] == ']' {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if c == '/' {
|
||||
state = stNormal
|
||||
prevSig = c
|
||||
}
|
||||
}
|
||||
}
|
||||
return src[start:], n // unterminated
|
||||
}
|
||||
|
||||
func isComponentTag(tag string) bool {
|
||||
if tag == "" {
|
||||
return false
|
||||
}
|
||||
if strings.ContainsAny(tag, ".") {
|
||||
return true // member expression component, e.g. <Foo.Bar>
|
||||
}
|
||||
c := tag[0]
|
||||
return c >= 'A' && c <= 'Z'
|
||||
}
|
||||
|
||||
func isTagChar(b byte) bool {
|
||||
return isASCIILetter(b) || (b >= '0' && b <= '9') || b == '-' || b == '.' || b == ':' || b == '_'
|
||||
}
|
||||
|
||||
func isAttrNameChar(b byte) bool {
|
||||
return isASCIILetter(b) || (b >= '0' && b <= '9') || b == '-' || b == ':' || b == '_'
|
||||
}
|
||||
|
||||
func isSpace(b byte) bool {
|
||||
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
|
||||
}
|
||||
Reference in New Issue
Block a user