From c2f08e91408aff7e44b9f3ff30390e5cf798c523 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Tue, 27 Jan 2026 09:55:09 -0500 Subject: [PATCH] initial commit --- .gitignore | 1 + AGENTS.md | 43 ++++++++++++++++++++++++++++ flake.lock | 61 +++++++++++++++++++++++++++++++++++++++ flake.nix | 35 +++++++++++++++++++++++ go.mod | 20 +++++++++++++ go.sum | 28 ++++++++++++++++++ main.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++ main_test.go | 69 +++++++++++++++++++++++++++++++++++++++++++++ test_data/test.ts | 24 ++++++++++++++++ 9 files changed, 353 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 main_test.go create mode 100644 test_data/test.ts 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)}`);