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/
│ ├── runtime/ # Runtime management, transpilation, execution
│ │ ├── runtime.go
│ │ ── runtime_test.go
│ │ ── runtime_test.go
│ │ └── options.go
│ ├── functions/ # Function registration framework
│ │ ├── collector.go
│ │ ├── registry.go
│ │ ├── types.go
│ │ ├── typescript.go
│ │ ├── typescript_test.go
│ │ └── functions_test.go
│ ├── tsconvert/ # Go-to-TypeScript type conversion utilities
│ │ ├── convert.go
│ │ ├── types.go
│ │ └── convert_test.go
│ └── stdlib/ # Standard library implementations
│ ├── fetch.go
│ └── fetch_test.go
@@ -40,8 +43,9 @@ reichard.io/poiesis/
## Key Packages
- `reichard.io/poiesis/internal/runtime` - Runtime management, TypeScript transpilation, JavaScript execution
- `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/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)
- `reichard.io/poiesis/internal/tsconvert` - Go-to-TypeScript type conversion utilities and type declaration generation
- `reichard.io/poiesis/internal/stdlib` - Standard library implementations (fetch)
## Function System
@@ -49,6 +53,7 @@ reichard.io/poiesis/
### Registration
Two types of functions:
- **Sync**: `RegisterFunction[T, R](name, fn)` - executes synchronously, returns value
- **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`
- Error logging uses `_, _ = fmt.Fprintf(stderr, ...)` pattern
- 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"
"os"
"reichard.io/poiesis/internal/functions"
"reichard.io/poiesis/internal/runtime"
)
func main() {
// Validate Arguments
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: poiesis <typescript-file>")
fmt.Fprintln(os.Stderr, " poiesis -print-types")
@@ -18,7 +18,11 @@ func main() {
// 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
}

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"
"strings"
"sync"
"reichard.io/poiesis/internal/tsconvert"
)
var (
functionRegistry = make(map[string]Function)
registryMutex sync.RWMutex
collector *typeCollector
)
func registerFunction[A Args, R any](name string, isAsync bool, fn GoFunc[A, R]) {
// Lock Registry
registryMutex.Lock()
defer registryMutex.Unlock()
if collector == nil {
collector = newTypeCollector()
}
// Validate Args Type
tType := reflect.TypeFor[A]()
if tType.Kind() != reflect.Struct {
panic(fmt.Sprintf("function %s: argument must be a struct type, got %v", name, tType))
}
// Collect Types and Generate Definition
fnType := reflect.TypeOf(fn)
types := collector.collectTypes(tType, fnType)
paramTypes := collector.getParamTypes()
types := tsconvert.CollectTypes(tType, fnType)
definition := tsconvert.GenerateFunctionDecl(name, tType, fnType, isAsync)
// Register Function
functionRegistry[name] = &functionImpl[A, R]{
name: name,
fn: fn,
types: types,
definition: generateTypeScriptDefinition(name, tType, fnType, isAsync, paramTypes),
types: types.All(),
definition: definition,
isAsync: isAsync,
}
}
func GetFunctionDeclarations() string {
// Lock Registry
registryMutex.RLock()
defer registryMutex.RUnlock()
// Collect Type Definitions
typeDefinitions := make(map[string]bool)
var typeDefs []string
var functionDecls []string
@@ -57,6 +60,7 @@ func GetFunctionDeclarations() string {
functionDecls = append(functionDecls, fn.Definition())
}
// Build Result
result := strings.Join(typeDefs, "\n\n")
if len(result) > 0 && len(functionDecls) > 0 {
result += "\n\n"
@@ -66,10 +70,33 @@ func GetFunctionDeclarations() string {
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()
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))
for k, v := range functionRegistry {
result[k] = v
@@ -78,9 +105,11 @@ func GetRegisteredFunctions() map[string]Function {
}
func RegisterFunction[T Args, R any](name string, fn GoFunc[T, R]) {
// Register Sync Function
registerFunction(name, false, fn)
}
func RegisterAsyncFunction[T Args, R any](name string, fn GoFunc[T, R]) {
// Register Async Function
registerFunction(name, true, fn)
}

View File

@@ -8,7 +8,7 @@ import (
type Function interface {
Name() string
Types() []string
Types() map[string]string
Definition() string
IsAsync() bool
Arguments() []reflect.Type
@@ -25,7 +25,7 @@ type functionImpl[A Args, R any] struct {
name string
fn GoFunc[A, R]
definition string
types []string
types map[string]string
isAsync bool
}
@@ -33,7 +33,7 @@ func (b *functionImpl[A, R]) Name() string {
return b.name
}
func (b *functionImpl[A, R]) Types() []string {
func (b *functionImpl[A, R]) Types() map[string]string {
return b.types
}
@@ -50,6 +50,7 @@ func (b *functionImpl[A, R]) Function() any {
}
func (b *functionImpl[A, R]) Arguments() []reflect.Type {
// Collect Argument Types
var allTypes []reflect.Type
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)) {
field := aVal.Field(i)
// Validate Field is Settable
if !field.CanSet() {
return zeroR, errors.New("cannot set field")
}
// Validate and Set Field Value
argVal := reflect.ValueOf(allArgs[i])
if !argVal.Type().AssignableTo(field.Type()) {
return zeroR, errors.New("cannot assign field")
}
field.Set(argVal)
}
// Execute Function
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 (
"context"
"reflect"
"sync"
"testing"
@@ -17,19 +16,26 @@ type TestBasicArgs struct {
func (t TestBasicArgs) Validate() error { return nil }
func TestBasicType(t *testing.T) {
// Reset Registry
resetRegistry()
// Register Function
RegisterFunction("basic", func(ctx context.Context, args TestBasicArgs) (string, error) {
return args.Name, nil
})
// Verify Declarations
defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function basic(name: string, age: number): string;")
assert.Contains(t, defs, "interface TestBasicArgs")
}
func resetRegistry() {
// Lock Registry
registryLock.Lock()
defer registryLock.Unlock()
// Clear Registry
functionRegistry = make(map[string]Function)
}
@@ -46,11 +52,15 @@ type TestComplexArgs struct {
func (t TestComplexArgs) Validate() error { return nil }
func TestComplexTypes(t *testing.T) {
// Reset Registry
resetRegistry()
// Register Function
RegisterFunction("complex", func(ctx context.Context, args TestComplexArgs) (bool, error) {
return args.Flag, nil
})
// Verify Declarations
defs := GetFunctionDeclarations()
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 TestNestedStruct(t *testing.T) {
// Reset Registry
resetRegistry()
// Register Function
RegisterFunction("nested", func(ctx context.Context, args TestNestedArgs) (string, error) {
return args.User.FirstName, nil
})
// Verify Declarations
defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function nested(user: {}): string;")
}
@@ -83,11 +97,15 @@ type TestOptionalArgs struct {
func (t TestOptionalArgs) Validate() error { return nil }
func TestOptionalFields(t *testing.T) {
// Reset Registry
resetRegistry()
// Register Function
RegisterFunction[TestOptionalArgs, string]("optional", func(ctx context.Context, args TestOptionalArgs) (string, error) {
return args.Name, nil
})
// Verify Declarations
defs := GetFunctionDeclarations()
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 TestResultStruct(t *testing.T) {
// Reset Registry
resetRegistry()
// Register Function
RegisterFunction[TestResultArgs, TestResult]("result", func(ctx context.Context, args TestResultArgs) (TestResult, error) {
return TestResult{ID: 1}, nil
})
// Verify Declarations
defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function result(input: string): TestResult;")
assert.Contains(t, defs, "interface TestResult {id: number; data: number[]}")
@@ -125,11 +147,15 @@ type TestAsyncResult struct {
}
func TestAsyncPromise(t *testing.T) {
// Reset Registry
resetRegistry()
// Register Async Function
RegisterAsyncFunction[TestAsyncArgs, *TestAsyncStatus]("async", func(ctx context.Context, args TestAsyncArgs) (*TestAsyncStatus, error) {
return &TestAsyncStatus{Code: 200}, nil
})
// Verify Declarations
defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function async(url: string): Promise<TestAsyncStatus | null>;")
assert.Contains(t, defs, "interface TestAsyncStatus")
@@ -150,11 +176,15 @@ type TestNestedPointerArgs struct {
func (t TestNestedPointerArgs) Validate() error { return nil }
func TestNestedPointerInResult(t *testing.T) {
// Reset Registry
resetRegistry()
// Register Function
RegisterFunction[TestNestedPointerArgs, *TestNestedPointerResult]("pointerResult", func(ctx context.Context, args TestNestedPointerArgs) (*TestNestedPointerResult, error) {
return &TestNestedPointerResult{Value: "test"}, nil
})
// Verify Declarations
defs := GetFunctionDeclarations()
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 TestUintType(t *testing.T) {
// Reset Registry
resetRegistry()
// Register Function
RegisterFunction[TestUintArgs, uint]("uint", func(ctx context.Context, args TestUintArgs) (uint, error) {
return args.Value, nil
})
// Verify Declarations
defs := GetFunctionDeclarations()
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 TestFloatType(t *testing.T) {
// Reset Registry
resetRegistry()
// Register Function
RegisterFunction[TestFloatArgs, float32]("float", func(ctx context.Context, args TestFloatArgs) (float32, error) {
return float32(args.Amount), nil
})
// Verify Declarations
defs := GetFunctionDeclarations()
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 TestNestedPointerStruct(t *testing.T) {
// Reset Registry
resetRegistry()
// Register Function
RegisterFunction[TestPointerInArgs, string]("nestedPointer", func(ctx context.Context, args TestPointerInArgs) (string, error) {
return "test", nil
})
// Verify Declarations
defs := GetFunctionDeclarations()
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 TestErrorOnlyReturn(t *testing.T) {
// Reset Registry
resetRegistry()
// Register Function
RegisterFunction[TestErrorOnlyArgs, struct{}]("errorOnly", func(ctx context.Context, args TestErrorOnlyArgs) (struct{}, error) {
return struct{}{}, nil
})
// Verify Declarations
defs := GetFunctionDeclarations()
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 {
return func(r *Runtime) {
// Set Stdout
r.opts.Stdout = stdout
}
}
func WithStderr(stderr io.Writer) RuntimeOption {
return func(r *Runtime) {
// Set Stderr
r.opts.Stderr = stderr
}
}

View File

@@ -14,8 +14,10 @@ import (
)
type Runtime struct {
ctx *qjs.Context
opts qjs.Option
ctx *qjs.Context
opts qjs.Option
funcs map[string]functions.Function
typeDecls map[string]string
}
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 {
for name, fn := range functions.GetRegisteredFunctions() {
// Register Main Function
// Initialize Maps
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() {
r.ctx.SetAsyncFunc(name, func(this *qjs.This) {
qjsVal, err := callFunc(this, fn)
@@ -62,6 +77,37 @@ func (r *Runtime) populateGlobals() error {
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 {
tsFileContent, err := os.ReadFile(filePath)
if err != nil {
@@ -86,9 +132,9 @@ func (r *Runtime) RunCode(tsCode string) error {
func (r *Runtime) transformCode(tsCode string) ([]byte, error) {
result := api.Transform(tsCode, api.TransformOptions{
Loader: api.LoaderTS,
Target: api.ES2022,
// Format: api.FormatIIFE,
Loader: api.LoaderTS,
Target: api.ES2022,
Format: api.FormatIIFE,
Sourcemap: api.SourceMapNone,
TreeShaking: api.TreeShakingFalse,
})

View File

@@ -23,11 +23,14 @@ func (t TestArgs) Validate() error {
}
func TestExecuteTypeScript(t *testing.T) {
// Create Buffers
var stdout, stderr bytes.Buffer
// Create Runtime
rt, err := New(context.Background(), WithStderr(&stderr), WithStdout(&stdout))
assert.NoError(t, err, "Expected no error")
// Create TypeScript Code
tsCode := `interface Person {
name: string;
age: number;
@@ -54,73 +57,107 @@ function calculateSum(a: number, b: number): number {
console.log("Sum of 5 and 10 is: " + calculateSum(5, 10));
`
// Create Temp File
tmpFile, err := os.CreateTemp("", "*.ts")
assert.NoError(t, err, "Failed to create temp file")
t.Cleanup(func() {
_ = os.Remove(tmpFile.Name())
})
// Write Code to File
_, err = tmpFile.WriteString(tsCode)
assert.NoError(t, err, "Failed to write to temp file")
err = tmpFile.Close()
assert.NoError(t, err, "Failed to close temp file")
// Run File
err = rt.RunFile(tmpFile.Name())
// Verify Execution
assert.NoError(t, err, "Expected no error")
assert.Empty(t, stderr.String(), "Expected no error output")
// Verify Output
output := stdout.String()
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, "Email: alice@example.com", "Should show email")
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")
assert.GreaterOrEqual(t, len(lines), 3, "Should have at least 3 output lines")
}
func TestAsyncFunctionResolution(t *testing.T) {
// Register Async Function
functions.RegisterAsyncFunction("resolveTest", func(_ context.Context, args TestArgs) (string, error) {
return "test-result", nil
})
// Create Runtime
r, err := New(context.Background())
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()`))
require.NoError(t, err)
require.NotNil(t, result)
defer result.Free()
// Verify Result
assert.Equal(t, "test-result", result.String())
}
func TestAsyncFunctionRejection(t *testing.T) {
// Register Async Function that Returns Error
functions.RegisterAsyncFunction("rejectTest", func(_ context.Context, args TestArgs) (string, error) {
return "", assert.AnError
})
// Create Runtime
r, err := New(context.Background())
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()`))
assert.Error(t, err)
}
func TestNonPromise(t *testing.T) {
// Register Sync Function
functions.RegisterFunction("nonPromiseTest", func(_ context.Context, args TestArgs) (string, error) {
return "sync-result", nil
})
// Create Runtime
r, err := New(context.Background())
assert.NoError(t, err)
// Execute Sync Function
result, err := r.ctx.Eval("test.js", qjs.Code(`nonPromiseTest("hello")`))
assert.NoError(t, err)
defer result.Free()
// Verify Result
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) {
// Set Default Method and Headers
method := "GET"
headers := make(map[string]string)
// Apply Init Options
if args.Init != nil {
if 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)
if err != nil {
return Response{}, fmt.Errorf("failed to create request: %w", err)
}
// Set Request Headers
for k, v := range headers {
req.Header.Set(k, v)
}
// Execute Request
resp, err := http.DefaultClient.Do(req)
if err != nil {
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()
}()
// Read Response Body
body, err := io.ReadAll(resp.Body)
if err != nil {
return Response{}, fmt.Errorf("failed to read body: %w", err)
}
// Collect Response Headers
resultHeaders := make(map[string]string)
for key, values := range resp.Header {
if len(values) > 0 {
@@ -85,6 +92,7 @@ func Fetch(ctx context.Context, args FetchArgs) (Response, error) {
}
}
// Return Response
return Response{
OK: resp.StatusCode >= 200 && resp.StatusCode < 300,
Status: resp.StatusCode,

View File

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