git.iko.soy/rowtype-yoga/purescript-yoga-bun-yoga
on main help sign in
Initial: yoga-bun-yoga
commit8905956cfefefafa8ca442b35dbcfb26f105588a authorMark <mark.eibes@gmail.com> dateWed 4 Feb 2026, 21:58 (8 months ago)
8 files changed+196
.github/workflows/ci.yml+9 .gitignore+3 package.json+12 spago.yaml+23 src/Yoga/Bun/HTTP.js+21 src/Yoga/Bun/HTTP.purs+81 src/Yoga/Bun/HTTP/Yoga/JSON.js+1 src/Yoga/Bun/HTTP/Yoga/JSON.purs+46
.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*/
package.jsonnew file+12
@@ -0,0 +1,12 @@
1+{
2+ "name": "purescript-yoga-bun-yoga",
3+ "version": "0.1.0",
4+ "description": "PureScript FFI bindings for Bun HTTP with GraphQL Yoga",
5+ "license": "MIT",
6+ "repository": {
7+ "type": "git",
8+ "url": "https://github.com/rowtype-yoga/purescript-yoga-backend-stack.git",
9+ "directory": "packages/yoga-bun-yoga"
10+ },
11+ "keywords": ["purescript", "ffi", "bindings", "yoga"]
12+}
spago.yamlnew file+23
@@ -0,0 +1,23 @@
1+workspace:
2+ package_set:
3+ registry: 51.1.0
4+
5+package:
6+ name: yoga-bun-yoga
7+ publish:
8+ version: 0.1.0
9+ license: MIT
10+ location:
11+ githubOwner: rowtype-yoga
12+ githubRepo: purescript-yoga-bun-yoga
13+ dependencies:
14+ - aff: ">=7.0.0 <8.0.0"
15+ - effect: ">=4.0.0 <5.0.0"
16+ - foreign: ">=7.0.0 <8.0.0"
17+ - foreign-object: ">=4.0.0 <5.0.0"
18+ - js-promise: ">=1.0.0 <2.0.0"
19+ - js-promise-aff: ">=1.0.0 <2.0.0"
20+ - prelude: ">=6.0.0 <7.0.0"
21+ - web-fetch: ">=4.0.0 <5.0.0"
22+ - yoga-json: ">=5.0.0 <6.0.0"
23+ - yoga-json: ">=5.0.0 <6.0.0"
src/Yoga/Bun/HTTP.jsnew file+21
@@ -0,0 +1,21 @@
1+export const serveImpl = (options) => Bun.serve(options);
2+
3+// String/text Response
4+export const stringResponseImpl = (body, stuff) => new Response(body, stuff);
5+
6+// JSON Response
7+export const jsonResponseImpl = (body, stuff) => Response.json(body, stuff);
8+
9+// Empty Response (for 204 No Content, etc.)
10+export const emptyResponseImpl = (stuff) => new Response(null, stuff);
11+
12+// Response with ArrayBuffer
13+export const arrayBufferResponseImpl = (buffer, stuff) => new Response(buffer, stuff);
14+
15+// Static Response constructors
16+export const responseRedirectImpl = (url, status) => Response.redirect(url, status);
17+
18+export const responseErrorImpl = () => Response.error();
19+
20+// Response cloning
21+export const cloneResponseImpl = (response) => response.clone();
src/Yoga/Bun/HTTP.pursnew file+81
@@ -0,0 +1,81 @@
1+module Yoga.Bun.HTTP where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Effect.Aff (Aff)
7+import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, runEffectFn1, runEffectFn2)
8+import Foreign (Foreign)
9+import Foreign.Object (Object)
10+import Prim.Row (class Union)
11+import Promise (Promise)
12+import Promise.Aff (toAffE, fromAff) as Promise
13+import Web.Fetch.Request (Request)
14+import Web.Fetch.Response (Response)
15+
16+type BunServerImpl = { stop :: EffectFn1 Boolean (Promise Unit) }
17+
18+data CloseActiveConnections = WaitForOpenConnections | ForceTerminateAllOpenConnections
19+
20+type BunServer = { stop :: CloseActiveConnections -> Aff Unit }
21+
22+foreign import serveImpl :: forall options. EffectFn1 options BunServerImpl
23+
24+type BunServeOptionsImpl = (fetch :: EffectFn1 Request (Promise Response), port :: Int, host :: String)
25+
26+serve :: forall opts opts_. Union opts opts_ BunServeOptionsImpl => { fetch :: EffectFn1 Request (Promise Response) | opts } -> Effect BunServer
27+serve opts = do
28+ serverImpl <- runEffectFn1 serveImpl opts
29+ pure
30+ { stop: \closeActiveConnections -> case closeActiveConnections of
31+ WaitForOpenConnections -> runEffectFn1 serverImpl.stop true # Promise.toAffE
32+ ForceTerminateAllOpenConnections -> runEffectFn1 serverImpl.stop false # Promise.toAffE
33+ }
34+
35+mkFetch :: (Request -> Aff Response) -> EffectFn1 Request (Promise Response)
36+mkFetch fetchAff = mkEffectFn1 (fetchAff >>> Promise.fromAff)
37+
38+-- Response Options Type
39+type ResponseOptions = (status :: Int, headers :: Object String, statusText :: String)
40+
41+-- String/text Response
42+foreign import stringResponseImpl :: forall opts. EffectFn2 String { | opts } Response
43+
44+stringResponse :: forall opts opts_. Union opts opts_ ResponseOptions => String -> { | opts } -> Effect Response
45+stringResponse body opts = runEffectFn2 stringResponseImpl body opts
46+
47+-- JSON Response
48+foreign import jsonResponseImpl :: forall opts. EffectFn2 Foreign { | opts } Response
49+
50+jsonResponse :: forall opts opts_. Union opts opts_ ResponseOptions => Foreign -> { | opts } -> Effect Response
51+jsonResponse json opts = runEffectFn2 jsonResponseImpl json opts
52+
53+-- Empty Response (useful for 204 No Content)
54+foreign import emptyResponseImpl :: forall opts. EffectFn1 { | opts } Response
55+
56+emptyResponse :: forall opts opts_. Union opts opts_ ResponseOptions => { | opts } -> Effect Response
57+emptyResponse opts = runEffectFn1 emptyResponseImpl opts
58+
59+-- Response with ArrayBuffer
60+foreign import arrayBufferResponseImpl :: forall opts. EffectFn2 Foreign { | opts } Response
61+
62+arrayBufferResponse :: forall opts opts_. Union opts opts_ ResponseOptions => Foreign -> { | opts } -> Effect Response
63+arrayBufferResponse buffer opts = runEffectFn2 arrayBufferResponseImpl buffer opts
64+
65+-- Response.redirect() - creates a redirect response
66+foreign import responseRedirectImpl :: EffectFn2 String Int Response
67+
68+responseRedirect :: String -> Int -> Effect Response
69+responseRedirect url status = runEffectFn2 responseRedirectImpl url status
70+
71+-- Response.error() - creates a network error response
72+foreign import responseErrorImpl :: Effect Response
73+
74+responseError :: Effect Response
75+responseError = responseErrorImpl
76+
77+-- Clone a Response
78+foreign import cloneResponseImpl :: Response -> Effect Response
79+
80+cloneResponse :: Response -> Effect Response
81+cloneResponse = cloneResponseImpl
src/Yoga/Bun/HTTP/Yoga/JSON.jsnew file+1
@@ -0,0 +1 @@
1+export const requestTextImpl = (request) => request.text();
src/Yoga/Bun/HTTP/Yoga/JSON.pursnew file+46
@@ -0,0 +1,46 @@
1+module Yoga.Bun.HTTP.Yoga.JSON
2+ ( jsonResponse
3+ , parseJSON
4+ , parseJSONRequest
5+ ) where
6+
7+import Prelude
8+
9+import Yoga.Bun.HTTP as BunHTTP
10+import Data.Either (Either(..))
11+import Effect (Effect)
12+import Effect.Aff (Aff, throwError)
13+import Effect.Class (liftEffect)
14+import Effect.Exception (error)
15+import Foreign (Foreign)
16+import Prim.Row (class Union)
17+import Promise.Aff as Promise
18+import Web.Fetch.Request (Request)
19+import Web.Fetch.Response (Response)
20+import Yoga.JSON (class ReadForeign, class WriteForeign, readJSON, writeImpl)
21+
22+-- | Create a JSON response with type-safe encoding
23+jsonResponse
24+ :: forall a opts opts_
25+ . WriteForeign a
26+ => Union opts opts_ BunHTTP.ResponseOptions
27+ => a
28+ -> { | opts }
29+ -> Effect Response
30+jsonResponse value opts =
31+ BunHTTP.jsonResponse (writeImpl value) opts
32+
33+-- | Parse JSON from a string
34+parseJSON :: forall a. ReadForeign a => String -> Aff a
35+parseJSON str = case readJSON str of
36+ Right value -> pure value
37+ Left err -> throwError $ error $ show err
38+
39+-- | Parse JSON from a Request body
40+foreign import requestTextImpl :: Request -> Effect (Promise.Promise String)
41+
42+parseJSONRequest :: forall a. ReadForeign a => Request -> Aff a
43+parseJSONRequest req = do
44+ textPromise <- liftEffect $ requestTextImpl req
45+ text <- Promise.toAff textPromise
46+ parseJSON text