- 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
203 lines
6.4 KiB
Markdown
203 lines
6.4 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
|
|
- **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
|
|
|
|
- `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...
|
|
```
|