Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tsx): Extract script and styles from TSX for language-server consumption #1019

Merged
merged 15 commits into from
Jul 16, 2024
Merged
22 changes: 21 additions & 1 deletion cmd/astro-wasm/astro-wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ func jsString(j js.Value) string {
return j.String()
}

func jsBoolOptional(j js.Value, defaultValue bool) bool {
if j.Equal(js.Undefined()) || j.Equal(js.Null()) {
return defaultValue
}
return j.Bool()
}

func jsBool(j js.Value) bool {
if j.Equal(js.Undefined()) || j.Equal(js.Null()) {
return false
Expand Down Expand Up @@ -148,6 +155,16 @@ func makeTransformOptions(options js.Value) transform.TransformOptions {
}
}

func makeTSXOptions(options js.Value) printer.TSXOptions {
includeScripts := jsBoolOptional(options.Get("includeScripts"), true)
includeStyles := jsBoolOptional(options.Get("includeStyles"), true)

return printer.TSXOptions{
IncludeScripts: includeScripts,
IncludeStyles: includeStyles,
}
}

type RawSourceMap struct {
File string `js:"file"`
Mappings string `js:"mappings"`
Expand Down Expand Up @@ -260,7 +277,10 @@ func ConvertToTSX() any {
if err != nil {
h.AppendError(err)
}
result := printer.PrintToTSX(source, doc, transformOptions, h)

tsxOptions := makeTSXOptions(js.Value(args[1]))

result := printer.PrintToTSX(source, doc, tsxOptions, transformOptions, h)

// AFTER printing, exec transformations to pickup any errors/warnings
transform.Transform(doc, transformOptions, h)
Expand Down
200 changes: 185 additions & 15 deletions internal/printer/print-to-tsx.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,20 @@ func getTSXPrefix() string {
return "/* @jsxImportSource astro */\n\n"
}

func PrintToTSX(sourcetext string, n *Node, opts transform.TransformOptions, h *handler.Handler) PrintResult {
type TSXOptions struct {
IncludeScripts bool
IncludeStyles bool
}

func PrintToTSX(sourcetext string, n *Node, opts TSXOptions, transformOpts transform.TransformOptions, h *handler.Handler) PrintResult {
p := &printer{
sourcetext: sourcetext,
opts: opts,
opts: transformOpts,
builder: sourcemap.MakeChunkBuilder(nil, sourcemap.GenerateLineOffsetTables(sourcetext, len(strings.Split(sourcetext, "\n")))),
}
p.print(getTSXPrefix())
renderTsx(p, n)
renderTsx(p, n, &opts)

return PrintResult{
Output: p.output,
SourceMapChunk: p.builder.GenerateChunk(p.output),
Expand All @@ -36,14 +42,137 @@ func PrintToTSX(sourcetext string, n *Node, opts transform.TransformOptions, h *
}

type TSXRanges struct {
Frontmatter loc.TSXRange `js:"frontmatter"`
Body loc.TSXRange `js:"body"`
Frontmatter loc.TSXRange `js:"frontmatter"`
Body loc.TSXRange `js:"body"`
Scripts []TSXExtractedTag `js:"scripts"`
Styles []TSXExtractedTag `js:"styles"`
}

var htmlEvents = map[string]bool{
"onabort": true,
"onafterprint": true,
"onauxclick": true,
"onbeforematch": true,
"onbeforeprint": true,
"onbeforeunload": true,
"onblur": true,
"oncancel": true,
"oncanplay": true,
"oncanplaythrough": true,
"onchange": true,
"onclick": true,
"onclose": true,
"oncontextlost": true,
"oncontextmenu": true,
"oncontextrestored": true,
"oncopy": true,
"oncuechange": true,
"oncut": true,
"ondblclick": true,
"ondrag": true,
"ondragend": true,
"ondragenter": true,
"ondragleave": true,
"ondragover": true,
"ondragstart": true,
"ondrop": true,
"ondurationchange": true,
"onemptied": true,
"onended": true,
"onerror": true,
"onfocus": true,
"onformdata": true,
"onhashchange": true,
"oninput": true,
"oninvalid": true,
"onkeydown": true,
"onkeypress": true,
"onkeyup": true,
"onlanguagechange": true,
"onload": true,
"onloadeddata": true,
"onloadedmetadata": true,
"onloadstart": true,
"onmessage": true,
"onmessageerror": true,
"onmousedown": true,
"onmouseenter": true,
"onmouseleave": true,
"onmousemove": true,
"onmouseout": true,
"onmouseover": true,
"onmouseup": true,
"onoffline": true,
"ononline": true,
"onpagehide": true,
"onpageshow": true,
"onpaste": true,
"onpause": true,
"onplay": true,
"onplaying": true,
"onpopstate": true,
"onprogress": true,
"onratechange": true,
"onrejectionhandled": true,
"onreset": true,
"onresize": true,
"onscroll": true,
"onscrollend": true,
"onsecuritypolicyviolation": true,
"onseeked": true,
"onseeking": true,
"onselect": true,
"onslotchange": true,
"onstalled": true,
"onstorage": true,
"onsubmit": true,
"onsuspend": true,
"ontimeupdate": true,
"ontoggle": true,
"onunhandledrejection": true,
"onunload": true,
"onvolumechange": true,
"onwaiting": true,
"onwheel": true,
}

func getScriptTypeForNode(n Node) string {
if n.Attr == nil || len(n.Attr) == 0 {
return "processed-module"
}

// If the script tag has `type="module"`, it's not processed, but it's still a module
for _, attr := range n.Attr {
if attr.Key == "type" {
if strings.Contains(attr.Val, "module") {
return "module"
}

if ScriptJSONMimeTypes[strings.ToLower(attr.Val)] {
return "json"
}
}

}

// Otherwise, it's an inline script
return "inline"
}

type TSXExtractedTag struct {
Loc loc.TSXRange `js:"position"`
Type string `js:"type"`
Content string `js:"content"`
}

func isScript(p *astro.Node) bool {
return p.DataAtom == atom.Script
}

func isStyle(p *astro.Node) bool {
return p.DataAtom == atom.Style
}

var ScriptMimeTypes map[string]bool = map[string]bool{
"module": true,
"text/typescript": true,
Expand All @@ -52,6 +181,13 @@ var ScriptMimeTypes map[string]bool = map[string]bool{
"application/node": true,
}

var ScriptJSONMimeTypes map[string]bool = map[string]bool{
"application/json": true,
"application/ld+json": true,
"importmap": true,
"speculationrules": true,
}

// This is not perfect (as in, you wouldn't use this to make a spec compliant parser), but it's good enough
// for the real world. Thankfully, JSX is also a bit more lax than JavaScript, so we can spare some work.
func isValidTSXAttribute(a Attribute) bool {
Expand Down Expand Up @@ -96,19 +232,23 @@ type TextType uint32
const (
RawText TextType = iota
ScriptText
StyleText
)

func getTextType(n *astro.Node) TextType {
if script := n.Closest(isScript); script != nil {
attr := astro.GetAttribute(script, "type")
if attr == nil || (attr != nil && ScriptMimeTypes[strings.ToLower(attr.Val)]) {
if attr == nil || ScriptMimeTypes[strings.ToLower(attr.Val)] {
return ScriptText
}
}
if style := n.Closest(isStyle); style != nil {
return StyleText
}
return RawText
}

func renderTsx(p *printer, n *Node) {
func renderTsx(p *printer, n *Node, o *TSXOptions) {
// Root of the document, print all children
if n.Type == DocumentNode {
source := []byte(p.sourcetext)
Expand Down Expand Up @@ -147,7 +287,7 @@ func renderTsx(p *printer, n *Node) {

hasChildren = true
}
renderTsx(p, c)
renderTsx(p, c, o)
}
p.addSourceMapping(loc.Loc{Start: len(p.sourcetext)})
p.print("\n")
Expand Down Expand Up @@ -206,7 +346,7 @@ declare const Astro: Readonly<import('astro').AstroGlobal<%s, typeof %s`, propsI
}
p.printTextWithSourcemap(c.Data, c.Loc[0])
} else {
renderTsx(p, c)
renderTsx(p, c, o)
}
}
if n.FirstChild != nil {
Expand All @@ -226,10 +366,22 @@ declare const Astro: Readonly<import('astro').AstroGlobal<%s, typeof %s`, propsI
case TextNode:
if getTextType(n) == ScriptText {
p.addNilSourceMapping()
p.print("\n{() => {")
p.printTextWithSourcemap(n.Data, n.Loc[0])
if o.IncludeScripts {
p.print("\n{() => {")
p.printTextWithSourcemap(n.Data, n.Loc[0])
p.addNilSourceMapping()
p.print("}}\n")
}
p.addSourceMapping(loc.Loc{Start: n.Loc[0].Start + len(n.Data)})
return
} else if getTextType(n) == StyleText {
p.addNilSourceMapping()
p.print("}}\n")
if o.IncludeStyles {
p.print("{`")
p.printTextWithSourcemap(escapeText(n.Data), n.Loc[0])
p.addNilSourceMapping()
p.print("`}")
}
p.addSourceMapping(loc.Loc{Start: n.Loc[0].Start + len(n.Data)})
return
} else if strings.ContainsAny(n.Data, "{}<>'\"") && n.Data[0] != '<' {
Expand Down Expand Up @@ -284,7 +436,7 @@ declare const Astro: Readonly<import('astro').AstroGlobal<%s, typeof %s`, propsI
p.addNilSourceMapping()
p.print(`<Fragment>`)
}
renderTsx(p, c)
renderTsx(p, c, o)
if c.NextSibling == nil || c.NextSibling.Type == TextNode {
p.addNilSourceMapping()
p.print(`</Fragment>`)
Expand All @@ -310,7 +462,7 @@ declare const Astro: Readonly<import('astro').AstroGlobal<%s, typeof %s`, propsI
if isImplicit {
// Render any child nodes
for c := n.FirstChild; c != nil; c = c.NextSibling {
renderTsx(p, c)
renderTsx(p, c, o)
}
return
}
Expand Down Expand Up @@ -360,6 +512,12 @@ declare const Astro: Readonly<import('astro').AstroGlobal<%s, typeof %s`, propsI
p.print(`"`)
endLoc = a.ValLoc.Start
}
if _, ok := htmlEvents[a.Key]; ok {
p.addTSXScript(a.ValLoc.Start-p.bytesToSkip, endLoc-p.bytesToSkip, a.Val, "event-attribute")
}
if a.Key == "style" {
p.addTSXStyle(a.ValLoc.Start-p.bytesToSkip, endLoc-p.bytesToSkip, a.Val, "style-attribute")
}
case astro.EmptyAttribute:
p.print(a.Key)
endLoc = a.KeyLoc.Start + len(a.Key)
Expand Down Expand Up @@ -521,15 +679,27 @@ declare const Astro: Readonly<import('astro').AstroGlobal<%s, typeof %s`, propsI
}
p.print(">")

startTagEnd := endLoc - p.bytesToSkip

// Render any child nodes
for c := n.FirstChild; c != nil; c = c.NextSibling {
renderTsx(p, c)
renderTsx(p, c, o)
if len(c.Loc) > 1 {
endLoc = c.Loc[1].Start + len(c.Data) + 1
} else if len(c.Loc) == 1 {
endLoc = c.Loc[0].Start + len(c.Data)
}
}

if n.FirstChild != nil && (n.DataAtom == atom.Script || n.DataAtom == atom.Style) {
if n.DataAtom == atom.Script {
p.addTSXScript(startTagEnd, endLoc-p.bytesToSkip, n.FirstChild.Data, getScriptTypeForNode(*n))
}
if n.DataAtom == atom.Style {
p.addTSXStyle(startTagEnd, endLoc-p.bytesToSkip, n.FirstChild.Data, "tag")
}
}

// Special case because of trailing expression close in scripts
if n.DataAtom == atom.Script {
p.printf("</%s>", n.Data)
Expand Down
28 changes: 28 additions & 0 deletions internal/printer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ type printer struct {

// Optional, used only for TSX output
ranges TSXRanges
// Keep track of how many multi-byte characters we've printed so that they can be skipped whenever we need a character-based index
// This could be directly in the token / node information, however this would require a fairly large refactor
bytesToSkip int
Copy link
Member Author

@Princesseuh Princesseuh Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this not really optimal, because it confuse the printed bytes with the source bytes. It's fine in 99.999% of cases, because we're not printing any multi-bytes ourselves that were not part of the source. But it's something we could do and would have to take into account then. (and is trivial to do, but still)

The real fix, but it'd require a large amount of work would be to have this information directly on the tokens / nodes. I first tried to do that, but I quickly realised that I was changing everything and ran into the risk of breaking things everywhere.

If this prove to be an issue, I'd definitely be willing to make the proper change, but in my testing this honestly proved to work just fine.

}

var TEMPLATE_TAG = "$$render"
Expand Down Expand Up @@ -82,6 +85,28 @@ func (p *printer) setTSXBodyRange(componentRange loc.TSXRange) {
p.ranges.Body = componentRange
}

func (p *printer) addTSXScript(start int, end int, content string, scriptType string) {
p.ranges.Scripts = append(p.ranges.Scripts, TSXExtractedTag{
Loc: loc.TSXRange{
Start: start,
End: end,
},
Content: content,
Type: scriptType,
})
}

func (p *printer) addTSXStyle(start int, end int, content string, styleType string) {
p.ranges.Styles = append(p.ranges.Styles, TSXExtractedTag{
Loc: loc.TSXRange{
Start: start,
End: end,
},
Content: content,
Type: styleType,
})
}

func (p *printer) printTextWithSourcemap(text string, l loc.Loc) {
start := l.Start
for pos, c := range text {
Expand All @@ -93,6 +118,9 @@ func (p *printer) printTextWithSourcemap(text string, l loc.Loc) {
continue
}
_, nextCharByteSize := utf8.DecodeRuneInString(text[pos:])
if nextCharByteSize > 1 {
p.bytesToSkip += nextCharByteSize - 1
}
p.addSourceMapping(loc.Loc{Start: start})
p.print(string(c))
start += nextCharByteSize
Expand Down
Loading
Loading