Skip to content

Commit

Permalink
feat: implement getStaticPaths hoisting (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
natemoo-re authored Nov 8, 2021
1 parent 8a434f9 commit 3e5ef91
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-crews-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/compiler': patch
---

Implement getStaticPaths hoisting
124 changes: 124 additions & 0 deletions internal/js_scanner/js_scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,130 @@ func HasExports(source []byte) bool {
}
}

type HoistedScripts struct {
Hoisted [][]byte
Body []byte
}

func HoistExports(source []byte) HoistedScripts {
shouldHoist := hasGetStaticPaths(source)
if !shouldHoist {
return HoistedScripts{
Body: source,
}
}

l := js.NewLexer(parse.NewInputBytes(source))
i := 0
pairs := make(map[byte]int)

// Let's lex the script until we find what we need!
for {
token, value := l.Next()

if token == js.ErrorToken {
if l.Err() != io.EOF {
return HoistedScripts{
Body: source,
}
}
break
}

// Common delimeters. Track their length, then skip.
if token == js.WhitespaceToken || token == js.LineTerminatorToken || token == js.SemicolonToken {
i += len(value)
continue
}

// Exports should be consumed until all opening braces are closed,
// a specifier is found, and a line terminator has been found
if token == js.ExportToken {
foundGetStaticPaths := false
foundSemicolonOrLineTerminator := false
start := i - 1
i += len(value)
for {
next, nextValue := l.Next()
i += len(nextValue)

if js.IsIdentifier(next) {
if !foundGetStaticPaths {
foundGetStaticPaths = string(nextValue) == "getStaticPaths"
}
} else if next == js.LineTerminatorToken || next == js.SemicolonToken {
foundSemicolonOrLineTerminator = true
} else if js.IsPunctuator(next) {
if nextValue[0] == '{' || nextValue[0] == '(' || nextValue[0] == '[' {
pairs[nextValue[0]]++
} else if nextValue[0] == '}' {
pairs['{']--
} else if nextValue[0] == ')' {
pairs['(']--
} else if nextValue[0] == ']' {
pairs['[']--
}
}

if next == js.ErrorToken {
return HoistedScripts{
Body: source,
}
}

if foundGetStaticPaths && foundSemicolonOrLineTerminator && pairs['{'] == 0 && pairs['('] == 0 && pairs['['] == 0 {
hoisted := make([][]byte, 1)
hoisted = append(hoisted, source[start:i])
body := make([]byte, 0)
body = append(body, source[0:start]...)
body = append(body, source[i:]...)
return HoistedScripts{
Hoisted: hoisted,
Body: body,
}
}
}
}

// Track opening and closing braces
if js.IsPunctuator(token) {
if value[0] == '{' || value[0] == '(' || value[0] == '[' {
pairs[value[0]]++
i += len(value)
continue
} else if value[0] == '}' {
pairs['{']--
} else if value[0] == ')' {
pairs['(']--
} else if value[0] == ']' {
pairs['[']--
}
}

// Track our current position
i += len(value)
}

// If we haven't found anything... there's nothing to find! Split at the start.
return HoistedScripts{
Body: source,
}
}

func hasGetStaticPaths(source []byte) bool {
l := js.NewLexer(parse.NewInputBytes(source))
for {
token, value := l.Next()
if token == js.ErrorToken {
// EOF or other error
return false
}
if token == js.IdentifierToken && string(value) == "getStaticPaths" {
return true
}
}
}

func AccessesPrivateVars(source []byte) bool {
l := js.NewLexer(parse.NewInputBytes(source))
for {
Expand Down
28 changes: 16 additions & 12 deletions internal/printer/print-to-js.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package printer

import (
"errors"
"fmt"
"sort"
"strings"
Expand Down Expand Up @@ -117,13 +116,17 @@ func render1(p *printer, n *Node, opts RenderOptions) {
p.addSourceMapping(n.Loc[0])
if renderBodyStart == -1 {
p.addSourceMapping(c.Loc[0])
if js_scanner.AccessesPrivateVars([]byte(c.Data)) {
panic(errors.New("Variables prefixed by \"$$\" are reserved for Astro's internal usage!"))
}
preprocessed := js_scanner.HoistExports([]byte(c.Data))

// 1. After imports put in the top-level Astro.
p.printTopLevelAstro()

if len(preprocessed.Hoisted) > 0 {
for _, hoisted := range preprocessed.Hoisted {
p.println(strings.TrimSpace(string(hoisted)))
}
}

// 2. The frontmatter.
p.print(strings.TrimSpace(c.Data))

Expand All @@ -134,14 +137,9 @@ func render1(p *printer, n *Node, opts RenderOptions) {
p.printFuncPrelude("$$Component")
} else {
importStatements := c.Data[0:renderBodyStart]
renderBody := c.Data[renderBodyStart:]
content := c.Data[renderBodyStart:]
preprocessed := js_scanner.HoistExports([]byte(content))

if js_scanner.HasExports([]byte(renderBody)) {
panic(errors.New("Export statements must be placed at the top of .astro files!"))
}
// {
// panic(errors.New("Variables prefixed by \"$$\" are reserved for Astro's internal usage!"))
// }
p.addSourceMapping(c.Loc[0])
p.println(strings.TrimSpace(importStatements))

Expand All @@ -150,10 +148,16 @@ func render1(p *printer, n *Node, opts RenderOptions) {
// 2. Top-level Astro global.
p.printTopLevelAstro()

if len(preprocessed.Hoisted) > 0 {
for _, hoisted := range preprocessed.Hoisted {
p.println(strings.TrimSpace(string(hoisted)))
}
}

// TODO: use the proper component name
p.printFuncPrelude("$$Component")
p.addSourceMapping(loc.Loc{Start: c.Loc[0].Start + renderBodyStart})
p.print(strings.TrimSpace(renderBody))
p.print(strings.TrimSpace(string(preprocessed.Body)))
}

// Print empty just to ensure a newline
Expand Down
62 changes: 59 additions & 3 deletions internal/printer/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ var CREATE_ASTRO_CALL = "const $$Astro = $$createAstro(import.meta.url, 'https:/
var NON_WHITESPACE_CHARS = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[];:'\",.?")

type want struct {
frontmatter []string
styles []string
scripts []string
frontmatter []string
styles []string
scripts []string
getStaticPaths string
metadata
code string
}
Expand Down Expand Up @@ -86,6 +87,57 @@ const href = '/about';
code: `<html><head></head><body><a${` + ADD_ATTRIBUTE + `(href, "href")}>About</a></body></html>`,
},
},
{
name: "getStaticPaths (basic)",
source: `---
export const getStaticPaths = async () => {
return { paths: [] }
}
---
<div></div>`,
want: want{
frontmatter: []string{`export const getStaticPaths = async () => {
return { paths: [] }
}`, ""},
code: `<html><head></head><body><div></div></body></html>`,
},
},
{
name: "getStaticPaths (hoisted)",
source: `---
const a = 0;
export const getStaticPaths = async () => {
return { paths: [] }
}
---
<div></div>`,
want: want{
frontmatter: []string{"", `const a = 0;`},
getStaticPaths: `export const getStaticPaths = async () => {
return { paths: [] }
}`,
code: `<html><head></head><body><div></div></body></html>`,
},
},
{
name: "getStaticPaths (hoisted II)",
source: `---
const a = 0;
export async function getStaticPaths() {
return { paths: [] }
}
const b = 0;
---
<div></div>`,
want: want{
frontmatter: []string{"", `const a = 0;
const b = 0;`},
getStaticPaths: `export async function getStaticPaths() {
return { paths: [] }
}`,
code: `<html><head></head><body><div></div></body></html>`,
},
},
{
name: "component",
source: `---
Expand Down Expand Up @@ -1076,8 +1128,12 @@ import * as $$module1 from 'react-bootstrap';`},
}
}
metadata += "] }"

toMatch += "\n\n" + fmt.Sprintf("export const %s = %s(import.meta.url, %s);\n\n", METADATA, CREATE_METADATA, metadata)
toMatch += test_utils.Dedent(CREATE_ASTRO_CALL) + "\n\n"
if len(tt.want.getStaticPaths) > 0 {
toMatch += strings.TrimSpace(test_utils.Dedent(tt.want.getStaticPaths)) + "\n\n"
}
toMatch += test_utils.Dedent(PRELUDE) + "\n"
if len(tt.want.frontmatter) > 1 {
toMatch += test_utils.Dedent(tt.want.frontmatter[1])
Expand Down

0 comments on commit 3e5ef91

Please sign in to comment.