bi test
This commit is contained in:
201
builtin.go
Normal file
201
builtin.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BuiltinFunction any
|
||||||
|
|
||||||
|
type Builtin struct {
|
||||||
|
Name string
|
||||||
|
Function any
|
||||||
|
Definition string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
builtinRegistry = make(map[string]Builtin)
|
||||||
|
registryMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterBuiltin[T any](name string, fn T) {
|
||||||
|
builtinRegistry[name] = createBuiltin(name, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBuiltin(name string, fn any) Builtin {
|
||||||
|
fnValue := reflect.ValueOf(fn)
|
||||||
|
fnType := fnValue.Type()
|
||||||
|
|
||||||
|
tsDef := generateTypeScriptDefinition(name, fnType)
|
||||||
|
|
||||||
|
return Builtin{
|
||||||
|
Name: name,
|
||||||
|
Function: fn,
|
||||||
|
Definition: tsDef,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTypeScriptDefinition(name string, fnType reflect.Type) string {
|
||||||
|
if fnType.Kind() != reflect.Func {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var params []string
|
||||||
|
for i := 0; i < fnType.NumIn(); i++ {
|
||||||
|
params = append(params, fmt.Sprintf("arg%d: %s", i, goTypeToTSType(fnType.In(i))))
|
||||||
|
}
|
||||||
|
|
||||||
|
returnSignature := "void"
|
||||||
|
if fnType.NumOut() > 0 {
|
||||||
|
returnType := fnType.Out(0)
|
||||||
|
returnSignature = goTypeToTSType(returnType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("declare function %s(%s): %s;", name, strings.Join(params, ", "), returnSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
func goTypeToTSType(t reflect.Type) string {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return "string"
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return "number"
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return "number"
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return "number"
|
||||||
|
case reflect.Bool:
|
||||||
|
return "boolean"
|
||||||
|
case reflect.Interface, reflect.Pointer:
|
||||||
|
if t.String() == "goja.Value" {
|
||||||
|
return "any"
|
||||||
|
}
|
||||||
|
return "any"
|
||||||
|
case reflect.Slice:
|
||||||
|
return fmt.Sprintf("%s[]", goTypeToTSType(t.Elem()))
|
||||||
|
case reflect.Map:
|
||||||
|
return "Record<string, any>"
|
||||||
|
case reflect.Struct:
|
||||||
|
return "any"
|
||||||
|
default:
|
||||||
|
return "any"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBuiltinsDeclarations() string {
|
||||||
|
registryMutex.RLock()
|
||||||
|
defer registryMutex.RUnlock()
|
||||||
|
|
||||||
|
var decls []string
|
||||||
|
for _, builtin := range builtinRegistry {
|
||||||
|
decls = append(decls, builtin.Definition)
|
||||||
|
}
|
||||||
|
return strings.Join(decls, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterBuiltins(vm *goja.Runtime) {
|
||||||
|
RegisterFetchBuiltin(vm)
|
||||||
|
|
||||||
|
registryMutex.RLock()
|
||||||
|
defer registryMutex.RUnlock()
|
||||||
|
|
||||||
|
for name, builtin := range builtinRegistry {
|
||||||
|
if builtin.Function != nil {
|
||||||
|
_ = vm.Set(name, builtin.Function)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetchResult struct {
|
||||||
|
OK bool
|
||||||
|
Status int
|
||||||
|
Body string
|
||||||
|
Headers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fetch(url string, options map[string]any) (*FetchResult, error) {
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := make(map[string]string)
|
||||||
|
for key, values := range resp.Header {
|
||||||
|
if len(values) > 0 {
|
||||||
|
headers[key] = values[0]
|
||||||
|
headers[strings.ToLower(key)] = values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FetchResult{
|
||||||
|
OK: resp.StatusCode >= 200 && resp.StatusCode < 300,
|
||||||
|
Status: resp.StatusCode,
|
||||||
|
Body: string(body),
|
||||||
|
Headers: headers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterFetchBuiltin(vm *goja.Runtime) {
|
||||||
|
_ = vm.Set("fetch", func(call goja.FunctionCall) goja.Value {
|
||||||
|
if len(call.Arguments) < 1 {
|
||||||
|
panic("fetch requires at least 1 argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := call.Arguments[0].String()
|
||||||
|
|
||||||
|
result, err := Fetch(url, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultObj := vm.NewObject()
|
||||||
|
_ = resultObj.Set("ok", result.OK)
|
||||||
|
_ = resultObj.Set("status", result.Status)
|
||||||
|
|
||||||
|
body := result.Body
|
||||||
|
_ = resultObj.Set("text", func() string {
|
||||||
|
return body
|
||||||
|
})
|
||||||
|
|
||||||
|
headersObj := vm.NewObject()
|
||||||
|
headers := result.Headers
|
||||||
|
_ = headersObj.Set("get", func(c goja.FunctionCall) goja.Value {
|
||||||
|
if len(c.Arguments) < 1 {
|
||||||
|
return goja.Undefined()
|
||||||
|
}
|
||||||
|
key := c.Arguments[0].String()
|
||||||
|
return vm.ToValue(headers[key])
|
||||||
|
})
|
||||||
|
_ = resultObj.Set("headers", headersObj)
|
||||||
|
|
||||||
|
return resultObj
|
||||||
|
})
|
||||||
|
|
||||||
|
builtinRegistry["fetch"] = Builtin{
|
||||||
|
Name: "fetch",
|
||||||
|
Function: nil,
|
||||||
|
Definition: "declare function fetch(url: string, options?: any): PromiseLike<any>;",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
}
|
||||||
88
builtin_test.go
Normal file
88
builtin_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFetch(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("X-Custom-Header", "test-value")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte(`{"status":"ok","message":"Hello from httptest"}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
result, err := Fetch(server.URL, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, result.OK)
|
||||||
|
assert.Equal(t, http.StatusOK, result.Status)
|
||||||
|
assert.Contains(t, result.Body, "Hello from httptest")
|
||||||
|
assert.Contains(t, result.Body, `"status":"ok"`)
|
||||||
|
assert.Equal(t, "application/json", result.Headers["Content-Type"])
|
||||||
|
assert.Equal(t, "test-value", result.Headers["X-Custom-Header"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchHTTPBin(t *testing.T) {
|
||||||
|
result, err := Fetch("https://httpbin.org/get", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, result.OK)
|
||||||
|
assert.Equal(t, http.StatusOK, result.Status)
|
||||||
|
assert.Contains(t, result.Body, `"args"`)
|
||||||
|
assert.Equal(t, "application/json", result.Headers["Content-Type"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchWith404(t *testing.T) {
|
||||||
|
result, err := Fetch("https://httpbin.org/status/404", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.False(t, result.OK)
|
||||||
|
assert.Equal(t, http.StatusNotFound, result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchWithInvalidURL(t *testing.T) {
|
||||||
|
_, err := Fetch("http://this-domain-does-not-exist-12345.com", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to fetch")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchBuiltinIntegration(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.Header().Set("X-Custom", "custom-value")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
var stdout, stderr strings.Builder
|
||||||
|
tsContent := `
|
||||||
|
const response = fetch("${URL}");
|
||||||
|
console.log("OK:", response.ok);
|
||||||
|
console.log("Status:", response.status);
|
||||||
|
console.log("Body:", response.text());
|
||||||
|
|
||||||
|
console.log("Content-Type:", response.headers.get("content-type") || "undefined");
|
||||||
|
console.log("Content-Type (case sensitive):", response.headers.get("Content-Type") || "undefined");
|
||||||
|
console.log("X-Custom:", response.headers.get("x-custom") || "undefined");
|
||||||
|
console.log("X-Custom (case sensitive):", response.headers.get("X-Custom") || "undefined");
|
||||||
|
`
|
||||||
|
tsContent = strings.Replace(tsContent, "${URL}", server.URL, 1)
|
||||||
|
|
||||||
|
err := executeTypeScriptContent(tsContent, &stdout, &stderr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, stderr.String(), "Expected no error output")
|
||||||
|
|
||||||
|
output := stdout.String()
|
||||||
|
assert.Contains(t, output, "OK: true")
|
||||||
|
assert.Contains(t, output, "Status: 200")
|
||||||
|
assert.Contains(t, output, "Body: Hello, World!")
|
||||||
|
}
|
||||||
47
main.go
47
main.go
@@ -34,6 +34,8 @@ func executeTypeScript(filePath string, stdout, stderr io.Writer) error {
|
|||||||
|
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
|
|
||||||
|
RegisterBuiltins(vm)
|
||||||
|
|
||||||
console := vm.NewObject()
|
console := vm.NewObject()
|
||||||
_ = vm.Set("console", console)
|
_ = vm.Set("console", console)
|
||||||
|
|
||||||
@@ -58,6 +60,51 @@ func executeTypeScript(filePath string, stdout, stderr io.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func executeTypeScriptContent(tsContent string, stdout, stderr io.Writer) error {
|
||||||
|
result := api.Transform(tsContent, api.TransformOptions{
|
||||||
|
Loader: api.LoaderTS,
|
||||||
|
Target: api.ES2020,
|
||||||
|
Format: api.FormatIIFE,
|
||||||
|
Sourcemap: api.SourceMapNone,
|
||||||
|
TreeShaking: api.TreeShakingFalse,
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(result.Errors) > 0 {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "Transpilation errors:\n")
|
||||||
|
for _, err := range result.Errors {
|
||||||
|
_, _ = fmt.Fprintf(stderr, " %s\n", err.Text)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("transpilation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
vm := goja.New()
|
||||||
|
|
||||||
|
RegisterBuiltins(vm)
|
||||||
|
|
||||||
|
console := vm.NewObject()
|
||||||
|
_ = vm.Set("console", console)
|
||||||
|
|
||||||
|
_ = console.Set("log", func(call goja.FunctionCall) goja.Value {
|
||||||
|
args := call.Arguments
|
||||||
|
for i, arg := range args {
|
||||||
|
if i > 0 {
|
||||||
|
_, _ = fmt.Fprint(stdout, " ")
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprint(stdout, arg.String())
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintln(stdout)
|
||||||
|
return goja.Undefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := vm.RunString(string(result.Code))
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "Execution error: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
fmt.Fprintln(os.Stderr, "Usage: program <typescript-file>")
|
fmt.Fprintln(os.Stderr, "Usage: program <typescript-file>")
|
||||||
|
|||||||
6
test_data/fetch.ts
Normal file
6
test_data/fetch.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const response = fetch("https://httpbin.org/get");
|
||||||
|
|
||||||
|
console.log("OK:", response.ok);
|
||||||
|
console.log("Status:", response.status);
|
||||||
|
console.log("Body:", response.text());
|
||||||
|
console.log("Content-Type:", response.headers.get("content-type"));
|
||||||
6
test_data/fetch_demo.ts
Normal file
6
test_data/fetch_demo.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const response = fetch("https://httpbin.org/get");
|
||||||
|
|
||||||
|
console.log("OK:", response.ok);
|
||||||
|
console.log("Status:", response.status);
|
||||||
|
console.log("Body:", response.text());
|
||||||
|
console.log("Content-Type:", response.headers.get("content-type"));
|
||||||
Reference in New Issue
Block a user