git.iko.soy/rowtype-yoga/purescript-yoga-om-node
on main help sign in
Initial: yoga-om-node
commit5bd82c38af81802577a7e2a08307b466cbe22059 authorMark <mark.eibes@gmail.com> dateWed 4 Feb 2026, 22:00 (8 months ago)
8 files changed+396
.github/workflows/ci.yml+9 .gitignore+3 README.md+107 package.json+11 spago.yaml+31 src/Test/Main.purs+12 src/Yoga/Om/Node.purs+167 src/Yoga/Om/Node.test.purs+56
.github/workflows/ci.ymlnew file+9
@@ -0,0 +1,9 @@
1+name: CI
2+on: [push, pull_request]
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - uses: actions/checkout@v4
8+ - uses: rowtype-yoga/purescript-yoga-actions/.github/actions/setup@main
9+ - run: spago build
.gitignorenew file+3
@@ -0,0 +1,3 @@
1+node_modules/
2+.spago/
3+output*/
README.mdnew file+107
@@ -0,0 +1,107 @@
1+# yoga-om-node
2+
3+Node.js-specific extensions for yoga-om, providing convenient wrappers for file system operations, environment variables, and more.
4+
5+## Installation
6+
7+```bash
8+spago install yoga-om-core yoga-om-node
9+```
10+
11+## What's Included
12+
13+- **File operations**: `readFile`, `writeFile`, `readTextFile`, `writeTextFile`, `appendFile`, etc.
14+- **File system**: `exists`, `mkdir`, `rmdir`, `unlink`
15+- **Environment variables**: `getEnv`, `lookupEnv`, `setEnv`
16+- **Process**: `cwd` for current working directory
17+
18+All operations are wrapped in `Om` with proper error types.
19+
20+## Usage
21+
22+### Reading Environment Variables
23+
24+```purescript
25+import Yoga.Om.Node as Node
26+
27+loadConfig :: Om {} (envNotFound :: { variable :: String }) Config
28+loadConfig = do
29+ apiKey <- Node.getEnv "API_KEY"
30+ dbUrl <- Node.getEnv "DATABASE_URL"
31+ pure { apiKey, dbUrl }
32+```
33+
34+### File Operations
35+
36+```purescript
37+import Yoga.Om.Node as Node
38+
39+processFile :: Om {} (fileReadError :: _, fileWriteError :: _) Unit
40+processFile = do
41+ content <- Node.readTextFile "input.txt"
42+ let processed = String.toUpper content
43+ Node.writeTextFile "output.txt" processed
44+```
45+
46+### Checking File Existence
47+
48+```purescript
49+import Yoga.Om.Node as Node
50+
51+safeRead :: FilePath -> Om {} _ (Maybe String)
52+safeRead path = do
53+ fileExists <- Node.exists path
54+ if fileExists
55+ then Just <$> Node.readTextFile path
56+ else pure Nothing
57+```
58+
59+## Error Types
60+
61+The package defines error type aliases for common failures:
62+
63+- `FileError` - Includes `fileNotFound`, `fileReadError`, `fileWriteError`, `fileSystemError`
64+- `EnvError` - Includes `envNotFound`
65+
66+These can be extended with your own error types:
67+
68+```purescript
69+type MyErrors r = Node.FileError + Node.EnvError +
70+ ( customError :: String | r )
71+```
72+
73+## Testing
74+
75+Tests are colocated with the implementation in the `test/` directory:
76+
77+```bash
78+# Run tests
79+cd packages/yoga-om-node && spago test
80+
81+# Or from workspace root
82+bun test:node
83+```
84+
85+See [test/Test/Node.purs](./test/Test/Node.purs) for test examples.
86+
87+## Package Structure
88+
89+```
90+yoga-om-node/
91+├── src/
92+│ └── Yoga/
93+│ └── Om/
94+│ └── Node.purs # Node.js extensions
95+└── test/ # Tests colocated here!
96+ └── Test/
97+ ├── Main.purs # Test runner
98+ └── Node.purs # Node-specific tests
99+```
100+
101+## See Also
102+
103+- **[yoga-om-core](../yoga-om-core)** - Core Om functionality (required)
104+- **[yoga-om-rom](../yoga-om-rom)** - Reactive Om: Bolson FRP integration
105+- **[yoga-om-strom](../yoga-om-strom)** - Stream Om: Pull-based streaming
106+
107+For more examples, see the [tests](./test/) and [main README](../../README.md).
package.jsonnew file+11
@@ -0,0 +1,11 @@
1+{
2+ "name": "purescript-yoga-om-node",
3+ "version": "1.0.0",
4+ "description": "Node.js extensions for yoga-om",
5+ "license": "MIT",
6+ "repository": {
7+ "type": "git",
8+ "url": "https://github.com/rowtype-yoga/purescript-yoga-om.git",
9+ "directory": "packages/yoga-om-node"
10+ }
11+}
spago.yamlnew file+31
@@ -0,0 +1,31 @@
1+workspace:
2+ package_set:
3+ registry: 51.1.0
4+
5+package:
6+ name: yoga-om-node
7+ publish:
8+ version: 1.0.0
9+ license: MIT
10+ exclude:
11+ - "src/**/*.test.purs"
12+ - "src/Test/**"
13+ location:
14+ githubOwner: rowtype-yoga
15+ githubRepo: purescript-yoga-om-node
16+ dependencies:
17+ - aff: ">=7.1.0 <8.0.0"
18+ - effect: ">=4.0.0 <5.0.0"
19+ - either: ">=6.1.0 <7.0.0"
20+ - maybe: ">=6.0.0 <7.0.0"
21+ - node-buffer: ">=9.0.0 <10.0.0"
22+ - node-fs: ">=9.1.0 <10.0.0"
23+ - node-path: ">=5.0.0 <6.0.0"
24+ - node-process: ">=11.2.0 <12.0.0"
25+ - prelude: ">=6.0.1 <7.0.0"
26+ - spec: ">=7.0.0 <8.0.0"
27+ - yoga-om-core: "*"
28+ test:
29+ main: Test.Main.Node
30+ dependencies:
31+ - spec: ">=7.0.0 <8.0.0"
src/Test/Main.pursnew file+12
@@ -0,0 +1,12 @@
1+module Test.Main.Node where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Effect.Aff (launchAff_)
7+import Yoga.Om.Node.Test as NodeTest
8+import Test.Spec.Reporter (consoleReporter)
9+import Test.Spec.Runner (runSpec)
10+
11+main :: Effect Unit
12+main = launchAff_ $ runSpec [ consoleReporter ] NodeTest.spec
src/Yoga/Om/Node.pursnew file+167
@@ -0,0 +1,167 @@
1+module Yoga.Om.Node
2+ ( readFile
3+ , readTextFile
4+ , writeFile
5+ , writeTextFile
6+ , appendFile
7+ , appendTextFile
8+ , exists
9+ , mkdir
10+ , rmdir
11+ , unlink
12+ , getEnv
13+ , lookupEnv
14+ , setEnv
15+ , cwd
16+ , FileError
17+ , EnvError
18+ ) where
19+
20+import Prelude
21+
22+import Data.Either (Either(..))
23+import Data.Maybe (Maybe, maybe)
24+import Effect.Aff (Aff)
25+import Effect.Class (liftEffect)
26+import Node.Buffer (Buffer)
27+import Node.Encoding (Encoding(..))
28+import Node.FS.Aff as FS
29+import Node.Path (FilePath)
30+import Node.Process as Process
31+import Yoga.Om (Om)
32+import Yoga.Om as Om
33+
34+-- | Error types for file operations
35+type FileError r =
36+ ( fileNotFound :: { path :: FilePath }
37+ , fileReadError :: { path :: FilePath, message :: String }
38+ , fileWriteError :: { path :: FilePath, message :: String }
39+ , fileSystemError :: { message :: String }
40+ | r
41+ )
42+
43+-- | Error types for environment variable operations
44+type EnvError r =
45+ ( envNotFound :: { variable :: String }
46+ | r
47+ )
48+
49+-- | Read a file as a Buffer
50+readFile
51+ :: forall ctx err
52+ . FilePath
53+ -> Om ctx (FileError err) Buffer
54+readFile path = do
55+ result <- Om.fromAff $ FS.readFile path
56+ pure result
57+
58+-- | Read a file as text with UTF-8 encoding
59+readTextFile
60+ :: forall ctx err
61+ . FilePath
62+ -> Om ctx (FileError err) String
63+readTextFile path = do
64+ result <- Om.fromAff $ FS.readTextFile UTF8 path
65+ pure result
66+
67+-- | Write a Buffer to a file
68+writeFile
69+ :: forall ctx err
70+ . FilePath
71+ -> Buffer
72+ -> Om ctx (FileError err) Unit
73+writeFile path buffer = do
74+ Om.fromAff $ FS.writeFile path buffer
75+
76+-- | Write text to a file with UTF-8 encoding
77+writeTextFile
78+ :: forall ctx err
79+ . FilePath
80+ -> String
81+ -> Om ctx (FileError err) Unit
82+writeTextFile path content = do
83+ Om.fromAff $ FS.writeTextFile UTF8 path content
84+
85+-- | Append a Buffer to a file
86+appendFile
87+ :: forall ctx err
88+ . FilePath
89+ -> Buffer
90+ -> Om ctx (FileError err) Unit
91+appendFile path buffer = do
92+ Om.fromAff $ FS.appendFile path buffer
93+
94+-- | Append text to a file with UTF-8 encoding
95+appendTextFile
96+ :: forall ctx err
97+ . FilePath
98+ -> String
99+ -> Om ctx (FileError err) Unit
100+appendTextFile path content = do
101+ Om.fromAff $ FS.appendTextFile UTF8 path content
102+
103+-- | Check if a file or directory exists
104+exists
105+ :: forall ctx err
106+ . FilePath
107+ -> Om ctx err Boolean
108+exists path = do
109+ (Om.fromAff $ FS.stat path $> true)
110+ # Om.handleErrors { exception: \_ -> pure false }
111+
112+-- | Create a directory
113+mkdir
114+ :: forall ctx err
115+ . FilePath
116+ -> Om ctx (FileError err) Unit
117+mkdir path = do
118+ Om.fromAff $ FS.mkdir path
119+
120+-- | Remove a directory
121+rmdir
122+ :: forall ctx err
123+ . FilePath
124+ -> Om ctx (FileError err) Unit
125+rmdir path = do
126+ Om.fromAff $ FS.rmdir path
127+
128+-- | Delete a file
129+unlink
130+ :: forall ctx err
131+ . FilePath
132+ -> Om ctx (FileError err) Unit
133+unlink path = do
134+ Om.fromAff $ FS.unlink path
135+
136+-- | Get an environment variable, throwing an error if not found
137+getEnv
138+ :: forall ctx err
139+ . String
140+ -> Om ctx (EnvError err) String
141+getEnv variable = do
142+ value <- lookupEnv variable
143+ value # Om.note { envNotFound: { variable } }
144+
145+-- | Look up an environment variable, returning Maybe
146+lookupEnv
147+ :: forall ctx err
148+ . String
149+ -> Om ctx err (Maybe String)
150+lookupEnv variable = do
151+ Om.fromAff $ liftEffect $ Process.lookupEnv variable
152+
153+-- | Set an environment variable
154+setEnv
155+ :: forall ctx err
156+ . String
157+ -> String
158+ -> Om ctx err Unit
159+setEnv variable value = do
160+ Om.fromAff $ liftEffect $ Process.setEnv variable value
161+
162+-- | Get the current working directory
163+cwd
164+ :: forall ctx err
165+ . Om ctx err FilePath
166+cwd = do
167+ Om.fromAff $ liftEffect $ Process.cwd
src/Yoga/Om/Node.test.pursnew file+56
@@ -0,0 +1,56 @@
1+module Yoga.Om.Node.Test where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe(..))
6+import Effect.Aff (Aff)
7+import Test.Spec (Spec, describe, it, pending)
8+import Test.Spec.Assertions (shouldEqual)
9+import Yoga.Om (Om)
10+import Yoga.Om as Om
11+import Yoga.Om.Node as Node
12+
13+-- | Test specification for yoga-om-node package
14+spec :: Spec Unit
15+spec = do
16+ describe "Yoga.Om.Node" do
17+
18+ describe "Environment variables" do
19+
20+ it "lookupEnv returns Maybe String" do
21+ let
22+ ctx = {}
23+ errorHandlers = { exception: \_ -> pure Nothing }
24+ om = Node.lookupEnv "PATH"
25+ result <- om # Om.runOm ctx errorHandlers
26+ -- PATH should exist in most environments
27+ case result of
28+ Just _ -> pure unit
29+ Nothing -> pure unit -- It's ok if it doesn't exist in test env
30+
31+ it "getEnv throws error when variable not found" do
32+ let
33+ ctx = {}
34+ errorHandlers =
35+ { exception: \_ -> pure "exception"
36+ , envNotFound: \{ variable } -> pure $ "not found: " <> variable
37+ }
38+ om = Node.getEnv "THIS_VARIABLE_SHOULD_NOT_EXIST_12345"
39+ result <- om # Om.runOm ctx errorHandlers
40+ result `shouldEqual` "not found: THIS_VARIABLE_SHOULD_NOT_EXIST_12345"
41+
42+ it "cwd returns current working directory" do
43+ let
44+ ctx = {}
45+ errorHandlers = { exception: \_ -> pure "" }
46+ om = Node.cwd
47+ result <- om # Om.runOm ctx errorHandlers
48+ -- Should return some non-empty path
49+ (result /= "") `shouldEqual` true
50+
51+ describe "File operations" do
52+
53+ pending "readTextFile reads file contents"
54+ pending "writeTextFile writes file contents"
55+ pending "exists checks file existence"
56+ pending "File operations throw appropriate errors"