diff --git a/README.md b/README.md index a5fb975..eb938e1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Poiesis -A Go tool that transpiles TypeScript to JavaScript using esbuild and executes it with goja, with an extensible builtin system. +A Go tool that transpiles TypeScript to JavaScript using esbuild and executes it with qjs, with an extensible function system. ## Project Structure @@ -10,35 +10,37 @@ reichard.io/poiesis/ │ └── poiesis/ # CLI application entry point │ └── main.go ├── internal/ -│ └── runtime/ -│ ├── pkg/ -│ │ └── builtin/ # Builtin framework (framework only) -│ │ └── builtin.go # Registration system & type conversion -│ ├── standard/ # Standard builtin implementations -│ │ ├── fetch.go # HTTP fetch builtin -│ │ └── fetch_test.go # Tests for fetch -│ ├── runtime.go # Transpilation & execution -│ └── runtime_test.go # Runtime tests +│ ├── runtime/ # Runtime management, transpilation, execution +│ │ ├── runtime.go # Core runtime, transpilation, execution +│ │ └── runtime_test.go # Runtime tests +│ ├── functions/ # Function registration framework +│ │ ├── registry.go # Registration system +│ │ ├── types.go # Core interfaces and types +│ │ ├── typescript.go # TypeScript definition generation +│ │ ├── collector.go # Type collection utilities +│ │ └── typescript_test.go # Type system tests +│ └── stdlib/ # Standard library implementations +│ ├── fetch.go # HTTP fetch implementation +│ └── fetch_test.go # Fetch tests ``` ## Architecture The project is cleanly separated into three packages: -1. **`internal/runtime/pkg/builtin`** - The framework for registering builtins and type conversion - - Generic registration with automatic type inference - - Bidirectional Go ↔ JavaScript type conversion - - No builtin implementations (pure framework) - -2. **`internal/runtime/standard`** - Standard builtin implementations - - `fetch`, `add`, `greet` - - Custom type converters for complex types - - Independent and easily extensible - -3. **`internal/runtime`** - Runtime management +1. **`internal/runtime`** - Runtime management - TypeScript transpilation with esbuild - - JavaScript execution with goja - - Automatically imports and registers standard builtins + - JavaScript execution with qjs + - Automatic function registration and execution + +2. **`internal/functions`** - Generic function registration framework + - Type-safe registration with generics + - Bidirectional Go ↔ JavaScript type conversion + - Automatic TypeScript declaration generation + +3. **`internal/stdlib`** - Standard library implementations + - `fetch` - HTTP requests + - Extensible for additional standard functions ## Installation & Build @@ -57,55 +59,74 @@ golangci-lint run ```bash poiesis +poiesis -print-types ``` -## Builtin System +## Function System -The builtin system allows you to easily expose Go functions to TypeScript/JavaScript. +The function system allows you to easily expose Go functions to TypeScript/JavaScript. -### Adding a Builtin +### Adding a Function Just write a Go function and register it: ```go -// Your function -func add(a, b int) int { - return a + b +package mystdlib + +import ( + "context" + "reichard.io/poiesis/internal/functions" +) + +type AddArgs struct { + A int `json:"a"` + B int `json:"b"` +} + +func (a AddArgs) Validate() error { + return nil +} + +func Add(_ context.Context, args AddArgs) (int, error) { + return args.A + args.B, nil } -// Register it func init() { - builtin.RegisterBuiltin("add", add) + functions.RegisterFunction[AddArgs, int]("add", Add) } ``` That's it! The framework automatically: - -- Converts TypeScript values to Go types +- Converts JavaScript values to Go types - Handles errors (panics as JS errors) - Generates TypeScript definitions -- Manages the goja integration +- Manages the qjs integration ### Example ```typescript // TypeScript code -console.log("5 + 10 =", add(5, 10)); - -const response = fetch("https://httpbin.org/get"); +const response = fetch({input: "https://httpbin.org/get"}); console.log("OK:", response.ok); console.log("Status:", response.status); -console.log("Body:", response.text()); +console.log("Body:", response.body); ``` ### Built-in Functions -- `fetch(url, options?)` - HTTP requests -- `add(a, b)` - Simple arithmetic example -- `greet(name)` - String manipulation example +- `fetch(options)` - HTTP requests + - `options.input` (string) - URL to fetch + - `options.init` (object) - Optional init object with `method`, `headers`, `body` ## Dependencies - `github.com/evanw/esbuild/pkg/api` - TypeScript transpilation -- `github.com/dop251/goja` - JavaScript execution +- `github.com/fastschema/qjs` - JavaScript execution (QuickJS) - `github.com/stretchr/testify/assert` - Test assertions + +## Development + +- **Test framework**: Go's built-in `testing` package +- **Code style**: Follow standard Go conventions +- **Linting**: `golangci-lint run` - must pass before committing +- **TypeScript test files**: Tests that require TypeScript files should create them inline using `os.CreateTemp()` instead of relying on external test files diff --git a/internal/functions/registry.go b/internal/functions/registry.go index 6843a6c..02f005a 100644 --- a/internal/functions/registry.go +++ b/internal/functions/registry.go @@ -67,7 +67,14 @@ func GetFunctionDeclarations() string { } func GetRegisteredFunctions() map[string]Function { - return functionRegistry + registryMutex.RLock() + defer registryMutex.RUnlock() + + result := make(map[string]Function, len(functionRegistry)) + for k, v := range functionRegistry { + result[k] = v + } + return result } func RegisterFunction[T Args, R any](name string, fn GoFunc[T, R]) { diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index d83c077..b11dbf3 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -94,11 +94,14 @@ func (r *Runtime) transformCode(tsCode string) ([]byte, error) { }) if len(result.Errors) > 0 { - var allErrs []string - for _, e := range result.Errors { - allErrs = append(allErrs, e.Text) + var b strings.Builder + for i, e := range result.Errors { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(e.Text) } - return nil, fmt.Errorf("transpilation failed: %s", strings.Join(allErrs, ", ")) + return nil, fmt.Errorf("transpilation failed: %s", b.String()) } return result.Code, nil @@ -112,7 +115,7 @@ func callFunc(this *qjs.This, fn functions.Function) (*qjs.Value, error) { for i := range min(len(fnArgs), len(qjsArgs)) { rVal, err := qjs.JsArgToGo(qjsArgs[i], fnArgs[i]) if err != nil { - panic(err) + return nil, fmt.Errorf("argument conversion failed: %w", err) } allArgs = append(allArgs, rVal.Interface()) } diff --git a/internal/stdlib/fetch.go b/internal/stdlib/fetch.go index 0557191..7903109 100644 --- a/internal/stdlib/fetch.go +++ b/internal/stdlib/fetch.go @@ -31,9 +31,6 @@ type RequestInit struct { } func (o *RequestInit) Validate() error { - if o.Method == "" { - o.Method = "GET" - } return nil } @@ -44,35 +41,20 @@ type Response struct { Headers map[string]string `json:"headers"` } -type AddArgs struct { - A int `json:"a"` - B int `json:"b"` -} - -func (a AddArgs) Validate() error { - return nil -} - -type GreetArgs struct { - Name string `json:"name"` -} - -func (g GreetArgs) Validate() error { - return nil -} - -func Fetch(_ context.Context, args FetchArgs) (Response, error) { +func Fetch(ctx context.Context, args FetchArgs) (Response, error) { method := "GET" headers := make(map[string]string) if args.Init != nil { - method = args.Init.Method + if args.Init.Method != "" { + method = args.Init.Method + } if args.Init.Headers != nil { maps.Copy(headers, args.Init.Headers) } } - req, err := http.NewRequest(method, args.Input, nil) + req, err := http.NewRequestWithContext(ctx, method, args.Input, nil) if err != nil { return Response{}, fmt.Errorf("failed to create request: %w", err) }