Compare commits

..

10 Commits

Author SHA1 Message Date
b709e772b9 style(tsconvert): enforce two-space indentation for TypeScript interfaces
- Add two spaces before each field in generated interfaces
- Use newlines between fields and closing brace for proper formatting
- Update test to validate proper indentation
- Document test requirements in AGENTS.md
2026-01-29 20:34:28 -05:00
d2df7a7c69 chore(runtime): Add logrus logging framework with structured logging
Add logrus as the primary logging framework with dependency injection
pattern. All errors now use WithError() for context, and structured
logging uses camelCase field names. Tag runtime service with service
field for better log organization.

- Add logrus dependency
- Update Runtime to accept logger via dependency injection
- Add WithLogger() option for logger configuration
- Log errors with WithError() for context
- Log async function failures with service context
- Document logging practices in AGENTS.md
2026-01-29 20:28:19 -05:00
1082f68822 feat(cli): add cobra-based CLI with subcommands
Replace simple argument parsing with cobra to provide better CLI experience:

- Add 'execute' subcommand for running TypeScript files
- Add 'types' subcommand for printing TypeScript type declarations
- Add help text and error handling
- Update documentation with new usage examples

Add dependencies: github.com/spf13/cobra, github.com/spf13/pflag
2026-01-29 20:07:35 -05:00
0d97f20e79 types 2026-01-29 19:54:19 -05:00
234c4718a4 more updates 2026-01-28 22:42:47 -05:00
e04fe8cef3 more cleanup 2026-01-28 22:28:42 -05:00
ffcb6f658b clean up 2026-01-28 21:59:04 -05:00
dcd516d970 migrate 2026-01-28 21:55:20 -05:00
513674b0c8 pre migrate 2026-01-28 20:58:26 -05:00
a613283539 wip rename 2026-01-28 14:22:35 -05:00
31 changed files with 1584 additions and 1377 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.opencode
/poiesis

148
AGENTS.md
View File

@@ -6,7 +6,7 @@
## Overview ## Overview
Go tool that transpiles TypeScript to JavaScript using esbuild API and executes it with modernc.org/quickjs. Features a flexible builtin system for exposing Go functions to TypeScript with support for both synchronous and asynchronous (Promise-based) operations. Go tool that transpiles TypeScript to JavaScript using esbuild API and executes it with github.com/fastschema/qjs. Features a flexible builtin system for exposing Go functions to TypeScript with support for both synchronous and asynchronous (Promise-based) operations.
## Build & Test ## Build & Test
@@ -25,40 +25,52 @@ 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
├── builtin/ # Builtin registration framework │ └── options.go
│ ├── types.go │ ├── functions/ # Function registration framework
│ │ ├── registry.go │ │ ├── registry.go
│ │ ├── wrapper.go │ │ ├── types.go
│ │ ├── typescript_test.go
│ │ └── functions_test.go
│ ├── tsconvert/ # Go-to-TypeScript type conversion utilities
│ │ ├── convert.go │ │ ├── convert.go
│ │ ├── typescript.go │ │ ├── types.go
│ │ └── builtin_test.go │ │ └── convert_test.go
│ └── standard/ # Standard builtin implementations │ └── stdlib/ # Standard library implementations
│ ├── fetch.go │ ├── fetch.go
── fetch_test.go ── fetch_test.go
│ └── fetch_promise_test.go
└── test_data/ # Test TypeScript files
``` ```
## 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/builtin` - Generic builtin 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/standard` - Standard builtin implementations (fetch, add, greet, etc.) - `reichard.io/poiesis/internal/tsconvert` - Go-to-TypeScript type conversion utilities and type declaration generation
- `reichard.io/poiesis/internal/stdlib` - Standard library implementations (fetch)
## Builtin System ## Function System
### Registration ### Registration
Two types of builtins: Two types of functions:
- **Sync**: `RegisterBuiltin[T, R](name, fn)` - executes synchronously, returns value
- **Async**: `RegisterAsyncBuiltin[T, R](name, fn)` - runs in goroutine, returns Promise - **Sync**: `RegisterFunction[T, R](name, fn)` - executes synchronously, returns value
- **Async**: `RegisterAsyncFunction[T, R](name, fn)` - runs in goroutine, returns Promise
### Requirements ### Requirements
- Args must be a struct implementing `Args` interface with `Validate() error` method - Args must be a struct implementing `Args` interface with `Validate() error` method
- Use JSON tags for TypeScript type definitions - Use JSON tags for TypeScript type definitions
- Async builtins automatically generate `Promise<R>` return types - Async functions automatically generate `Promise<R>` return types
### Calling Convention
**Important**: Functions have different calling conventions on Go vs JavaScript sides:
- **Go side**: Function receives a **single argument struct** with all parameters
- **JavaScript side**: Function is called with **individual arguments** matching the struct fields (in field order)
Fields are ordered by their position in the struct, with the generated TypeScript signature using those field names as argument names.
### Example ### Example
@@ -74,11 +86,11 @@ func Add(_ context.Context, args AddArgs) (int, error) {
return args.A + args.B, nil return args.A + args.B, nil
} }
// Register sync builtin // Register sync function
builtin.RegisterBuiltin[AddArgs, int]("add", Add) functions.RegisterFunction[AddArgs, int]("add", Add)
// Register async builtin // Register async function
builtin.RegisterAsyncBuiltin[FetchArgs, *FetchResult]("fetch", Fetch) functions.RegisterAsyncFunction[FetchArgs, *FetchResult]("fetch", Fetch)
``` ```
## Testing Patterns ## Testing Patterns
@@ -87,12 +99,77 @@ builtin.RegisterAsyncBuiltin[FetchArgs, *FetchResult]("fetch", Fetch)
- **Assertions**: `github.com/stretchr/testify/assert` and `require` - **Assertions**: `github.com/stretchr/testify/assert` and `require`
- **Linting**: `golangci-lint run` - must pass before committing - **Linting**: `golangci-lint run` - must pass before committing
- **Test organization**: Test files use `_test.go` suffix, test functions prefixed with `Test` - **Test organization**: Test files use `_test.go` suffix, test functions prefixed with `Test`
- **TypeScript test files**: Tests that require TypeScript files should create them inline using `os.CreateTemp()` instead of relying on external test files
- **CRITICAL**: You must run `go test ./...` after any code change to validate that tests pass before committing
## Test Requirements
Before any commit, ensure:
1. All tests pass: `go test ./...`
2. All linting passes: `golangci-lint run`
3. Type declarations are properly formatted (two spaces indentation)
## Dependencies ## Dependencies
- `github.com/evanw/esbuild/pkg/api` - TypeScript transpilation - `github.com/evanw/esbuild/pkg/api` - TypeScript transpilation
- `github.com/dop251/goja` - JavaScript execution - `github.com/fastschema/qjs` - JavaScript execution (CGO-free QuickJS runtime)
- `github.com/stretchr/testify/assert` - Test assertions - `github.com/stretchr/testify/assert` - Test assertions
- `github.com/sirupsen/logrus` - Logging framework
## Logging Practices
### Primary Logger
Use `github.com/sirupsen/logrus` as the primary logging framework throughout the codebase.
### Error Handling
- Use `WithError(err)` when logging errors to include the error context
- Every error should eventually be logged somewhere appropriate in the call chain
- Don't log on every error condition - find the appropriate upstream logging location
- Use structured logging with context fields when relevant for debugging
### Structured Logging
- Use `WithField(key, value)` for contextual information
- Use `WithFields(fields)` when adding multiple fields
- Field names must be in camelCase (e.g., `WithField("filePath", path)`)
- Only use WithField(s) when the field is relevant and helpful for debugging issues
- Tag services with their name: `logger.WithField("service", "runtime")`
### Example Usage
```go
import "github.com/sirupsen/logrus"
func New(ctx context.Context) (*Runtime, error) {
logger := logrus.New()
r := &Runtime{logger: logger.WithField("service", "runtime")}
rt, err := qjs.New(r.opts)
if err != nil {
logger.WithError(err).Error("Failed to create QuickJS context")
return nil, err
}
return r, nil
}
func (r *Runtime) ExecuteFile(filePath string) error {
data, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("read file %s: %w", filePath, err)
}
result, err := r.ctx.Eval("code.ts", qjs.Code(string(data)))
if err != nil {
r.logger.WithField("filePath", filePath).WithError(err).Error("Execution failed")
return err
}
return nil
}
```
## Code Conventions ## Code Conventions
@@ -100,3 +177,26 @@ builtin.RegisterAsyncBuiltin[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...
```

140
README.md
View File

@@ -1,6 +1,6 @@
# Poiesis # 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 ## Project Structure
@@ -10,35 +10,37 @@ reichard.io/poiesis/
│ └── poiesis/ # CLI application entry point │ └── poiesis/ # CLI application entry point
│ └── main.go │ └── main.go
├── internal/ ├── internal/
── runtime/ ── runtime/ # Runtime management, transpilation, execution
├── pkg/ ├── runtime.go # Core runtime, transpilation, execution
│ └── builtin/ # Builtin framework (framework only) │ │ └── runtime_test.go # Runtime tests
└── builtin.go # Registration system & type conversion ├── functions/ # Function registration framework
├── standard/ # Standard builtin implementations ├── registry.go # Registration system
│ ├── fetch.go # HTTP fetch builtin │ │ ├── types.go # Core interfaces and types
── fetch_test.go # Tests for fetch │ │ ── typescript.go # TypeScript definition generation
├── runtime.go # Transpilation & execution ├── collector.go # Type collection utilities
└── runtime_test.go # Runtime tests └── typescript_test.go # Type system tests
│ └── stdlib/ # Standard library implementations
│ ├── fetch.go # HTTP fetch implementation
│ └── fetch_test.go # Fetch tests
``` ```
## Architecture ## Architecture
The project is cleanly separated into three packages: The project is cleanly separated into three packages:
1. **`internal/runtime/pkg/builtin`** - The framework for registering builtins and type conversion 1. **`internal/runtime`** - Runtime management
- 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
- TypeScript transpilation with esbuild - TypeScript transpilation with esbuild
- JavaScript execution with goja - JavaScript execution with qjs
- Automatically imports and registers standard builtins - 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 ## Installation & Build
@@ -46,6 +48,12 @@ The project is cleanly separated into three packages:
go build ./cmd/poiesis go build ./cmd/poiesis
``` ```
## CLI Options
- `[file]` - Path to TypeScript file to execute (optional)
- `--print-types` - Print TypeScript type declarations for all registered functions
- `--help` - Show help information
## Testing ## Testing
```bash ```bash
@@ -56,56 +64,102 @@ golangci-lint run
## Usage ## Usage
```bash ```bash
poiesis <typescript-file> poiesis execute [file] # Run TypeScript file
poiesis types # Print TypeScript type declarations
poiesis --help # Show help
``` ```
## 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: Just write a Go function and register it:
```go ```go
// Your function package mystdlib
func add(a, b int) int {
return a + b 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() { func init() {
builtin.RegisterBuiltin("add", add) functions.RegisterFunction[AddArgs, int]("add", Add)
} }
``` ```
That's it! The framework automatically: That's it! The framework automatically:
- Converts JavaScript values to Go types
- Converts TypeScript values to Go types
- Handles errors (panics as JS errors) - Handles errors (panics as JS errors)
- Generates TypeScript definitions - Generates TypeScript definitions
- Manages the goja integration - Manages the qjs integration
### Calling Convention
**Important**: There's an important difference between how functions are defined in Go versus how they're called in JavaScript:
- **Go side**: The function receives a **single argument struct** containing all parameters
- **JavaScript side**: The function is called with the **struct fields as individual arguments** (in the order they appear in the struct)
```go
// Go: Single struct argument
type AddArgs struct {
A int `json:"a"`
B int `json:"b"`
}
func Add(_ context.Context, args AddArgs) (int, error) {
return args.A + args.B, nil
}
```
```typescript
// JavaScript: Individual arguments (not an object!)
const result = add(5, 10); // NOT add({ a: 5, b: 10 })
```
The framework extracts the JSON tags from the struct fields and uses them to generate the correct TypeScript function signature.
### Example ### Example
```typescript ```typescript
// TypeScript code // TypeScript code - call with individual arguments matching struct fields
console.log("5 + 10 =", add(5, 10));
const response = fetch("https://httpbin.org/get"); const response = fetch("https://httpbin.org/get");
console.log("OK:", response.ok); console.log("OK:", response.ok);
console.log("Status:", response.status); console.log("Status:", response.status);
console.log("Body:", response.text()); console.log("Body:", response.body);
``` ```
### Built-in Functions ### Built-in Functions
- `fetch(url, options?)` - HTTP requests - `fetch(options)` - HTTP requests
- `add(a, b)` - Simple arithmetic example - `options.input` (string) - URL to fetch
- `greet(name)` - String manipulation example - `options.init` (object) - Optional init object with `method`, `headers`, `body`
## Dependencies ## Dependencies
- `github.com/evanw/esbuild/pkg/api` - TypeScript transpilation - `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 - `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

View File

@@ -5,33 +5,70 @@ import (
"fmt" "fmt"
"os" "os"
"reichard.io/poiesis/internal/builtin" "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"reichard.io/poiesis/internal/runtime" "reichard.io/poiesis/internal/runtime"
_ "reichard.io/poiesis/internal/standard"
) )
func main() { var runCmd = &cobra.Command{
if len(os.Args) < 2 { Use: "execute [file]",
fmt.Fprintln(os.Stderr, "Usage: poiesis <typescript-file>") Short: "Transpile and execute TypeScript code",
fmt.Fprintln(os.Stderr, " poiesis -print-types") Long: `Poiesis transpiles TypeScript to JavaScript using esbuild and executes it with QuickJS.
It also provides a builtin system for exposing Go functions to TypeScript.`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
logger := logrus.New()
rt, err := runtime.New(ctx, runtime.WithLogger(logger))
if err != nil {
logger.WithError(err).Error("Failed to create runtime")
fmt.Fprintf(os.Stderr, "Failed to create runtime: %v\n", err)
os.Exit(1) os.Exit(1)
} }
// Print Types if len(args) == 0 {
if os.Args[1] == "-print-types" { fmt.Fprintln(os.Stderr, "Usage: poiesis execute [file]")
fmt.Println(builtin.GetBuiltinsDeclarations()) _ = cmd.Help()
return os.Exit(1)
} }
// Create Runtime if err := rt.RunFile(args[0]); err != nil {
rt, err := runtime.New(context.Background()) logger.WithError(err).Error("Failed to run file")
fmt.Fprintf(os.Stderr, "Failed to run file: %v\n", err)
os.Exit(1)
}
},
}
var typesCmd = &cobra.Command{
Use: "types",
Short: "Print TypeScript type declarations",
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
logger := logrus.New()
rt, err := runtime.New(ctx, runtime.WithLogger(logger))
if err != nil { if err != nil {
panic(err) logger.WithError(err).Error("Failed to create runtime")
fmt.Fprintf(os.Stderr, "Failed to create runtime: %v\n", err)
os.Exit(1)
}
fmt.Println(rt.GetTypeDeclarations())
},
} }
// Run File var rootCmd = &cobra.Command{
filePath := os.Args[1] Use: "poiesis",
if err := rt.RunFile(filePath, os.Stdout, os.Stderr); err != nil { Short: "TypeScript transpiler and executor",
panic(err) }
func init() {
rootCmd.AddCommand(runCmd)
rootCmd.AddCommand(typesCmd)
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
} }
} }

17
go.mod
View File

@@ -4,23 +4,18 @@ go 1.25.5
require ( require (
github.com/evanw/esbuild v0.27.2 github.com/evanw/esbuild v0.27.2
github.com/fastschema/qjs v0.0.6
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
modernc.org/quickjs v0.17.1
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sirupsen/logrus v1.9.4 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.37.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.67.1 // indirect
modernc.org/libquickjs v0.12.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
) )

66
go.sum
View File

@@ -1,66 +1,30 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/evanw/esbuild v0.27.2 h1:3xBEws9y/JosfewXMM2qIyHAi+xRo8hVx475hVkJfNg= github.com/evanw/esbuild v0.27.2 h1:3xBEws9y/JosfewXMM2qIyHAi+xRo8hVx475hVkJfNg=
github.com/evanw/esbuild v0.27.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/evanw/esbuild v0.27.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/fastschema/qjs v0.0.6 h1:C45KMmQMd21UwsUAmQHxUxiWOfzwTg1GJW0DA0AbFEE=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/fastschema/qjs v0.0.6/go.mod h1:bbg36wxXnx8g0FdKIe5+nCubrQvHa7XEVWqUptjHt/A=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/libquickjs v0.12.3 h1:2IU9B6njBmce2PuYttJDkXeoLRV9WnvgP+eU5HAC8YI=
modernc.org/libquickjs v0.12.3/go.mod h1:iCsgVxnHTX3i0YPxxHBmJk0GLA5sVUHXWI/090UXgeE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/quickjs v0.17.1 h1:CbYnbTf7ksZk9YZ1rRM2Ab1Zfi+X6s50kXiOhpd2NIg=
modernc.org/quickjs v0.17.1/go.mod h1:hATT7DIJc33I5Q/Fjffhm0tpUHNSqdKHma/ossibTA0=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

View File

@@ -1,95 +0,0 @@
package builtin
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"modernc.org/quickjs"
)
type TestArgs struct {
Field1 string `json:"field1"`
}
func (t TestArgs) Validate() error {
return nil
}
func TestAsyncBuiltin(t *testing.T) {
RegisterAsyncBuiltin("testAsync", func(_ context.Context, args TestArgs) (string, error) {
return "result: " + args.Field1, nil
})
registryMutex.RLock()
builtin, ok := builtinRegistry["testAsync"]
registryMutex.RUnlock()
require.True(t, ok, "testAsync should be registered")
assert.Contains(t, builtin.Definition(), "Promise<string>", "definition should include Promise<string>")
}
func TestAsyncBuiltinResolution(t *testing.T) {
RegisterAsyncBuiltin("resolveTest", func(_ context.Context, args TestArgs) (string, error) {
return "test-result", nil
})
vm, err := quickjs.NewVM()
require.NoError(t, err)
defer func() {
_ = vm.Close()
}()
vm.SetCanBlock(true)
RegisterBuiltins(context.Background(), vm)
result, err := vm.Eval(`resolveTest("hello")`, quickjs.EvalGlobal)
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestAsyncBuiltinRejection(t *testing.T) {
RegisterAsyncBuiltin("rejectTest", func(_ context.Context, args TestArgs) (string, error) {
return "", assert.AnError
})
vm, err := quickjs.NewVM()
require.NoError(t, err)
defer func() {
_ = vm.Close()
}()
vm.SetCanBlock(true)
RegisterBuiltins(context.Background(), vm)
result, err := vm.Eval(`rejectTest({field1: "hello"})`, quickjs.EvalGlobal)
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestNonPromise(t *testing.T) {
RegisterBuiltin("nonPromiseTest", func(_ context.Context, args TestArgs) (string, error) {
return "sync-result", nil
})
vm, err := quickjs.NewVM()
require.NoError(t, err)
defer func() {
_ = vm.Close()
}()
vm.SetCanBlock(true)
RegisterBuiltins(context.Background(), vm)
result, err := vm.Eval(`nonPromiseTest({field1: "hello"})`, quickjs.EvalGlobal)
require.NoError(t, err)
if obj, ok := result.(*quickjs.Object); ok {
var arr []any
if err := obj.Into(&arr); err == nil && len(arr) > 0 {
assert.Equal(t, "sync-result", arr[0])
}
}
}

View File

@@ -1,170 +0,0 @@
package builtin
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

@@ -1,78 +0,0 @@
package builtin
import (
"fmt"
"reflect"
"strings"
"sync"
)
var (
builtinRegistry = make(map[string]Builtin)
registryMutex sync.RWMutex
collector *typeCollector
)
func registerBuiltin[A Args, R any](name string, isAsync bool, fn Func[A, R]) {
registryMutex.Lock()
defer registryMutex.Unlock()
if collector == nil {
collector = newTypeCollector()
}
tType := reflect.TypeFor[A]()
if tType.Kind() != reflect.Struct {
panic(fmt.Sprintf("builtin %s: argument must be a struct type, got %v", name, tType))
}
fnType := reflect.TypeOf(fn)
types := collector.collectTypes(tType, fnType)
paramTypes := collector.getParamTypes()
builtinRegistry[name] = &builtinImpl[A, R]{
name: name,
fn: fn,
types: types,
definition: generateTypeScriptDefinition(name, tType, fnType, isAsync, paramTypes),
}
}
func GetBuiltinsDeclarations() string {
registryMutex.RLock()
defer registryMutex.RUnlock()
typeDefinitions := make(map[string]bool)
var typeDefs []string
var functionDecls []string
for _, builtin := range builtinRegistry {
for _, t := range builtin.Types() {
if !typeDefinitions[t] {
typeDefinitions[t] = true
typeDefs = append(typeDefs, t)
}
}
functionDecls = append(functionDecls, builtin.Definition())
}
result := strings.Join(typeDefs, "\n\n")
if len(result) > 0 && len(functionDecls) > 0 {
result += "\n\n"
}
result += strings.Join(functionDecls, "\n")
return result
}
func RegisterBuiltin[T Args, R any](name string, fn Func[T, R]) {
registerBuiltin(name, false, fn)
}
func RegisterAsyncBuiltin[T Args, R any](name string, fn Func[T, R]) {
registerBuiltin(name, true, fn)
}
func GetBuiltins() map[string]Builtin {
return builtinRegistry
}

View File

@@ -1,76 +0,0 @@
package builtin
import (
"context"
"errors"
"reflect"
)
type Builtin interface {
Name() string
Types() []string
Definition() string
WrapFn(context.Context) func(...any) (any, error)
}
type Func[A Args, R any] func(ctx context.Context, args A) (R, error)
type Args interface {
Validate() error
}
type builtinImpl[A Args, R any] struct {
name string
fn Func[A, R]
definition string
types []string
}
func (b *builtinImpl[A, R]) Name() string {
return b.name
}
func (b *builtinImpl[A, R]) Types() []string {
return b.types
}
func (b *builtinImpl[A, R]) Definition() string {
return b.definition
}
func (b *builtinImpl[A, R]) WrapFn(ctx context.Context) func(...any) (any, error) {
return func(allArgs ...any) (any, error) {
// Populate Arguments
var fnArgs A
aVal := reflect.ValueOf(&fnArgs).Elem()
// Populate Fields
for i := range min(aVal.NumField(), len(allArgs)) {
field := aVal.Field(i)
if !field.CanSet() {
return nil, errors.New("cannot set field")
}
argVal := reflect.ValueOf(allArgs[i])
if !argVal.Type().AssignableTo(field.Type()) {
return nil, errors.New("cannot assign field")
}
field.Set(argVal)
}
// Validate
if err := fnArgs.Validate(); err != nil {
return nil, errors.New("cannot validate args")
}
// Call Function
resp, err := b.fn(ctx, fnArgs)
if err != nil {
return nil, err
}
return resp, nil
}
}

View File

@@ -1,73 +0,0 @@
package builtin
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

@@ -1 +0,0 @@
package builtin

View File

@@ -0,0 +1,115 @@
package functions
import (
"fmt"
"reflect"
"strings"
"sync"
"reichard.io/poiesis/internal/tsconvert"
)
var (
functionRegistry = make(map[string]Function)
registryMutex sync.RWMutex
)
func registerFunction[A Args, R any](name string, isAsync bool, fn GoFunc[A, R]) {
// Lock Registry
registryMutex.Lock()
defer registryMutex.Unlock()
// 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 := tsconvert.CollectTypes(tType, fnType)
definition := tsconvert.GenerateFunctionDecl(name, tType, fnType, isAsync)
// Register Function
functionRegistry[name] = &functionImpl[A, R]{
name: name,
fn: fn,
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
for _, fn := range functionRegistry {
for _, t := range fn.Types() {
if !typeDefinitions[t] {
typeDefinitions[t] = true
typeDefs = append(typeDefs, t)
}
}
functionDecls = append(functionDecls, fn.Definition())
}
// Build Result
result := strings.Join(typeDefs, "\n\n")
if len(result) > 0 && len(functionDecls) > 0 {
result += "\n\n"
}
result += strings.Join(functionDecls, "\n")
return result
}
// 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
}
return result
}
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

@@ -0,0 +1,92 @@
package functions
import (
"context"
"errors"
"reflect"
)
type Function interface {
Name() string
Types() map[string]string
Definition() string
IsAsync() bool
Arguments() []reflect.Type
Call(context.Context, []any) (any, error)
}
type GoFunc[A Args, R any] func(context.Context, A) (R, error)
type Args interface {
Validate() error
}
type functionImpl[A Args, R any] struct {
name string
fn GoFunc[A, R]
definition string
types map[string]string
isAsync bool
}
func (b *functionImpl[A, R]) Name() string {
return b.name
}
func (b *functionImpl[A, R]) Types() map[string]string {
return b.types
}
func (b *functionImpl[A, R]) Definition() string {
return b.definition
}
func (b *functionImpl[A, R]) IsAsync() bool {
return b.isAsync
}
func (b *functionImpl[A, R]) Function() any {
return b.fn
}
func (b *functionImpl[A, R]) Arguments() []reflect.Type {
// Collect Argument Types
var allTypes []reflect.Type
rType := reflect.TypeFor[A]()
for i := range rType.NumField() {
allTypes = append(allTypes, rType.Field(i).Type)
}
return allTypes
}
func (b *functionImpl[A, R]) Call(ctx context.Context, allArgs []any) (any, error) {
return b.CallGeneric(ctx, allArgs)
}
func (b *functionImpl[A, R]) CallGeneric(ctx context.Context, allArgs []any) (zeroR R, err error) {
// Populate Arguments
var fnArgs A
aVal := reflect.ValueOf(&fnArgs).Elem()
// Populate Fields
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,8 +1,7 @@
package builtin package functions
import ( import (
"context" "context"
"reflect"
"sync" "sync"
"testing" "testing"
@@ -17,20 +16,27 @@ 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()
RegisterBuiltin[TestBasicArgs, string]("basic", func(ctx context.Context, args TestBasicArgs) (string, error) {
// Register Function
RegisterFunction("basic", func(ctx context.Context, args TestBasicArgs) (string, error) {
return args.Name, nil return args.Name, nil
}) })
defs := GetBuiltinsDeclarations() // Verify Declarations
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()
builtinRegistry = make(map[string]Builtin)
// Clear Registry
functionRegistry = make(map[string]Function)
} }
var ( var (
@@ -46,12 +52,16 @@ 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()
RegisterBuiltin[TestComplexArgs, bool]("complex", func(ctx context.Context, args TestComplexArgs) (bool, error) {
// Register Function
RegisterFunction("complex", func(ctx context.Context, args TestComplexArgs) (bool, error) {
return args.Flag, nil return args.Flag, nil
}) })
defs := GetBuiltinsDeclarations() // Verify Declarations
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,12 +75,16 @@ 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()
RegisterBuiltin[TestNestedArgs, string]("nested", func(ctx context.Context, args TestNestedArgs) (string, error) {
// Register Function
RegisterFunction("nested", func(ctx context.Context, args TestNestedArgs) (string, error) {
return args.User.FirstName, nil return args.User.FirstName, nil
}) })
defs := GetBuiltinsDeclarations() // Verify Declarations
defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function nested(user: {}): string;") assert.Contains(t, defs, "declare function nested(user: {}): string;")
} }
@@ -83,12 +97,16 @@ 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()
RegisterBuiltin[TestOptionalArgs, string]("optional", func(ctx context.Context, args TestOptionalArgs) (string, error) {
// Register Function
RegisterFunction[TestOptionalArgs, string]("optional", func(ctx context.Context, args TestOptionalArgs) (string, error) {
return args.Name, nil return args.Name, nil
}) })
defs := GetBuiltinsDeclarations() // Verify Declarations
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,14 +122,18 @@ 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()
RegisterBuiltin[TestResultArgs, TestResult]("result", func(ctx context.Context, args TestResultArgs) (TestResult, error) {
// Register Function
RegisterFunction[TestResultArgs, TestResult]("result", func(ctx context.Context, args TestResultArgs) (TestResult, error) {
return TestResult{ID: 1}, nil return TestResult{ID: 1}, nil
}) })
defs := GetBuiltinsDeclarations() // Verify Declarations
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 {\n id: number\n data: number[]\n}")
} }
type TestAsyncArgs struct { type TestAsyncArgs struct {
@@ -125,12 +147,16 @@ type TestAsyncResult struct {
} }
func TestAsyncPromise(t *testing.T) { func TestAsyncPromise(t *testing.T) {
// Reset Registry
resetRegistry() resetRegistry()
RegisterAsyncBuiltin[TestAsyncArgs, *TestAsyncStatus]("async", func(ctx context.Context, args TestAsyncArgs) (*TestAsyncStatus, error) {
// Register Async Function
RegisterAsyncFunction[TestAsyncArgs, *TestAsyncStatus]("async", func(ctx context.Context, args TestAsyncArgs) (*TestAsyncStatus, error) {
return &TestAsyncStatus{Code: 200}, nil return &TestAsyncStatus{Code: 200}, nil
}) })
defs := GetBuiltinsDeclarations() // Verify Declarations
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,12 +176,16 @@ 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()
RegisterBuiltin[TestNestedPointerArgs, *TestNestedPointerResult]("pointerResult", func(ctx context.Context, args TestNestedPointerArgs) (*TestNestedPointerResult, error) {
// Register Function
RegisterFunction[TestNestedPointerArgs, *TestNestedPointerResult]("pointerResult", func(ctx context.Context, args TestNestedPointerArgs) (*TestNestedPointerResult, error) {
return &TestNestedPointerResult{Value: "test"}, nil return &TestNestedPointerResult{Value: "test"}, nil
}) })
defs := GetBuiltinsDeclarations() // Verify Declarations
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,12 +196,16 @@ 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()
RegisterBuiltin[TestUintArgs, uint]("uint", func(ctx context.Context, args TestUintArgs) (uint, error) {
// Register Function
RegisterFunction[TestUintArgs, uint]("uint", func(ctx context.Context, args TestUintArgs) (uint, error) {
return args.Value, nil return args.Value, nil
}) })
defs := GetBuiltinsDeclarations() // Verify Declarations
defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function uint(value: number): number;") assert.Contains(t, defs, "declare function uint(value: number): number;")
} }
@@ -182,12 +216,16 @@ 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()
RegisterBuiltin[TestFloatArgs, float32]("float", func(ctx context.Context, args TestFloatArgs) (float32, error) {
// Register Function
RegisterFunction[TestFloatArgs, float32]("float", func(ctx context.Context, args TestFloatArgs) (float32, error) {
return float32(args.Amount), nil return float32(args.Amount), nil
}) })
defs := GetBuiltinsDeclarations() // Verify Declarations
defs := GetFunctionDeclarations()
assert.Contains(t, defs, "declare function float(amount: number): number;") assert.Contains(t, defs, "declare function float(amount: number): number;")
} }
@@ -200,12 +238,16 @@ 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()
RegisterBuiltin[TestPointerInArgs, string]("nestedPointer", func(ctx context.Context, args TestPointerInArgs) (string, error) {
// Register Function
RegisterFunction[TestPointerInArgs, string]("nestedPointer", func(ctx context.Context, args TestPointerInArgs) (string, error) {
return "test", nil return "test", nil
}) })
defs := GetBuiltinsDeclarations() // Verify Declarations
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()
RegisterBuiltin[TestErrorOnlyArgs, struct{}]("errorOnly", func(ctx context.Context, args TestErrorOnlyArgs) (struct{}, error) {
// Register Function
RegisterFunction[TestErrorOnlyArgs, struct{}]("errorOnly", func(ctx context.Context, args TestErrorOnlyArgs) (struct{}, error) {
return struct{}{}, nil return struct{}{}, nil
}) })
defs := GetBuiltinsDeclarations() // Verify Declarations
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

@@ -0,0 +1,29 @@
package runtime
import (
"io"
"github.com/sirupsen/logrus"
)
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
}
}
func WithLogger(logger *logrus.Logger) RuntimeOption {
return func(r *Runtime) {
r.logger = logger.WithField("service", "runtime")
}
}

View File

@@ -3,33 +3,44 @@ package runtime
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"os" "os"
"strings"
"github.com/evanw/esbuild/pkg/api" "github.com/evanw/esbuild/pkg/api"
"modernc.org/quickjs" "github.com/fastschema/qjs"
"reichard.io/poiesis/internal/builtin" "github.com/sirupsen/logrus"
"reichard.io/poiesis/internal/functions"
_ "reichard.io/poiesis/internal/stdlib"
) )
type Runtime struct { type Runtime struct {
vm *quickjs.VM ctx *qjs.Context
ctx context.Context opts qjs.Option
funcs map[string]functions.Function
stdout io.Writer typeDecls map[string]string
stderr io.Writer logger *logrus.Entry
} }
func New(ctx context.Context) (*Runtime, error) { func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) {
// Create VM // Create Runtime
vm, err := quickjs.NewVM() logger := logrus.New()
r := &Runtime{opts: qjs.Option{Context: ctx}, logger: logger.WithField("service", "runtime")}
for _, opt := range opts {
opt(r)
}
// Create QuickJS Context
rt, err := qjs.New(r.opts)
if err != nil { if err != nil {
logger.WithError(err).Error("Failed to create QuickJS context")
return nil, err return nil, err
} }
vm.SetCanBlock(true) r.ctx = rt.Context()
// Create Runtime // Populate Globals
r := &Runtime{vm: vm, ctx: ctx, stdout: os.Stdout, stderr: os.Stderr}
if err := r.populateGlobals(); err != nil { if err := r.populateGlobals(); err != nil {
logger.WithError(err).Error("Failed to populate globals")
return nil, err return nil, err
} }
@@ -37,129 +48,95 @@ func New(ctx context.Context) (*Runtime, error) {
} }
func (r *Runtime) populateGlobals() error { func (r *Runtime) populateGlobals() error {
// Add Helpers // Initialize Maps
if err := r.vm.StdAddHelpers(); err != nil { r.funcs = make(map[string]functions.Function)
return err 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)
}
} }
// Add Log Hook // Register Functions with QuickJS
if err := r.vm.RegisterFunc("customLog", func(args ...any) { for name, fn := range r.funcs {
for i, arg := range args { if fn.IsAsync() {
if i > 0 { r.ctx.SetAsyncFunc(name, func(this *qjs.This) {
_, _ = fmt.Fprint(r.stdout, " ") qjsVal, err := callFunc(this, fn)
if err != nil {
r.logger.WithError(err).Errorf("Async function %s failed", name)
_ = this.Promise().Reject(this.Context().NewError(err))
return
} }
_, _ = fmt.Fprint(r.stdout, arg) _ = this.Promise().Resolve(qjsVal)
} })
_, _ = fmt.Fprintln(r.stdout) } else {
}, false); err != nil { r.ctx.SetFunc(name, func(this *qjs.This) (*qjs.Value, error) {
return err return callFunc(this, fn)
} })
if _, err := r.vm.Eval("console.log = customLog;", quickjs.EvalGlobal); err != nil {
return err
}
// Register Custom Functions
for name, builtin := range builtin.GetBuiltins() {
// Register Main Function
if err := r.vm.RegisterFunc(name, builtin.WrapFn(r.ctx), false); err != nil {
return err
}
// Wrap Exception - The QuickJS library does not allow us to throw exceptions, so we
// wrap the function with native JS to appropriately throw on error.
if _, err := r.vm.Eval(fmt.Sprintf(`
(function() {
const original = globalThis[%q];
globalThis[%q] = function(...args) {
const [result, error] = original.apply(this, args);
if (error) {
throw new Error(error);
}
return result;
};
})();
`, name, name), quickjs.EvalGlobal); err != nil {
return err
} }
} }
return nil return nil
} }
func (r *Runtime) RunFile(filePath string, stdout, stderr io.Writer) error { // addFunctionTypes adds types from a function to the runtime's type declarations.
r.stdout = stdout // Returns an error if there's a type conflict (same name, different definition).
r.stderr = stderr func (r *Runtime) addFunctionTypes(fn functions.Function) error {
for name, def := range fn.Types() {
content, err := r.transformFile(filePath) if existing, ok := r.typeDecls[name]; ok && existing != def {
if err != nil { return fmt.Errorf("type conflict: %s has conflicting definitions (existing: %s, new: %s)",
_, _ = fmt.Fprintf(stderr, "Error: %v\n", err) name, existing, def)
return err
} }
r.typeDecls[name] = def
if len(content.errors) > 0 {
_, _ = fmt.Fprintf(stderr, "Transpilation errors:\n")
for _, err := range content.errors {
_, _ = fmt.Fprintf(stderr, " %s\n", err.Text)
} }
return fmt.Errorf("transpilation failed")
}
_, err = r.vm.Eval(content.code, quickjs.EvalGlobal)
if err != nil {
_, _ = fmt.Fprintf(stderr, "Execution error: %v\n", err)
return err
}
return nil return nil
} }
func (r *Runtime) RunCode(tsCode string, stdout, stderr io.Writer) error { // GetTypeDeclarations returns all TypeScript type declarations for this runtime.
r.stdout = stdout // Includes both type definitions and function declarations.
r.stderr = stderr func (r *Runtime) GetTypeDeclarations() string {
var decls []string
content := r.transformCode(tsCode) // Add Type Definitions
for _, def := range r.typeDecls {
if len(content.errors) > 0 { decls = append(decls, def)
_, _ = fmt.Fprintf(stderr, "Transpilation errors:\n")
for _, err := range content.errors {
_, _ = fmt.Fprintf(stderr, " %s\n", err.Text)
}
return fmt.Errorf("transpilation failed")
} }
_, err := r.vm.Eval(content.code, quickjs.EvalGlobal) // Add Function Declarations
if err != nil { for _, fn := range r.funcs {
_, _ = fmt.Fprintf(stderr, "Execution error: %v\n", err) decls = append(decls, fn.Definition())
return err
} }
return nil return strings.Join(decls, "\n\n")
} }
type transformResult struct { func (r *Runtime) RunFile(filePath string) error {
code string
errors []api.Message
}
func (r *Runtime) transformFile(filePath string) (*transformResult, error) {
tsFileContent, err := os.ReadFile(filePath) tsFileContent, err := os.ReadFile(filePath)
if err != nil { if err != nil {
return nil, fmt.Errorf("error reading file: %w", err) return fmt.Errorf("error reading file: %w", err)
} }
return r.transformCode(string(tsFileContent)), nil return r.RunCode(string(tsFileContent))
} }
func (r *Runtime) transformCode(tsCode string) *transformResult { func (r *Runtime) RunCode(tsCode string) error {
// wrappedCode := `(async () => { transformedCode, err := r.transformCode(tsCode)
// try { if err != nil {
// ` + tsCode + ` return err
// } catch (err) { }
// console.error(err);
// }
// })()`
result, err := r.ctx.Eval("code.ts", qjs.Code(string(transformedCode)))
if result != nil {
result.Free()
}
return err
}
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,
@@ -168,8 +145,42 @@ func (r *Runtime) transformCode(tsCode string) *transformResult {
TreeShaking: api.TreeShakingFalse, TreeShaking: api.TreeShakingFalse,
}) })
return &transformResult{ if len(result.Errors) > 0 {
code: string(result.Code), var b strings.Builder
errors: result.Errors, for i, e := range result.Errors {
if i > 0 {
b.WriteString(", ")
} }
b.WriteString(e.Text)
}
return nil, fmt.Errorf("transpilation failed: %s", b.String())
}
return result.Code, nil
}
func callFunc(this *qjs.This, fn functions.Function) (*qjs.Value, error) {
qjsArgs := this.Args()
fnArgs := fn.Arguments()
var allArgs []any
for i := range min(len(fnArgs), len(qjsArgs)) {
rVal, err := qjs.JsArgToGo(qjsArgs[i], fnArgs[i])
if err != nil {
return nil, fmt.Errorf("argument conversion failed: %w", err)
}
allArgs = append(allArgs, rVal.Interface())
}
result, err := fn.Call(this.Context(), allArgs)
if err != nil {
return nil, err
}
val, err := qjs.ToJsValue(this.Context(), result)
if err != nil {
return nil, err
}
return val, nil
} }

View File

@@ -3,48 +3,161 @@ package runtime
import ( import (
"bytes" "bytes"
"context" "context"
"os"
"strings" "strings"
"testing" "testing"
_ "reichard.io/poiesis/internal/standard" "github.com/fastschema/qjs"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"reichard.io/poiesis/internal/functions"
) )
type TestArgs struct {
Field1 string `json:"field1"`
}
func (t TestArgs) Validate() error {
return nil
}
func TestExecuteTypeScript(t *testing.T) { func TestExecuteTypeScript(t *testing.T) {
// Create Buffers
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
rt, err := New(context.Background()) // Create Runtime
rt, err := New(context.Background(), WithStderr(&stderr), WithStdout(&stdout))
assert.NoError(t, err, "Expected no error") assert.NoError(t, err, "Expected no error")
err = rt.RunFile("../../test_data/test.ts", &stdout, &stderr) // Create TypeScript Code
tsCode := `interface Person {
name: string;
age: number;
email: string;
}
function greet(person: Person): string {
return "Hello, " + person.name + "! You are " + person.age + " years old.";
}
const user: Person = {
name: "Alice",
age: 30,
email: "alice@example.com",
};
console.log(greet(user));
console.log("Email: " + user.email);
function calculateSum(a: number, b: number): number {
return a + b;
}
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.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 TestFetchBuiltinIntegration(t *testing.T) { func TestAsyncFunctionResolution(t *testing.T) {
rt, err := New(context.Background()) // Register Async Function
assert.NoError(t, err, "Expected no error") functions.RegisterAsyncFunction("resolveTest", func(_ context.Context, args TestArgs) (string, error) {
return "test-result", nil
})
tsContent := ` // Create Runtime
const result = add({a: 5, b: 10}); r, err := New(context.Background())
console.log("Result:", result);
`
var stdout, stderr bytes.Buffer
err = rt.RunCode(tsContent, &stdout, &stderr)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, stdout.String(), "Result:")
// 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)
// 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

@@ -1,60 +0,0 @@
package standard
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"modernc.org/quickjs"
"reichard.io/poiesis/internal/builtin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFetchReturnsPromise(t *testing.T) {
vm, err := quickjs.NewVM()
require.NoError(t, err)
defer func() {
_ = vm.Close()
}()
vm.SetCanBlock(true)
builtin.RegisterBuiltins(context.Background(), vm)
result, err := vm.Eval(`fetch({input: "https://example.com"})`, quickjs.EvalGlobal)
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestFetchAsyncAwait(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"ok"}`))
}))
defer server.Close()
vm, err := quickjs.NewVM()
require.NoError(t, err)
defer func() {
_ = vm.Close()
}()
vm.SetCanBlock(true)
builtin.RegisterBuiltins(context.Background(), vm)
result, err := vm.Eval(`fetch({input: "`+server.URL+`"})`, quickjs.EvalGlobal)
require.NoError(t, err)
if obj, ok := result.(*quickjs.Object); ok {
var arr []any
if err := obj.Into(&arr); err == nil && len(arr) > 0 {
if response, ok := arr[0].(map[string]any); ok {
assert.True(t, response["ok"].(bool))
}
}
}
}

View File

@@ -1,4 +1,4 @@
package standard package stdlib
import ( import (
"context" "context"
@@ -8,9 +8,13 @@ import (
"net/http" "net/http"
"strings" "strings"
"reichard.io/poiesis/internal/builtin" "reichard.io/poiesis/internal/functions"
) )
func init() {
functions.RegisterAsyncFunction("fetch", Fetch)
}
type FetchArgs struct { type FetchArgs struct {
Input string `json:"input"` Input string `json:"input"`
Init *RequestInit `json:"init,omitempty"` Init *RequestInit `json:"init,omitempty"`
@@ -27,9 +31,6 @@ type RequestInit struct {
} }
func (o *RequestInit) Validate() error { func (o *RequestInit) Validate() error {
if o.Method == "" {
o.Method = "GET"
}
return nil return nil
} }
@@ -40,43 +41,33 @@ type Response struct {
Headers map[string]string `json:"headers"` Headers map[string]string `json:"headers"`
} }
type AddArgs struct { func Fetch(ctx context.Context, args FetchArgs) (Response, error) {
A int `json:"a"` // Set Default Method and Headers
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) {
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 != "" {
method = args.Init.Method method = args.Init.Method
}
if args.Init.Headers != nil { if args.Init.Headers != nil {
maps.Copy(headers, args.Init.Headers) maps.Copy(headers, args.Init.Headers)
} }
} }
req, err := http.NewRequest(method, args.Input, nil) // Create Request
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)
@@ -85,11 +76,13 @@ func Fetch(_ 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 {
@@ -99,6 +92,7 @@ func Fetch(_ 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,
@@ -106,17 +100,3 @@ func Fetch(_ context.Context, args FetchArgs) (Response, error) {
Headers: resultHeaders, Headers: resultHeaders,
}, nil }, nil
} }
func Add(_ context.Context, args AddArgs) (int, error) {
return args.A + args.B, nil
}
func Greet(_ context.Context, args GreetArgs) (string, error) {
return fmt.Sprintf("Hello, %s!", args.Name), nil
}
func init() {
builtin.RegisterAsyncBuiltin("fetch", Fetch)
builtin.RegisterBuiltin("add", Add)
builtin.RegisterBuiltin("greet", Greet)
}

View File

@@ -1,4 +1,4 @@
package standard package stdlib
import ( import (
"context" "context"
@@ -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,12 +37,22 @@ func TestFetch(t *testing.T) {
} }
func TestFetchHTTPBin(t *testing.T) { func TestFetchHTTPBin(t *testing.T) {
t.Skip("httpbin.org test is flaky") // Create Context
ctx := context.Background() ctx := context.Background()
result, err := Fetch(ctx, FetchArgs{Input: "https://httpbin.org/get"}) // 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)
_, _ = w.Write([]byte(`{"args":{}}`))
}))
defer server.Close()
// Execute Fetch
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"`)
@@ -45,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)
@@ -70,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",
} }
@@ -77,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)
@@ -91,30 +122,9 @@ 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)
assert.True(t, result.OK) assert.True(t, result.OK)
} }
func TestAdd(t *testing.T) {
ctx := context.Background()
result, err := Add(ctx, AddArgs{A: 5, B: 10})
require.NoError(t, err)
assert.Equal(t, 15, result)
result, err = Add(ctx, AddArgs{A: -3, B: 7})
require.NoError(t, err)
assert.Equal(t, 4, result)
}
func TestGreet(t *testing.T) {
ctx := context.Background()
result, err := Greet(ctx, GreetArgs{Name: "World"})
require.NoError(t, err)
assert.Equal(t, "Hello, World!", result)
result, err = Greet(ctx, GreetArgs{Name: "Alice"})
require.NoError(t, err)
assert.Equal(t, "Hello, Alice!", result)
}

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 {\n%s\n}", name, strings.Join(fields, "\n"))
_ = 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

View File

@@ -1,14 +0,0 @@
try {
console.log(1);
const response = fetch("https://httpbin.org/get");
console.log(2);
console.log(response);
console.log("OK:", response.ok);
console.log("Status:", response.status);
console.log("Body:", response.body);
console.log("Content-Type:", response.headers["content-type"]);
} catch (e) {
console.log(e.message);
console.log("exception");
}

View File

@@ -1,20 +0,0 @@
var done = false;
async function main() {
try {
console.log(11);
const response = fetch("https://httpbin.org/get");
console.log(response);
console.log("OK:", response.ok);
console.log("Status:", response.status);
console.log("Body:", response.body);
console.log("Content-Type:", response.headers["content-type"]);
} catch (e) {
console.log(e);
}
done = true;
}
console.log(1);
main();
console.log(2);

View File

@@ -1,6 +0,0 @@
const response = fetch("https://httpbin.org/get");
console.log("OK:", response.ok);
console.log("Status:", response.status);
console.log("Body:", response.text());
console.log("Content-Type:", response.headers.get("content-type"));

View File

@@ -1,11 +0,0 @@
async function logPromiseResult() {
try {
const response = await fetch({input: "https://httpbin.org/get"});
console.log("Fetch successful, OK:", response.ok);
console.log("Status:", response.status);
} catch (error) {
console.log("Fetch failed:", error);
}
}
logPromiseResult();

View File

@@ -1,9 +0,0 @@
const sum = add(5, 10);
console.log("5 + 10 =", sum);
const greeting = greet("World");
console.log(greeting);
const response = fetch("https://httpbin.org/get");
console.log("Fetch OK:", response.ok);
console.log("Fetch Status:", response.status);

View File

@@ -1,24 +0,0 @@
interface Person {
name: string;
age: number;
email: string;
}
function greet(person: Person): string {
return `Hello, ${person.name}! You are ${person.age} years old.`;
}
const user: Person = {
name: "Alice",
age: 30,
email: "alice@example.com",
};
console.log(greet(user));
console.log(`Email: ${user.email}`);
function calculateSum(a: number, b: number): number {
return a + b;
}
console.log(`Sum of 5 and 10 is: ${calculateSum(5, 10)}`);