diff --git a/plans/BUILTIN.md b/plans/BUILTIN.md index 3fca068..8da9931 100644 --- a/plans/BUILTIN.md +++ b/plans/BUILTIN.md @@ -1,68 +1,324 @@ -# JS Framework +# Builtin System Design -## Core Interfaces +## Overview -### JSValue +Type-safe builtin system for exposing Go functions to TypeScript/JavaScript with automatic type conversion and defaults support. -Base interface for any JavaScript value. Provides type-safe access to underlying values. +## 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 -package jsframe +type FetchArgs struct { + URL string `json:"url"` + Options *FetchOptions `json:"options"` +} -import "reflect" +func Fetch(args FetchArgs) (*FetchResult, error) { + // Implementation +} -type JSValue interface { - GoType() reflect.Type - GoValue() reflect.Value +// JavaScript calls: +// fetch("https://example.com") +// fetch("https://example.com", { method: "POST" }) +``` - SliceValues() ([]JSValue, bool) - MapValues() (map[string]JSValue, bool) +**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) } ``` -**Methods:** +## Type Conversion -- `GoType()` - Returns the Go `reflect.Type` of the underlying value -- `GoValue()` - Returns the underlying Go value -- `SliceValues()` - Returns `(values, true)` if the value is a slice/array, `(nil, false)` otherwise -- `MapValues()` - Returns `(values, true)` if the value is an object/map, `(nil, false)` otherwise +### JavaScript → Go ---- +Automatic conversion from goja values to Go struct fields: -### JSCallable +| 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) | -Interface for callable values (functions). Extends `JSValue` by adding call semantics and type information. +**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 -type JSCallable interface { - JSValue +func Fetch(args FetchArgs) (*FetchResult, error) { + if someError { + return nil, fmt.Errorf("fetch failed") + } + return result, nil +} - ArgumentTypes() []TypeInfo - ReturnType() TypeInfo +// JavaScript: fetch calls that error will throw +``` - Call(ctx Context, args ...JSValue) (JSValue, error) +## 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` +- `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)) + } } ``` -**Methods:** - -- `Call(ctx, args)` - Executes the function with provided arguments. Returns a JSValue and an error. The framework converts errors to JavaScript exceptions. -- `ArgumentTypes()` - Returns type information for each argument -- `ReturnType()` - Returns type information for the return value - -## Callable / Builtin Registrar +### Field Name Extraction ```go -type BuiltinFunc[T any, R any] func(args T) (R, error) - -type BuiltinArgs[T any] interface { - T - Defaults() T +func getFieldName(field reflect.StructField) string { + jsonTag := field.Tag.Get("json") + if jsonTag != "" && jsonTag != "-" { + name, _, _ := strings.Cut(jsonTag, ",") + return name + } + return field.Name } - -func RegisterBuiltin[T any, R any](name string, fn BuiltinFunc[T, R]) JSCallable ``` -## Goja Types <-> Go Types Converstion +## Migration Examples -Implement bidirectional conversion between goja values and go values. +### 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