Files
poiesis/AGENTS.md
Evan Reichard f308970531 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 21:32:00 -05:00

195 lines
6.1 KiB
Markdown

# 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<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
```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...
```