diff --git a/.changeset/afraid-elephants-live.md b/.changeset/afraid-elephants-live.md new file mode 100644 index 000000000..b4527682d --- /dev/null +++ b/.changeset/afraid-elephants-live.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Add `fragment` node types to AST definitions, expose Fragment helper to utils diff --git a/.changeset/chilled-colts-kick.md b/.changeset/chilled-colts-kick.md new file mode 100644 index 000000000..8342a186b --- /dev/null +++ b/.changeset/chilled-colts-kick.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Adds metadata on client:only components diff --git a/.changeset/long-kids-wait.md b/.changeset/long-kids-wait.md new file mode 100644 index 000000000..453b63efa --- /dev/null +++ b/.changeset/long-kids-wait.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Expose AST types via `@astrojs/compiler/types` diff --git a/.changeset/modern-dots-destroy.md b/.changeset/modern-dots-destroy.md new file mode 100644 index 000000000..77999481f --- /dev/null +++ b/.changeset/modern-dots-destroy.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Export `./types` rather than `./types.d.ts` diff --git a/.changeset/poor-maps-fly.md b/.changeset/poor-maps-fly.md new file mode 100644 index 000000000..406d59185 --- /dev/null +++ b/.changeset/poor-maps-fly.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Fix edge case with Fragment parsing in head, add `fragment` node to AST output diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000..004bc7d7f --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,24 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "@astrojs/compiler": "0.9.2" + }, + "changesets": [ + "chilled-colts-kick", + "long-kids-wait", + "modern-dots-destroy", + "poor-maps-fly", + "purple-jobs-retire", + "quick-lamps-stare", + "short-tips-begin", + "strange-humans-roll", + "stupid-colts-grab", + "tasty-garlics-call", + "thirty-jobs-jam", + "unlucky-swans-destroy", + "wicked-forks-do", + "wise-shrimps-knock", + "yellow-numbers-leave" + ] +} diff --git a/.changeset/purple-jobs-retire.md b/.changeset/purple-jobs-retire.md new file mode 100644 index 000000000..785c5bebd --- /dev/null +++ b/.changeset/purple-jobs-retire.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Fix behavior inside of head diff --git a/.changeset/quick-lamps-stare.md b/.changeset/quick-lamps-stare.md new file mode 100644 index 000000000..a2d0d9a00 --- /dev/null +++ b/.changeset/quick-lamps-stare.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Improve head injection behavior diff --git a/.changeset/short-tips-begin.md b/.changeset/short-tips-begin.md new file mode 100644 index 000000000..aee8f4384 --- /dev/null +++ b/.changeset/short-tips-begin.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Update exposed types diff --git a/.changeset/strange-humans-roll.md b/.changeset/strange-humans-roll.md new file mode 100644 index 000000000..af1654ee4 --- /dev/null +++ b/.changeset/strange-humans-roll.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Remove usage of `escapeHTML` util diff --git a/.changeset/stupid-colts-grab.md b/.changeset/stupid-colts-grab.md new file mode 100644 index 000000000..7c33ba2c7 --- /dev/null +++ b/.changeset/stupid-colts-grab.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Export all types from shared types diff --git a/.changeset/tasty-garlics-call.md b/.changeset/tasty-garlics-call.md new file mode 100644 index 000000000..bee3440cd --- /dev/null +++ b/.changeset/tasty-garlics-call.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': minor +--- + +Do not render implicit tags created during the parsing process diff --git a/.changeset/thirty-jobs-jam.md b/.changeset/thirty-jobs-jam.md new file mode 100644 index 000000000..12048639d --- /dev/null +++ b/.changeset/thirty-jobs-jam.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Fix `head` behavior and a bug related to ParseFragment diff --git a/.changeset/unlucky-swans-destroy.md b/.changeset/unlucky-swans-destroy.md new file mode 100644 index 000000000..5770de78d --- /dev/null +++ b/.changeset/unlucky-swans-destroy.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': patch +--- + +Adds a warning when using an expression with a hoisted script diff --git a/.changeset/wicked-forks-do.md b/.changeset/wicked-forks-do.md new file mode 100644 index 000000000..470bf71ca --- /dev/null +++ b/.changeset/wicked-forks-do.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': minor +--- + +Remove "as" option, treats all documents as fragments that generate no implicit tags diff --git a/.changeset/wise-shrimps-knock.md b/.changeset/wise-shrimps-knock.md new file mode 100644 index 000000000..c3f314203 --- /dev/null +++ b/.changeset/wise-shrimps-knock.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': minor +--- + +Add `parse` function which generates an AST diff --git a/.changeset/yellow-numbers-leave.md b/.changeset/yellow-numbers-leave.md new file mode 100644 index 000000000..3079a7bc3 --- /dev/null +++ b/.changeset/yellow-numbers-leave.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': minor +--- + +Adds support for `Astro.self` (as accepted in the [Recursive Components RFC](https://github.com/withastro/rfcs/blob/main/active-rfcs/0000-recursive-components.md)). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 293416fff..f974890b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: Test on: push: - branches: ['main'] + branches: ['main', 'next'] pull_request: - branches: ['main'] + branches: ['main', 'next'] # Automatically cancel in-progress actions on the same branch concurrency: @@ -53,7 +53,7 @@ jobs: run: yarn build:compiler - name: Test WASM - run: yarn test + run: yarn test:ci lint: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 124d276ab..e2ca2a1a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - next jobs: release: diff --git a/cmd/astro-wasm/astro-wasm.go b/cmd/astro-wasm/astro-wasm.go index 91aef5971..d8e958815 100644 --- a/cmd/astro-wasm/astro-wasm.go +++ b/cmd/astro-wasm/astro-wasm.go @@ -14,9 +14,9 @@ import ( "github.com/norunners/vert" astro "github.com/withastro/compiler/internal" "github.com/withastro/compiler/internal/printer" + t "github.com/withastro/compiler/internal/t" "github.com/withastro/compiler/internal/transform" wasm_utils "github.com/withastro/compiler/internal_wasm/utils" - "golang.org/x/net/html/atom" ) var done chan bool @@ -25,6 +25,7 @@ func main() { js.Global().Set("@astrojs/compiler", js.ValueOf(make(map[string]interface{}))) module := js.Global().Get("@astrojs/compiler") module.Set("transform", Transform()) + module.Set("parse", Parse()) <-make(chan struct{}) } @@ -43,6 +44,19 @@ func jsBool(j js.Value) bool { return j.Bool() } +func makeParseOptions(options js.Value) t.ParseOptions { + position := true + + pos := options.Get("position") + if !pos.IsNull() && !pos.IsUndefined() { + position = pos.Bool() + } + + return t.ParseOptions{ + Position: position, + } +} + func makeTransformOptions(options js.Value, hash string) transform.TransformOptions { filename := jsString(options.Get("sourcefile")) if filename == "" { @@ -54,11 +68,6 @@ func makeTransformOptions(options js.Value, hash string) transform.TransformOpti pathname = "" } - as := jsString(options.Get("as")) - if as == "" { - as = "document" - } - internalURL := jsString(options.Get("internalURL")) if internalURL == "" { internalURL = "astro/internal" @@ -87,7 +96,6 @@ func makeTransformOptions(options js.Value, hash string) transform.TransformOpti preprocessStyle := options.Get("preprocessStyle") return transform.TransformOptions{ - As: as, Scope: hash, Filename: filename, Pathname: pathname, @@ -115,6 +123,10 @@ type HoistedScript struct { Type string `js:"type"` } +type ParseResult struct { + AST string `js:"ast"` +} + type TransformResult struct { Code string `js:"code"` Map string `js:"map"` @@ -141,6 +153,24 @@ func preprocessStyle(i int, style *astro.Node, transformOptions transform.Transf style.FirstChild.Data = str } +func Parse() interface{} { + return js.FuncOf(func(this js.Value, args []js.Value) interface{} { + source := jsString(args[0]) + parseOptions := makeParseOptions(js.Value(args[1])) + + var doc *astro.Node + doc, err := astro.Parse(strings.NewReader(source)) + if err != nil { + fmt.Println(err) + } + result := printer.PrintToJSON(source, doc, parseOptions) + + return vert.ValueOf(ParseResult{ + AST: string(result.Output), + }) + }) +} + func Transform() interface{} { return js.FuncOf(func(this js.Value, args []js.Value) interface{} { source := jsString(args[0]) @@ -153,29 +183,9 @@ func Transform() interface{} { go func() { var doc *astro.Node - if transformOptions.As == "document" { - docNode, err := astro.Parse(strings.NewReader(source)) - doc = docNode - if err != nil { - fmt.Println(err) - } - } else if transformOptions.As == "fragment" { - nodes, err := astro.ParseFragment(strings.NewReader(source), &astro.Node{ - Type: astro.ElementNode, - Data: atom.Template.String(), - DataAtom: atom.Template, - }) - if err != nil { - fmt.Println(err) - } - doc = &astro.Node{ - Type: astro.DocumentNode, - HydrationDirectives: make(map[string]bool), - } - for i := 0; i < len(nodes); i++ { - n := nodes[i] - doc.AppendChild(n) - } + doc, err := astro.Parse(strings.NewReader(source)) + if err != nil { + fmt.Println(err) } // Hoist styles and scripts to the top-level diff --git a/internal/const.go b/internal/const.go index 3aad85779..370a3185d 100644 --- a/internal/const.go +++ b/internal/const.go @@ -4,6 +4,8 @@ package astro +import a "golang.org/x/net/html/atom" + // Section 12.2.4.2 of the HTML5 specification says "The following elements // have varying levels of special parsing rules". // https://html.spec.whatwg.org/multipage/syntax.html#the-stack-of-open-elements @@ -109,3 +111,26 @@ func isSpecialElement(element *Node) bool { } return false } + +var knownDirectiveMap = map[string]bool{ + "client:load": true, + "client:idle": true, + "client:visible": true, + "client:only": true, + "class:list": true, + "set:text": true, + "set:html": true, +} + +func IsKnownDirective(element *Node, attr *Attribute) bool { + if knownDirectiveMap[attr.Key] { + return true + } + if element.DataAtom == a.Script { + return attr.Key == "hoist" + } + if element.DataAtom == a.Style { + return attr.Key == "global" + } + return false +} diff --git a/internal/node.go b/internal/node.go index 18c85c213..ec1c1e81b 100644 --- a/internal/node.go +++ b/internal/node.go @@ -30,6 +30,29 @@ const ( ExpressionNode ) +func (t NodeType) String() string { + switch t { + case ErrorNode: + return "error" + case TextNode: + return "text" + case DocumentNode: + return "root" + case ElementNode: + return "element" + case CommentNode: + return "comment" + case DoctypeNode: + return "doctype" + case FrontmatterNode: + return "frontmatter" + case ExpressionNode: + return "expression" + default: + return "" + } +} + // Used as an Attribute Key to mark implicit nodes const ImplicitNodeMarker = "\x00implicit" diff --git a/internal/parser.go b/internal/parser.go index 520d938be..4c76da1e7 100644 --- a/internal/parser.go +++ b/internal/parser.go @@ -365,6 +365,7 @@ func (p *parser) addFrontmatter(empty bool) { } if empty { p.frontmatterState = FrontmatterClosed + p.fm.Attr = append(p.fm.Attr, Attribute{Key: ImplicitNodeMarker, Type: EmptyAttribute}) } else { p.frontmatterState = FrontmatterOpen p.oe = append(p.oe, p.fm) @@ -749,6 +750,9 @@ func inHeadIM(p *parser) bool { return true } p.tok.Data = s + } else if p.oe.top() != nil && (isComponent(p.oe.top().Data) || isFragment((p.oe.top().Data))) { + p.addText(p.tok.Data) + return true } case StartTagToken: // Allow components in Head @@ -769,6 +773,16 @@ func inHeadIM(p *parser) bool { p.oe.pop() p.acknowledgeSelfClosingTag() return true + case a.Slot: + p.addElement() + p.setOriginalIM() + p.im = inBodyIM + if p.hasSelfClosingToken { + p.addLoc() + p.oe.pop() + p.acknowledgeSelfClosingTag() + } + return true case a.Noscript: if p.scripting { p.parseGenericRawTextElement() diff --git a/internal/print-to-source.go b/internal/print-to-source.go index 99eba68db..fbd71779e 100644 --- a/internal/print-to-source.go +++ b/internal/print-to-source.go @@ -14,42 +14,53 @@ func PrintToSource(buf *strings.Builder, node *Node) { case TextNode: buf.WriteString(node.Data) case ElementNode: - buf.WriteString(fmt.Sprintf(`<%s`, node.Data)) - for _, attr := range node.Attr { - if attr.Key == ImplicitNodeMarker { - continue + isImplicit := false + for _, a := range node.Attr { + if a.Key == ImplicitNodeMarker { + isImplicit = true + break } - if attr.Namespace != "" { - buf.WriteString(attr.Namespace) - buf.WriteString(":") - } - buf.WriteString(" ") - switch attr.Type { - case QuotedAttribute: - buf.WriteString(attr.Key) - buf.WriteString("=") - buf.WriteString(`"` + attr.Val + `"`) - case EmptyAttribute: - buf.WriteString(attr.Key) - case ExpressionAttribute: - buf.WriteString(attr.Key) - buf.WriteString("=") - buf.WriteString(`{` + strings.TrimSpace(attr.Val) + `}`) - case SpreadAttribute: - buf.WriteString(`{...` + strings.TrimSpace(attr.Val) + `}`) - case ShorthandAttribute: - buf.WriteString(attr.Key) - buf.WriteString("=") - buf.WriteString(`{` + strings.TrimSpace(attr.Key) + `}`) - case TemplateLiteralAttribute: - buf.WriteString(attr.Key) - buf.WriteString("=`" + strings.TrimSpace(attr.Val) + "`") + } + if !isImplicit { + buf.WriteString(fmt.Sprintf(`<%s`, node.Data)) + for _, attr := range node.Attr { + if attr.Key == ImplicitNodeMarker { + continue + } + if attr.Namespace != "" { + buf.WriteString(attr.Namespace) + buf.WriteString(":") + } + buf.WriteString(" ") + switch attr.Type { + case QuotedAttribute: + buf.WriteString(attr.Key) + buf.WriteString("=") + buf.WriteString(`"` + attr.Val + `"`) + case EmptyAttribute: + buf.WriteString(attr.Key) + case ExpressionAttribute: + buf.WriteString(attr.Key) + buf.WriteString("=") + buf.WriteString(`{` + strings.TrimSpace(attr.Val) + `}`) + case SpreadAttribute: + buf.WriteString(`{...` + strings.TrimSpace(attr.Val) + `}`) + case ShorthandAttribute: + buf.WriteString(attr.Key) + buf.WriteString("=") + buf.WriteString(`{` + strings.TrimSpace(attr.Key) + `}`) + case TemplateLiteralAttribute: + buf.WriteString(attr.Key) + buf.WriteString("=`" + strings.TrimSpace(attr.Val) + "`") + } } + buf.WriteString(`>`) } - buf.WriteString(`>`) for c := node.FirstChild; c != nil; c = c.NextSibling { PrintToSource(buf, c) } - buf.WriteString(fmt.Sprintf(``, node.Data)) + if !isImplicit { + buf.WriteString(fmt.Sprintf(``, node.Data)) + } } } diff --git a/internal/printer/print-to-js.go b/internal/printer/print-to-js.go index 23bf42bdf..2c60e20d2 100644 --- a/internal/printer/print-to-js.go +++ b/internal/printer/print-to-js.go @@ -211,7 +211,9 @@ func render1(p *printer, n *Node, opts RenderOptions) { depth: depth + 1, opts: opts.opts, }) - p.addSourceMapping(loc.Loc{Start: n.Loc[1].Start - 3}) + if len(n.Loc) > 1 { + p.addSourceMapping(loc.Loc{Start: n.Loc[1].Start - 3}) + } } } return @@ -335,6 +337,13 @@ func render1(p *printer, n *Node, opts RenderOptions) { isComponent := isFragment || n.Component || n.CustomElement isClientOnly := isComponent && transform.HasAttr(n, "client:only") isSlot := n.DataAtom == atom.Slot + isImplicit := false + for _, a := range n.Attr { + if transform.IsImplictNodeMarker(a) { + isImplicit = true + break + } + } p.addSourceMapping(n.Loc[0]) switch true { @@ -344,6 +353,8 @@ func render1(p *printer, n *Node, opts RenderOptions) { p.print(fmt.Sprintf("${%s(%s,'%s',", RENDER_COMPONENT, RESULT, n.Data)) case isSlot: p.print(fmt.Sprintf("${%s(%s,%s[", RENDER_SLOT, RESULT, SLOTS)) + case isImplicit: + // do nothing default: p.print("<") @@ -357,12 +368,14 @@ func render1(p *printer, n *Node, opts RenderOptions) { p.print("null") case !isSlot && n.CustomElement: p.print(fmt.Sprintf("'%s'", n.Data)) - case !isSlot: + case !isSlot && !isImplicit: p.print(n.Data) } p.addSourceMapping(n.Loc[0]) - if isComponent { + if isImplicit { + // do nothing + } else if isComponent { p.print(",") p.printAttributesToObject(n) } else if isSlot { @@ -556,7 +569,10 @@ func render1(p *printer, n *Node, opts RenderOptions) { } if isComponent || isSlot { p.print(")}") - } else { + } else if !isImplicit { + if n.DataAtom == atom.Head { + p.printRenderHead() + } p.print(``) } } diff --git a/internal/printer/print-to-json.go b/internal/printer/print-to-json.go new file mode 100644 index 000000000..89dd4ffc0 --- /dev/null +++ b/internal/printer/print-to-json.go @@ -0,0 +1,245 @@ +package printer + +import ( + "fmt" + "regexp" + "strings" + + . "github.com/withastro/compiler/internal" + "github.com/withastro/compiler/internal/loc" + "github.com/withastro/compiler/internal/sourcemap" + "github.com/withastro/compiler/internal/t" + "github.com/withastro/compiler/internal/transform" +) + +type ASTPosition struct { + Start ASTPoint `json:"start,omitempty"` + End ASTPoint `json:"end,omitempty"` +} + +type ASTPoint struct { + Line int `json:"line,omitempty"` + Column int `json:"column,omitempty"` + Offset int `json:"offset,omitempty"` +} + +type ASTNode struct { + Type string `json:"type"` + Name string `json:"name"` + Value string `json:"value,omitempty"` + Attributes []ASTNode `json:"attributes,omitempty"` + Directives []ASTNode `json:"directives,omitempty"` + Children []ASTNode `json:"children,omitempty"` + Position ASTPosition `json:"position,omitempty"` + + // Attributes only + Kind string `json:"kind,omitempty"` +} + +func escapeForJSON(value string) string { + newlines := regexp.MustCompile(`\n`) + value = newlines.ReplaceAllString(value, `\n`) + doublequotes := regexp.MustCompile(`"`) + value = doublequotes.ReplaceAllString(value, `\"`) + amp := regexp.MustCompile(`&`) + value = amp.ReplaceAllString(value, `\&`) + r := regexp.MustCompile(`\r`) + value = r.ReplaceAllString(value, `\r`) + t := regexp.MustCompile(`\t`) + value = t.ReplaceAllString(value, `\t`) + f := regexp.MustCompile(`\f`) + value = f.ReplaceAllString(value, `\f`) + return value +} + +func (n ASTNode) String() string { + str := fmt.Sprintf(`{"type":"%s"`, n.Type) + if n.Kind != "" { + str += fmt.Sprintf(`,"kind":"%s"`, n.Kind) + } + if n.Name != "" { + str += fmt.Sprintf(`,"name":"%s"`, escapeForJSON(n.Name)) + } else if n.Type == "fragment" { + str += `,"name":""` + } + if n.Value != "" || n.Type == "attribute" { + str += fmt.Sprintf(`,"value":"%s"`, escapeForJSON(n.Value)) + } + if len(n.Attributes) > 0 { + str += `,"attributes":[` + for i, attr := range n.Attributes { + str += attr.String() + if i < len(n.Attributes)-1 { + str += "," + } + } + str += `]` + } + if len(n.Directives) > 0 { + str += `,"directives":[` + for i, attr := range n.Directives { + str += attr.String() + if i < len(n.Directives)-1 { + str += "," + } + } + str += `]` + } + if len(n.Children) > 0 { + str += `,"children":[` + for i, node := range n.Children { + str += node.String() + if i < len(n.Children)-1 { + str += "," + } + } + str += `]` + } + if n.Position.Start.Line != 0 { + str += `,"position":{` + str += fmt.Sprintf(`"start":{"line":%d,"column":%d,"offset":%d}`, n.Position.Start.Line, n.Position.Start.Column, n.Position.Start.Offset) + if n.Position.End.Line != 0 { + str += fmt.Sprintf(`,"end":{"line":%d,"column":%d,"offset":%d}`, n.Position.End.Line, n.Position.End.Column, n.Position.End.Offset) + } + str += "}" + } + str += "}" + return str +} + +func PrintToJSON(sourcetext string, n *Node, opts t.ParseOptions) PrintResult { + p := &printer{ + builder: sourcemap.MakeChunkBuilder(nil, sourcemap.GenerateLineOffsetTables(sourcetext, len(strings.Split(sourcetext, "\n")))), + } + root := ASTNode{} + renderNode(p, &root, n, opts) + doc := root.Children[0] + return PrintResult{ + Output: []byte(doc.String()), + } +} + +func locToPoint(p *printer, loc loc.Loc) ASTPoint { + offset := loc.Start + info := p.builder.GetLineAndColumnForLocation(loc) + line := info[0] + column := info[1] + + return ASTPoint{ + Line: line, + Column: column, + Offset: offset, + } +} + +func positionAt(p *printer, n *Node, opts t.ParseOptions) ASTPosition { + if !opts.Position { + return ASTPosition{} + } + + if len(n.Loc) == 1 { + s := n.Loc[0] + start := locToPoint(p, s) + + return ASTPosition{ + Start: start, + } + } + + if len(n.Loc) == 2 { + s := n.Loc[0] + e := n.Loc[1] + start := locToPoint(p, s) + end := locToPoint(p, e) + + return ASTPosition{ + Start: start, + End: end, + } + } + return ASTPosition{} +} + +func attrPositionAt(p *printer, n *Attribute, opts t.ParseOptions) ASTPosition { + if !opts.Position { + return ASTPosition{} + } + + k := n.KeyLoc + start := locToPoint(p, k) + + return ASTPosition{ + Start: start, + } +} + +func renderNode(p *printer, parent *ASTNode, n *Node, opts t.ParseOptions) { + isImplicit := false + for _, a := range n.Attr { + if transform.IsImplictNodeMarker(a) { + isImplicit = true + break + } + } + hasChildren := n.FirstChild != nil + if isImplicit { + if hasChildren { + for c := n.FirstChild; c != nil; c = c.NextSibling { + renderNode(p, parent, c, opts) + } + } + return + } + var node ASTNode + + node.Position = positionAt(p, n, opts) + + if n.Type == ElementNode { + if n.Expression { + node.Type = "expression" + } else { + node.Name = n.Data + if n.Component { + node.Type = "component" + } else if n.CustomElement { + node.Type = "custom-element" + } else if n.Fragment { + node.Type = "fragment" + } else { + node.Type = "element" + } + + for _, attr := range n.Attr { + attrNode := ASTNode{ + Type: "attribute", + Kind: attr.Type.String(), + Position: attrPositionAt(p, &attr, opts), + Name: attr.Key, + Value: attr.Val, + } + if IsKnownDirective(n, &attr) { + attrNode.Type = "directive" + node.Directives = append(node.Directives, attrNode) + } else { + node.Attributes = append(node.Attributes, attrNode) + } + } + } + } else { + node.Type = n.Type.String() + if n.Type == TextNode || n.Type == CommentNode || n.Type == DoctypeNode { + node.Value = n.Data + } + } + if n.Type == FrontmatterNode && hasChildren { + node.Value = n.FirstChild.Data + } else { + if !isImplicit && hasChildren { + for c := n.FirstChild; c != nil; c = c.NextSibling { + renderNode(p, &node, c, opts) + } + } + } + + parent.Children = append(parent.Children, node) +} diff --git a/internal/printer/printer.go b/internal/printer/printer.go index 18b7a7a0d..e529b5661 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -30,7 +30,6 @@ var TEMPLATE_TAG = "$$render" var CREATE_ASTRO = "$$createAstro" var CREATE_COMPONENT = "$$createComponent" var RENDER_COMPONENT = "$$renderComponent" -var ESCAPE_HTML = "$$escapeHTML" var UNESCAPE_HTML = "$$unescapeHTML" var RENDER_SLOT = "$$renderSlot" var ADD_ATTRIBUTE = "$$addAttribute" @@ -62,7 +61,6 @@ func (p *printer) printInternalImports(importSpecifier string) { p.print("createAstro as " + CREATE_ASTRO + ",\n ") p.print("createComponent as " + CREATE_COMPONENT + ",\n ") p.print("renderComponent as " + RENDER_COMPONENT + ",\n ") - p.print("escapeHTML as " + ESCAPE_HTML + ",\n ") p.print("unescapeHTML as " + UNESCAPE_HTML + ",\n ") p.print("renderSlot as " + RENDER_SLOT + ",\n ") p.print("addAttribute as " + ADD_ATTRIBUTE + ",\n ") @@ -90,6 +88,11 @@ func (p *printer) printCSSImports(cssLen int) { p.hasCSSImports = true } +func (p *printer) printRenderHead() { + p.addNilSourceMapping() + p.print("") +} + func (p *printer) printReturnOpen() { p.addNilSourceMapping() p.print("return ") @@ -324,6 +327,7 @@ func (p *printer) printTopLevelAstro(opts transform.TransformOptions) { func (p *printer) printComponentMetadata(doc *astro.Node, opts transform.TransformOptions, source []byte) { var specs []string var asrts []string + var conlyspecs []string modCount := 1 loc, statement := js_scanner.NextImportStatement(source, 0) @@ -344,6 +348,7 @@ func (p *printer) printComponentMetadata(doc *astro.Node, opts transform.Transfo Type: astro.ExpressionAttribute, } n.Attr = append(n.Attr, pathAttr) + conlyspecs = append(conlyspecs, statement.Specifier) exportAttr := astro.Attribute{ Key: "client:component-export", @@ -363,6 +368,7 @@ func (p *printer) printComponentMetadata(doc *astro.Node, opts transform.Transfo Type: astro.ExpressionAttribute, } n.Attr = append(n.Attr, pathAttr) + conlyspecs = append(conlyspecs, statement.Specifier) exportAttr := astro.Attribute{ Key: "client:component-export", @@ -433,6 +439,14 @@ func (p *printer) printComponentMetadata(doc *astro.Node, opts transform.Transfo p.print(node.Data) } } + // Client-Only Components + p.print("], clientOnlyComponents: [") + for i, spec := range conlyspecs { + if i > 0 { + p.print(", ") + } + p.print(fmt.Sprintf("'%s'", spec)) + } p.print("], hydrationDirectives: new Set([") i := 0 for directive := range doc.HydrationDirectives { @@ -442,6 +456,7 @@ func (p *printer) printComponentMetadata(doc *astro.Node, opts transform.Transfo p.print(fmt.Sprintf("'%s'", directive)) i++ } + // Hoisted scripts p.print("]), hoisted: [") for i, node := range doc.Scripts { if i > 0 { @@ -455,5 +470,6 @@ func (p *printer) printComponentMetadata(doc *astro.Node, opts transform.Transfo p.print(fmt.Sprintf("{ type: 'inline', value: `%s` }", escapeInterpolation(escapeBackticks(node.FirstChild.Data)))) } } + p.print("] });\n\n") } diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go index 356ed23cb..e870922b1 100644 --- a/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -9,6 +9,7 @@ import ( "testing" astro "github.com/withastro/compiler/internal" + types "github.com/withastro/compiler/internal/t" "github.com/withastro/compiler/internal/test_utils" "github.com/withastro/compiler/internal/transform" ) @@ -19,7 +20,6 @@ var INTERNAL_IMPORTS = fmt.Sprintf("import {\n %s\n} from \"%s\";\n", strings.J "createAstro as " + CREATE_ASTRO, "createComponent as " + CREATE_COMPONENT, "renderComponent as " + RENDER_COMPONENT, - "escapeHTML as " + ESCAPE_HTML, "unescapeHTML as " + UNESCAPE_HTML, "renderSlot as " + RENDER_SLOT, "addAttribute as " + ADD_ATTRIBUTE, @@ -41,6 +41,7 @@ var STYLE_SUFFIX = "];\nfor (const STYLE of STYLES) $$result.styles.add(STYLE);\ var SCRIPT_PRELUDE = "const SCRIPTS = [\n" var SCRIPT_SUFFIX = "];\nfor (const SCRIPT of SCRIPTS) $$result.scripts.add(SCRIPT);\n" var CREATE_ASTRO_CALL = "const $$Astro = $$createAstro(import.meta.url, 'https://astro.build', '.');\nconst Astro = $$Astro;" +var RENDER_HEAD_RESULT = "" // SPECIAL TEST FIXTURES var NON_WHITESPACE_CHARS = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[];:'\",.?") @@ -56,10 +57,11 @@ type want struct { } type metadata struct { - hoisted []string - hydratedComponents []string - modules []string - hydrationDirectives []string + hoisted []string + hydratedComponents []string + clientOnlyComponents []string + modules []string + hydrationDirectives []string } type testcase struct { @@ -70,6 +72,12 @@ type testcase struct { want want } +type jsonTestcase struct { + name string + source string + want []ASTNode +} + func TestPrinter(t *testing.T) { longRandomString := "" for i := 0; i < 4080; i++ { @@ -81,7 +89,21 @@ func TestPrinter(t *testing.T) { name: "basic (no frontmatter)", source: ``, want: want{ - code: ``, + code: ``, + }, + }, + { + name: "basic renderHead", + source: `Ah`, + want: want{ + code: `Ah` + RENDER_HEAD_RESULT + ``, + }, + }, + { + name: "head slot", + source: ``, + want: want{ + code: `${$$renderSlot($$result,$$slots["default"])}` + RENDER_HEAD_RESULT + ``, }, }, { @@ -92,7 +114,7 @@ const href = '/about'; About`, want: want{ frontmatter: []string{"", "const href = '/about';"}, - code: `About`, + code: `About`, }, }, { @@ -107,7 +129,7 @@ export const getStaticPaths = async () => { frontmatter: []string{`export const getStaticPaths = async () => { return { paths: [] } }`, ""}, - code: `
`, + code: `
`, }, }, { @@ -124,7 +146,7 @@ export const getStaticPaths = async () => { getStaticPaths: `export const getStaticPaths = async () => { return { paths: [] } }`, - code: `
`, + code: `
`, }, }, { @@ -143,7 +165,7 @@ const b = 0;`}, getStaticPaths: `export async function getStaticPaths() { return { paths: [] } }`, - code: `
`, + code: `
`, }, }, { @@ -151,35 +173,34 @@ const b = 0;`}, source: `--- import data from "test" assert { type: 'json' }; --- -`, +`, want: want{ frontmatter: []string{ `import data from "test" assert { type: 'json' };`, }, metadata: metadata{modules: []string{`{ module: $$module1, specifier: 'test', assert: {type:'json'} }`}}, styles: []string{}, - code: ``, }, }, { name: "solidus in template literal expression", source: "
", want: want{ - code: "
", + code: "", }, }, { name: "nested template literal expression", source: "
", want: want{ - code: "
", + code: "", }, }, { name: "complex nested template literal expression", source: "
", want: want{ - code: "
", + code: "", }, }, { @@ -203,10 +224,11 @@ import VueComponent from '../components/Vue.vue'; code: ` Hello world - + ` + RENDER_HEAD_RESULT + ` ${` + RENDER_COMPONENT + `($$result,'VueComponent',VueComponent,{})} - `, + + `, }, }, { @@ -229,10 +251,11 @@ import * as ns from '../components'; code: ` Hello world - + ` + RENDER_HEAD_RESULT + ` ${` + RENDER_COMPONENT + `($$result,'ns.Component',ns.Component,{})} - `, + + `, }, }, { @@ -248,12 +271,13 @@ import * as ns from '../components'; `, want: want{ code: ` - + ` + RENDER_HEAD_RESULT + ` - `, + + `, }, }, { @@ -272,13 +296,14 @@ import Component from '../components'; want: want{ frontmatter: []string{"import Component from '../components';"}, metadata: metadata{ - hydrationDirectives: []string{"only"}, + hydrationDirectives: []string{"only"}, + clientOnlyComponents: []string{"../components"}, }, // Specifically do NOT render any metadata here, we need to skip this import code: ` Hello world - + ` + RENDER_HEAD_RESULT + ` ${` + RENDER_COMPONENT + `($$result,'Component',null,{"client:only":true,"client:component-hydration":"only","client:component-path":($$metadata.resolvePath("../components")),"client:component-export":"default"})} `, @@ -300,13 +325,14 @@ import { Component } from '../components'; want: want{ frontmatter: []string{"import { Component } from '../components';"}, metadata: metadata{ - hydrationDirectives: []string{"only"}, + hydrationDirectives: []string{"only"}, + clientOnlyComponents: []string{"../components"}, }, // Specifically do NOT render any metadata here, we need to skip this import code: ` Hello world - + ` + RENDER_HEAD_RESULT + ` ${` + RENDER_COMPONENT + `($$result,'Component',null,{"client:only":true,"client:component-hydration":"only","client:component-path":($$metadata.resolvePath("../components")),"client:component-export":"Component"})} `, @@ -328,13 +354,14 @@ import * as components from '../components'; want: want{ frontmatter: []string{"import * as components from '../components';"}, metadata: metadata{ - hydrationDirectives: []string{"only"}, + hydrationDirectives: []string{"only"}, + clientOnlyComponents: []string{"../components"}, }, // Specifically do NOT render any metadata here, we need to skip this import code: ` Hello world - + ` + RENDER_HEAD_RESULT + ` ${` + RENDER_COMPONENT + `($$result,'components.A',null,{"client:only":true,"client:component-hydration":"only","client:component-path":($$metadata.resolvePath("../components")),"client:component-export":"A"})} `, @@ -344,35 +371,35 @@ import * as components from '../components'; name: "iframe", source: `", + code: "", }, }, { name: "conditional render", source: `{false ?
#f
:
#t
}`, want: want{ - code: "${false ? $$render`
#f
` : $$render`
#t
`}", + code: "${false ? $$render`
#f
` : $$render`
#t
`}", }, }, { name: "conditional noscript", source: `{mode === "production" && }`, want: want{ - code: "${mode === \"production\" && $$render``}", + code: "${mode === \"production\" && $$render``}", }, }, { name: "conditional iframe", source: `{bool && }`, want: want{ - code: "${bool && $$render``}", + code: "${bool && $$render``}", }, }, { name: "simple ternary", source: `{link ? {link} :
no link
}`, want: want{ - code: fmt.Sprintf(`${link ? $$render%s${link}%s : $$render%s
no link
%s}`, BACKTICK, BACKTICK, BACKTICK, BACKTICK), + code: fmt.Sprintf(`${link ? $$render%s${link}%s : $$render%s
no link
%s}`, BACKTICK, BACKTICK, BACKTICK, BACKTICK), }, }, { @@ -387,25 +414,25 @@ const items = [0, 1, 2]; `, want: want{ frontmatter: []string{"", "const items = [0, 1, 2];"}, - code: fmt.Sprintf(`
    + code: fmt.Sprintf(`
      ${items.map(item => { return $$render%s
    • ${item}
    • %s; })} -
    `, BACKTICK, BACKTICK), +
`, BACKTICK, BACKTICK), }, }, { name: "map without component", source: `
`, want: want{ - code: fmt.Sprintf(`
`, BACKTICK, BACKTICK), + code: fmt.Sprintf(`
`, BACKTICK, BACKTICK), }, }, { name: "map with component", source: `
`, want: want{ - code: fmt.Sprintf(`
${$$renderComponent($$result,'Hello',Hello,{})}
`, BACKTICK, BACKTICK), + code: fmt.Sprintf(`
${$$renderComponent($$result,'Hello',Hello,{})}
`, BACKTICK, BACKTICK), }, }, { @@ -425,28 +452,28 @@ const groups = [[0, 1, 2], [3, 4, 5]]; want: want{ frontmatter: []string{"", "const groups = [[0, 1, 2], [3, 4, 5]];"}, styles: []string{}, - code: fmt.Sprintf(`
+ code: fmt.Sprintf(`
${groups.map(items => { return %s
    ${ items.map(item => { return %s
  • ${item}
  • %s; }) }
%s})} -
`, "$$render"+BACKTICK, "$$render"+BACKTICK, BACKTICK, BACKTICK), +
`, "$$render"+BACKTICK, "$$render"+BACKTICK, BACKTICK, BACKTICK), }, }, { name: "backtick in HTML comment", source: "", want: want{ - code: "", + code: "", }, }, { name: "nested expressions", source: ``, want: want{ - code: `
${(previous || next) && $$render` + BACKTICK + `` + BACKTICK + `}
`, + code: `
${(previous || next) && $$render` + BACKTICK + `` + BACKTICK + `}
`, }, }, { @@ -465,7 +492,7 @@ const items = ['red', 'yellow', 'blue']; `, want: want{ frontmatter: []string{"", "const items = ['red', 'yellow', 'blue'];"}, - code: `
+ code: `
${items.map((item) => ( // foo < > < } $$render` + "`" + `color
` + "`" + ` @@ -473,7 +500,7 @@ $$render` + "`" + `color
` + "`" + ` ${items.map((item) => ( /* foo < > < } */$$render` + "`" + `color` + "`" + ` ))} -`, +`, }, }, { @@ -491,7 +518,7 @@ $$render` + "`" + `color` + "`" + ` } `, want: want{ - code: `
+ code: `
${ () => { let generate = (input) => { @@ -501,7 +528,7 @@ ${ }; } } -
`, +
`, }, }, { @@ -563,10 +590,11 @@ const name = "world"; code: ` Hello ${name} - + ` + RENDER_HEAD_RESULT + `
- `, + + `, }, }, { @@ -586,10 +614,8 @@ const name = "world";

I’m a page

`, want: want{ styles: []string{"{props:{\"data-astro-id\":\"DPOHFLYM\"},children:`.title.astro-DPOHFLYM{font-family:fantasy;font-size:28px;}.body.astro-DPOHFLYM{font-size:1em;}`}"}, - code: ` - -

Page Title

-

I’m a page

`, + code: "\n\n\t\t" + `

Page Title

+

I’m a page

`, }, }, { @@ -646,7 +672,7 @@ const name = "world"; - +` + RENDER_HEAD_RESULT + ` @@ -722,13 +748,14 @@ import Counter from '../components/Counter.jsx'`, - - + + ` + RENDER_HEAD_RESULT + `
${$$renderComponent($$result,'Counter',Counter,{...(someProps),"client:visible":true,"client:component-hydration":"visible","client:component-path":($$metadata.getPath(Counter)),"client:component-export":($$metadata.getExport(Counter)),"class":"astro-HMNNHVCQ"},{"default": () => $$render` + "`" + `

Hello React!

` + "`" + `,})}
- `, + + `, }, }, { @@ -753,7 +780,7 @@ import Widget2 from '../components/Widget2.astro';`}, code: ` - `, + ` + RENDER_HEAD_RESULT + ``, }, }, { @@ -766,7 +793,7 @@ import Widget2 from '../components/Widget2.astro';`}, styles: []string{}, scripts: []string{fmt.Sprintf(`{props:{"type":"module","hoist":true},children:%sconsole.log("Hello");%s}`, BACKTICK, BACKTICK)}, metadata: metadata{hoisted: []string{fmt.Sprintf(`{ type: 'inline', value: %sconsole.log("Hello");%s }`, BACKTICK, BACKTICK)}}, - code: ``, + code: ``, }, }, { @@ -779,7 +806,7 @@ import Widget2 from '../components/Widget2.astro';`}, styles: []string{}, scripts: []string{`{props:{"type":"module","hoist":true,"src":"url"}}`}, metadata: metadata{hoisted: []string{`{ type: 'remote', src: 'url' }`}}, - code: "", + code: "", }, }, { @@ -792,37 +819,37 @@ import Widget2 from '../components/Widget2.astro';`}, styles: []string{}, scripts: []string{"{props:{\"type\":\"module\",\"hoist\":true},children:`console.log(\"Hello\");`}"}, metadata: metadata{hoisted: []string{fmt.Sprintf(`{ type: 'inline', value: %sconsole.log("Hello");%s }`, BACKTICK, BACKTICK)}}, - code: `
+ code: `
-
`, +
`, }, }, { name: "script nohoist", source: `
`, want: want{ - code: `
`, + code: `
`, }, }, { name: "script define:vars", source: `
`, want: want{ - code: fmt.Sprintf(`
`, DEFINE_SCRIPT_VARS), + code: fmt.Sprintf(`
`, DEFINE_SCRIPT_VARS), }, }, { name: "text after title expression", source: `a {expr} b`, want: want{ - code: `a ${expr} b`, + code: `a ${expr} b`, }, }, { name: "text after title expressions", source: `a {expr} b {expr} c`, want: want{ - code: `a ${expr} b ${expr} c`, + code: `a ${expr} b ${expr} c`, }, }, { @@ -845,14 +872,14 @@ import Widget2 from '../components/Widget2.astro';`}, name: "condition expressions at the top-level", source: `{cond && }{cond && }`, want: want{ - code: "${cond && $$render``}${cond && $$render``}", + code: "${cond && $$render``}${cond && $$render``}", }, }, { name: "condition expressions at the top-level with head content", source: `{cond && }{cond && My title}`, want: want{ - code: "${cond && $$render``}${cond && $$render`My title`}", + code: "${cond && $$render``}${cond && $$render`My title`}", }, }, { @@ -865,7 +892,7 @@ import 'test'; frontmatter: []string{`import 'test';`}, styles: []string{}, metadata: metadata{modules: []string{`{ module: $$module1, specifier: 'test', assert: {} }`}}, - code: `${$$renderComponent($$result,'my-element','my-element',{})}`, + code: `${$$renderComponent($$result,'my-element','my-element',{})}`, }, }, { @@ -917,35 +944,35 @@ ${$$renderComponent($$result,'my-element','my-element',{"client:load":true,"clie name: "Self-closing script in head works", source: ``, + code: `` + RENDER_HEAD_RESULT + ``, }, }, { name: "Self-closing title", source: ``, want: want{ - code: `<html><head><title>`, + code: ``, }, }, { name: "Self-closing title II", source: `</head><body></body></html>`, want: want{ - code: `<html><head><title>`, + code: `` + RENDER_HEAD_RESULT + ``, }, }, { name: "Self-closing components in head can have siblings", source: ``, want: want{ - code: `${$$renderComponent($$result,'BaseHead',BaseHead,{})}`, + code: `${$$renderComponent($$result,'BaseHead',BaseHead,{})}` + RENDER_HEAD_RESULT + ``, }, }, { name: "Self-closing formatting elements", source: `
`, want: want{ - code: `
`, + code: `
`, }, }, { @@ -956,11 +983,11 @@ ${$$renderComponent($$result,'my-element','my-element',{"client:load":true,"clie
`, want: want{ - code: ` + code: `
-`, +`, }, }, { @@ -974,7 +1001,7 @@ const canonicalURL = new URL('http://example.com'); frontmatter: []string{"", `const image = './penguin.png'; const canonicalURL = new URL('http://example.com');`}, styles: []string{}, - code: "${image && ($$render``)}", + code: "${image && ($$render``)}", }, }, { @@ -996,7 +1023,7 @@ let allPosts = Astro.fetchContent('./post/*.md'); } let allPosts = Astro.fetchContent('./post/*.md');`}, styles: []string{}, - code: "
testing
", + code: "
testing
", }, }, { @@ -1020,10 +1047,10 @@ import ZComponent from '../components/ZComponent.jsx';`}, `{ module: $$module2, specifier: '../components/ZComponent.jsx', assert: {} }`, }, }, - code: ` + code: ` ${` + RENDER_COMPONENT + `($$result,'AComponent',AComponent,{})} ${` + RENDER_COMPONENT + `($$result,'ZComponent',ZComponent,{})} -`, +`, }, }, { @@ -1035,16 +1062,16 @@ import ZComponent from '../components/ZComponent.jsx';`}, src="https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1200&q=75" srcSet="https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1200&q=75 800w,https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1200&q=75 1200w,https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1600&q=75 1600w,https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=2400&q=75 2400w" sizes="(max-width: 800px) 800px, (max-width: 1200px) 1200px, (max-width: 1600px) 1600px, (max-width: 2400px) 2400px, 1200px" ->`, +>`, want: want{ - code: `` + longRandomString + ``, + code: `` + longRandomString + ``, }, }, { name: "SVG styles", source: ``, want: want{ - code: ``, + code: ``, }, }, { @@ -1055,7 +1082,7 @@ const title = 'icon'; {title ?? null}`, want: want{ frontmatter: []string{"", "const title = 'icon';"}, - code: `${title ?? null}`, + code: `${title ?? null}`, }, }, { @@ -1066,7 +1093,7 @@ const title = 'icon'; {title ? {title} : null}`, want: want{ frontmatter: []string{"", "const title = 'icon';"}, - code: `${title ? $$render` + BACKTICK + `${title}` + BACKTICK + ` : null}`, + code: `${title ? $$render` + BACKTICK + `${title}` + BACKTICK + ` : null}`, }, }, { @@ -1074,7 +1101,7 @@ const title = 'icon'; source: ``, want: want{ scripts: []string{`{props:{"hoist":true}}`}, - code: ``, + code: ``, }, }, { @@ -1082,7 +1109,7 @@ const title = 'icon'; source: ``, want: want{ styles: []string{`{props:{"define:vars":({ color: "Gainsboro" }),"data-astro-id":"7HAAVZPE"}}`}, - code: ``, + code: ``, }, }, { @@ -1129,7 +1156,7 @@ const title = 'icon'; gtag('config', 'G-TEL60V1WM9'); -->`, want: want{ - code: ` + code: ` @@ -1168,7 +1195,7 @@ const title = 'icon'; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-TEL60V1WM9'); - -->`, + -->`, }, }, { @@ -1205,49 +1232,63 @@ import { Container, Col, Row } from 'react-bootstrap'; "{props:{\"data-astro-id\":\"EX5CHM4O\"},children:`div.astro-EX5CHM4O{color:green;}`}", "{props:{\"global\":true},children:`div { color: red }`}", }, - code: "\n\n\n\n\n\n\n\n
", + code: "\n\n\n\n\n\n\n" + RENDER_HEAD_RESULT + "\n
", }, }, { name: "Fragment", source: `
Default
Named
`, want: want{ - code: `${$$renderComponent($$result,'Fragment',Fragment,{},{"default": () => $$render` + BACKTICK + `
Default
Named
` + BACKTICK + `,})}`, + code: `${$$renderComponent($$result,'Fragment',Fragment,{},{"default": () => $$render` + BACKTICK + `
Default
Named
` + BACKTICK + `,})}`, }, }, { name: "Fragment shorthand", source: `<>
Default
Named
`, want: want{ - code: `${$$renderComponent($$result,'Fragment',Fragment,{},{"default": () => $$render` + BACKTICK + `
Default
Named
` + BACKTICK + `,})}`, + code: `${$$renderComponent($$result,'Fragment',Fragment,{},{"default": () => $$render` + BACKTICK + `
Default
Named
` + BACKTICK + `,})}`, + }, + }, + { + name: "Fragment shorthand only", + source: `<>Hello`, + want: want{ + code: `${$$renderComponent($$result,'Fragment',Fragment,{},{"default": () => $$render` + BACKTICK + `Hello` + BACKTICK + `,})}`, + }, + }, + { + name: "Fragment literal only", + source: `world`, + want: want{ + code: `${$$renderComponent($$result,'Fragment',Fragment,{},{"default": () => $$render` + BACKTICK + `world` + BACKTICK + `,})}`, }, }, { name: "Fragment slotted", source: `<>
Default
Named
`, want: want{ - code: `${$$renderComponent($$result,'Component',Component,{},{"default": () => $$render` + BACKTICK + `${$$renderComponent($$result,'Fragment',Fragment,{},{"default": () => $$render` + BACKTICK + `
Default
Named
` + BACKTICK + `,})}` + BACKTICK + `,})}`, + code: `${$$renderComponent($$result,'Component',Component,{},{"default": () => $$render` + BACKTICK + `${$$renderComponent($$result,'Fragment',Fragment,{},{"default": () => $$render` + BACKTICK + `
Default
Named
` + BACKTICK + `,})}` + BACKTICK + `,})}`, }, }, { name: "Fragment slotted with name", source: `
Default
Named
`, want: want{ - code: `${$$renderComponent($$result,'Component',Component,{},{"named": () => $$render` + BACKTICK + `${$$renderComponent($$result,'Fragment',Fragment,{"slot":"named"},{"default": () => $$render` + BACKTICK + `
Default
Named
` + BACKTICK + `,})}` + BACKTICK + `,})}`, + code: `${$$renderComponent($$result,'Component',Component,{},{"named": () => $$render` + BACKTICK + `${$$renderComponent($$result,'Fragment',Fragment,{"slot":"named"},{"default": () => $$render` + BACKTICK + `
Default
Named
` + BACKTICK + `,})}` + BACKTICK + `,})}`, }, }, { name: "Preserve slots inside custom-element", source: `
Name
Default
`, want: want{ - code: `${$$renderComponent($$result,'my-element','my-element',{},{"default": () => $$render` + BACKTICK + `
Name
Default
` + BACKTICK + `,})}`, + code: `${$$renderComponent($$result,'my-element','my-element',{},{"default": () => $$render` + BACKTICK + `
Name
Default
` + BACKTICK + `,})}`, }, }, { name: "Preserve namespaces", source: ``, want: want{ - code: ``, + code: ``, }, }, { @@ -1304,7 +1345,7 @@ const { product } = Astro.props; - +` + RENDER_HEAD_RESULT + ` ${$$renderComponent($$result,'Header',Header,{})}
@@ -1313,7 +1354,8 @@ const { product } = Astro.props;
${$$renderComponent($$result,'Footer',Footer,{})} -`, + +`, frontmatter: []string{ `import Header from '../../components/Header.jsx' import Footer from '../../components/Footer.astro' @@ -1345,6 +1387,13 @@ import ProductPageContent from '../../components/ProductPageContent.jsx';`, }, }, }, + { + name: "doctype", + source: `
`, + want: want{ + code: `
`, + }, + }, { name: "select option expression", source: `--- @@ -1353,7 +1402,7 @@ const value = 'test'; `, want: want{ frontmatter: []string{"", "const value = 'test';"}, - code: ``, + code: ``, }, }, { @@ -1364,7 +1413,7 @@ const value = 'test'; `, want: want{ frontmatter: []string{"", "const value = 'test';"}, - code: ``, + code: ``, }, }, { @@ -1375,14 +1424,14 @@ const value = 'test'; `, want: want{ frontmatter: []string{"", "const value = 'test';"}, - code: ``, + code: ``, }, }, { name: "textarea inside expression", source: `{bool && } {!bool && }`, want: want{ - code: `${bool && $$render` + BACKTICK + `` + BACKTICK + `} ${!bool && $$render` + BACKTICK + `` + BACKTICK + `}`, + code: `${bool && $$render` + BACKTICK + `` + BACKTICK + `} ${!bool && $$render` + BACKTICK + `` + BACKTICK + `}`, }, }, { @@ -1393,7 +1442,7 @@ const items = ["Dog", "Cat", "Platipus"]; {items.map(item => ())}
{item}
`, want: want{ frontmatter: []string{"", `const items = ["Dog", "Cat", "Platipus"];`}, - code: `${items.map(item => ($$render` + BACKTICK + `` + BACKTICK + `))}
${item}
`, + code: `${items.map(item => ($$render` + BACKTICK + `` + BACKTICK + `))}
${item}
`, }, }, { @@ -1404,7 +1453,7 @@ const items = ["Dog", "Cat", "Platipus"]; {items.map(item => ())}
Name
{item}
`, want: want{ frontmatter: []string{"", `const items = ["Dog", "Cat", "Platipus"];`}, - code: `${items.map(item => ($$render` + BACKTICK + `` + BACKTICK + `))}
Name
${item}
`, + code: `${items.map(item => ($$render` + BACKTICK + `` + BACKTICK + `))}
Name
${item}
`, }, }, { @@ -1415,63 +1464,63 @@ const items = ["Dog", "Cat", "Platipus"]; {items.map(item => ())}
Name
{item}{item + 's'}
`, want: want{ frontmatter: []string{"", `const items = ["Dog", "Cat", "Platipus"];`}, - code: `${items.map(item => ($$render` + BACKTICK + `` + BACKTICK + `))}
Name
${item}${item + 's'}
`, + code: `${items.map(item => ($$render` + BACKTICK + `` + BACKTICK + `))}
Name
${item}${item + 's'}
`, }, }, { name: "td expressions", source: `

Row 1

{title}
`, want: want{ - code: `

Row 1

${title}
`, + code: `

Row 1

${title}
`, }, }, { name: "th expressions", source: `
{title}
`, want: want{ - code: `
${title}
`, + code: `
${title}
`, }, }, { name: "anchor expressions", source: `{expr}`, want: want{ - code: `${expr}`, + code: `${expr}`, }, }, { name: "anchor inside expression", source: `{true && expr}`, want: want{ - code: `${true && $$render` + BACKTICK + `expr` + BACKTICK + `}`, + code: `${true && $$render` + BACKTICK + `expr` + BACKTICK + `}`, }, }, { name: "anchor content", source: `

  • {expr}
`, want: want{ - code: `

  • ${expr}
`, + code: `

  • ${expr}
`, }, }, { name: "small expression", source: `
{a}{data.map(a => )}
`, want: want{ - code: `
${a}${data.map(a => $$render` + BACKTICK + `${$$renderComponent($$result,'Component',Component,{"value":(a)})}` + BACKTICK + `)}
`, + code: `
${a}${data.map(a => $$render` + BACKTICK + `${$$renderComponent($$result,'Component',Component,{"value":(a)})}` + BACKTICK + `)}
`, }, }, { name: "division inside expression", source: `
{16 / 4}
`, want: want{ - code: `
${16 / 4}
`, + code: `
${16 / 4}
`, }, }, { name: "escaped entity", source: `A person saying "hello"`, want: want{ - code: `A person saying "hello"`, + code: `A person saying "hello"`, }, }, { @@ -1492,7 +1541,7 @@ const items = ["Dog", "Cat", "Platipus"]; name: "user-defined `implicit` is printed", source: ``, want: want{ - code: ``, + code: ``, }, }, { @@ -1500,15 +1549,11 @@ const items = ["Dog", "Cat", "Platipus"]; source: ` - -
My Text
`, +
My Text
`, want: want{ - styles: []string{fmt.Sprintf(`{props:{"data-astro-id":"RN5ULUD7"},children:%s/* comment */.container.astro-RN5ULUD7{padding:2rem;}%s}`, BACKTICK, BACKTICK)}, - code: ` - -
My Text
`, + styles: []string{fmt.Sprintf(`{props:{"data-astro-id":"SJ3WYE6H"},children:%s/* comment */.container.astro-SJ3WYE6H{padding:2rem;}%s}`, BACKTICK, BACKTICK)}, + code: `
My Text
`, }, }, { @@ -1521,7 +1566,7 @@ const items = ["Dog", "Cat", "Platipus"]; `, want: want{ - code: fmt.Sprintf(` + code: fmt.Sprintf(` ${true ? ($$render%s%s) : null} ${true ? ($$render%s%s) : null} @@ -1541,21 +1586,21 @@ const items = ["Dog", "Cat", "Platipus"]; name: "Empty expression", source: "({})", want: want{ - code: `(${(void 0)})`, + code: `(${(void 0)})`, }, }, { name: "Empty attribute expression", source: "", want: want{ - code: ``, + code: ``, }, }, { name: "is:raw", source: "
<% awesome %>
", want: want{ - code: `
<% awesome %>
`, + code: `
<% awesome %>
`, }, }, { @@ -1569,14 +1614,14 @@ const items = ["Dog", "Cat", "Platipus"]; name: "set:html", source: "
", want: want{ - code: `
${$$unescapeHTML(content)}
`, + code: `
${$$unescapeHTML(content)}
`, }, }, { name: "set:text", source: "
", want: want{ - code: `
${$$escapeHTML(content)}
`, + code: `
${content}
`, }, }, { @@ -1590,42 +1635,42 @@ const items = ["Dog", "Cat", "Platipus"]; name: "set:text on Component", source: "", want: want{ - code: `${$$renderComponent($$result,'Component',Component,{},{"default": () => $$render` + "`${$$escapeHTML(content)}`," + `})}`, + code: `${$$renderComponent($$result,'Component',Component,{},{"default": () => $$render` + "`${content}`," + `})}`, }, }, { name: "set:html on custom-element", source: "", want: want{ - code: `${$$renderComponent($$result,'custom-element','custom-element',{},{"default": () => $$render` + "`${$$unescapeHTML(content)}`," + `})}`, + code: `${$$renderComponent($$result,'custom-element','custom-element',{},{"default": () => $$render` + "`${$$unescapeHTML(content)}`," + `})}`, }, }, { name: "set:text on custom-element", source: "", want: want{ - code: `${$$renderComponent($$result,'custom-element','custom-element',{},{"default": () => $$render` + "`${$$escapeHTML(content)}`," + `})}`, + code: `${$$renderComponent($$result,'custom-element','custom-element',{},{"default": () => $$render` + "`${content}`," + `})}`, }, }, { name: "set:html on self-closing tag", source: "
", want: want{ - code: `
${$$unescapeHTML(content)}
`, + code: `
${$$unescapeHTML(content)}
`, }, }, { name: "set:html with other attributes", source: "
", want: want{ - code: `
${$$unescapeHTML(content)}
`, + code: `
${$$unescapeHTML(content)}
`, }, }, { name: "set:html on empty tag", source: "
", want: want{ - code: `
${$$unescapeHTML(content)}
`, + code: `
${$$unescapeHTML(content)}
`, }, }, { @@ -1633,35 +1678,35 @@ const items = ["Dog", "Cat", "Platipus"]; name: "set:html and set:text", source: "
", want: want{ - code: `
${$$unescapeHTML(content)}
`, + code: `
${$$unescapeHTML(content)}
`, }, }, { name: "set:html on tag with children", source: "
!!!
", want: want{ - code: `
${$$unescapeHTML(content)}
`, + code: `
${$$unescapeHTML(content)}
`, }, }, { name: "set:html on tag with empty whitespace", source: "
", want: want{ - code: `
${$$unescapeHTML(content)}
`, + code: `
${$$unescapeHTML(content)}
`, }, }, { name: "set:html on script", source: "`, + code: ``, }, }, { name: "set:html on style", source: "`, + code: ``, }, }, { @@ -1669,7 +1714,7 @@ const items = ["Dog", "Cat", "Platipus"]; source: "

testing

", staticExtraction: true, want: want{ - code: `

testing

`, + code: `

testing

`, styles: []string{ "{props:{\"define:vars\":({color:'green'}),\"data-astro-id\":\"BDWJYR3C\"},children:`h1.astro-BDWJYR3C{color:var(--color);}`}", }, @@ -1684,7 +1729,7 @@ const items = ["Dog", "Cat", "Platipus"]; source: ``, staticExtraction: true, want: want{ - code: ``, + code: ``, metadata: metadata{ hoisted: []string{"{ type: 'inline', value: `var two = 'two';` }"}, }, @@ -1715,7 +1760,7 @@ const items = ["Dog", "Cat", "Platipus"]; transform.ExtractStyles(doc) transform.Transform(doc, transform.TransformOptions{Scope: hash}) // note: we want to test Transform in context here, but more advanced cases could be tested separately result := PrintToJS(code, doc, 0, transform.TransformOptions{ - Scope: "astro-XXXX", + Scope: "XXXX", Site: "https://astro.build", InternalURL: "http://localhost:3000/", ProjectRoot: ".", @@ -1768,6 +1813,17 @@ const items = ["Dog", "Cat", "Platipus"]; } } metadata += "]" + // metadata.clientOnlyComponents + metadata += ", clientOnlyComponents: [" + if len(tt.want.metadata.clientOnlyComponents) > 0 { + for i, c := range tt.want.clientOnlyComponents { + if i > 0 { + metadata += ", " + } + metadata += fmt.Sprintf("'%s'", c) + } + } + metadata += "]" // directives metadata += ", hydrationDirectives: new Set([" if len(tt.want.hydrationDirectives) > 0 { @@ -1830,3 +1886,83 @@ const items = ["Dog", "Cat", "Platipus"]; }) } } + +func TestPrintToJSON(t *testing.T) { + tests := []jsonTestcase{ + { + name: "basic", + source: `

Hello world!

`, + want: []ASTNode{{Type: "element", Name: "h1", Children: []ASTNode{{Type: "text", Value: "Hello world!"}}}}, + }, + { + name: "expression", + source: `

Hello {world}

`, + want: []ASTNode{{Type: "element", Name: "h1", Children: []ASTNode{{Type: "text", Value: "Hello "}, {Type: "expression", Children: []ASTNode{{Type: "text", Value: "world"}}}}}}, + }, + { + name: "Component", + source: ``, + want: []ASTNode{{Type: "component", Name: "Component"}}, + }, + { + name: "custom-element", + source: ``, + want: []ASTNode{{Type: "custom-element", Name: "custom-element"}}, + }, + { + name: "Doctype", + source: ``, + want: []ASTNode{{Type: "doctype", Value: "html"}}, + }, + { + name: "Comment", + source: ``, + want: []ASTNode{{Type: "comment", Value: "hello"}}, + }, + { + name: "Comment preserves whitespace", + source: ``, + want: []ASTNode{{Type: "comment", Value: " hello "}}, + }, + { + name: "Fragment Shorthand", + source: `<>Hello`, + want: []ASTNode{{Type: "fragment", Name: "", Children: []ASTNode{{Type: "text", Value: "Hello"}}}}, + }, + { + name: "Fragment Literal", + source: `World`, + want: []ASTNode{{Type: "fragment", Name: "Fragment", Children: []ASTNode{{Type: "text", Value: "World"}}}}, + }, + { + name: "Frontmatter", + source: `--- +const a = "hey" +--- +
{a}
`, + want: []ASTNode{{Type: "frontmatter", Value: "\nconst a = \"hey\"\n"}, {Type: "element", Name: "div", Children: []ASTNode{{Type: "expression", Children: []ASTNode{{Type: "text", Value: "a"}}}}}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // transform output from source + code := test_utils.Dedent(tt.source) + + doc, err := astro.Parse(strings.NewReader(code)) + + if err != nil { + t.Error(err) + } + + root := ASTNode{Type: "root", Children: tt.want} + toMatch := root.String() + + result := PrintToJSON(code, doc, types.ParseOptions{Position: false}) + + if diff := test_utils.ANSIDiff(test_utils.Dedent(string(toMatch)), test_utils.Dedent(string(result.Output))); diff != "" { + t.Error(fmt.Sprintf("mismatch (-want +got):\n%s", diff)) + } + }) + } +} diff --git a/internal/sourcemap/sourcemap.go b/internal/sourcemap/sourcemap.go index 69425e5a1..b4250ccb1 100644 --- a/internal/sourcemap/sourcemap.go +++ b/internal/sourcemap/sourcemap.go @@ -607,6 +607,36 @@ func MakeChunkBuilder(inputSourceMap *SourceMap, lineOffsetTables []LineOffsetTa } } +func (b *ChunkBuilder) GetLineAndColumnForLocation(location loc.Loc) []int { + b.prevLoc = location + + // Binary search to find the line + lineOffsetTables := b.lineOffsetTables + count := len(lineOffsetTables) + originalLine := 0 + for count > 0 { + step := count / 2 + i := originalLine + step + if lineOffsetTables[i].byteOffsetToStartOfLine <= location.Start { + originalLine = i + 1 + count = count - step - 1 + } else { + count = step + } + } + originalLine-- + + // Use the line to compute the column + line := &lineOffsetTables[originalLine] + originalColumn := int(location.Start - line.byteOffsetToStartOfLine) + if line.columnsForNonASCII != nil && originalColumn >= int(line.byteOffsetToFirstNonASCII) { + originalColumn = int(line.columnsForNonASCII[originalColumn-int(line.byteOffsetToFirstNonASCII)]) + } + + // 1-based line, 1-based column + return []int{originalLine + 1, originalColumn + 1} +} + func (b *ChunkBuilder) AddSourceMapping(location loc.Loc, output []byte) { if location == b.prevLoc { return diff --git a/internal/t/t.go b/internal/t/t.go new file mode 100644 index 000000000..f81714daa --- /dev/null +++ b/internal/t/t.go @@ -0,0 +1,5 @@ +package t + +type ParseOptions struct { + Position bool +} diff --git a/internal/token.go b/internal/token.go index 31c941d68..795182942 100644 --- a/internal/token.go +++ b/internal/token.go @@ -67,6 +67,24 @@ const ( // AttributeType is the type of an Attribute type AttributeType uint32 +func (t AttributeType) String() string { + switch t { + case QuotedAttribute: + return "quoted" + case EmptyAttribute: + return "empty" + case ExpressionAttribute: + return "expression" + case SpreadAttribute: + return "spread" + case ShorthandAttribute: + return "shorthand" + case TemplateLiteralAttribute: + return "template-literal" + } + return "Invalid(" + strconv.Itoa(int(t)) + ")" +} + const ( QuotedAttribute AttributeType = iota EmptyAttribute @@ -1305,7 +1323,7 @@ func (z *Tokenizer) readTagAttrExpression() { } func (z *Tokenizer) Loc() loc.Loc { - return loc.Loc{Start: z.raw.Start} + return loc.Loc{Start: z.data.Start} } // An expression boundary means the next tokens should be treated as a JS expression diff --git a/internal/transform/transform.go b/internal/transform/transform.go index 4ad42969d..e61e7b3d8 100644 --- a/internal/transform/transform.go +++ b/internal/transform/transform.go @@ -11,7 +11,6 @@ import ( ) type TransformOptions struct { - As string Scope string Filename string Pathname string @@ -26,7 +25,7 @@ type TransformOptions struct { func Transform(doc *astro.Node, opts TransformOptions) *astro.Node { shouldScope := len(doc.Styles) > 0 && ScopeStyle(doc.Styles, opts) walk(doc, func(n *astro.Node) { - ExtractScript(doc, n) + ExtractScript(doc, n, &opts) AddComponentProps(doc, n) if shouldScope { ScopeElement(n, opts) @@ -39,42 +38,6 @@ func Transform(doc *astro.Node, opts TransformOptions) *astro.Node { script.Parent.RemoveChild(script) } - // Sometimes files have leading `; + +let result; +test.before(async () => { + result = await transform(FIXTURE); +}); + +test('script fragment', () => { + assert.ok(result.code, 'Can compile script fragment'); +}); + +test.run(); diff --git a/lib/compiler/test/basic/top-level-expressions.ts b/lib/compiler/test/basic/top-level-expressions.ts new file mode 100644 index 000000000..c83df90c7 --- /dev/null +++ b/lib/compiler/test/basic/top-level-expressions.ts @@ -0,0 +1,40 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { transform } from '@astrojs/compiler'; + +const FIXTURE = ` +--- +const { items, emptyItems } = Astro.props; + +const internal = []; +--- + + +{false && ( + +)} + + +{null && ( + +)} + + +{true && ( + +)} + + +{false && ()} +`; + +let result; +test.before(async () => { + result = await transform(FIXTURE); +}); + +test('top-level expressions', () => { + assert.ok(result.code, 'Can compile top-level expressions'); +}); + +test.run(); diff --git a/lib/compiler/test/basic/trailing-space.ts b/lib/compiler/test/basic/trailing-space.ts new file mode 100644 index 000000000..a5d9e4658 --- /dev/null +++ b/lib/compiler/test/basic/trailing-space.ts @@ -0,0 +1,34 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { transform } from '@astrojs/compiler'; + +const FIXTURE = `--- +import { Markdown } from 'astro/components'; +import Layout from '../layouts/content.astro'; +--- + + + + +
+ + ## Interesting Topic + +
+
`; // NOTE: the lack of trailing space is important to this test! + +let result; +test.before(async () => { + result = await transform(FIXTURE); +}); + +test('trailing space', () => { + assert.ok(result.code, 'Expected to compiler'); + assert.not.match(result.code, 'html', 'Expected output to not contain '); +}); + +test.run(); diff --git a/lib/compiler/test/body-expression.test.mjs b/lib/compiler/test/body-expression.test.mjs deleted file mode 100644 index ac8cc802f..000000000 --- a/lib/compiler/test/body-expression.test.mjs +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable no-console */ -import { transform } from '@astrojs/compiler'; - -const contents = `--- -const slugs = ['one', 'two', 'three']; ---- - - - - - - {slugs.map((slug) => ( - {slug} - ))} - - -`; - -async function run() { - const result = await transform(contents, { - sourcemap: true, - as: 'document', - site: undefined, - sourcefile: 'MoreMenu.astro', - sourcemap: 'both', - internalURL: 'astro/internal', - }); - - console.log(result.code); - - if (!result.code) { - throw new Error('Unable to compile body expression!'); - } -} - -await run(); diff --git a/lib/compiler/test/body-fragment.test.mjs b/lib/compiler/test/body-fragment.test.mjs deleted file mode 100644 index 6bbcc7a74..000000000 --- a/lib/compiler/test/body-fragment.test.mjs +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-disable no-console */ - -import { transform } from '@astrojs/compiler'; - -async function run() { - const result = await transform( - `--- -import ThemeToggleButton from './ThemeToggleButton.tsx'; ---- - -Uhhh - -
Hello!
`, - { - sourcemap: true, - as: 'fragment', - site: undefined, - sourcefile: 'MoreMenu.astro', - sourcemap: 'both', - internalURL: 'astro/internal', - } - ); - - if (!result.code.includes('
Hello!
')) { - throw new Error('Expected output to contain
Hello!
'); - } -} - -await run(); diff --git a/lib/compiler/test/component-name.test.mjs b/lib/compiler/test/component-name.test.mjs deleted file mode 100644 index 025debb31..000000000 --- a/lib/compiler/test/component-name.test.mjs +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable no-console */ - -import { transform } from '@astrojs/compiler'; - -async function run() { - const result = await transform(`
Hello world!
`, { - sourcemap: true, - pathname: '/src/components/Cool.astro', - }); - - if (!result.code.includes('export default $$Cool')) { - throw new Error(`Expected component export to be named "Cool"!`); - } -} - -await run(); diff --git a/lib/compiler/test/component-only.test.mjs b/lib/compiler/test/component-only.test.mjs deleted file mode 100644 index b55de90bb..000000000 --- a/lib/compiler/test/component-only.test.mjs +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-disable no-console */ - -import { transform } from '@astrojs/compiler'; -import sass from 'sass'; - -function transformSass(value) { - return new Promise((resolve, reject) => { - sass.render({ data: value }, (err, result) => { - if (err) { - reject(err); - return; - } - resolve({ code: result.css.toString('utf8'), map: result.map }); - return; - }); - }); -} - -async function run() { - const result = await transform( - `--- -import { Markdown } from 'astro/components'; -import Layout from '../layouts/content.astro'; ---- - - - - -
- - ## Interesting Topic - -
-
`, // NOTE: the lack of trailing space is important to this test! - { - sourcemap: true, - preprocessStyle: async (value, attrs) => { - if (attrs.lang === 'scss') { - try { - return transformSass(value); - } catch (err) { - console.error(err); - } - } - return null; - }, - as: 'document', - } - ); - - if (result.code.includes('html')) { - throw new Error('Result did not remove '); - } -} - -await run(); diff --git a/lib/compiler/test/empty-style.test.mjs b/lib/compiler/test/empty-style.test.mjs deleted file mode 100644 index fcb975354..000000000 --- a/lib/compiler/test/empty-style.test.mjs +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-disable no-console */ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -import { transform } from '@astrojs/compiler'; -import sass from 'sass'; - -function transformSass(value) { - return new Promise((resolve, reject) => { - sass.render({ data: value }, (err, result) => { - if (err) { - reject(err); - return; - } - resolve({ code: result.css.toString('utf8'), map: result.map }); - return; - }); - }); -} - -async function run() { - const result = await transform( - `--- -let value = 'world'; ---- - - - -
Hello world!
- -
Ahhh
-`, - { - sourcemap: true, - preprocessStyle: async (value, attrs) => { - if (!attrs.lang) { - return null; - } - if (attrs.lang === 'scss') { - try { - return transformSass(value); - } catch (err) { - console.error(err); - } - } - return null; - }, - } - ); -} - -await run(); diff --git a/lib/compiler/test/extract.test.mjs b/lib/compiler/test/extract.test.mjs deleted file mode 100644 index ad726dd8e..000000000 --- a/lib/compiler/test/extract.test.mjs +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-disable no-console */ - -import { transform } from '@astrojs/compiler'; - -async function run() { - const result = await transform( - `--- ---- -`, - { - sourcemap: true, - as: 'fragment', - site: undefined, - sourcefile: 'MoreMenu.astro', - sourcemap: 'both', - internalURL: 'astro/internal', - experimentalStaticExtraction: true, - } - ); - - const cssLen = result.css.length; - if (cssLen !== 1) { - throw new Error(`Incorrect CSS returned. Expected a length of 1 and got ${cssLen}`); - } -} - -await run(); diff --git a/lib/compiler/test/output.test.mjs b/lib/compiler/test/output.test.mjs deleted file mode 100644 index d48ef97cf..000000000 --- a/lib/compiler/test/output.test.mjs +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable no-console */ -import { transform } from '@astrojs/compiler'; - -async function run() { - const result = await transform( - `
- -
- `, - { - site: undefined, - sourcefile: '/Users/matthew/dev/astro/packages/astro/test/fixtures/astro-attrs/src/pages/namespaced.astro', - sourcemap: 'both', - internalURL: 'astro/internal', - preprocessStyle: async (_value, _attrs) => { - return null; - }, - } - ); - - if (result.code[0] === '\x00') { - throw new Error('Corrupt output'); - } -} - -await run().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/lib/compiler/test/parse/ast.ts b/lib/compiler/test/parse/ast.ts new file mode 100644 index 000000000..180aa001f --- /dev/null +++ b/lib/compiler/test/parse/ast.ts @@ -0,0 +1,33 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { parse } from '@astrojs/compiler'; + +const FIXTURE = ` +--- +let value = 'world'; +--- + +

Hello {value}

+`; + +let result; +test.before(async () => { + result = await parse(FIXTURE); +}); + +test('ast', () => { + assert.type(result, 'object', `Expected "parse" to return an object!`); + assert.equal(result.ast.type, 'root', `Expected "ast" root node to be of type "root"`); +}); + +test('frontmatter', () => { + const [frontmatter] = result.ast.children; + assert.equal(frontmatter.type, 'frontmatter', `Expected first child node to be of type "frontmatter"`); +}); + +test('element', () => { + const [, element] = result.ast.children; + assert.equal(element.type, 'element', `Expected first child node to be of type "element"`); +}); + +test.run(); diff --git a/lib/compiler/test/parse/fragment.ts b/lib/compiler/test/parse/fragment.ts new file mode 100644 index 000000000..76ba3bbd3 --- /dev/null +++ b/lib/compiler/test/parse/fragment.ts @@ -0,0 +1,24 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { parse } from '@astrojs/compiler'; + +const FIXTURE = `<>HelloWorld`; + +let result; +test.before(async () => { + result = await parse(FIXTURE); +}); + +test('fragment shorthand', () => { + const [first] = result.ast.children; + assert.equal(first.type, 'fragment', 'Expected first child to be of type "fragment"'); + assert.equal(first.name, '', 'Expected first child to have name of ""'); +}); + +test('fragment literal', () => { + const [, second] = result.ast.children; + assert.equal(second.type, 'fragment', 'Expected second child to be of type "fragment"'); + assert.equal(second.name, 'Fragment', 'Expected second child to have name of ""'); +}); + +test.run(); diff --git a/lib/compiler/test/parse/orphan-head.ts b/lib/compiler/test/parse/orphan-head.ts new file mode 100644 index 000000000..e73c00bf1 --- /dev/null +++ b/lib/compiler/test/parse/orphan-head.ts @@ -0,0 +1,35 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { parse } from '@astrojs/compiler'; + +const FIXTURE = ` + + + + + + +Document + + +

+ Hello world!

+ + +`; + +let result; +test.before(async () => { + result = await parse(FIXTURE); +}); + +test('orphan head', () => { + assert.ok(result, 'able to parse'); + + const [doctype, html, ...others] = result.ast.children; + assert.equal(others.length, 0, `Expected only two child nodes!`); + assert.equal(doctype.type, 'doctype', `Expected first child node to be of type "doctype"`); + assert.equal(html.type, 'element', `Expected first child node to be of type "element"`); +}); + +test.run(); diff --git a/lib/compiler/test/script-fragment.test.mjs b/lib/compiler/test/script-fragment.test.mjs deleted file mode 100644 index 93d9e01de..000000000 --- a/lib/compiler/test/script-fragment.test.mjs +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable no-console */ - -import { transform } from '@astrojs/compiler'; - -async function run() { - const result = await transform(``, { - sourcemap: true, - as: 'fragment', - site: undefined, - sourcefile: 'MoreMenu.astro', - sourcemap: 'both', - internalURL: 'astro/internal', - }); - - if (!result.code) { - throw new Error('Unable to compile script fragment!'); - } -} - -await run(); diff --git a/lib/compiler/test/static-extraction/css.ts b/lib/compiler/test/static-extraction/css.ts new file mode 100644 index 000000000..9f5aede3f --- /dev/null +++ b/lib/compiler/test/static-extraction/css.ts @@ -0,0 +1,24 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { transform } from '@astrojs/compiler'; + +const FIXTURE = ` +--- +--- + +`; + +let result; +test.before(async () => { + result = await transform(FIXTURE, { + experimentalStaticExtraction: true, + }); +}); + +test('extracts styles', () => { + assert.equal(result.css.length, 1, `Incorrect CSS returned. Expected a length of 1 and got ${result.css.length}`); +}); + +test.run(); diff --git a/lib/compiler/test/static-extraction/hoist-expression.ts b/lib/compiler/test/static-extraction/hoist-expression.ts new file mode 100644 index 000000000..fbcbac3e6 --- /dev/null +++ b/lib/compiler/test/static-extraction/hoist-expression.ts @@ -0,0 +1,21 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { transform } from '@astrojs/compiler'; + +const FIXTURE = ` +--- +const url = 'foo'; +--- + +`; + +let result; +test.before(async () => { + result = await transform(FIXTURE, { experimentalStaticExtraction: true }); +}); + +test('logs warning with hoisted expression', () => { + assert.ok(result.code); +}); + +test.run(); diff --git a/lib/compiler/test/stress.test.mjs b/lib/compiler/test/stress/index.ts similarity index 98% rename from lib/compiler/test/stress.test.mjs rename to lib/compiler/test/stress/index.ts index ebef6e914..9728aa11c 100644 --- a/lib/compiler/test/stress.test.mjs +++ b/lib/compiler/test/stress/index.ts @@ -1,13 +1,9 @@ -/* eslint-disable no-console */ -/* eslint-disable no-unused-vars */ -/* eslint-disable @typescript-eslint/no-unused-vars */ - /* eslint-disable no-console */ import { transform } from '@astrojs/compiler'; async function run() { - const result = await transform( + await transform( `--- import CartItems from './CartItems.astro'; --- diff --git a/lib/compiler/test/styles/empty-style.ts b/lib/compiler/test/styles/empty-style.ts new file mode 100644 index 000000000..bc8b0db45 --- /dev/null +++ b/lib/compiler/test/styles/empty-style.ts @@ -0,0 +1,30 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { transform } from '@astrojs/compiler'; +import { preprocessStyle } from '../utils'; + +const FIXTURE = ` +--- +let value = 'world'; +--- + + + +
Hello world!
+ +
Ahhh
+`; + +let result; +test.before(async () => { + result = await transform(FIXTURE, { + sourcemap: true, + preprocessStyle, + }); +}); + +test('can compile empty style', () => { + assert.ok(result.code, 'Expected to compile with empty style.'); +}); + +test.run(); diff --git a/lib/compiler/test/styles/sass.ts b/lib/compiler/test/styles/sass.ts new file mode 100644 index 000000000..083d91bd4 --- /dev/null +++ b/lib/compiler/test/styles/sass.ts @@ -0,0 +1,47 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { transform } from '@astrojs/compiler'; +import { preprocessStyle } from '../utils'; + +const FIXTURE = ` +--- +let value = 'world'; +--- + + + +
Hello world!
+ +
Ahhh
+ + +`; + +let result; +test.before(async () => { + result = await transform(FIXTURE, { + sourcemap: true, + preprocessStyle, + }); +}); + +test('transforms scss one', () => { + assert.match(result.code, 'color:red', 'Expected "color:red" to be present.'); +}); + +test('transforms scss two', () => { + assert.match(result.code, 'color:green', 'Expected "color:green" to be present.'); +}); + +test.run(); diff --git a/lib/compiler/test/test.mjs b/lib/compiler/test/test.mjs deleted file mode 100644 index d2030b1e7..000000000 --- a/lib/compiler/test/test.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import './basic.test.mjs'; -import './body-fragment.test.mjs'; -import './body-expression.test.mjs'; -import './component-only.test.mjs'; -import './component-name.test.mjs'; -import './empty-style.test.mjs'; -import './output.test.mjs'; -import './script-fragment.test.mjs'; -import './top-level-expression.test.mjs'; -import './stress.test.mjs'; diff --git a/lib/compiler/test/top-level-expression.test.mjs b/lib/compiler/test/top-level-expression.test.mjs deleted file mode 100644 index e74f784cb..000000000 --- a/lib/compiler/test/top-level-expression.test.mjs +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable no-console */ -import { transform } from '@astrojs/compiler'; - -const contents = ` ---- -const { items, emptyItems } = Astro.props; - -const internal = []; ---- - - -{false && ( - -)} - - -{null && ( - -)} - - -{true && ( - -)} - - -{false && ()} -`; - -async function run() { - const result = await transform(contents, { - sourcemap: true, - as: 'fragment', - site: undefined, - sourcefile: 'MoreMenu.astro', - sourcemap: 'both', - internalURL: 'astro/internal', - }); - - if (!result.code) { - throw new Error('Unable to compile top-level expression!'); - } -} - -await run(); diff --git a/lib/compiler/test/utils.ts b/lib/compiler/test/utils.ts new file mode 100644 index 000000000..4a59ad3f2 --- /dev/null +++ b/lib/compiler/test/utils.ts @@ -0,0 +1,24 @@ +import sass from 'sass'; + +export async function preprocessStyle(value, attrs): Promise { + if (!attrs.lang) { + return null; + } + if (attrs.lang === 'scss') { + return transformSass(value); + } + return null; +} + +export function transformSass(value: string) { + return new Promise((resolve, reject) => { + sass.render({ data: value }, (err, result) => { + if (err) { + reject(err); + return; + } + resolve({ code: result.css.toString('utf8'), map: result.map }); + return; + }); + }); +} diff --git a/lib/compiler/test/visible.test.mjs b/lib/compiler/test/visible.test.mjs deleted file mode 100644 index 80ad3a312..000000000 --- a/lib/compiler/test/visible.test.mjs +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable no-console */ -/* eslint-disable no-unused-vars */ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -import { transform } from '@astrojs/compiler'; - -async function run() { - const result = await transform( - `--- - import ThemeToggleButton from './ThemeToggleButton.tsx'; - --- - -
- -
`, - { - sourcemap: true, - as: 'fragment', - site: undefined, - sourcefile: 'MoreMenu.astro', - sourcemap: 'both', - internalURL: 'astro/internal', - preprocessStyle: async (value, attrs) => { - return null; - }, - } - ); - - // test - if (!result.code.includes(`{ modules: [{ module: $$module1, specifier: './ThemeToggleButton.tsx' }], hydratedComponents: [ThemeToggleButton], hoisted: [] }`)) { - throw new Error('Hydrated components not included'); - } -} - -await run(); diff --git a/lib/compiler/types.d.ts b/lib/compiler/types.d.ts new file mode 100644 index 000000000..df533bd29 --- /dev/null +++ b/lib/compiler/types.d.ts @@ -0,0 +1 @@ +export * from './shared/ast' diff --git a/lib/compiler/utils.d.ts b/lib/compiler/utils.d.ts new file mode 100644 index 000000000..644c24206 --- /dev/null +++ b/lib/compiler/utils.d.ts @@ -0,0 +1 @@ +export * from './node/utils'; diff --git a/package.json b/package.json index 6315136a5..eb134b946 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,15 @@ "url": "https://github.com/withastro/compiler.git" }, "scripts": { - "build": "make astro-wasm && lerna run build --scope @astrojs/compiler", + "build": "make wasm && lerna run build --scope @astrojs/compiler", "build:compiler": "lerna run build --scope @astrojs/compiler", "lint": "eslint \"lib/**/*.{cjs,js,jsx,mjs,ts,tsx}\"", "format": "prettier -w .", "prerelease": "yarn build:compiler", "release": "changeset publish", - "test": "node ./lib/compiler/test/test.mjs" + "test": "tsm node_modules/uvu/bin.js lib test -i utils -i stress", + "test:stress": "tsm lib/compiler/test/stress/index.ts", + "test:ci": "yarn test && yarn test:stress" }, "workspaces": [ "lib/*" diff --git a/yarn.lock b/yarn.lock index ab3b4d150..6462e630e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1157,6 +1157,11 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/node@*": + version "17.0.21" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" + integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ== + "@types/node@^12.7.1": version "12.20.28" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.28.tgz#4b20048c6052b5f51a8d5e0d2acbf63d5a17e1e2" @@ -1177,6 +1182,13 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/sass@^1.43.1": + version "1.43.1" + resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.1.tgz#86bb0168e9e881d7dade6eba16c9ed6d25dc2f68" + integrity sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g== + dependencies: + "@types/node" "*" + "@types/semver@^6.0.0": version "6.2.3" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.2.3.tgz#5798ecf1bec94eaa64db39ee52808ec0693315aa" @@ -2045,6 +2057,11 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +dequal@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d" + integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug== + detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" @@ -2063,6 +2080,11 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +diff@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -2180,6 +2202,126 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +esbuild-android-arm64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.23.tgz#c89b3c50b4f47668dcbeb0b34ee4615258818e71" + integrity sha512-k9sXem++mINrZty1v4FVt6nC5BQCFG4K2geCIUUqHNlTdFnuvcqsY7prcKZLFhqVC1rbcJAr9VSUGFL/vD4vsw== + +esbuild-darwin-64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.23.tgz#1c131e8cb133ed935ca32f824349a117c896a15b" + integrity sha512-lB0XRbtOYYL1tLcYw8BoBaYsFYiR48RPrA0KfA/7RFTr4MV7Bwy/J4+7nLsVnv9FGuQummM3uJ93J3ptaTqFug== + +esbuild-darwin-arm64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.23.tgz#3c6245a50109dd84953f53d7833bd3b4f0e8c6fa" + integrity sha512-yat73Z/uJ5tRcfRiI4CCTv0FSnwErm3BJQeZAh+1tIP0TUNh6o+mXg338Zl5EKChD+YGp6PN+Dbhs7qa34RxSw== + +esbuild-freebsd-64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.23.tgz#0cdc54e72d3dd9cd992f9c2960055e68a7f8650c" + integrity sha512-/1xiTjoLuQ+LlbfjJdKkX45qK/M7ARrbLmyf7x3JhyQGMjcxRYVR6Dw81uH3qlMHwT4cfLW4aEVBhP1aNV7VsA== + +esbuild-freebsd-arm64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.23.tgz#1d11faed3a0c429e99b7dddef84103eb509788b2" + integrity sha512-uyPqBU/Zcp6yEAZS4LKj5jEE0q2s4HmlMBIPzbW6cTunZ8cyvjG6YWpIZXb1KK3KTJDe62ltCrk3VzmWHp+iLg== + +esbuild-linux-32@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.23.tgz#fd9f033fc27dcab61100cb1eb1c936893a68c841" + integrity sha512-37R/WMkQyUfNhbH7aJrr1uCjDVdnPeTHGeDhZPUNhfoHV0lQuZNCKuNnDvlH/u/nwIYZNdVvz1Igv5rY/zfrzQ== + +esbuild-linux-64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.23.tgz#c04c438514f1359ecb1529205d0c836d4165f198" + integrity sha512-H0gztDP60qqr8zoFhAO64waoN5yBXkmYCElFklpd6LPoobtNGNnDe99xOQm28+fuD75YJ7GKHzp/MLCLhw2+vQ== + +esbuild-linux-arm64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.23.tgz#d1b3ab2988ab0734886eb9e811726f7db099ab96" + integrity sha512-c4MLOIByNHR55n3KoYf9hYDfBRghMjOiHLaoYLhkQkIabb452RWi+HsNgB41sUpSlOAqfpqKPFNg7VrxL3UX9g== + +esbuild-linux-arm@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.23.tgz#df7558b6a5076f5eb9fd387c8704f768b61d97fb" + integrity sha512-x64CEUxi8+EzOAIpCUeuni0bZfzPw/65r8tC5cy5zOq9dY7ysOi5EVQHnzaxS+1NmV+/RVRpmrzGw1QgY2Xpmw== + +esbuild-linux-mips64le@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.23.tgz#bb4c47fccc9493d460ffeb1f88e8a97a98a14f8b" + integrity sha512-kHKyKRIAedYhKug2EJpyJxOUj3VYuamOVA1pY7EimoFPzaF3NeY7e4cFBAISC/Av0/tiV0xlFCt9q0HJ68IBIw== + +esbuild-linux-ppc64le@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.23.tgz#a332dbc8a1b4e30cfe1261bfaa5cef57c9c8c02a" + integrity sha512-7ilAiJEPuJJnJp/LiDO0oJm5ygbBPzhchJJh9HsHZzeqO+3PUzItXi+8PuicY08r0AaaOe25LA7sGJ0MzbfBag== + +esbuild-linux-riscv64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.23.tgz#85675f3f931f5cd7cfb238fd82f77a62ffcb6d86" + integrity sha512-fbL3ggK2wY0D8I5raPIMPhpCvODFE+Bhb5QGtNP3r5aUsRR6TQV+ZBXIaw84iyvKC8vlXiA4fWLGhghAd/h/Zg== + +esbuild-linux-s390x@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.23.tgz#a526282a696e6d846f4c628f5315475518c0c0f0" + integrity sha512-GHMDCyfy7+FaNSO8RJ8KCFsnax8fLUsOrj9q5Gi2JmZMY0Zhp75keb5abTFCq2/Oy6KVcT0Dcbyo/bFb4rIFJA== + +esbuild-netbsd-64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.23.tgz#8e456605694719aa1be4be266d6cd569c06dfaf5" + integrity sha512-ovk2EX+3rrO1M2lowJfgMb/JPN1VwVYrx0QPUyudxkxLYrWeBxDKQvc6ffO+kB4QlDyTfdtAURrVzu3JeNdA2g== + +esbuild-openbsd-64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.23.tgz#f2fc51714b4ddabc86e4eb30ca101dd325db2f7d" + integrity sha512-uYYNqbVR+i7k8ojP/oIROAHO9lATLN7H2QeXKt2H310Fc8FJj4y3Wce6hx0VgnJ4k1JDrgbbiXM8rbEgQyg8KA== + +esbuild-sunos-64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.23.tgz#a408f33ea20e215909e20173a0fd78b1aaad1f8e" + integrity sha512-hAzeBeET0+SbScknPzS2LBY6FVDpgE+CsHSpe6CEoR51PApdn2IB0SyJX7vGelXzlyrnorM4CAsRyb9Qev4h9g== + +esbuild-windows-32@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.23.tgz#b9005bbff54dac3975ff355d5de2b5e37165d128" + integrity sha512-Kttmi3JnohdaREbk6o9e25kieJR379TsEWF0l39PQVHXq3FR6sFKtVPgY8wk055o6IB+rllrzLnbqOw/UV60EA== + +esbuild-windows-64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.23.tgz#2b5a99befeaca6aefdad32d738b945730a60a060" + integrity sha512-JtIT0t8ymkpl6YlmOl6zoSWL5cnCgyLaBdf/SiU/Eg3C13r0NbHZWNT/RDEMKK91Y6t79kTs3vyRcNZbfu5a8g== + +esbuild-windows-arm64@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.23.tgz#edc560bbadb097eb45fc235aeacb942cb94a38c0" + integrity sha512-cTFaQqT2+ik9e4hePvYtRZQ3pqOvKDVNarzql0VFIzhc0tru/ZgdLoXd6epLiKT+SzoSce6V9YJ+nn6RCn6SHw== + +esbuild@^0.14.0: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.23.tgz#95e842cb22bc0c7d82c140adc16788aac91469fe" + integrity sha512-XjnIcZ9KB6lfonCa+jRguXyRYcldmkyZ99ieDksqW/C8bnyEX299yA4QH2XcgijCgaddEZePPTgvx/2imsq7Ig== + optionalDependencies: + esbuild-android-arm64 "0.14.23" + esbuild-darwin-64 "0.14.23" + esbuild-darwin-arm64 "0.14.23" + esbuild-freebsd-64 "0.14.23" + esbuild-freebsd-arm64 "0.14.23" + esbuild-linux-32 "0.14.23" + esbuild-linux-64 "0.14.23" + esbuild-linux-arm "0.14.23" + esbuild-linux-arm64 "0.14.23" + esbuild-linux-mips64le "0.14.23" + esbuild-linux-ppc64le "0.14.23" + esbuild-linux-riscv64 "0.14.23" + esbuild-linux-s390x "0.14.23" + esbuild-netbsd-64 "0.14.23" + esbuild-openbsd-64 "0.14.23" + esbuild-sunos-64 "0.14.23" + esbuild-windows-32 "0.14.23" + esbuild-windows-64 "0.14.23" + esbuild-windows-arm64 "0.14.23" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -3317,6 +3459,11 @@ kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +kleur@^4.0.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d" + integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA== + lerna@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/lerna/-/lerna-4.0.0.tgz#b139d685d50ea0ca1be87713a7c2f44a5b678e9e" @@ -3753,6 +3900,11 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -4744,6 +4896,13 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" +sade@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -5255,6 +5414,13 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tsm@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tsm/-/tsm-2.2.1.tgz#3e78e86a03b2f569c20cff4a9f66c4ec8fce65fc" + integrity sha512-qvJB0baPnxQJolZru11mRgGTdNlx17WqgJnle7eht3Vhb+VUR4/zFA5hFl6NqRe7m8BD9w/6yu0B2XciRrdoJA== + dependencies: + esbuild "^0.14.0" + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -5428,6 +5594,16 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uvu@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.3.tgz#3d83c5bc1230f153451877bfc7f4aea2392219ae" + integrity sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw== + dependencies: + dequal "^2.0.0" + diff "^5.0.0" + kleur "^4.0.3" + sade "^1.7.3" + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
Row 1
Row 2