initial commit
This commit is contained in:
19
internal/runtime/options.go
Normal file
19
internal/runtime/options.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package runtime
|
||||
|
||||
import "io"
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
180
internal/runtime/runtime.go
Normal file
180
internal/runtime/runtime.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/evanw/esbuild/pkg/api"
|
||||
"github.com/fastschema/qjs"
|
||||
|
||||
"reichard.io/poiesis/internal/functions"
|
||||
_ "reichard.io/poiesis/internal/stdlib"
|
||||
)
|
||||
|
||||
type Runtime struct {
|
||||
ctx *qjs.Context
|
||||
opts qjs.Option
|
||||
funcs map[string]functions.Function
|
||||
typeDecls map[string]string
|
||||
}
|
||||
|
||||
func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) {
|
||||
// Create Runtime
|
||||
r := &Runtime{opts: qjs.Option{Context: ctx}}
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
|
||||
// Create QuickJS Context
|
||||
rt, err := qjs.New(r.opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.ctx = rt.Context()
|
||||
|
||||
// Populate Globals
|
||||
if err := r.populateGlobals(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *Runtime) populateGlobals() error {
|
||||
// Initialize Maps
|
||||
r.funcs = make(map[string]functions.Function)
|
||||
r.typeDecls = make(map[string]string)
|
||||
|
||||
// Load Requested Functions
|
||||
allFuncs := functions.GetRegisteredFunctions()
|
||||
for name, fn := range allFuncs {
|
||||
r.funcs[name] = fn
|
||||
if err := r.addFunctionTypes(fn); err != nil {
|
||||
return fmt.Errorf("failed to add types for function %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Register Functions with QuickJS
|
||||
for name, fn := range r.funcs {
|
||||
if fn.IsAsync() {
|
||||
r.ctx.SetAsyncFunc(name, func(this *qjs.This) {
|
||||
qjsVal, err := callFunc(this, fn)
|
||||
if err != nil {
|
||||
_ = this.Promise().Reject(this.Context().NewError(err))
|
||||
return
|
||||
}
|
||||
_ = this.Promise().Resolve(qjsVal)
|
||||
})
|
||||
} else {
|
||||
r.ctx.SetFunc(name, func(this *qjs.This) (*qjs.Value, error) {
|
||||
return callFunc(this, fn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addFunctionTypes adds types from a function to the runtime's type declarations.
|
||||
// Returns an error if there's a type conflict (same name, different definition).
|
||||
func (r *Runtime) addFunctionTypes(fn functions.Function) error {
|
||||
for name, def := range fn.Types() {
|
||||
if existing, ok := r.typeDecls[name]; ok && existing != def {
|
||||
return fmt.Errorf("type conflict: %s has conflicting definitions (existing: %s, new: %s)",
|
||||
name, existing, def)
|
||||
}
|
||||
r.typeDecls[name] = def
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTypeDeclarations returns all TypeScript type declarations for this runtime.
|
||||
// Includes both type definitions and function declarations.
|
||||
func (r *Runtime) GetTypeDeclarations() string {
|
||||
var decls []string
|
||||
|
||||
// Add Type Definitions
|
||||
for _, def := range r.typeDecls {
|
||||
decls = append(decls, def)
|
||||
}
|
||||
|
||||
// Add Function Declarations
|
||||
for _, fn := range r.funcs {
|
||||
decls = append(decls, fn.Definition())
|
||||
}
|
||||
|
||||
return strings.Join(decls, "\n\n")
|
||||
}
|
||||
|
||||
func (r *Runtime) RunFile(filePath string) error {
|
||||
tsFileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading file: %w", err)
|
||||
}
|
||||
|
||||
return r.RunCode(string(tsFileContent))
|
||||
}
|
||||
|
||||
func (r *Runtime) RunCode(tsCode string) error {
|
||||
transformedCode, err := r.transformCode(tsCode)
|
||||
if err != nil {
|
||||
return 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{
|
||||
Loader: api.LoaderTS,
|
||||
Target: api.ES2022,
|
||||
Format: api.FormatIIFE,
|
||||
Sourcemap: api.SourceMapNone,
|
||||
TreeShaking: api.TreeShakingFalse,
|
||||
})
|
||||
|
||||
if len(result.Errors) > 0 {
|
||||
var b strings.Builder
|
||||
for i, e := range result.Errors {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.WriteString(e.Text)
|
||||
}
|
||||
return nil, fmt.Errorf("transpilation failed: %s", 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
|
||||
}
|
||||
163
internal/runtime/runtime_test.go
Normal file
163
internal/runtime/runtime_test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fastschema/qjs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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) {
|
||||
// Create Buffers
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
// Create Runtime
|
||||
rt, err := New(context.Background(), WithStderr(&stderr), WithStdout(&stdout))
|
||||
assert.NoError(t, err, "Expected no error")
|
||||
|
||||
// Create TypeScript Code
|
||||
tsCode := `interface Person {
|
||||
name: string;
|
||||
age: number;
|
||||
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.Empty(t, stderr.String(), "Expected no error output")
|
||||
|
||||
// Verify Output
|
||||
output := stdout.String()
|
||||
assert.Contains(t, output, "Hello, Alice!", "Should greet Alice")
|
||||
assert.Contains(t, output, "You are 30 years old", "Should show age")
|
||||
assert.Contains(t, output, "Email: alice@example.com", "Should show email")
|
||||
assert.Contains(t, output, "Sum of 5 and 10 is: 15", "Should calculate sum correctly")
|
||||
|
||||
// Verify Line Count
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
assert.GreaterOrEqual(t, len(lines), 3, "Should have at least 3 output lines")
|
||||
}
|
||||
|
||||
func TestAsyncFunctionResolution(t *testing.T) {
|
||||
// Register Async Function
|
||||
functions.RegisterAsyncFunction("resolveTest", func(_ context.Context, args TestArgs) (string, error) {
|
||||
return "test-result", nil
|
||||
})
|
||||
|
||||
// Create Runtime
|
||||
r, err := New(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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")
|
||||
}
|
||||
Reference in New Issue
Block a user