diff --git a/.changeset/bright-plums-suffer.md b/.changeset/bright-plums-suffer.md new file mode 100644 index 000000000..2966ce635 --- /dev/null +++ b/.changeset/bright-plums-suffer.md @@ -0,0 +1,5 @@ +--- +"@astrojs/compiler": patch +--- + +Transform top level returns into throws in the TSX output diff --git a/internal/js_scanner/js_scanner.go b/internal/js_scanner/js_scanner.go index 17fa15e24..38f48a80e 100644 --- a/internal/js_scanner/js_scanner.go +++ b/internal/js_scanner/js_scanner.go @@ -12,6 +12,60 @@ import ( "github.com/withastro/compiler/internal/loc" ) +func FindTopLevelReturns(source []byte) []int { + l := js.NewLexer(parse.NewInputBytes(source)) + i := 0 + returns := make([]int, 0) + pairs := make(map[byte]int) + inFunction := false + + for { + token, value := l.Next() + + if token == js.DivToken || token == js.DivEqToken { + lns := bytes.Split(source[i+1:], []byte{'\n'}) + if bytes.Contains(lns[0], []byte{'/'}) { + token, value = l.RegExp() + } + } + + if token == js.ErrorToken { + if l.Err() != io.EOF { + return returns + } + break + } + + if js.IsPunctuator(token) { + if value[0] == '{' { + pairs[value[0]]++ + i += len(value) + continue + } else if value[0] == '}' { + pairs['{']-- + } + } + + // Track function declarations + if token == js.FunctionToken { + inFunction = true + } + + // Track end of function declarations + if inFunction && token == js.CloseBraceToken && pairs['{'] == 1 { + inFunction = false + } + + if token == js.ReturnToken && !inFunction { + returns = append(returns, i) + } + + i += len(value) + } + + return returns +} + type HoistedScripts struct { Hoisted [][]byte HoistedLocs []loc.Loc diff --git a/internal/printer/print-to-tsx.go b/internal/printer/print-to-tsx.go index 34ef022df..7e79f3be1 100644 --- a/internal/printer/print-to-tsx.go +++ b/internal/printer/print-to-tsx.go @@ -2,6 +2,7 @@ package printer import ( "fmt" + "slices" "strings" "unicode" @@ -366,11 +367,31 @@ declare const Astro: Readonly 0 { p.addSourceMapping(c.Loc[0]) } - p.printTextWithSourcemap(c.Data, c.Loc[0]) + // Remplace all the top level returns with a `throw` + if len(topLevelReturn) > 0 { + // Loop over the characters and replace the top level returns with a `throw` + newString := []byte{} + + i := 0 + for i < len(c.Data) { + if slices.Contains(topLevelReturn, i) { + newString = append(newString, []byte("throw ")...) + i += len("return") + } else { + newString = append(newString, c.Data[i]) + i++ + } + } + + p.printTextWithSourcemap(string(newString), c.Loc[0]) + } else { + p.printTextWithSourcemap(c.Data, c.Loc[0]) + } } else { renderTsx(p, c, o) } diff --git a/packages/compiler/test/tsx/top-level-returns.ts b/packages/compiler/test/tsx/top-level-returns.ts new file mode 100644 index 000000000..6e5c95933 --- /dev/null +++ b/packages/compiler/test/tsx/top-level-returns.ts @@ -0,0 +1,44 @@ +import { convertToTSX } from '@astrojs/compiler'; +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { TSXPrefix } from '../utils.js'; + +test('transforms top-level returns to throw statements', async () => { + const input = `--- +if (something) { + return Astro.redirect(); +} + +function thatDoesSomething() { + return "Hey"; +} + +class Component { + render() { + return "wow"! + } +} +---`; + const output = `${TSXPrefix} +if (something) { + throw Astro.redirect(); +} + +function thatDoesSomething() { + return "Hey"; +} + +class Component { + render() { + return "wow"! + } +} + + +export default function __AstroComponent_(_props: Record): any {} +`; + const { code } = await convertToTSX(input, { sourcemap: 'external' }); + assert.snapshot(code, output, 'expected code to match snapshot'); +}); + +test.run();