This commit is contained in:
2026-01-27 10:23:07 -05:00
parent a275364cd1
commit 60fb12e52c
10 changed files with 302 additions and 238 deletions

View File

@@ -1,28 +1,46 @@
# Poiesis
## Module Name
`reichard.io/poiesis`
## Overview
Go tool that transpiles TypeScript to JavaScript using esbuild API and executes it with goja.
Go tool that transpiles TypeScript to JavaScript using esbuild API and executes it with goja. Features a flexible builtin system for exposing Go functions to TypeScript.
## Build & Test
```bash
go build
go test
go build ./cmd/poiesis
go test ./...
golangci-lint run
```
## Project Structure
- `main.go` - Entry point with `executeTypeScript()` function
- `main_test.go` - Test suite using testify assertions
- `test_data/` - Test TypeScript files
- `go.mod` - Dependencies
```
reichard.io/poiesis/
├── cmd/poiesis/ # CLI entry point
│ └── main.go
├── internal/
│ ├── builtin/ # Builtin function framework
│ │ ├── builtin.go
│ │ └── builtin_test.go
│ └── runtime/ # TypeScript transpilation + execution
│ ├── runtime.go
│ └── runtime_test.go
└── test_data/ # Test TypeScript files
```
## Key Packages
- `reichard.io/poiesis/internal/runtime` - Runtime management, TypeScript transpilation, execution
- `reichard.io/poiesis/internal/builtin` - Builtin registration and type conversion
## Testing Patterns
- **Test framework**: Go's built-in `testing` package
- **Assertions**: `github.com/stretchr/testify/assert`
- **Assertions**: `github.com/stretchr/testify/assert` and `require`
- **Linting**: `golangci-lint run` - must pass before committing
- **Test organization**: Test files use `_test.go` suffix, test functions prefixed with `Test`
@@ -32,12 +50,9 @@ golangci-lint run
- `github.com/dop251/goja` - JavaScript execution
- `github.com/stretchr/testify/assert` - Test assertions
## Key Functions
- `executeTypeScript(filePath string, stdout, stderr io.Writer) error` - Main transpilation and execution logic
## Code Conventions
- Handle all return values from external functions (enforced by golangci-lint)
- Use `os` package instead of deprecated `io/ioutil`
- Error logging uses `_, _ = fmt.Fprintf(stderr, ...)` pattern
- Package structure follows standard Go project layout with internal packages

89
README.md Normal file
View File

@@ -0,0 +1,89 @@
# Poiesis
A Go tool that transpiles TypeScript to JavaScript using esbuild and executes it with goja, with an extensible builtin system.
## Project Structure
```
reichard.io/poiesis/
├── cmd/
│ └── poiesis/ # CLI application entry point
│ └── main.go
├── internal/
│ ├── builtin/ # Builtin function framework
│ │ ├── builtin.go
│ │ └── builtin_test.go
│ └── runtime/ # TypeScript transpilation and execution
│ ├── runtime.go
│ └── runtime_test.go
└── examples/ # Example TypeScript files
```
## Installation & Build
```bash
go build ./cmd/poiesis
```
## Testing
```bash
go test ./...
golangci-lint run
```
## Usage
```bash
poiesis <typescript-file>
```
## Builtin System
The builtin system allows you to easily expose Go functions to TypeScript/JavaScript.
### Adding a Builtin
Just write a Go function and register it:
```go
// Your function
func add(a, b int) int {
return a + b
}
// Register it
func init() {
builtin.RegisterBuiltin("add", add)
}
```
That's it! The framework automatically:
- Converts TypeScript values to Go types
- Handles errors (panics as JS errors)
- Generates TypeScript definitions
- Manages the goja integration
### Example
```typescript
// TypeScript code
console.log("5 + 10 =", add(5, 10));
const response = fetch("https://httpbin.org/get");
console.log("OK:", response.ok);
console.log("Status:", response.status);
console.log("Body:", response.text());
```
### Built-in Functions
- `fetch(url, options?)` - HTTP requests
- `add(a, b)` - Simple arithmetic example
- `greet(name)` - String manipulation example
## Dependencies
- `github.com/evanw/esbuild/pkg/api` - TypeScript transpilation
- `github.com/dop251/goja` - JavaScript execution
- `github.com/stretchr/testify/assert` - Test assertions

4
go.mod
View File

@@ -1,10 +1,11 @@
module poiesis
module reichard.io/poiesis
go 1.25.5
require (
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3
github.com/evanw/esbuild v0.27.2
github.com/stretchr/testify v1.11.1
)
require (
@@ -13,7 +14,6 @@ require (
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/text v0.3.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

1
go.sum
View File

@@ -21,6 +21,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9w
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
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/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@@ -1,4 +1,4 @@
package main
package builtin
import (
"fmt"

View File

@@ -1,9 +1,8 @@
package main
package builtin
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -53,36 +52,3 @@ func TestFetchWithInvalidURL(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to fetch")
}
func TestFetchBuiltinIntegration(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("X-Custom", "custom-value")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("Hello, World!"))
}))
defer server.Close()
var stdout, stderr strings.Builder
tsContent := `
const response = fetch("${URL}");
console.log("OK:", response.ok);
console.log("Status:", response.status);
console.log("Body:", response.text());
console.log("Content-Type:", response.headers.get("content-type") || "undefined");
console.log("Content-Type (case sensitive):", response.headers.get("Content-Type") || "undefined");
console.log("X-Custom:", response.headers.get("x-custom") || "undefined");
console.log("X-Custom (case sensitive):", response.headers.get("X-Custom") || "undefined");
`
tsContent = strings.Replace(tsContent, "${URL}", server.URL, 1)
err := executeTypeScriptContent(tsContent, &stdout, &stderr)
require.NoError(t, err)
require.Empty(t, stderr.String(), "Expected no error output")
output := stdout.String()
assert.Contains(t, output, "OK: true")
assert.Contains(t, output, "Status: 200")
assert.Contains(t, output, "Body: Hello, World!")
}

137
internal/runtime/runtime.go Normal file
View File

@@ -0,0 +1,137 @@
package runtime
import (
"fmt"
"io"
"os"
"github.com/dop251/goja"
"github.com/evanw/esbuild/pkg/api"
"reichard.io/poiesis/internal/builtin"
)
type Runtime struct {
vm *goja.Runtime
stdout io.Writer
stderr io.Writer
}
func New() *Runtime {
vm := goja.New()
r := &Runtime{vm: vm, stdout: os.Stdout, stderr: os.Stderr}
r.setupConsole()
builtin.RegisterBuiltins(vm)
return r
}
func (r *Runtime) setupConsole() {
console := r.vm.NewObject()
_ = r.vm.Set("console", console)
_ = console.Set("log", func(call goja.FunctionCall) goja.Value {
args := call.Arguments
for i, arg := range args {
if i > 0 {
_, _ = fmt.Fprint(r.stdout, " ")
}
_, _ = fmt.Fprint(r.stdout, arg.String())
}
_, _ = fmt.Fprintln(r.stdout)
return goja.Undefined()
})
}
func (r *Runtime) SetOutput(stdout, stderr io.Writer) {
r.stdout = stdout
r.stderr = stderr
consoleObj := r.vm.Get("console")
if consoleObj != nil {
console := consoleObj.ToObject(r.vm)
if console != nil {
r.setupConsole()
}
}
}
func (r *Runtime) RunFile(filePath string, stdout, stderr io.Writer) error {
r.stdout = stdout
r.stderr = stderr
r.setupConsole()
content, err := r.transformFile(filePath)
if err != nil {
_, _ = fmt.Fprintf(stderr, "Error: %v\n", err)
return err
}
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.RunString(content.code)
if err != nil {
_, _ = fmt.Fprintf(stderr, "Execution error: %v\n", err)
return err
}
return nil
}
func (r *Runtime) RunCode(tsCode string, stdout, stderr io.Writer) error {
r.stdout = stdout
r.stderr = stderr
r.setupConsole()
content := r.transformCode(tsCode)
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.RunString(content.code)
if err != nil {
_, _ = fmt.Fprintf(stderr, "Execution error: %v\n", err)
return err
}
return nil
}
type transformResult struct {
code string
errors []api.Message
}
func (r *Runtime) transformFile(filePath string) (*transformResult, error) {
tsFileContent, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("error reading file: %w", err)
}
return r.transformCode(string(tsFileContent)), nil
}
func (r *Runtime) transformCode(tsCode string) *transformResult {
result := api.Transform(tsCode, api.TransformOptions{
Loader: api.LoaderTS,
Target: api.ES2020,
Format: api.FormatIIFE,
Sourcemap: api.SourceMapNone,
TreeShaking: api.TreeShakingFalse,
})
return &transformResult{
code: string(result.Code),
errors: result.Errors,
}
}

View File

@@ -0,0 +1,44 @@
package runtime
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExecuteTypeScript(t *testing.T) {
var stdout, stderr bytes.Buffer
rt := New()
err := rt.RunFile("../../test_data/test.ts", &stdout, &stderr)
assert.NoError(t, err, "Expected no error")
assert.Empty(t, stderr.String(), "Expected no error 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")
lines := strings.Split(strings.TrimSpace(output), "\n")
assert.GreaterOrEqual(t, len(lines), 3, "Should have at least 3 output lines")
}
func TestFetchBuiltinIntegration(t *testing.T) {
rt := New()
tsContent := `
const result = add(5, 10);
console.log("Result:", result);
`
var stdout, stderr bytes.Buffer
err := rt.RunCode(tsContent, &stdout, &stderr)
require.NoError(t, err)
assert.Contains(t, stdout.String(), "Result: 15")
}

119
main.go
View File

@@ -1,119 +0,0 @@
package main
import (
"fmt"
"io"
"os"
"github.com/dop251/goja"
"github.com/evanw/esbuild/pkg/api"
)
func executeTypeScript(filePath string, stdout, stderr io.Writer) error {
tsContent, err := os.ReadFile(filePath)
if err != nil {
_, _ = fmt.Fprintf(stderr, "Error reading file: %v\n", err)
return err
}
result := api.Transform(string(tsContent), api.TransformOptions{
Loader: api.LoaderTS,
Target: api.ES2020,
Format: api.FormatIIFE,
Sourcemap: api.SourceMapNone,
TreeShaking: api.TreeShakingFalse,
})
if len(result.Errors) > 0 {
_, _ = fmt.Fprintf(stderr, "Transpilation errors:\n")
for _, err := range result.Errors {
_, _ = fmt.Fprintf(stderr, " %s\n", err.Text)
}
return fmt.Errorf("transpilation failed")
}
vm := goja.New()
RegisterBuiltins(vm)
console := vm.NewObject()
_ = vm.Set("console", console)
_ = console.Set("log", func(call goja.FunctionCall) goja.Value {
args := call.Arguments
for i, arg := range args {
if i > 0 {
_, _ = fmt.Fprint(stdout, " ")
}
_, _ = fmt.Fprint(stdout, arg.String())
}
_, _ = fmt.Fprintln(stdout)
return goja.Undefined()
})
_, err = vm.RunString(string(result.Code))
if err != nil {
_, _ = fmt.Fprintf(stderr, "Execution error: %v\n", err)
return err
}
return nil
}
func executeTypeScriptContent(tsContent string, stdout, stderr io.Writer) error {
result := api.Transform(tsContent, api.TransformOptions{
Loader: api.LoaderTS,
Target: api.ES2020,
Format: api.FormatIIFE,
Sourcemap: api.SourceMapNone,
TreeShaking: api.TreeShakingFalse,
})
if len(result.Errors) > 0 {
_, _ = fmt.Fprintf(stderr, "Transpilation errors:\n")
for _, err := range result.Errors {
_, _ = fmt.Fprintf(stderr, " %s\n", err.Text)
}
return fmt.Errorf("transpilation failed")
}
vm := goja.New()
RegisterBuiltins(vm)
console := vm.NewObject()
_ = vm.Set("console", console)
_ = console.Set("log", func(call goja.FunctionCall) goja.Value {
args := call.Arguments
for i, arg := range args {
if i > 0 {
_, _ = fmt.Fprint(stdout, " ")
}
_, _ = fmt.Fprint(stdout, arg.String())
}
_, _ = fmt.Fprintln(stdout)
return goja.Undefined()
})
_, err := vm.RunString(string(result.Code))
if err != nil {
_, _ = fmt.Fprintf(stderr, "Execution error: %v\n", err)
return err
}
return nil
}
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: program <typescript-file>")
os.Exit(1)
}
filePath := os.Args[1]
if err := executeTypeScript(filePath, os.Stdout, os.Stderr); err != nil {
os.Exit(1)
}
}

View File

@@ -1,69 +0,0 @@
package main
import (
"bytes"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExecuteTypeScript(t *testing.T) {
var stdout, stderr bytes.Buffer
err := executeTypeScript("test_data/test.ts", &stdout, &stderr)
assert.NoError(t, err, "Expected no error")
assert.Empty(t, stderr.String(), "Expected no error 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")
lines := strings.Split(strings.TrimSpace(output), "\n")
assert.GreaterOrEqual(t, len(lines), 3, "Should have at least 3 output lines")
}
func TestExecuteTypeScriptWithNonExistentFile(t *testing.T) {
var stdout, stderr bytes.Buffer
err := executeTypeScript("non_existent_file.ts", &stdout, &stderr)
assert.Error(t, err, "Expected error for non-existent file")
assert.Contains(t, stderr.String(), "Error reading file", "Should show read error")
}
func TestExecuteTypeScriptWithSyntaxError(t *testing.T) {
var stdout, stderr bytes.Buffer
tsContent := `
interface Person {
name: string;
age: number;
}
const user: Person = {
name: "Bob",
age: 25,
};
console.log(user.name)
console.log(user.age +);
`
err := os.WriteFile("test_data/invalid.ts", []byte(tsContent), 0644)
if err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
defer func() {
_ = os.Remove("test_data/invalid.ts")
}()
err = executeTypeScript("test_data/invalid.ts", &stdout, &stderr)
assert.Error(t, err, "Expected error for invalid TypeScript")
}