qip
This commit is contained in:
336
plans/BUILTIN.md
336
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<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))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
Reference in New Issue
Block a user