From de1dcaceefed3ab90c908fea45a06780ad40b1a2 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Tue, 27 Jan 2026 10:08:07 -0500 Subject: [PATCH] bi test --- builtin.go | 201 ++++++++++++++++++++++++++++++++++++++++ builtin_test.go | 88 ++++++++++++++++++ main.go | 47 ++++++++++ test_data/fetch.ts | 6 ++ test_data/fetch_demo.ts | 6 ++ 5 files changed, 348 insertions(+) create mode 100644 builtin.go create mode 100644 builtin_test.go create mode 100644 test_data/fetch.ts create mode 100644 test_data/fetch_demo.ts diff --git a/builtin.go b/builtin.go new file mode 100644 index 0000000..11fc483 --- /dev/null +++ b/builtin.go @@ -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" + 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;", + } +} + +func init() { +} diff --git a/builtin_test.go b/builtin_test.go new file mode 100644 index 0000000..4c1ff25 --- /dev/null +++ b/builtin_test.go @@ -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!") +} diff --git a/main.go b/main.go index e8d3b41..1992540 100644 --- a/main.go +++ b/main.go @@ -34,6 +34,8 @@ func executeTypeScript(filePath string, stdout, stderr io.Writer) error { vm := goja.New() + RegisterBuiltins(vm) + console := vm.NewObject() _ = vm.Set("console", console) @@ -58,6 +60,51 @@ func executeTypeScript(filePath string, stdout, stderr io.Writer) error { 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() { if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, "Usage: program ") diff --git a/test_data/fetch.ts b/test_data/fetch.ts new file mode 100644 index 0000000..2f5ea84 --- /dev/null +++ b/test_data/fetch.ts @@ -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")); diff --git a/test_data/fetch_demo.ts b/test_data/fetch_demo.ts new file mode 100644 index 0000000..2f5ea84 --- /dev/null +++ b/test_data/fetch_demo.ts @@ -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"));