This commit is contained in:
2026-01-29 10:07:33 -05:00
parent 234c4718a4
commit 0d97f20e79
16 changed files with 929 additions and 646 deletions

View File

@@ -25,14 +25,17 @@ reichard.io/poiesis/
├── internal/ ├── internal/
│ ├── runtime/ # Runtime management, transpilation, execution │ ├── runtime/ # Runtime management, transpilation, execution
│ │ ├── runtime.go │ │ ├── runtime.go
│ │ ── runtime_test.go │ │ ── runtime_test.go
│ │ └── options.go
│ ├── functions/ # Function registration framework │ ├── functions/ # Function registration framework
│ │ ├── collector.go
│ │ ├── registry.go │ │ ├── registry.go
│ │ ├── types.go │ │ ├── types.go
│ │ ├── typescript.go
│ │ ├── typescript_test.go │ │ ├── typescript_test.go
│ │ └── functions_test.go │ │ └── functions_test.go
│ ├── tsconvert/ # Go-to-TypeScript type conversion utilities
│ │ ├── convert.go
│ │ ├── types.go
│ │ └── convert_test.go
│ └── stdlib/ # Standard library implementations │ └── stdlib/ # Standard library implementations
│ ├── fetch.go │ ├── fetch.go
│ └── fetch_test.go │ └── fetch_test.go
@@ -40,8 +43,9 @@ reichard.io/poiesis/
## Key Packages ## Key Packages
- `reichard.io/poiesis/internal/runtime` - Runtime management, TypeScript transpilation, JavaScript execution - `reichard.io/poiesis/internal/runtime` - Runtime management, TypeScript transpilation, JavaScript execution, per-runtime type management
- `reichard.io/poiesis/internal/functions` - Generic function registration framework (sync/async wrappers, automatic JS/Go conversion via JSON, type definition generation) - `reichard.io/poiesis/internal/functions` - Generic function registration framework (sync/async wrappers, automatic JS/Go conversion via JSON)
- `reichard.io/poiesis/internal/tsconvert` - Go-to-TypeScript type conversion utilities and type declaration generation
- `reichard.io/poiesis/internal/stdlib` - Standard library implementations (fetch) - `reichard.io/poiesis/internal/stdlib` - Standard library implementations (fetch)
## Function System ## Function System
@@ -49,6 +53,7 @@ reichard.io/poiesis/
### Registration ### Registration
Two types of functions: Two types of functions:
- **Sync**: `RegisterFunction[T, R](name, fn)` - executes synchronously, returns value - **Sync**: `RegisterFunction[T, R](name, fn)` - executes synchronously, returns value
- **Async**: `RegisterAsyncFunction[T, R](name, fn)` - runs in goroutine, returns Promise - **Async**: `RegisterAsyncFunction[T, R](name, fn)` - runs in goroutine, returns Promise
@@ -108,3 +113,26 @@ functions.RegisterAsyncFunction[FetchArgs, *FetchResult]("fetch", Fetch)
- Use `os` package instead of deprecated `io/ioutil` - Use `os` package instead of deprecated `io/ioutil`
- Error logging uses `_, _ = fmt.Fprintf(stderr, ...)` pattern - Error logging uses `_, _ = fmt.Fprintf(stderr, ...)` pattern
- Package structure follows standard Go project layout with internal packages - Package structure follows standard Go project layout with internal packages
### Comment Style
Code blocks (even within functions) should be separated with title-cased comments describing what the block does:
```go
// Create Runtime
r := &Runtime{opts: qjs.Option{Context: ctx}}
// Create QuickJS Context
rt, err := qjs.New(r.opts)
// Populate Globals
if err := r.populateGlobals(); err != nil {
return nil, err
}
```
For more complex blocks, use a hyphen to add elaboration:
```go
// Does Thing - We do this here because we need to do xyz...
```

View File

@@ -5,11 +5,11 @@ import (
"fmt" "fmt"
"os" "os"
"reichard.io/poiesis/internal/functions"
"reichard.io/poiesis/internal/runtime" "reichard.io/poiesis/internal/runtime"
) )
func main() { func main() {
// Validate Arguments
if len(os.Args) < 2 { if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: poiesis <typescript-file>") fmt.Fprintln(os.Stderr, "Usage: poiesis <typescript-file>")
fmt.Fprintln(os.Stderr, " poiesis -print-types") fmt.Fprintln(os.Stderr, " poiesis -print-types")
@@ -18,7 +18,11 @@ func main() {
// Print Types // Print Types
if os.Args[1] == "-print-types" { if os.Args[1] == "-print-types" {
fmt.Println(functions.GetFunctionDeclarations()) rt, err := runtime.New(context.Background())
if err != nil {
panic(err)
}
fmt.Println(rt.GetTypeDeclarations())
return return
} }

View File

@@ -1,170 +0,0 @@
package functions
import (
"fmt"
"reflect"
"strings"
"sync"
)
type typeCollector struct {
mu sync.Mutex
types map[string]string
paramTypes map[string]bool
}
func newTypeCollector() *typeCollector {
return &typeCollector{
types: make(map[string]string),
paramTypes: make(map[string]bool),
}
}
func (tc *typeCollector) collectTypes(argsType reflect.Type, fnType reflect.Type) []string {
tc.mu.Lock()
defer tc.mu.Unlock()
tc.types = make(map[string]string)
tc.paramTypes = make(map[string]bool)
var result []string
tc.collectStruct(argsType, argsType.Name())
for i := 0; i < argsType.NumField(); i++ {
field := argsType.Field(i)
if field.Type.Kind() == reflect.Pointer || strings.Contains(field.Tag.Get("json"), ",omitempty") {
tc.collectParamType(field.Type)
}
}
if fnType.Kind() == reflect.Func && fnType.NumOut() > 0 {
lastIndex := fnType.NumOut() - 1
lastType := fnType.Out(lastIndex)
if lastType.Implements(reflect.TypeOf((*error)(nil)).Elem()) {
if fnType.NumOut() > 1 {
tc.collectType(fnType.Out(0))
}
} else {
tc.collectType(lastType)
}
}
for _, t := range tc.types {
result = append(result, t)
}
return result
}
func (tc *typeCollector) collectParamType(t reflect.Type) {
if t.Kind() == reflect.Pointer {
tc.collectParamType(t.Elem())
return
}
if t.Kind() == reflect.Struct && t.Name() != "" {
tc.paramTypes[t.Name()+" | null"] = true
}
}
func (tc *typeCollector) getParamTypes() map[string]bool {
return tc.paramTypes
}
func (tc *typeCollector) collectType(t reflect.Type) {
if t.Kind() == reflect.Pointer {
tc.collectType(t.Elem())
return
}
if t.Kind() == reflect.Struct {
name := t.Name()
if _, exists := tc.types[name]; !exists {
tc.collectStruct(t, name)
}
}
}
func (tc *typeCollector) collectStruct(t reflect.Type, name string) {
if t.Kind() != reflect.Struct {
return
}
var fields []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.Anonymous || !field.IsExported() {
continue
}
fieldName := getFieldName(field)
var tsType string
var isOptional bool
isPointer := field.Type.Kind() == reflect.Pointer
if isPointer {
isOptional = true
tsType = goTypeToTSType(field.Type, false)
} else {
isOptional = strings.Contains(field.Tag.Get("json"), ",omitempty")
tsType = goTypeToTSType(field.Type, false)
}
if isOptional {
fieldName += "?"
}
fields = append(fields, fmt.Sprintf("%s: %s", fieldName, tsType))
tc.collectType(field.Type)
}
tc.types[name] = fmt.Sprintf("interface %s {%s}", name, strings.Join(fields, "; "))
}
func goTypeToTSType(t reflect.Type, isPointer bool) string {
if t.Kind() == reflect.Pointer {
return goTypeToTSType(t.Elem(), true)
}
baseType := ""
switch t.Kind() {
case reflect.String:
baseType = "string"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
baseType = "number"
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
baseType = "number"
case reflect.Float32, reflect.Float64:
baseType = "number"
case reflect.Bool:
baseType = "boolean"
case reflect.Interface:
baseType = "any"
case reflect.Slice:
baseType = fmt.Sprintf("%s[]", goTypeToTSType(t.Elem(), false))
case reflect.Map:
if t.Key().Kind() == reflect.String && t.Elem().Kind() == reflect.Interface {
baseType = "Record<string, any>"
} else {
baseType = "Record<string, any>"
}
case reflect.Struct:
name := t.Name()
if name == "" {
baseType = "{}"
} else {
baseType = name
}
default:
baseType = "any"
}
if isPointer {
baseType += " | null"
}
return baseType
}

View File

@@ -5,44 +5,47 @@ import (
"reflect" "reflect"
"strings" "strings"
"sync" "sync"
"reichard.io/poiesis/internal/tsconvert"
) )
var ( var (
functionRegistry = make(map[string]Function) functionRegistry = make(map[string]Function)
registryMutex sync.RWMutex registryMutex sync.RWMutex
collector *typeCollector
) )
func registerFunction[A Args, R any](name string, isAsync bool, fn GoFunc[A, R]) { func registerFunction[A Args, R any](name string, isAsync bool, fn GoFunc[A, R]) {
// Lock Registry
registryMutex.Lock() registryMutex.Lock()
defer registryMutex.Unlock() defer registryMutex.Unlock()
if collector == nil { // Validate Args Type
collector = newTypeCollector()
}
tType := reflect.TypeFor[A]() tType := reflect.TypeFor[A]()
if tType.Kind() != reflect.Struct { if tType.Kind() != reflect.Struct {
panic(fmt.Sprintf("function %s: argument must be a struct type, got %v", name, tType)) panic(fmt.Sprintf("function %s: argument must be a struct type, got %v", name, tType))
} }
// Collect Types and Generate Definition
fnType := reflect.TypeOf(fn) fnType := reflect.TypeOf(fn)
types := collector.collectTypes(tType, fnType) types := tsconvert.CollectTypes(tType, fnType)
paramTypes := collector.getParamTypes() definition := tsconvert.GenerateFunctionDecl(name, tType, fnType, isAsync)
// Register Function
functionRegistry[name] = &functionImpl[A, R]{ functionRegistry[name] = &functionImpl[A, R]{
name: name, name: name,
fn: fn, fn: fn,
types: types, types: types.All(),
definition: generateTypeScriptDefinition(name, tType, fnType, isAsync, paramTypes), definition: definition,
isAsync: isAsync, isAsync: isAsync,
} }
} }
func GetFunctionDeclarations() string { func GetFunctionDeclarations() string {
// Lock Registry
registryMutex.RLock() registryMutex.RLock()
defer registryMutex.RUnlock() defer registryMutex.RUnlock()
// Collect Type Definitions
typeDefinitions := make(map[string]bool) typeDefinitions := make(map[string]bool)
var typeDefs []string var typeDefs []string
var functionDecls []string var functionDecls []string
@@ -57,6 +60,7 @@ func GetFunctionDeclarations() string {
functionDecls = append(functionDecls, fn.Definition()) functionDecls = append(functionDecls, fn.Definition())
} }
// Build Result
result := strings.Join(typeDefs, "\n\n") result := strings.Join(typeDefs, "\n\n")
if len(result) > 0 && len(functionDecls) > 0 { if len(result) > 0 && len(functionDecls) > 0 {
result += "\n\n" result += "\n\n"
@@ -66,10 +70,33 @@ func GetFunctionDeclarations() string {
return result return result
} }
func GetRegisteredFunctions() map[string]Function { // GetTypeDeclarations returns all type declarations from all registered functions.
// This is used for aggregating types across multiple functions.
func GetTypeDeclarations() map[string]string {
// Lock Registry
registryMutex.RLock() registryMutex.RLock()
defer registryMutex.RUnlock() defer registryMutex.RUnlock()
// Collect All Types
allTypes := make(map[string]string)
for _, fn := range functionRegistry {
for name, def := range fn.Types() {
if existing, ok := allTypes[name]; ok && existing != def {
// Type Conflict Detected - Skip
continue
}
allTypes[name] = def
}
}
return allTypes
}
func GetRegisteredFunctions() map[string]Function {
// Lock Registry
registryMutex.RLock()
defer registryMutex.RUnlock()
// Copy Registry
result := make(map[string]Function, len(functionRegistry)) result := make(map[string]Function, len(functionRegistry))
for k, v := range functionRegistry { for k, v := range functionRegistry {
result[k] = v result[k] = v
@@ -78,9 +105,11 @@ func GetRegisteredFunctions() map[string]Function {
} }
func RegisterFunction[T Args, R any](name string, fn GoFunc[T, R]) { func RegisterFunction[T Args, R any](name string, fn GoFunc[T, R]) {
// Register Sync Function
registerFunction(name, false, fn) registerFunction(name, false, fn)
} }
func RegisterAsyncFunction[T Args, R any](name string, fn GoFunc[T, R]) { func RegisterAsyncFunction[T Args, R any](name string, fn GoFunc[T, R]) {
// Register Async Function
registerFunction(name, true, fn) registerFunction(name, true, fn)
} }

View File

@@ -8,7 +8,7 @@ import (
type Function interface { type Function interface {
Name() string Name() string
Types() []string Types() map[string]string
Definition() string Definition() string
IsAsync() bool IsAsync() bool
Arguments() []reflect.Type Arguments() []reflect.Type
@@ -25,7 +25,7 @@ type functionImpl[A Args, R any] struct {
name string name string
fn GoFunc[A, R] fn GoFunc[A, R]
definition string definition string
types []string types map[string]string
isAsync bool isAsync bool
} }
@@ -33,7 +33,7 @@ func (b *functionImpl[A, R]) Name() string {
return b.name return b.name
} }
func (b *functionImpl[A, R]) Types() []string { func (b *functionImpl[A, R]) Types() map[string]string {
return b.types return b.types
} }
@@ -50,6 +50,7 @@ func (b *functionImpl[A, R]) Function() any {
} }
func (b *functionImpl[A, R]) Arguments() []reflect.Type { func (b *functionImpl[A, R]) Arguments() []reflect.Type {
// Collect Argument Types
var allTypes []reflect.Type var allTypes []reflect.Type
rType := reflect.TypeFor[A]() rType := reflect.TypeFor[A]()
@@ -73,17 +74,19 @@ func (b *functionImpl[A, R]) CallGeneric(ctx context.Context, allArgs []any) (ze
for i := range min(aVal.NumField(), len(allArgs)) { for i := range min(aVal.NumField(), len(allArgs)) {
field := aVal.Field(i) field := aVal.Field(i)
// Validate Field is Settable
if !field.CanSet() { if !field.CanSet() {
return zeroR, errors.New("cannot set field") return zeroR, errors.New("cannot set field")
} }
// Validate and Set Field Value
argVal := reflect.ValueOf(allArgs[i]) argVal := reflect.ValueOf(allArgs[i])
if !argVal.Type().AssignableTo(field.Type()) { if !argVal.Type().AssignableTo(field.Type()) {
return zeroR, errors.New("cannot assign field") return zeroR, errors.New("cannot assign field")
} }
field.Set(argVal) field.Set(argVal)
} }
// Execute Function
return b.fn(ctx, fnArgs) return b.fn(ctx, fnArgs)
} }

View File

@@ -1,73 +0,0 @@
package functions
import (
"fmt"
"reflect"
"strings"
)
func getFieldName(field reflect.StructField) string {
jsonTag := field.Tag.Get("json")
if jsonTag != "" && jsonTag != "-" {
name, _, _ := strings.Cut(jsonTag, ",")
return name
}
return field.Name
}
func generateTypeScriptDefinition(name string, argsType reflect.Type, fnType reflect.Type, isPromise bool, paramTypes map[string]bool) string {
if argsType.Kind() != reflect.Struct {
return ""
}
var params []string
for i := 0; i < argsType.NumField(); i++ {
field := argsType.Field(i)
fieldName := getFieldName(field)
goType := field.Type
var tsType string
var isOptional bool
isPointer := goType.Kind() == reflect.Pointer
if isPointer {
isOptional = true
tsType = goTypeToTSType(goType, true)
if !strings.Contains(tsType, " | null") {
tsType += " | null"
}
} else {
isOptional = strings.Contains(field.Tag.Get("json"), ",omitempty")
tsType = goTypeToTSType(goType, false)
if isOptional && paramTypes[tsType+" | null"] {
tsType += " | null"
}
}
if isOptional {
fieldName += "?"
}
params = append(params, fmt.Sprintf("%s: %s", fieldName, tsType))
}
returnSignature := "any"
if fnType.Kind() == reflect.Func && fnType.NumOut() > 0 {
lastIndex := fnType.NumOut() - 1
lastType := fnType.Out(lastIndex)
if lastType.Implements(reflect.TypeOf((*error)(nil)).Elem()) {
if fnType.NumOut() > 1 {
returnType := fnType.Out(0)
returnSignature = goTypeToTSType(returnType, returnType.Kind() == reflect.Pointer)
}
} else {
returnSignature = goTypeToTSType(lastType, lastType.Kind() == reflect.Pointer)
}
}
if isPromise {
returnSignature = fmt.Sprintf("Promise<%s>", returnSignature)
}
return fmt.Sprintf("declare function %s(%s): %s;", name, strings.Join(params, ", "), returnSignature)
}

View File

@@ -2,7 +2,6 @@ package functions
import ( import (
"context" "context"
"reflect"
"sync" "sync"
"testing" "testing"
@@ -17,19 +16,26 @@ type TestBasicArgs struct {
func (t TestBasicArgs) Validate() error { return nil } func (t TestBasicArgs) Validate() error { return nil }
func TestBasicType(t *testing.T) { func TestBasicType(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
// Register Function
RegisterFunction("basic", func(ctx context.Context, args TestBasicArgs) (string, error) { RegisterFunction("basic", func(ctx context.Context, args TestBasicArgs) (string, error) {
return args.Name, nil return args.Name, nil
}) })
// Verify Declarations
defs := GetFunctionDeclarations() defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function basic(name: string, age: number): string;") assert.Contains(t, defs, "declare function basic(name: string, age: number): string;")
assert.Contains(t, defs, "interface TestBasicArgs") assert.Contains(t, defs, "interface TestBasicArgs")
} }
func resetRegistry() { func resetRegistry() {
// Lock Registry
registryLock.Lock() registryLock.Lock()
defer registryLock.Unlock() defer registryLock.Unlock()
// Clear Registry
functionRegistry = make(map[string]Function) functionRegistry = make(map[string]Function)
} }
@@ -46,11 +52,15 @@ type TestComplexArgs struct {
func (t TestComplexArgs) Validate() error { return nil } func (t TestComplexArgs) Validate() error { return nil }
func TestComplexTypes(t *testing.T) { func TestComplexTypes(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
// Register Function
RegisterFunction("complex", func(ctx context.Context, args TestComplexArgs) (bool, error) { RegisterFunction("complex", func(ctx context.Context, args TestComplexArgs) (bool, error) {
return args.Flag, nil return args.Flag, nil
}) })
// Verify Declarations
defs := GetFunctionDeclarations() defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function complex(items: number[], data: Record<string, any>, flag: boolean): boolean;") assert.Contains(t, defs, "declare function complex(items: number[], data: Record<string, any>, flag: boolean): boolean;")
} }
@@ -65,11 +75,15 @@ type TestNestedArgs struct {
func (t TestNestedArgs) Validate() error { return nil } func (t TestNestedArgs) Validate() error { return nil }
func TestNestedStruct(t *testing.T) { func TestNestedStruct(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
// Register Function
RegisterFunction("nested", func(ctx context.Context, args TestNestedArgs) (string, error) { RegisterFunction("nested", func(ctx context.Context, args TestNestedArgs) (string, error) {
return args.User.FirstName, nil return args.User.FirstName, nil
}) })
// Verify Declarations
defs := GetFunctionDeclarations() defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function nested(user: {}): string;") assert.Contains(t, defs, "declare function nested(user: {}): string;")
} }
@@ -83,11 +97,15 @@ type TestOptionalArgs struct {
func (t TestOptionalArgs) Validate() error { return nil } func (t TestOptionalArgs) Validate() error { return nil }
func TestOptionalFields(t *testing.T) { func TestOptionalFields(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
// Register Function
RegisterFunction[TestOptionalArgs, string]("optional", func(ctx context.Context, args TestOptionalArgs) (string, error) { RegisterFunction[TestOptionalArgs, string]("optional", func(ctx context.Context, args TestOptionalArgs) (string, error) {
return args.Name, nil return args.Name, nil
}) })
// Verify Declarations
defs := GetFunctionDeclarations() defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function optional(name: string, age?: number | null, score?: number | null): string;") assert.Contains(t, defs, "declare function optional(name: string, age?: number | null, score?: number | null): string;")
} }
@@ -104,11 +122,15 @@ type TestResultArgs struct {
func (t TestResultArgs) Validate() error { return nil } func (t TestResultArgs) Validate() error { return nil }
func TestResultStruct(t *testing.T) { func TestResultStruct(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
// Register Function
RegisterFunction[TestResultArgs, TestResult]("result", func(ctx context.Context, args TestResultArgs) (TestResult, error) { RegisterFunction[TestResultArgs, TestResult]("result", func(ctx context.Context, args TestResultArgs) (TestResult, error) {
return TestResult{ID: 1}, nil return TestResult{ID: 1}, nil
}) })
// Verify Declarations
defs := GetFunctionDeclarations() defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function result(input: string): TestResult;") assert.Contains(t, defs, "declare function result(input: string): TestResult;")
assert.Contains(t, defs, "interface TestResult {id: number; data: number[]}") assert.Contains(t, defs, "interface TestResult {id: number; data: number[]}")
@@ -125,11 +147,15 @@ type TestAsyncResult struct {
} }
func TestAsyncPromise(t *testing.T) { func TestAsyncPromise(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
// Register Async Function
RegisterAsyncFunction[TestAsyncArgs, *TestAsyncStatus]("async", func(ctx context.Context, args TestAsyncArgs) (*TestAsyncStatus, error) { RegisterAsyncFunction[TestAsyncArgs, *TestAsyncStatus]("async", func(ctx context.Context, args TestAsyncArgs) (*TestAsyncStatus, error) {
return &TestAsyncStatus{Code: 200}, nil return &TestAsyncStatus{Code: 200}, nil
}) })
// Verify Declarations
defs := GetFunctionDeclarations() defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function async(url: string): Promise<TestAsyncStatus | null>;") assert.Contains(t, defs, "declare function async(url: string): Promise<TestAsyncStatus | null>;")
assert.Contains(t, defs, "interface TestAsyncStatus") assert.Contains(t, defs, "interface TestAsyncStatus")
@@ -150,11 +176,15 @@ type TestNestedPointerArgs struct {
func (t TestNestedPointerArgs) Validate() error { return nil } func (t TestNestedPointerArgs) Validate() error { return nil }
func TestNestedPointerInResult(t *testing.T) { func TestNestedPointerInResult(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
// Register Function
RegisterFunction[TestNestedPointerArgs, *TestNestedPointerResult]("pointerResult", func(ctx context.Context, args TestNestedPointerArgs) (*TestNestedPointerResult, error) { RegisterFunction[TestNestedPointerArgs, *TestNestedPointerResult]("pointerResult", func(ctx context.Context, args TestNestedPointerArgs) (*TestNestedPointerResult, error) {
return &TestNestedPointerResult{Value: "test"}, nil return &TestNestedPointerResult{Value: "test"}, nil
}) })
// Verify Declarations
defs := GetFunctionDeclarations() defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function pointerResult(id: number): TestNestedPointerResult | null;") assert.Contains(t, defs, "declare function pointerResult(id: number): TestNestedPointerResult | null;")
} }
@@ -166,11 +196,15 @@ type TestUintArgs struct {
func (t TestUintArgs) Validate() error { return nil } func (t TestUintArgs) Validate() error { return nil }
func TestUintType(t *testing.T) { func TestUintType(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
// Register Function
RegisterFunction[TestUintArgs, uint]("uint", func(ctx context.Context, args TestUintArgs) (uint, error) { RegisterFunction[TestUintArgs, uint]("uint", func(ctx context.Context, args TestUintArgs) (uint, error) {
return args.Value, nil return args.Value, nil
}) })
// Verify Declarations
defs := GetFunctionDeclarations() defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function uint(value: number): number;") assert.Contains(t, defs, "declare function uint(value: number): number;")
} }
@@ -182,11 +216,15 @@ type TestFloatArgs struct {
func (t TestFloatArgs) Validate() error { return nil } func (t TestFloatArgs) Validate() error { return nil }
func TestFloatType(t *testing.T) { func TestFloatType(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
// Register Function
RegisterFunction[TestFloatArgs, float32]("float", func(ctx context.Context, args TestFloatArgs) (float32, error) { RegisterFunction[TestFloatArgs, float32]("float", func(ctx context.Context, args TestFloatArgs) (float32, error) {
return float32(args.Amount), nil return float32(args.Amount), nil
}) })
// Verify Declarations
defs := GetFunctionDeclarations() defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function float(amount: number): number;") assert.Contains(t, defs, "declare function float(amount: number): number;")
} }
@@ -200,11 +238,15 @@ type TestPointerInArgs struct {
func (t TestPointerInArgs) Validate() error { return nil } func (t TestPointerInArgs) Validate() error { return nil }
func TestNestedPointerStruct(t *testing.T) { func TestNestedPointerStruct(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
// Register Function
RegisterFunction[TestPointerInArgs, string]("nestedPointer", func(ctx context.Context, args TestPointerInArgs) (string, error) { RegisterFunction[TestPointerInArgs, string]("nestedPointer", func(ctx context.Context, args TestPointerInArgs) (string, error) {
return "test", nil return "test", nil
}) })
// Verify Declarations
defs := GetFunctionDeclarations() defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function nestedPointer(user?: {} | null): string;") assert.Contains(t, defs, "declare function nestedPointer(user?: {} | null): string;")
} }
@@ -216,58 +258,15 @@ type TestErrorOnlyArgs struct {
func (t TestErrorOnlyArgs) Validate() error { return nil } func (t TestErrorOnlyArgs) Validate() error { return nil }
func TestErrorOnlyReturn(t *testing.T) { func TestErrorOnlyReturn(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
// Register Function
RegisterFunction[TestErrorOnlyArgs, struct{}]("errorOnly", func(ctx context.Context, args TestErrorOnlyArgs) (struct{}, error) { RegisterFunction[TestErrorOnlyArgs, struct{}]("errorOnly", func(ctx context.Context, args TestErrorOnlyArgs) (struct{}, error) {
return struct{}{}, nil return struct{}{}, nil
}) })
// Verify Declarations
defs := GetFunctionDeclarations() defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function errorOnly(input: string): {};") assert.Contains(t, defs, "declare function errorOnly(input: string): {};")
} }
func TestGoTypeToTSTypeBasic(t *testing.T) {
tests := []struct {
input reflect.Type
inputPtr bool
expected string
}{
{reflect.TypeOf(""), false, "string"},
{reflect.TypeOf(0), false, "number"},
{reflect.TypeOf(int64(0)), false, "number"},
{reflect.TypeOf(uint(0)), false, "number"},
{reflect.TypeOf(3.14), false, "number"},
{reflect.TypeOf(float32(0.0)), false, "number"},
{reflect.TypeOf(true), false, "boolean"},
{reflect.TypeOf([]string{}), false, "string[]"},
{reflect.TypeOf([]int{}), false, "number[]"},
{reflect.TypeOf(map[string]any{}), false, "Record<string, any>"},
{reflect.TypeOf(map[string]int{}), false, "Record<string, any>"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
result := goTypeToTSType(tt.input, tt.inputPtr)
assert.Equal(t, tt.expected, result)
})
}
}
type TestNestedStructField struct {
Inner struct {
Name string `json:"name"`
} `json:"inner"`
}
func TestGoTypeToTSTypeNestedStruct(t *testing.T) {
result := goTypeToTSType(reflect.TypeOf(TestNestedStructField{}), false)
assert.Equal(t, "TestNestedStructField", result)
}
type TestArrayField struct {
Items []string `json:"items"`
}
func TestGoTypeToTSTypeArray(t *testing.T) {
result := goTypeToTSType(reflect.TypeOf(TestArrayField{}), false)
assert.Equal(t, "TestArrayField", result)
}

View File

@@ -6,12 +6,14 @@ type RuntimeOption func(*Runtime)
func WithStdout(stdout io.Writer) RuntimeOption { func WithStdout(stdout io.Writer) RuntimeOption {
return func(r *Runtime) { return func(r *Runtime) {
// Set Stdout
r.opts.Stdout = stdout r.opts.Stdout = stdout
} }
} }
func WithStderr(stderr io.Writer) RuntimeOption { func WithStderr(stderr io.Writer) RuntimeOption {
return func(r *Runtime) { return func(r *Runtime) {
// Set Stderr
r.opts.Stderr = stderr r.opts.Stderr = stderr
} }
} }

View File

@@ -14,8 +14,10 @@ import (
) )
type Runtime struct { type Runtime struct {
ctx *qjs.Context ctx *qjs.Context
opts qjs.Option opts qjs.Option
funcs map[string]functions.Function
typeDecls map[string]string
} }
func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) { func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) {
@@ -41,8 +43,21 @@ func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) {
} }
func (r *Runtime) populateGlobals() error { func (r *Runtime) populateGlobals() error {
for name, fn := range functions.GetRegisteredFunctions() { // Initialize Maps
// Register Main Function r.funcs = make(map[string]functions.Function)
r.typeDecls = make(map[string]string)
// Load Requested Functions
allFuncs := functions.GetRegisteredFunctions()
for name, fn := range allFuncs {
r.funcs[name] = fn
if err := r.addFunctionTypes(fn); err != nil {
return fmt.Errorf("failed to add types for function %s: %w", name, err)
}
}
// Register Functions with QuickJS
for name, fn := range r.funcs {
if fn.IsAsync() { if fn.IsAsync() {
r.ctx.SetAsyncFunc(name, func(this *qjs.This) { r.ctx.SetAsyncFunc(name, func(this *qjs.This) {
qjsVal, err := callFunc(this, fn) qjsVal, err := callFunc(this, fn)
@@ -62,6 +77,37 @@ func (r *Runtime) populateGlobals() error {
return nil return nil
} }
// addFunctionTypes adds types from a function to the runtime's type declarations.
// Returns an error if there's a type conflict (same name, different definition).
func (r *Runtime) addFunctionTypes(fn functions.Function) error {
for name, def := range fn.Types() {
if existing, ok := r.typeDecls[name]; ok && existing != def {
return fmt.Errorf("type conflict: %s has conflicting definitions (existing: %s, new: %s)",
name, existing, def)
}
r.typeDecls[name] = def
}
return nil
}
// GetTypeDeclarations returns all TypeScript type declarations for this runtime.
// Includes both type definitions and function declarations.
func (r *Runtime) GetTypeDeclarations() string {
var decls []string
// Add Type Definitions
for _, def := range r.typeDecls {
decls = append(decls, def)
}
// Add Function Declarations
for _, fn := range r.funcs {
decls = append(decls, fn.Definition())
}
return strings.Join(decls, "\n\n")
}
func (r *Runtime) RunFile(filePath string) error { func (r *Runtime) RunFile(filePath string) error {
tsFileContent, err := os.ReadFile(filePath) tsFileContent, err := os.ReadFile(filePath)
if err != nil { if err != nil {
@@ -86,9 +132,9 @@ func (r *Runtime) RunCode(tsCode string) error {
func (r *Runtime) transformCode(tsCode string) ([]byte, error) { func (r *Runtime) transformCode(tsCode string) ([]byte, error) {
result := api.Transform(tsCode, api.TransformOptions{ result := api.Transform(tsCode, api.TransformOptions{
Loader: api.LoaderTS, Loader: api.LoaderTS,
Target: api.ES2022, Target: api.ES2022,
// Format: api.FormatIIFE, Format: api.FormatIIFE,
Sourcemap: api.SourceMapNone, Sourcemap: api.SourceMapNone,
TreeShaking: api.TreeShakingFalse, TreeShaking: api.TreeShakingFalse,
}) })

View File

@@ -23,11 +23,14 @@ func (t TestArgs) Validate() error {
} }
func TestExecuteTypeScript(t *testing.T) { func TestExecuteTypeScript(t *testing.T) {
// Create Buffers
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
// Create Runtime
rt, err := New(context.Background(), WithStderr(&stderr), WithStdout(&stdout)) rt, err := New(context.Background(), WithStderr(&stderr), WithStdout(&stdout))
assert.NoError(t, err, "Expected no error") assert.NoError(t, err, "Expected no error")
// Create TypeScript Code
tsCode := `interface Person { tsCode := `interface Person {
name: string; name: string;
age: number; age: number;
@@ -54,73 +57,107 @@ function calculateSum(a: number, b: number): number {
console.log("Sum of 5 and 10 is: " + calculateSum(5, 10)); console.log("Sum of 5 and 10 is: " + calculateSum(5, 10));
` `
// Create Temp File
tmpFile, err := os.CreateTemp("", "*.ts") tmpFile, err := os.CreateTemp("", "*.ts")
assert.NoError(t, err, "Failed to create temp file") assert.NoError(t, err, "Failed to create temp file")
t.Cleanup(func() { t.Cleanup(func() {
_ = os.Remove(tmpFile.Name()) _ = os.Remove(tmpFile.Name())
}) })
// Write Code to File
_, err = tmpFile.WriteString(tsCode) _, err = tmpFile.WriteString(tsCode)
assert.NoError(t, err, "Failed to write to temp file") assert.NoError(t, err, "Failed to write to temp file")
err = tmpFile.Close() err = tmpFile.Close()
assert.NoError(t, err, "Failed to close temp file") assert.NoError(t, err, "Failed to close temp file")
// Run File
err = rt.RunFile(tmpFile.Name()) err = rt.RunFile(tmpFile.Name())
// Verify Execution
assert.NoError(t, err, "Expected no error") assert.NoError(t, err, "Expected no error")
assert.Empty(t, stderr.String(), "Expected no error output") assert.Empty(t, stderr.String(), "Expected no error output")
// Verify Output
output := stdout.String() output := stdout.String()
assert.Contains(t, output, "Hello, Alice!", "Should greet Alice") assert.Contains(t, output, "Hello, Alice!", "Should greet Alice")
assert.Contains(t, output, "You are 30 years old", "Should show age") assert.Contains(t, output, "You are 30 years old", "Should show age")
assert.Contains(t, output, "Email: alice@example.com", "Should show email") assert.Contains(t, output, "Email: alice@example.com", "Should show email")
assert.Contains(t, output, "Sum of 5 and 10 is: 15", "Should calculate sum correctly") assert.Contains(t, output, "Sum of 5 and 10 is: 15", "Should calculate sum correctly")
// Verify Line Count
lines := strings.Split(strings.TrimSpace(output), "\n") lines := strings.Split(strings.TrimSpace(output), "\n")
assert.GreaterOrEqual(t, len(lines), 3, "Should have at least 3 output lines") assert.GreaterOrEqual(t, len(lines), 3, "Should have at least 3 output lines")
} }
func TestAsyncFunctionResolution(t *testing.T) { func TestAsyncFunctionResolution(t *testing.T) {
// Register Async Function
functions.RegisterAsyncFunction("resolveTest", func(_ context.Context, args TestArgs) (string, error) { functions.RegisterAsyncFunction("resolveTest", func(_ context.Context, args TestArgs) (string, error) {
return "test-result", nil return "test-result", nil
}) })
// Create Runtime
r, err := New(context.Background()) r, err := New(context.Background())
require.NoError(t, err) require.NoError(t, err)
// Async functions need to be awaited in an async context // Execute Async Function - Must be awaited in async context
result, err := r.ctx.Eval("test.js", qjs.Code(`async function run() { return await resolveTest("hello"); }; run()`)) result, err := r.ctx.Eval("test.js", qjs.Code(`async function run() { return await resolveTest("hello"); }; run()`))
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, result) require.NotNil(t, result)
defer result.Free() defer result.Free()
// Verify Result
assert.Equal(t, "test-result", result.String()) assert.Equal(t, "test-result", result.String())
} }
func TestAsyncFunctionRejection(t *testing.T) { func TestAsyncFunctionRejection(t *testing.T) {
// Register Async Function that Returns Error
functions.RegisterAsyncFunction("rejectTest", func(_ context.Context, args TestArgs) (string, error) { functions.RegisterAsyncFunction("rejectTest", func(_ context.Context, args TestArgs) (string, error) {
return "", assert.AnError return "", assert.AnError
}) })
// Create Runtime
r, err := New(context.Background()) r, err := New(context.Background())
require.NoError(t, err) require.NoError(t, err)
// Rejected promises throw when awaited // Execute Async Function - Rejected Promises Throw When Awaited
_, err = r.ctx.Eval("test.js", qjs.Code(`async function run() { return await rejectTest("hello"); }; run()`)) _, err = r.ctx.Eval("test.js", qjs.Code(`async function run() { return await rejectTest("hello"); }; run()`))
assert.Error(t, err) assert.Error(t, err)
} }
func TestNonPromise(t *testing.T) { func TestNonPromise(t *testing.T) {
// Register Sync Function
functions.RegisterFunction("nonPromiseTest", func(_ context.Context, args TestArgs) (string, error) { functions.RegisterFunction("nonPromiseTest", func(_ context.Context, args TestArgs) (string, error) {
return "sync-result", nil return "sync-result", nil
}) })
// Create Runtime
r, err := New(context.Background()) r, err := New(context.Background())
assert.NoError(t, err) assert.NoError(t, err)
// Execute Sync Function
result, err := r.ctx.Eval("test.js", qjs.Code(`nonPromiseTest("hello")`)) result, err := r.ctx.Eval("test.js", qjs.Code(`nonPromiseTest("hello")`))
assert.NoError(t, err) assert.NoError(t, err)
defer result.Free() defer result.Free()
// Verify Result
assert.Equal(t, "sync-result", result.String()) assert.Equal(t, "sync-result", result.String())
} }
func TestGetTypeDeclarations(t *testing.T) {
// Register Function
functions.RegisterFunction("testFunc", func(_ context.Context, args TestArgs) (string, error) {
return "result", nil
})
// Create Runtime
r, err := New(context.Background())
require.NoError(t, err)
// Get Type Declarations
decls := r.GetTypeDeclarations()
// Verify Declarations
assert.Contains(t, decls, "interface TestArgs")
assert.Contains(t, decls, "declare function testFunc")
assert.Contains(t, decls, "field1: string")
}

View File

@@ -42,9 +42,11 @@ type Response struct {
} }
func Fetch(ctx context.Context, args FetchArgs) (Response, error) { func Fetch(ctx context.Context, args FetchArgs) (Response, error) {
// Set Default Method and Headers
method := "GET" method := "GET"
headers := make(map[string]string) headers := make(map[string]string)
// Apply Init Options
if args.Init != nil { if args.Init != nil {
if args.Init.Method != "" { if args.Init.Method != "" {
method = args.Init.Method method = args.Init.Method
@@ -54,15 +56,18 @@ func Fetch(ctx context.Context, args FetchArgs) (Response, error) {
} }
} }
// Create Request
req, err := http.NewRequestWithContext(ctx, method, args.Input, nil) req, err := http.NewRequestWithContext(ctx, method, args.Input, nil)
if err != nil { if err != nil {
return Response{}, fmt.Errorf("failed to create request: %w", err) return Response{}, fmt.Errorf("failed to create request: %w", err)
} }
// Set Request Headers
for k, v := range headers { for k, v := range headers {
req.Header.Set(k, v) req.Header.Set(k, v)
} }
// Execute Request
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return Response{}, fmt.Errorf("failed to fetch: %w", err) return Response{}, fmt.Errorf("failed to fetch: %w", err)
@@ -71,11 +76,13 @@ func Fetch(ctx context.Context, args FetchArgs) (Response, error) {
_ = resp.Body.Close() _ = resp.Body.Close()
}() }()
// Read Response Body
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return Response{}, fmt.Errorf("failed to read body: %w", err) return Response{}, fmt.Errorf("failed to read body: %w", err)
} }
// Collect Response Headers
resultHeaders := make(map[string]string) resultHeaders := make(map[string]string)
for key, values := range resp.Header { for key, values := range resp.Header {
if len(values) > 0 { if len(values) > 0 {
@@ -85,6 +92,7 @@ func Fetch(ctx context.Context, args FetchArgs) (Response, error) {
} }
} }
// Return Response
return Response{ return Response{
OK: resp.StatusCode >= 200 && resp.StatusCode < 300, OK: resp.StatusCode >= 200 && resp.StatusCode < 300,
Status: resp.StatusCode, Status: resp.StatusCode,

View File

@@ -11,7 +11,10 @@ import (
) )
func TestFetch(t *testing.T) { func TestFetch(t *testing.T) {
// Create Context
ctx := context.Background() ctx := context.Background()
// Create Test Server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Custom-Header", "test-value") w.Header().Set("X-Custom-Header", "test-value")
@@ -20,9 +23,11 @@ func TestFetch(t *testing.T) {
})) }))
defer server.Close() defer server.Close()
// Execute Fetch
result, err := Fetch(ctx, FetchArgs{Input: server.URL}) result, err := Fetch(ctx, FetchArgs{Input: server.URL})
require.NoError(t, err) require.NoError(t, err)
// Verify Response
assert.True(t, result.OK) assert.True(t, result.OK)
assert.Equal(t, http.StatusOK, result.Status) assert.Equal(t, http.StatusOK, result.Status)
assert.Contains(t, result.Body, "Hello from httptest") assert.Contains(t, result.Body, "Hello from httptest")
@@ -32,7 +37,10 @@ func TestFetch(t *testing.T) {
} }
func TestFetchHTTPBin(t *testing.T) { func TestFetchHTTPBin(t *testing.T) {
// Create Context
ctx := context.Background() ctx := context.Background()
// Create Test Server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@@ -40,9 +48,11 @@ func TestFetchHTTPBin(t *testing.T) {
})) }))
defer server.Close() defer server.Close()
// Execute Fetch
result, err := Fetch(ctx, FetchArgs{Input: server.URL}) result, err := Fetch(ctx, FetchArgs{Input: server.URL})
require.NoError(t, err) require.NoError(t, err)
// Verify Response
assert.True(t, result.OK) assert.True(t, result.OK)
assert.Equal(t, http.StatusOK, result.Status) assert.Equal(t, http.StatusOK, result.Status)
assert.Contains(t, result.Body, `"args"`) assert.Contains(t, result.Body, `"args"`)
@@ -50,23 +60,33 @@ func TestFetchHTTPBin(t *testing.T) {
} }
func TestFetchWith404(t *testing.T) { func TestFetchWith404(t *testing.T) {
// Create Context
ctx := context.Background() ctx := context.Background()
// Execute Fetch
result, err := Fetch(ctx, FetchArgs{Input: "https://httpbin.org/status/404"}) result, err := Fetch(ctx, FetchArgs{Input: "https://httpbin.org/status/404"})
require.NoError(t, err) require.NoError(t, err)
// Verify Response
assert.False(t, result.OK) assert.False(t, result.OK)
assert.Equal(t, http.StatusNotFound, result.Status) assert.Equal(t, http.StatusNotFound, result.Status)
} }
func TestFetchWithInvalidURL(t *testing.T) { func TestFetchWithInvalidURL(t *testing.T) {
// Create Context
ctx := context.Background() ctx := context.Background()
// Execute Fetch - Should Fail
_, err := Fetch(ctx, FetchArgs{Input: "http://this-domain-does-not-exist-12345.com"}) _, err := Fetch(ctx, FetchArgs{Input: "http://this-domain-does-not-exist-12345.com"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to fetch") assert.Contains(t, err.Error(), "failed to fetch")
} }
func TestFetchWithHeaders(t *testing.T) { func TestFetchWithHeaders(t *testing.T) {
// Create Context
ctx := context.Background() ctx := context.Background()
// Create Test Server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
assert.Equal(t, "GET", r.Method) assert.Equal(t, "GET", r.Method)
@@ -75,6 +95,7 @@ func TestFetchWithHeaders(t *testing.T) {
})) }))
defer server.Close() defer server.Close()
// Configure Request Options
headers := map[string]string{ headers := map[string]string{
"Authorization": "Bearer test-token", "Authorization": "Bearer test-token",
} }
@@ -82,13 +103,18 @@ func TestFetchWithHeaders(t *testing.T) {
Method: "GET", Method: "GET",
Headers: headers, Headers: headers,
} }
// Execute Fetch with Headers
result, err := Fetch(ctx, FetchArgs{Input: server.URL, Init: options}) result, err := Fetch(ctx, FetchArgs{Input: server.URL, Init: options})
require.NoError(t, err) require.NoError(t, err)
assert.True(t, result.OK) assert.True(t, result.OK)
} }
func TestFetchDefaults(t *testing.T) { func TestFetchDefaults(t *testing.T) {
// Create Context
ctx := context.Background() ctx := context.Background()
// Create Test Server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method, "default method should be GET") assert.Equal(t, "GET", r.Method, "default method should be GET")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@@ -96,6 +122,7 @@ func TestFetchDefaults(t *testing.T) {
})) }))
defer server.Close() defer server.Close()
// Execute Fetch with Empty Options
options := &RequestInit{} options := &RequestInit{}
result, err := Fetch(ctx, FetchArgs{Input: server.URL, Init: options}) result, err := Fetch(ctx, FetchArgs{Input: server.URL, Init: options})
require.NoError(t, err) require.NoError(t, err)

View File

@@ -0,0 +1,242 @@
package tsconvert
import (
"fmt"
"reflect"
"strings"
)
// ConvertType converts a Go reflect.Type to a TypeScript type string.
func ConvertType(t reflect.Type) string {
return goTypeToTSType(t, false)
}
// goTypeToTSType converts a Go type to TypeScript type string.
// isPointer tracks if we're inside a pointer chain.
func goTypeToTSType(t reflect.Type, isPointer bool) string {
// Handle Pointer Types
if t.Kind() == reflect.Pointer {
return goTypeToTSType(t.Elem(), true)
}
// Determine Base Type
baseType := ""
switch t.Kind() {
case reflect.String:
baseType = "string"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
baseType = "number"
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
baseType = "number"
case reflect.Float32, reflect.Float64:
baseType = "number"
case reflect.Bool:
baseType = "boolean"
case reflect.Interface:
baseType = "any"
case reflect.Slice:
baseType = fmt.Sprintf("%s[]", goTypeToTSType(t.Elem(), false))
case reflect.Map:
if t.Key().Kind() == reflect.String && t.Elem().Kind() == reflect.Interface {
baseType = "Record<string, any>"
} else {
baseType = "Record<string, any>"
}
case reflect.Struct:
name := t.Name()
if name == "" {
baseType = "{}"
} else {
baseType = name
}
default:
baseType = "any"
}
// Add Null for Pointer Types
if isPointer {
baseType += " | null"
}
return baseType
}
// CollectTypes extracts all type declarations from a function signature.
// It analyzes the args type and return type to find all struct types.
func CollectTypes(argsType, fnType reflect.Type) *TypeSet {
// Create TypeSet
ts := NewTypeSet()
// Collect Types from Args Struct
collectStructTypes(argsType, ts)
// Collect Types from Return Type
if fnType.Kind() == reflect.Func && fnType.NumOut() > 0 {
lastIndex := fnType.NumOut() - 1
lastType := fnType.Out(lastIndex)
if lastType.Implements(reflect.TypeOf((*error)(nil)).Elem()) {
if fnType.NumOut() > 1 {
collectType(fnType.Out(0), ts)
}
} else {
collectType(lastType, ts)
}
}
return ts
}
// collectType recursively collects struct types.
func collectType(t reflect.Type, ts *TypeSet) {
// Handle Pointer Types
if t.Kind() == reflect.Pointer {
collectType(t.Elem(), ts)
return
}
// Collect Struct Types
if t.Kind() == reflect.Struct {
name := t.Name()
if name != "" {
// Only Process if Not Already Processed
if _, exists := ts.Get(name); !exists {
collectStructTypes(t, ts)
}
}
}
}
// collectStructTypes converts a Go struct to TypeScript interface and adds to TypeSet.
func collectStructTypes(t reflect.Type, ts *TypeSet) {
// Validate Struct Type
if t.Kind() != reflect.Struct {
return
}
// Get Struct Name
name := t.Name()
if name == "" {
return // Skip Anonymous Structs
}
// Check if Already Processed
if _, exists := ts.Get(name); exists {
return
}
// Collect Fields
var fields []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Skip Anonymous and Unexported Fields
if field.Anonymous || !field.IsExported() {
continue
}
// Get Field Name
fieldName := getFieldName(field)
// Determine Type and Optionality
var tsType string
var isOptional bool
isPointer := field.Type.Kind() == reflect.Pointer
if isPointer {
isOptional = true
tsType = goTypeToTSType(field.Type, true)
} else {
isOptional = strings.Contains(field.Tag.Get("json"), ",omitempty")
tsType = goTypeToTSType(field.Type, false)
}
// Mark Optional Fields
if isOptional {
fieldName += "?"
}
fields = append(fields, fmt.Sprintf("%s: %s", fieldName, tsType))
// Recursively Collect Nested Types
collectType(field.Type, ts)
}
// Add Type Definition
definition := fmt.Sprintf("interface %s {%s}", name, strings.Join(fields, "; "))
_ = ts.Add(name, definition)
}
// getFieldName extracts the field name from json tag or uses the Go field name.
func getFieldName(field reflect.StructField) string {
// Get JSON Tag
jsonTag := field.Tag.Get("json")
if jsonTag != "" && jsonTag != "-" {
name, _, _ := strings.Cut(jsonTag, ",")
return name
}
// Use Go Field Name
return field.Name
}
// GenerateFunctionDecl creates a TypeScript function declaration.
func GenerateFunctionDecl(name string, argsType, fnType reflect.Type, isAsync bool) string {
// Validate Args Type
if argsType.Kind() != reflect.Struct {
return ""
}
// Collect Parameters
var params []string
for i := 0; i < argsType.NumField(); i++ {
field := argsType.Field(i)
fieldName := getFieldName(field)
goType := field.Type
// Determine Type and Optionality
var tsType string
var isOptional bool
isPointer := goType.Kind() == reflect.Pointer
if isPointer {
isOptional = true
tsType = goTypeToTSType(goType, true)
if !strings.Contains(tsType, " | null") {
tsType += " | null"
}
} else {
isOptional = strings.Contains(field.Tag.Get("json"), ",omitempty")
tsType = goTypeToTSType(goType, false)
}
// Mark Optional Fields
if isOptional {
fieldName += "?"
}
params = append(params, fmt.Sprintf("%s: %s", fieldName, tsType))
}
// Determine Return Type
returnSignature := "any"
if fnType.Kind() == reflect.Func && fnType.NumOut() > 0 {
lastIndex := fnType.NumOut() - 1
lastType := fnType.Out(lastIndex)
if lastType.Implements(reflect.TypeOf((*error)(nil)).Elem()) {
if fnType.NumOut() > 1 {
returnType := fnType.Out(0)
returnSignature = goTypeToTSType(returnType, returnType.Kind() == reflect.Pointer)
}
} else {
returnSignature = goTypeToTSType(lastType, lastType.Kind() == reflect.Pointer)
}
}
// Wrap in Promise for Async Functions
if isAsync {
returnSignature = fmt.Sprintf("Promise<%s>", returnSignature)
}
// Generate Declaration
return fmt.Sprintf("declare function %s(%s): %s;", name, strings.Join(params, ", "), returnSignature)
}

View File

@@ -0,0 +1,333 @@
package tsconvert
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Test types for conversion
type SimpleStruct struct {
Name string `json:"name"`
Count int `json:"count"`
}
type NestedStruct struct {
ID int `json:"id"`
Simple SimpleStruct `json:"simple"`
Pointer *SimpleStruct `json:"pointer,omitempty"`
}
type OptionalFields struct {
Required string `json:"required"`
Optional *string `json:"optional,omitempty"`
Number int `json:"number,omitempty"`
}
type ComplexTypes struct {
Strings []string `json:"strings"`
Numbers []int `json:"numbers"`
Mapping map[string]any `json:"mapping"`
Nested []SimpleStruct `json:"nested"`
}
func TestConvertType(t *testing.T) {
tests := []struct {
name string
input any
expected string
}{
{"string", "", "string"},
{"int", int(0), "number"},
{"int8", int8(0), "number"},
{"int16", int16(0), "number"},
{"int32", int32(0), "number"},
{"int64", int64(0), "number"},
{"uint", uint(0), "number"},
{"float32", float32(0), "number"},
{"float64", float64(0), "number"},
{"bool", true, "boolean"},
{"interface", (*any)(nil), "any | null"},
{"slice of strings", []string{}, "string[]"},
{"slice of ints", []int{}, "number[]"},
{"map", map[string]any{}, "Record<string, any>"},
{"struct", SimpleStruct{}, "SimpleStruct"},
{"pointer to string", (*string)(nil), "string | null"},
{"pointer to struct", (*SimpleStruct)(nil), "SimpleStruct | null"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Get Input Type
var inputType reflect.Type
if tt.input == nil {
inputType = reflect.TypeOf((*any)(nil)).Elem()
} else {
inputType = reflect.TypeOf(tt.input)
}
// Convert Type
result := ConvertType(inputType)
// Verify Result
assert.Equal(t, tt.expected, result)
})
}
}
func TestCollectTypes(t *testing.T) {
t.Run("simple struct", func(t *testing.T) {
// Collect Types
argsType := reflect.TypeOf(SimpleStruct{})
fnType := reflect.TypeOf(func() (SimpleStruct, error) { return SimpleStruct{}, nil })
ts := CollectTypes(argsType, fnType)
// Verify TypeSet
require.NotNil(t, ts)
assert.Len(t, ts.All(), 1)
// Verify SimpleStruct Definition
def, ok := ts.Get("SimpleStruct")
assert.True(t, ok)
assert.Contains(t, def, "interface SimpleStruct")
assert.Contains(t, def, "name: string")
assert.Contains(t, def, "count: number")
})
t.Run("nested struct", func(t *testing.T) {
// Collect Types
argsType := reflect.TypeOf(NestedStruct{})
fnType := reflect.TypeOf(func() (NestedStruct, error) { return NestedStruct{}, nil })
ts := CollectTypes(argsType, fnType)
// Verify TypeSet
require.NotNil(t, ts)
all := ts.All()
assert.Len(t, all, 2) // NestedStruct and SimpleStruct
// Verify NestedStruct Definition
def, ok := ts.Get("NestedStruct")
assert.True(t, ok)
assert.Contains(t, def, "interface NestedStruct")
assert.Contains(t, def, "id: number")
assert.Contains(t, def, "simple: SimpleStruct")
assert.Contains(t, def, "pointer?: SimpleStruct | null")
// Verify SimpleStruct is Also Included
_, ok = ts.Get("SimpleStruct")
assert.True(t, ok)
})
t.Run("optional fields", func(t *testing.T) {
// Collect Types
argsType := reflect.TypeOf(OptionalFields{})
fnType := reflect.TypeOf(func() (OptionalFields, error) { return OptionalFields{}, nil })
ts := CollectTypes(argsType, fnType)
// Verify TypeSet
require.NotNil(t, ts)
// Verify OptionalFields Definition
def, ok := ts.Get("OptionalFields")
assert.True(t, ok)
assert.Contains(t, def, "required: string")
assert.Contains(t, def, "optional?: string | null")
assert.Contains(t, def, "number?: number")
})
t.Run("complex types", func(t *testing.T) {
// Collect Types
argsType := reflect.TypeOf(ComplexTypes{})
fnType := reflect.TypeOf(func() (ComplexTypes, error) { return ComplexTypes{}, nil })
ts := CollectTypes(argsType, fnType)
// Verify TypeSet
require.NotNil(t, ts)
// Verify ComplexTypes Definition
def, ok := ts.Get("ComplexTypes")
assert.True(t, ok)
assert.Contains(t, def, "strings: string[]")
assert.Contains(t, def, "numbers: number[]")
assert.Contains(t, def, "mapping: Record<string, any>")
assert.Contains(t, def, "nested: SimpleStruct[]")
})
t.Run("no return type", func(t *testing.T) {
// Collect Types
argsType := reflect.TypeOf(SimpleStruct{})
fnType := reflect.TypeOf(func() error { return nil })
ts := CollectTypes(argsType, fnType)
// Verify TypeSet - Only SimpleStruct from args
require.NotNil(t, ts)
assert.Len(t, ts.All(), 1)
})
}
func TestTypeSet(t *testing.T) {
t.Run("add and get", func(t *testing.T) {
// Create TypeSet
ts := NewTypeSet()
// Add Type
err := ts.Add("User", "interface User { name: string }")
require.NoError(t, err)
// Verify Type
def, ok := ts.Get("User")
assert.True(t, ok)
assert.Equal(t, "interface User { name: string }", def)
})
t.Run("duplicate same definition", func(t *testing.T) {
// Create TypeSet
ts := NewTypeSet()
// Add Type
err := ts.Add("User", "interface User { name: string }")
require.NoError(t, err)
// Add Same Type with Same Definition - Should Not Error
err = ts.Add("User", "interface User { name: string }")
require.NoError(t, err)
// Verify Count
assert.Len(t, ts.All(), 1)
})
t.Run("conflicting definitions", func(t *testing.T) {
// Create TypeSet
ts := NewTypeSet()
// Add Type
err := ts.Add("User", "interface User { name: string }")
require.NoError(t, err)
// Add Same Type with Different Definition - Should Error
err = ts.Add("User", "interface User { id: number }")
require.Error(t, err)
assert.Contains(t, err.Error(), "type conflict")
assert.Contains(t, err.Error(), "User")
})
t.Run("merge type sets", func(t *testing.T) {
// Create First TypeSet
ts1 := NewTypeSet()
err := ts1.Add("User", "interface User { name: string }")
require.NoError(t, err)
// Create Second TypeSet
ts2 := NewTypeSet()
err = ts2.Add("Post", "interface Post { title: string }")
require.NoError(t, err)
// Merge TypeSets
err = ts1.Merge(ts2)
require.NoError(t, err)
// Verify Merged Types
assert.Len(t, ts1.All(), 2)
_, ok := ts1.Get("User")
assert.True(t, ok)
_, ok = ts1.Get("Post")
assert.True(t, ok)
})
t.Run("merge with conflict", func(t *testing.T) {
// Create First TypeSet
ts1 := NewTypeSet()
err := ts1.Add("User", "interface User { name: string }")
require.NoError(t, err)
// Create Second TypeSet with Conflicting Type
ts2 := NewTypeSet()
err = ts2.Add("User", "interface User { id: number }")
require.NoError(t, err)
// Merge Should Fail Due to Conflict
err = ts1.Merge(ts2)
require.Error(t, err)
assert.Contains(t, err.Error(), "type conflict")
})
}
func TestExtractName(t *testing.T) {
tests := []struct {
definition string
expected string
}{
{"interface User { name: string }", "User"},
{"interface MyType { }", "MyType"},
{"type MyAlias = string", "MyAlias"},
{"type ComplexType = { a: number }", "ComplexType"},
{"invalid syntax here", ""},
{"", ""},
}
for _, tt := range tests {
t.Run(tt.definition, func(t *testing.T) {
// Extract Name
result := ExtractName(tt.definition)
// Verify Result
assert.Equal(t, tt.expected, result)
})
}
}
func TestGenerateFunctionDecl(t *testing.T) {
t.Run("simple function", func(t *testing.T) {
// Generate Declaration
argsType := reflect.TypeOf(SimpleStruct{})
fnType := reflect.TypeOf(func() (SimpleStruct, error) { return SimpleStruct{}, nil })
decl := GenerateFunctionDecl("myFunc", argsType, fnType, false)
// Verify Declaration
assert.Equal(t, "declare function myFunc(name: string, count: number): SimpleStruct;", decl)
})
t.Run("async function", func(t *testing.T) {
// Generate Declaration
argsType := reflect.TypeOf(SimpleStruct{})
fnType := reflect.TypeOf(func() (SimpleStruct, error) { return SimpleStruct{}, nil })
decl := GenerateFunctionDecl("myAsyncFunc", argsType, fnType, true)
// Verify Declaration
assert.Equal(t, "declare function myAsyncFunc(name: string, count: number): Promise<SimpleStruct>;", decl)
})
t.Run("function with optional fields", func(t *testing.T) {
// Generate Declaration
argsType := reflect.TypeOf(OptionalFields{})
fnType := reflect.TypeOf(func() (OptionalFields, error) { return OptionalFields{}, nil })
decl := GenerateFunctionDecl("optionalFunc", argsType, fnType, false)
// Verify Declaration
assert.Contains(t, decl, "required: string")
assert.Contains(t, decl, "optional?: string | null")
assert.Contains(t, decl, "number?: number")
})
t.Run("function with no return", func(t *testing.T) {
// Generate Declaration
argsType := reflect.TypeOf(SimpleStruct{})
fnType := reflect.TypeOf(func() error { return nil })
decl := GenerateFunctionDecl("noReturn", argsType, fnType, false)
// Verify Declaration
assert.Equal(t, "declare function noReturn(name: string, count: number): any;", decl)
})
t.Run("non-struct args returns empty", func(t *testing.T) {
// Generate Declaration
argsType := reflect.TypeOf("")
fnType := reflect.TypeOf(func() error { return nil })
decl := GenerateFunctionDecl("invalid", argsType, fnType, false)
// Verify Declaration
assert.Equal(t, "", decl)
})
}

View File

@@ -0,0 +1,92 @@
// Package tsconvert provides utilities for converting Go types to TypeScript definitions.
package tsconvert
import (
"fmt"
"regexp"
)
// TypeDecl represents a TypeScript type declaration.
type TypeDecl struct {
Name string // e.g., "UserConfig"
Definition string // e.g., "interface UserConfig { name: string }"
}
// TypeSet manages a collection of type declarations with deduplication support.
type TypeSet struct {
types map[string]string // name -> definition
}
// NewTypeSet creates a new empty TypeSet.
func NewTypeSet() *TypeSet {
return &TypeSet{
types: make(map[string]string),
}
}
// Add adds a type declaration to the set.
// Returns an error if a type with the same name but different definition already exists.
func (ts *TypeSet) Add(name, definition string) error {
if existing, ok := ts.types[name]; ok {
if existing != definition {
return fmt.Errorf("type conflict: %s has conflicting definitions", name)
}
// Same name and definition, no conflict
return nil
}
ts.types[name] = definition
return nil
}
// Get retrieves a type definition by name.
func (ts *TypeSet) Get(name string) (string, bool) {
def, ok := ts.types[name]
return def, ok
}
// All returns all type declarations as a map.
func (ts *TypeSet) All() map[string]string {
result := make(map[string]string, len(ts.types))
for k, v := range ts.types {
result[k] = v
}
return result
}
// Names returns all type names in the set.
func (ts *TypeSet) Names() []string {
names := make([]string, 0, len(ts.types))
for name := range ts.types {
names = append(names, name)
}
return names
}
// Merge merges another TypeSet into this one.
// Returns an error if there are conflicting definitions.
func (ts *TypeSet) Merge(other *TypeSet) error {
for name, def := range other.types {
if err := ts.Add(name, def); err != nil {
return err
}
}
return nil
}
// ExtractName extracts the type name from a TypeScript declaration.
// Supports "interface Name {...}" and "type Name = ..." patterns.
func ExtractName(definition string) string {
// Try interface pattern: "interface Name { ... }"
interfaceRe := regexp.MustCompile(`^interface\s+(\w+)`)
if matches := interfaceRe.FindStringSubmatch(definition); len(matches) > 1 {
return matches[1]
}
// Try type alias pattern: "type Name = ..."
typeRe := regexp.MustCompile(`^type\s+(\w+)`)
if matches := typeRe.FindStringSubmatch(definition); len(matches) > 1 {
return matches[1]
}
return ""
}

View File

@@ -1,324 +0,0 @@
# Builtin System Design
## Overview
Type-safe builtin system for exposing Go functions to TypeScript/JavaScript with automatic type conversion and defaults support.
## Core Design
### Single Argument Pattern
All builtins accept a **single struct argument** on the Go side, but are called as **multi-argument functions** on the JavaScript side.
```go
type FetchArgs struct {
URL string `json:"url"`
Options *FetchOptions `json:"options"`
}
func Fetch(args FetchArgs) (*FetchResult, error) {
// Implementation
}
// JavaScript calls:
// fetch("https://example.com")
// fetch("https://example.com", { method: "POST" })
```
**Mapping Rules:**
- JavaScript arguments map **positionally** to struct fields in order
- Field names in JavaScript come from `json:""` struct tag, or field name if tag is omitted
- Missing JavaScript arguments only allowed for **pointer fields** (non-pointer fields must always be provided)
### Default Values
Argument structs **must** implement `Defaults()` receiver method to provide default values:
```go
type FetchOptions struct {
Method string `json:"method"`
Headers map[string]string `json:"headers"`
}
func (f *FetchOptions) Defaults() *FetchOptions {
if f.Method == "" {
f.Method = "GET"
}
return f
}
// If called with just URL, Options will be nil initially,
// then we create default: &FetchOptions{Method: "GET"}
```
**Defaults flow:**
1. Create zero-value struct
2. Fill in provided JavaScript arguments positionally
3. Call `Defaults()` on the struct to fill in remaining defaults
### Empty Arguments
Zero-argument builtins must still use a struct (can be empty):
```go
// Helper type for no-argument builtins
type EmptyArgs struct {}
func Ping(args EmptyArgs) bool {
return true
}
// JavaScript: ping()
```
## Registration API
```go
package builtin
// RegisterBuiltin registers a Go function as a builtin
// The function must accept a single struct argument T and return (R, error) or R
func RegisterBuiltin[T any, R any](name string, fn func(T) (R, error))
```
**Usage:**
```go
func init() {
builtin.RegisterBuiltin("fetch", Fetch)
builtin.RegisterBuiltin("add", Add)
}
```
## Type Conversion
### JavaScript → Go
Automatic conversion from goja values to Go struct fields:
| JavaScript | Go Type |
|------------|---------|
| string | string |
| number | int, int8-64, uint, uint8-64, float32, float64 |
| boolean | bool |
| object/map | map[string]any |
| null/undefined | nil (for pointer fields only) |
**Struct Field Conversion:**
- Primitives: converted by type
- Maps: `map[string]any` → Go map with string keys
- Nested structs: Recursive conversion to nested struct
### Go → JavaScript (Return Types)
Automatic conversion from Go return values to JavaScript:
| Go Type | JavaScript |
|---------|------------|
| string, number, bool | primitive |
| map[string]any | object |
| map[string]string | object |
| []any | array |
| struct | object with fields from json tags |
**Struct Return Types:**
- Auto-converted to JavaScript objects
- Field names from `json:""` struct tags
- Nested structs become nested objects
- No custom converters needed
**Example:**
```go
type FetchResult struct {
OK bool `json:"ok"`
Status int `json:"status"`
Body string `json:"body"`
}
// Returns: { ok: true, status: 200, body: "..." }
```
## Error Handling
Go errors are automatically converted to JavaScript exceptions:
```go
func Fetch(args FetchArgs) (*FetchResult, error) {
if someError {
return nil, fmt.Errorf("fetch failed")
}
return result, nil
}
// JavaScript: fetch calls that error will throw
```
## TypeScript Declaration Generation
TypeScript definitions are auto-generated from Go function signatures:
```go
// Go:
func Fetch(args FetchArgs) (*FetchResult, error)
// Generated TypeScript:
declare function fetch(url: string, options?: FetchOptions): FetchResult | never;
```
**Type mapping:**
- `string``string`
- `int/int8-64/uint/uint8-64/float32/float64``number`
- `bool``boolean`
- `*T``T | null` (or `T` if optional)
- `map[string]any``Record<string, any>`
- `struct` → Interface with fields
**Optional parameters:**
- Pointer fields become optional parameters with `T | null`
- Non-pointer fields are required
## Implementation Notes
### Reflection-Based Wrapping
```go
func createWrapper[T any, R any](fn func(T) (R, error)) func(*goja.Runtime, goja.FunctionCall) goja.Value {
return func(vm *goja.Runtime, call goja.FunctionCall) goja.Value {
// 1. Create zero-value struct
var args T
argsValue := reflect.ValueOf(&args).Elem()
// 2. Map JavaScript args to struct fields positionally
for i := 0; i < min(len(call.Arguments), argsValue.NumField()); i++ {
jsArg := call.Arguments[i]
field := argsValue.Field(i)
// Skip nil/undefined if field is pointer
if goja.IsUndefined(jsArg) || goja.IsNull(jsArg) {
if field.Kind() == reflect.Ptr {
continue
}
}
// Convert and assign
converted, err := convertJSValueToGo(vm, jsArg, field.Type())
if err != nil {
panic(err)
}
field.Set(reflect.ValueOf(converted))
}
// 3. Call Defaults() if defined
if defaults, ok := args.(interface{ Defaults() T }); ok {
args = defaults.Defaults()
}
// 4. Call the Go function
result, err := fn(args)
if err != nil {
panic(err)
}
// 5. Convert return value to JavaScript
return convertGoValueToJS(vm, reflect.ValueOf(result))
}
}
```
### Field Name Extraction
```go
func getFieldName(field reflect.StructField) string {
jsonTag := field.Tag.Get("json")
if jsonTag != "" && jsonTag != "-" {
name, _, _ := strings.Cut(jsonTag, ",")
return name
}
return field.Name
}
```
## Migration Examples
### Old → New
**Old (current system):**
```go
func add(a, b int) int {
return a + b
}
// Register: builtin.RegisterBuiltin("add", add)
// JS: add(5, 10)
```
**New system:**
```go
type AddArgs struct {
A int `json:"a"`
B int `json:"b"`
}
func add(args AddArgs) int {
return args.A + args.B
}
// Register: builtin.RegisterBuiltin("add", add)
// JS: add(5, 10) // positional
// JS: add(a=5, b=10) // named (if supported)
```
**Old fetch:**
```go
func Fetch(url string, options map[string]any) (*FetchResult, error)
// Requires custom converter for result
```
**New fetch:**
```go
type FetchArgs struct {
URL string `json:"url"`
Options *FetchOptions `json:"options"`
}
type FetchOptions struct {
Method string `json:"method"`
Headers map[string]string `json:"headers"`
}
func (o *FetchOptions) Defaults() *FetchOptions {
if o.Method == "" {
o.Method = "GET"
}
return o
}
type FetchResult struct {
OK bool `json:"ok"`
Status int `json:"status"`
Body string `json:"body"`
}
func Fetch(args FetchArgs) (*FetchResult, error) {
// Implementation - no custom converter needed
}
// JS: fetch("https://example.com")
// JS: fetch("https://example.com", { method: "POST" })
```
## Testing
All existing tests must pass after migration:
- `internal/runtime/runtime_test.go`
- `internal/runtime/standard/fetch_test.go`
- `test_data/*.ts` test files
Verify:
- TypeScript definitions are correct
- Positional argument mapping works
- Defaults are applied correctly
- Named JSON tags work
- Error handling propagates
- Return value conversion works