elm-ssr 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +48 -342
  2. package/elm-src/ElmSsr/Island/Sse.elm +151 -0
  3. package/package.json +53 -24
  4. package/{packages/elm-ssr/src → src}/client-runtime/islands.ts +113 -15
  5. package/src/sse.ts +162 -0
  6. package/AGENTS.md +0 -289
  7. package/CHANGELOG.md +0 -87
  8. package/LICENSE +0 -21
  9. package/bun.lock +0 -259
  10. package/docker-compose.yml +0 -33
  11. package/docs/README.md +0 -51
  12. package/docs/backends.md +0 -146
  13. package/docs/cli.md +0 -117
  14. package/docs/effects.md +0 -91
  15. package/docs/getting-started.md +0 -94
  16. package/docs/islands.md +0 -197
  17. package/docs/loaders-and-actions.md +0 -241
  18. package/docs/middleware.md +0 -93
  19. package/docs/migrations.md +0 -143
  20. package/docs/routing.md +0 -108
  21. package/docs/sessions.md +0 -218
  22. package/docs/tasks.md +0 -149
  23. package/docs/testing.md +0 -84
  24. package/elm-ssr.config.json +0 -14
  25. package/examples/basic/elm.json +0 -27
  26. package/examples/basic/migrations/0001_guestbook.down.sql +0 -1
  27. package/examples/basic/migrations/0001_guestbook.sql +0 -10
  28. package/examples/basic/package.json +0 -10
  29. package/examples/basic/runtime.ts +0 -148
  30. package/examples/basic/src/Example/Basic/Islands/Counter.elm +0 -110
  31. package/examples/basic/src/Example/Basic/Islands/Observer.elm +0 -67
  32. package/examples/basic/src/Example/Basic/Islands/Tasks.elm +0 -151
  33. package/examples/basic/src/Example/Basic/Routes/Chart.elm +0 -87
  34. package/examples/basic/src/Example/Basic/Routes/Counter.elm +0 -42
  35. package/examples/basic/src/Example/Basic/Routes/Echo.elm +0 -76
  36. package/examples/basic/src/Example/Basic/Routes/Greet/Name_.elm +0 -37
  37. package/examples/basic/src/Example/Basic/Routes/Guestbook.elm +0 -86
  38. package/examples/basic/src/Example/Basic/Routes/Index.elm +0 -41
  39. package/examples/basic/src/Example/Basic/Routes/NotFound.elm +0 -37
  40. package/examples/basic/src/Example/Basic/Routes/Profile.elm +0 -112
  41. package/examples/basic/src/Example/Basic/Routes/Session.elm +0 -89
  42. package/examples/basic/src/Example/Basic/Routes/Status.elm +0 -90
  43. package/examples/basic/src/Example/Basic/View/Shared.elm +0 -60
  44. package/examples/basic/styles.ts +0 -204
  45. package/examples/basic/worker.ts +0 -3
  46. package/examples/crypto-dashboard/elm.json +0 -30
  47. package/examples/crypto-dashboard/package.json +0 -10
  48. package/examples/crypto-dashboard/runtime.ts +0 -97
  49. package/examples/crypto-dashboard/src/CryptoDashboard/Islands/MarketOverview.elm +0 -204
  50. package/examples/crypto-dashboard/src/CryptoDashboard/Islands/PriceChart.elm +0 -200
  51. package/examples/crypto-dashboard/src/CryptoDashboard/Routes/Index.elm +0 -67
  52. package/examples/crypto-dashboard/src/CryptoDashboard/Routes/NotFound.elm +0 -30
  53. package/examples/crypto-dashboard/src/CryptoDashboard/View/Shared.elm +0 -39
  54. package/examples/crypto-dashboard/styles.ts +0 -23
  55. package/examples/crypto-dashboard/worker.ts +0 -3
  56. package/llms.txt +0 -69
  57. package/packages/elm-ssr/README.md +0 -67
  58. package/packages/elm-ssr/package.json +0 -61
  59. package/scripts/benchmark.mjs +0 -60
  60. package/test/action.test.ts +0 -81
  61. package/test/adapters.test.ts +0 -173
  62. package/test/advanced-robustness.test.ts +0 -75
  63. package/test/app.test.ts +0 -209
  64. package/test/browser-island.test.ts +0 -184
  65. package/test/cli-migrate.test.ts +0 -97
  66. package/test/cli.test.ts +0 -94
  67. package/test/cookies.test.ts +0 -156
  68. package/test/crypto-dashboard.test.ts +0 -35
  69. package/test/effects.test.ts +0 -117
  70. package/test/http.test.ts +0 -50
  71. package/test/integration/redis-postgres.test.ts +0 -174
  72. package/test/island-runtime.test.ts +0 -214
  73. package/test/middleware.test.ts +0 -134
  74. package/test/migrations.test.ts +0 -244
  75. package/test/profile.test.ts +0 -159
  76. package/test/robustness.test.ts +0 -135
  77. package/test/serialize.test.ts +0 -92
  78. package/test/sessions.test.ts +0 -429
  79. package/test/svg.test.ts +0 -65
  80. package/tsconfig.json +0 -20
  81. package/wrangler.jsonc +0 -11
  82. /package/{packages/elm-ssr/bin → bin}/elm-ssr.mjs +0 -0
  83. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Action.elm +0 -0
  84. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document/Encode.elm +0 -0
  85. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document/Events.elm +0 -0
  86. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document.elm +0 -0
  87. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html/Attributes.elm +0 -0
  88. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html/Events.elm +0 -0
  89. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html.elm +0 -0
  90. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Island/Shared.elm +0 -0
  91. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Island.elm +0 -0
  92. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Loader.elm +0 -0
  93. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Page.elm +0 -0
  94. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Route.elm +0 -0
  95. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Runtime.elm +0 -0
  96. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Svg/Attributes.elm +0 -0
  97. /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Svg.elm +0 -0
  98. /package/{packages/elm-ssr/lib → lib}/build.mjs +0 -0
  99. /package/{packages/elm-ssr/lib → lib}/migrate.mjs +0 -0
  100. /package/{packages/elm-ssr/lib → lib}/scaffold.mjs +0 -0
  101. /package/{packages/elm-ssr/lib → lib}/workspace.mjs +0 -0
  102. /package/{packages/elm-ssr/src → src}/app.ts +0 -0
  103. /package/{packages/elm-ssr/src → src}/backends.ts +0 -0
  104. /package/{packages/elm-ssr/src → src}/effects.ts +0 -0
  105. /package/{packages/elm-ssr/src → src}/http.ts +0 -0
  106. /package/{packages/elm-ssr/src → src}/middleware.ts +0 -0
  107. /package/{packages/elm-ssr/src → src}/migrations.ts +0 -0
  108. /package/{packages/elm-ssr/src → src}/protocol.ts +0 -0
  109. /package/{packages/elm-ssr/src → src}/render.ts +0 -0
  110. /package/{packages/elm-ssr/src → src}/request-handler.ts +0 -0
  111. /package/{packages/elm-ssr/src → src}/response-headers.ts +0 -0
  112. /package/{packages/elm-ssr/src → src}/serialize.ts +0 -0
  113. /package/{packages/elm-ssr/src → src}/sessions/crypto.ts +0 -0
  114. /package/{packages/elm-ssr/src → src}/sessions/effects.ts +0 -0
  115. /package/{packages/elm-ssr/src → src}/sessions/index.ts +0 -0
  116. /package/{packages/elm-ssr/src → src}/sessions/middleware.ts +0 -0
  117. /package/{packages/elm-ssr/src → src}/sessions/store.ts +0 -0
  118. /package/{packages/elm-ssr/src → src}/sessions/types.ts +0 -0
  119. /package/{packages/elm-ssr/src → src}/tasks.ts +0 -0
@@ -1,241 +0,0 @@
1
- # Loaders and Actions
2
-
3
- `Loader` and `Action` are **descriptions of work**, not side effects. The
4
- author composes them; the runtime pumps the actual IO through the Worker's
5
- effect adapter and feeds the results back until each one terminates.
6
-
7
- This keeps Elm pure end-to-end — no manual ports, no flag-shaped JSON
8
- threading, no `Cmd` for server work.
9
-
10
- ## Loader (data fetching, for `page`)
11
-
12
- A `Loader a` resolves to a value of type `a` after the runtime has executed
13
- any effects it requested. The page can then render based on that value.
14
-
15
- ### Constructors
16
-
17
- ```elm
18
- -- pure success
19
- Loader.succeed : a -> Loader a
20
-
21
- -- abort with an HTTP status and message
22
- Loader.fail : Int -> String -> Loader a
23
- ```
24
-
25
- ### Composition
26
-
27
- ```elm
28
- Loader.map : (a -> b) -> Loader a -> Loader b
29
- Loader.map2 : (a -> b -> c) -> Loader a -> Loader b -> Loader c
30
- Loader.andThen : (a -> Loader b) -> Loader a -> Loader b
31
- ```
32
-
33
- Loaders are sequential. `andThen` runs the next effect only after the prior
34
- one resolves.
35
-
36
- ### Effects
37
-
38
- See [Effects](effects.md) for the full vocabulary. Quick reference:
39
-
40
- ```elm
41
- Loader.fetchJson : { url : String, decoder : Decoder a } -> Loader a
42
- Loader.cacheGet : { key : String, decoder : Decoder a } -> Loader (Maybe a)
43
- Loader.cachePut : { key : String, value : Value, ttlSeconds : Maybe Int } -> Loader ()
44
- Loader.query : { sql : String, params : List Value, decoder : Decoder a } -> Loader (List a)
45
- Loader.queryOne : { sql : String, params : List Value, decoder : Decoder a } -> Loader (Maybe a)
46
- Loader.execute : { sql : String, params : List Value } -> Loader { rowsAffected : Int }
47
- Loader.env : String -> Loader (Maybe String)
48
- Loader.enqueue : { task : String, payload : Value } -> Loader ()
49
- ```
50
-
51
- `getCookie` is also available via `Loader` — see [Effects](effects.md).
52
-
53
- ### Example: cache → fetch → cache
54
-
55
- ```elm
56
- cachedStatus : Loader Status
57
- cachedStatus =
58
- Loader.cacheGet { key = "status", decoder = statusDecoder }
59
- |> Loader.andThen
60
- (\cached ->
61
- case cached of
62
- Just status ->
63
- Loader.succeed status
64
-
65
- Nothing ->
66
- Loader.fetchJson
67
- { url = "https://api.example.com/status"
68
- , decoder = statusDecoder
69
- }
70
- |> Loader.andThen
71
- (\status ->
72
- Loader.cachePut
73
- { key = "status"
74
- , value = encodeStatus status
75
- , ttlSeconds = Just 60
76
- }
77
- |> Loader.map (\_ -> status)
78
- )
79
- )
80
- ```
81
-
82
- ## Action (form handling, for `action`)
83
-
84
- `Action a` is the non-GET equivalent. It can resolve to a value, fail, redirect
85
- (303-style Post/Redirect/Get), or respond with JSON.
86
-
87
- ### Constructors
88
-
89
- ```elm
90
- Action.succeed : a -> Action a
91
- Action.fail : Int -> String -> Action a
92
- Action.redirect : String -> Action a
93
- Action.json : Value -> Action a
94
- ```
95
-
96
- ### Cookies
97
-
98
- Any `Action` can attach `Set-Cookie` headers to its response — including
99
- redirects, JSON responses, and even failures. Cookies travel through
100
- `map`/`andThen`/`fromLoader` so you can compose freely.
101
-
102
- ```elm
103
- Action.Cookie : { name : String, value : String, maxAge : Maybe Int, expires : Maybe String, domain : Maybe String, path : Maybe String, secure : Bool, httpOnly : Bool, sameSite : Maybe SameSite }
104
-
105
- Action.SameSite = Lax | Strict | None
106
-
107
- -- Build a cookie:
108
- Action.defaultCookie : String -> String -> Cookie -- permissive (Path=/), fill in what you need
109
- Action.sessionCookie : String -> String -> Cookie -- HARDENED: Secure, HttpOnly, SameSite=Lax, Max-Age=7d
110
-
111
- -- Attach to an action:
112
- Action.setCookie : Cookie -> Action a -> Action a
113
- Action.clearCookie : { name : String, path : Maybe String, domain : Maybe String } -> Action a -> Action a
114
- ```
115
-
116
- **Security defaults matter.** `Action.sessionCookie` is what you should reach
117
- for any time a cookie grants authority (session IDs, auth tokens). It sets:
118
-
119
- - `HttpOnly` — JavaScript cannot read it (XSS-resistant).
120
- - `Secure` — only sent over HTTPS.
121
- - `SameSite=Lax` — sent on top-level navigations, blocked on cross-site
122
- sub-requests (CSRF-resistant for unsafe verbs).
123
- - `Path=/`, `Max-Age=7 days`.
124
-
125
- `Action.defaultCookie` is the unopinionated escape hatch for non-sensitive
126
- cookies (preferences, analytics consent, etc).
127
-
128
- ### Example: login (PRG + hardened session cookie)
129
-
130
- ```elm
131
- action : Request -> Action (Document Never)
132
- action request =
133
- case Route.formValue "username" request of
134
- Just username ->
135
- Action.fromLoader (mintSessionToken username)
136
- |> Action.andThen
137
- (\token ->
138
- Action.redirect "/dashboard"
139
- |> Action.setCookie (Action.sessionCookie "session" token)
140
- )
141
-
142
- Nothing ->
143
- Action.fail 422 "Username is required"
144
- ```
145
-
146
- ### Example: logout
147
-
148
- ```elm
149
- action _ =
150
- Action.redirect "/login"
151
- |> Action.clearCookie { name = "session", path = Just "/", domain = Nothing }
152
- ```
153
-
154
- ### Reading cookies back
155
-
156
- From the matching `page` (or any `Loader`), use `Loader.getCookie`:
157
-
158
- ```elm
159
- page _ =
160
- Loader.getCookie "session"
161
- |> Loader.andThen
162
- (\session ->
163
- case session of
164
- Just token ->
165
- Loader.map renderDashboard (lookupUser token)
166
-
167
- Nothing ->
168
- Loader.succeed renderLoginPrompt
169
- )
170
- ```
171
-
172
- ### Stacking multiple cookies
173
-
174
- `setCookie` is composable — call it more than once to attach multiple
175
- cookies to the same response:
176
-
177
- ```elm
178
- Action.redirect "/onboarding/step-2"
179
- |> Action.setCookie (Action.sessionCookie "session" token)
180
- |> Action.setCookie ({ defaultCookie "onboarding" "step-2" | sameSite = Just Lax })
181
- ```
182
-
183
- ### Local dev gotcha
184
-
185
- `Secure` cookies are rejected by modern browsers on plain `http://`. If
186
- you're hitting your app on `http://localhost`, either run it over HTTPS
187
- (wrangler dev does, by default) or override `secure = False` on the cookie.
188
- See [examples/basic/src/Example/Basic/Routes/Session.elm](../examples/basic/src/Example/Basic/Routes/Session.elm)
189
- for the pattern.
190
-
191
- ### Composition
192
-
193
- ```elm
194
- Action.map : (a -> b) -> Action a -> Action b
195
- Action.andThen : (a -> Action b) -> Action a -> Action b
196
-
197
- -- Lift any Loader (and all its effects: cacheGet, query, execute, env, ...)
198
- -- into an Action so it runs as part of the action's effect chain.
199
- Action.fromLoader : Loader a -> Action a
200
- ```
201
-
202
- `fromLoader` is how actions do server work — there's no separate "action
203
- effect" type; the entire `Loader` effect vocabulary is reusable.
204
-
205
- ### Example: a guestbook POST (PRG pattern)
206
-
207
- ```elm
208
- action : Request -> Action (Document Never)
209
- action request =
210
- case Route.formValue "message" request of
211
- Nothing ->
212
- Action.fail 422 "Message is required."
213
-
214
- Just message ->
215
- if String.isEmpty (String.trim message) then
216
- Action.fail 422 "Message is required."
217
-
218
- else
219
- Action.fromLoader
220
- (Loader.execute
221
- { sql = "INSERT INTO entries (message) VALUES (?)"
222
- , params = [ Encode.string message ]
223
- }
224
- )
225
- |> Action.andThen (\_ -> Action.redirect "/guestbook")
226
- ```
227
-
228
- The client `POST /guestbook`s with the form, the row is inserted, the action
229
- redirects with `303 See Other` to `/guestbook`, the browser issues a `GET`, the
230
- page re-renders with the new entry. No JS required.
231
-
232
- ## Failures
233
-
234
- Both `Loader.fail` and `Action.fail` take an HTTP status. The runtime turns
235
- them into the matching response (with the message). Decode failures during
236
- `fetchJson`/`cacheGet`/`query` map to `502` automatically.
237
-
238
- ## What next
239
-
240
- - [Effects](effects.md) — the full effect surface and what each kind does.
241
- - [Backends](backends.md) — choosing/composing the adapter that runs effects.
@@ -1,93 +0,0 @@
1
- # Middleware
2
-
3
- `createWorkerApp` wraps the request handler in a fixed stack of middlewares.
4
- You usually don't need to touch this — the defaults give you request IDs,
5
- timing headers, structured request logging, error normalization, and HEAD
6
- support out of the box.
7
-
8
- ## Default stack
9
-
10
- In order (outermost first):
11
-
12
- 1. **`errorMiddleware`** — catches anything thrown downstream, logs
13
- `elm_ssr_request_failed { requestId, path, error }`, and returns
14
- `{ "error": "internal_error", "requestId": ... }` (JSON, status 500) for
15
- `/api/*` or plain `Internal Server Error` (status 500) for everything
16
- else.
17
- 2. **`requestIdMiddleware`** — uses the incoming `x-request-id` header if
18
- non-empty; otherwise generates a fresh UUID. Echoes the value back in the
19
- response header.
20
- 3. **`timingMiddleware`** — adds `server-timing: app;dur=…` and
21
- `x-response-time: …ms` based on `performance.now() - context.startedAt`.
22
- 4. **`loggingMiddleware`** — after the response is built, logs a JSON line
23
- `{ event: "request_completed", requestId, method, path, status, durationMs }`.
24
- On Cloudflare the logging is scheduled via `ctx.waitUntil` so it doesn't
25
- block the response.
26
- 5. **`headMiddleware`** — for `HEAD` requests, strips the response body while
27
- preserving status + headers.
28
-
29
- ## Composing your own
30
-
31
- ```ts
32
- import {
33
- composeMiddleware,
34
- errorMiddleware,
35
- requestIdMiddleware,
36
- timingMiddleware,
37
- loggingMiddleware,
38
- headMiddleware,
39
- type Middleware
40
- } from "elm-ssr/middleware";
41
-
42
- const authMiddleware: Middleware = async (context, next) => {
43
- if (context.url.pathname.startsWith("/admin/")) {
44
- const session = context.request.headers.get("cookie") ?? "";
45
- if (!session.includes("admin=1")) {
46
- return new Response("Forbidden", { status: 403 });
47
- }
48
- }
49
- return next(context);
50
- };
51
-
52
- const handler = composeMiddleware(myRouteHandler, [
53
- errorMiddleware,
54
- requestIdMiddleware,
55
- timingMiddleware,
56
- loggingMiddleware(),
57
- authMiddleware,
58
- headMiddleware
59
- ]);
60
- ```
61
-
62
- `composeMiddleware(handler, [a, b, c])` runs `a` outermost: `a → b → c →
63
- handler → c → b → a`.
64
-
65
- If you want to customize the **default** stack used by `createWorkerApp`,
66
- you can't drop middlewares from it directly — pass your own `log` to override
67
- the logger, or build your own `WorkerHandler` from scratch with
68
- `composeMiddleware` if you need different ordering.
69
-
70
- ## `AppContext`
71
-
72
- Every middleware and handler receives an `AppContext`:
73
-
74
- ```ts
75
- interface AppContext {
76
- request: Request;
77
- url: URL;
78
- requestId: string; // set by requestIdMiddleware (empty before)
79
- startedAt: number; // performance.now() at request start
80
- executionCtx?: WorkerExecutionContext; // present on Cloudflare; has waitUntil
81
- env?: Record<string, unknown>; // Cloudflare bindings
82
- }
83
- ```
84
-
85
- `executionCtx?.waitUntil(promise)` is how the logging and task adapters
86
- schedule work after the response. If absent (Bun, tests), middlewares fall
87
- back to fire-and-forget.
88
-
89
- ## Source
90
-
91
- - [packages/elm-ssr/src/middleware.ts](../packages/elm-ssr/src/middleware.ts)
92
- - [packages/elm-ssr/src/http.ts](../packages/elm-ssr/src/http.ts) — types
93
- - [packages/elm-ssr/src/app.ts](../packages/elm-ssr/src/app.ts) — default stack
@@ -1,143 +0,0 @@
1
- # SQL migrations
2
-
3
- A small, backend-neutral, transactional SQL-file migration runner. Files live
4
- in a directory (e.g. `migrations/0001_init.sql`), get applied in alphabetical
5
- order, and are tracked in `__elm_ssr_migrations(name PRIMARY KEY, applied_at)`.
6
-
7
- The runner is also wired into the CLI as
8
- [`elm-ssr migrate up|down|status`](cli.md).
9
-
10
- ## Files
11
-
12
- ```
13
- migrations/
14
- 0001_init.sql
15
- 0001_init.down.sql # optional, paired for `migrate down`
16
- 0002_add_users.sql
17
- 0002_add_users.down.sql
18
- ```
19
-
20
- - Numeric prefix → alphabetical sort matches creation order.
21
- - `.down.sql` is optional but required if you want to revert that migration.
22
- - The runner ignores any non-`.sql` files and ignores `.down.sql` on the up
23
- pass.
24
-
25
- ## Safety
26
-
27
- Each migration runs inside a single `BEGIN…COMMIT` transaction **together
28
- with** its tracking insert. If the SQL fails, the transaction rolls back and
29
- the tracking row is never written, so the next run picks up exactly where
30
- this one stopped.
31
-
32
- Backends with native transaction support can override this by providing
33
- `runInTransaction(fn)` on the adapter. The Postgres adapter the CLI ships
34
- uses `sql.begin(fn)` for proper transactional safety; SQLite uses raw
35
- `BEGIN`/`COMMIT`.
36
-
37
- ## CLI
38
-
39
- ```sh
40
- elm-ssr migrate up # Apply all pending
41
- elm-ssr migrate down # Revert the most recent
42
- elm-ssr migrate down --count 3 # Revert the last 3
43
- elm-ssr migrate status # Show applied (with timestamps) + pending
44
-
45
- # Connection string options:
46
- elm-ssr migrate up --db postgres://user:pass@localhost:5432/db
47
- elm-ssr migrate up --db sqlite://./app.db
48
- elm-ssr migrate up --db ./app.db # bare path = SQLite
49
-
50
- # Reads DATABASE_URL if --db is omitted.
51
- DATABASE_URL=postgres://... elm-ssr migrate up
52
-
53
- # Other flags:
54
- elm-ssr migrate up --dir ./db/migrations # default: ./migrations
55
- elm-ssr migrate up --table schema_history # default: __elm_ssr_migrations
56
- ```
57
-
58
- ## Programmatic API
59
-
60
- ```ts
61
- import {
62
- runMigrations,
63
- revertMigrations,
64
- listMigrations,
65
- type MigrationsAdapter
66
- } from "elm-ssr/migrations";
67
-
68
- // Adapter shape — wire any backend:
69
- interface MigrationsAdapter {
70
- exec(sql: string): Promise<void>; // multi-statement, no params
71
- list(sql: string): Promise<Array<Record<string, unknown>>>; // query
72
- runInTransaction?(fn: () => Promise<void>): Promise<void>; // optional native txn
73
- }
74
- ```
75
-
76
- ### bun:sqlite
77
-
78
- ```ts
79
- import { Database } from "bun:sqlite";
80
-
81
- const db = new Database("./app.db");
82
- const adapter: MigrationsAdapter = {
83
- exec: async (sql) => { db.exec(sql); },
84
- list: async (sql) => db.query(sql).all() as Array<Record<string, unknown>>
85
- };
86
-
87
- await runMigrations(adapter, { dir: "./migrations" });
88
- ```
89
-
90
- ### Postgres (Bun.sql)
91
-
92
- ```ts
93
- import { SQL } from "bun";
94
-
95
- const sql = new SQL(process.env.DATABASE_URL!);
96
- const adapter: MigrationsAdapter = {
97
- exec: async (text) => { await sql.unsafe(text); },
98
- list: async (text) => {
99
- const rows = await sql.unsafe(text);
100
- return Array.isArray(rows) ? rows : [...(rows ?? [])];
101
- },
102
- runInTransaction: async (fn) => { await sql.begin(fn); }
103
- };
104
-
105
- await runMigrations(adapter, { dir: "./migrations" });
106
- ```
107
-
108
- ### D1 (Cloudflare)
109
-
110
- D1 doesn't expose multi-statement `exec` with `runInTransaction`, so use one
111
- statement per migration file and run from a scheduled Worker or a build step.
112
-
113
- ## Return values
114
-
115
- ```ts
116
- type MigrationResult = { applied: string[]; skipped: string[] };
117
- type RevertResult = { reverted: string[] };
118
- type MigrationsStatus = {
119
- applied: Array<{ name: string; appliedAt: string }>;
120
- pending: string[];
121
- };
122
- ```
123
-
124
- ## What you can rely on
125
-
126
- - **Idempotent.** Re-running `up` skips anything already in
127
- `__elm_ssr_migrations`.
128
- - **Forward-only by default.** `down` errors loudly if the paired
129
- `.down.sql` is missing.
130
- - **Safe failure.** A throwing migration rolls back (assuming a transactional
131
- backend); the tracking row is never written.
132
- - **Custom table name.** Pass `tableName` if `__elm_ssr_migrations` clashes
133
- with anything. The name is validated against
134
- `^[A-Za-z_][A-Za-z0-9_]*$` to prevent SQL injection via the option.
135
-
136
- ## Source
137
-
138
- - [packages/elm-ssr/src/migrations.ts](../packages/elm-ssr/src/migrations.ts)
139
- - [packages/elm-ssr/lib/migrate.mjs](../packages/elm-ssr/lib/migrate.mjs)
140
- - Tests:
141
- [test/migrations.test.ts](../test/migrations.test.ts),
142
- [test/cli-migrate.test.ts](../test/cli-migrate.test.ts),
143
- [test/integration/redis-postgres.test.ts](../test/integration/redis-postgres.test.ts)
package/docs/routing.md DELETED
@@ -1,108 +0,0 @@
1
- # Routing
2
-
3
- Routing is file-based, Next/Remix-style. The CLI scans
4
- `src/<Namespace>/Routes/` and generates a router that dispatches by URL path
5
- and HTTP method.
6
-
7
- ## File layout → URL
8
-
9
- | File | URL |
10
- | ---- | --- |
11
- | `Routes/Index.elm` | `/` |
12
- | `Routes/About.elm` | `/about` |
13
- | `Routes/Posts/Index.elm` | `/posts` |
14
- | `Routes/Posts/Slug_.elm` | `/posts/:slug` (dynamic) |
15
- | `Routes/Greet/Name_.elm` | `/greet/:name` |
16
- | `Routes/NotFound.elm` | fallback for everything else |
17
-
18
- Trailing-underscore names are **dynamic segments**, captured into
19
- `Request.params` and read with `Route.param "slug"`.
20
-
21
- ## Every route exposes `page` and `action`
22
-
23
- ```elm
24
- module Demo.Routes.About exposing (page, action)
25
-
26
- import ElmSsr.Action as Action exposing (Action)
27
- import ElmSsr.Document exposing (Document)
28
- import ElmSsr.Html exposing (p, text)
29
- import ElmSsr.Loader as Loader exposing (Loader)
30
- import ElmSsr.Page as Page
31
- import ElmSsr.Route exposing (Request)
32
-
33
-
34
- page : Request -> Loader (Document Never)
35
- page _ =
36
- Loader.succeed view
37
-
38
-
39
- action : Request -> Action (Document Never)
40
- action _ =
41
- Action.fail 405 "Method not allowed"
42
-
43
-
44
- view : Document Never
45
- view =
46
- Page.page
47
- { title = "About"
48
- , head = []
49
- , body = [ p [] [ text "About this app" ] ]
50
- }
51
- ```
52
-
53
- - `page` handles `GET` and `HEAD`. Returns a `Loader (Document Never)`.
54
- - `action` handles `POST` (and other non-GET). Returns an
55
- `Action (Document Never)`. Most pages reject non-GET with `Action.fail 405`.
56
-
57
- See [Loaders and Actions](loaders-and-actions.md) for the data-fetching side.
58
-
59
- ## Reading the request
60
-
61
- ```elm
62
- import ElmSsr.Route as Route exposing (Request)
63
-
64
-
65
- page : Request -> Loader (Document Never)
66
- page request =
67
- case Route.param "slug" request of
68
- Just slug ->
69
- -- dynamic segment captured from the filename
70
- loadPost slug
71
-
72
- Nothing ->
73
- Loader.fail 400 "Missing slug"
74
- ```
75
-
76
- `ElmSsr.Route` exports:
77
- - `method : Request -> String` — uppercased HTTP method.
78
- - `segments : Request -> List String` — path split into non-empty parts.
79
- - `query : String -> Request -> Maybe String` — query-string parameter.
80
- - `param : String -> Request -> Maybe String` — dynamic route segment.
81
- - `params : Request -> List ( String, String )` — all dynamic segments.
82
- - `formValue : String -> Request -> Maybe String` — form field, for actions.
83
-
84
- ## NotFound
85
-
86
- `Routes/NotFound.elm` is the fallback. Use `Page.notFound` (which sets the
87
- right status) instead of `Page.page`:
88
-
89
- ```elm
90
- view : Document Never
91
- view =
92
- Page.notFound
93
- { title = "Not found"
94
- , head = []
95
- , body = [ p [] [ text "This route does not exist." ] ]
96
- }
97
- ```
98
-
99
- If you don't ship a `NotFound.elm`, unknown routes get a plain text 404 from
100
- the runtime.
101
-
102
- ## Tips
103
-
104
- - Keep route modules small. Move shared markup into `View/Shared.elm`.
105
- - For a static (no-data) page, `Loader.succeed view` is enough — no effects
106
- are pumped.
107
- - For interactive bits inside a page, embed an island. The page itself stays
108
- SSR-only; only the island ships JS. See [Islands](islands.md).