commit c2f08e91408aff7e44b9f3ff30390e5cf798c523 Author: Evan Reichard Date: Tue Jan 27 09:55:09 2026 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f74c6bd --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +poiesis diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0224a9c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,43 @@ +# Poiesis + +## Overview + +Go tool that transpiles TypeScript to JavaScript using esbuild API and executes it with goja. + +## Build & Test + +```bash +go build +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 + +## Testing Patterns + +- **Test framework**: Go's built-in `testing` package +- **Assertions**: `github.com/stretchr/testify/assert` +- **Linting**: `golangci-lint run` - must pass before committing +- **Test organization**: Test files use `_test.go` suffix, test functions prefixed with `Test` + +## Dependencies + +- `github.com/evanw/esbuild/pkg/api` - TypeScript transpilation +- `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 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..beb68bf --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1769318308, + "narHash": "sha256-Mjx6p96Pkefks3+aA+72lu1xVehb6mv2yTUUqmSet6Q=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1cd347bf3355fce6c64ab37d3967b4a2cb4b878c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..aa471fb --- /dev/null +++ b/flake.nix @@ -0,0 +1,35 @@ +{ + description = "Development Environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { self + , nixpkgs + , flake-utils + , + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = ( + import nixpkgs { + system = system; + } + ); + in + { + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + # Backend + go + gopls + golangci-lint + ]; + }; + } + ); +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6614c4f --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module poiesis + +go 1.25.5 + +require ( + github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 + github.com/evanw/esbuild v0.27.2 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect + 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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..45ed008 --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 h1:bVp3yUzvSAJzu9GqID+Z96P+eu5TKnIMJSV4QaZMauM= +github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/evanw/esbuild v0.27.2 h1:3xBEws9y/JosfewXMM2qIyHAi+xRo8hVx475hVkJfNg= +github.com/evanw/esbuild v0.27.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +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/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= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e8d3b41 --- /dev/null +++ b/main.go @@ -0,0 +1,72 @@ +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() + + 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 ") + os.Exit(1) + } + + filePath := os.Args[1] + + if err := executeTypeScript(filePath, os.Stdout, os.Stderr); err != nil { + os.Exit(1) + } +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..c5e8555 --- /dev/null +++ b/main_test.go @@ -0,0 +1,69 @@ +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") +} diff --git a/test_data/test.ts b/test_data/test.ts new file mode 100644 index 0000000..f467c10 --- /dev/null +++ b/test_data/test.ts @@ -0,0 +1,24 @@ +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)}`);