# Poiesis ## Module Name `reichard.io/poiesis` ## Overview 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 ```bash go build ./cmd/poiesis go test ./... golangci-lint run ``` ## Project Structure ``` reichard.io/poiesis/ ├── cmd/poiesis/ # CLI entry point │ └── main.go ├── internal/ │ ├── runtime/ # Runtime management, transpilation, execution │ │ ├── runtime.go │ │ ├── runtime_test.go │ │ └── options.go │ ├── functions/ # Function registration framework │ │ ├── registry.go │ │ ├── types.go │ │ ├── typescript_test.go │ │ └── functions_test.go │ ├── tsconvert/ # Go-to-TypeScript type conversion utilities │ │ ├── convert.go │ │ ├── types.go │ │ └── convert_test.go │ └── stdlib/ # Standard library implementations │ ├── fetch.go │ └── fetch_test.go ``` ## Key Packages - `reichard.io/poiesis/internal/runtime` - Runtime management, TypeScript transpilation, JavaScript execution, per-runtime type management - `reichard.io/poiesis/internal/functions` - Generic function registration framework (sync/async wrappers, automatic JS/Go conversion via JSON) - `reichard.io/poiesis/internal/tsconvert` - Go-to-TypeScript type conversion utilities and type declaration generation - `reichard.io/poiesis/internal/stdlib` - Standard library implementations (fetch) ## Function System ### Registration Two types of functions: - **Sync**: `RegisterFunction[T, R](name, fn)` - executes synchronously, returns value - **Async**: `RegisterAsyncFunction[T, R](name, fn)` - runs in goroutine, returns Promise ### Requirements - Args must be a struct implementing `Args` interface with `Validate() error` method - Use JSON tags for TypeScript type definitions - Async functions automatically generate `Promise` 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 ```go 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 sync function functions.RegisterFunction[AddArgs, int]("add", Add) // Register async function functions.RegisterAsyncFunction[FetchArgs, *FetchResult]("fetch", Fetch) ``` ## Testing Patterns - **Test framework**: Go's built-in `testing` package - **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` - **TypeScript test files**: Tests that require TypeScript files should create them inline using `os.CreateTemp()` instead of relying on external test files ## Dependencies - `github.com/evanw/esbuild/pkg/api` - TypeScript transpilation - `github.com/fastschema/qjs` - JavaScript execution (CGO-free QuickJS runtime) - `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 - 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 ### 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... ```