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
package/docs/cli.md DELETED
@@ -1,117 +0,0 @@
1
- # CLI
2
-
3
- The `elm-ssr` command. Installed when you `bun add elm-ssr`, run as
4
- `bun elm-ssr <command>` in a workspace or `npx elm-ssr <command>`.
5
-
6
- ## Commands
7
-
8
- ### `elm-ssr build`
9
-
10
- Reads `elm-ssr.config.json`, then for each configured app:
11
-
12
- 1. Scans `src/<Namespace>/Routes/` and `Islands/`.
13
- 2. Generates `<root>/.elm-ssr/Main.elm` (the router; file-based routes +
14
- dynamic segments) and the islands manifest.
15
- 3. Syncs the Elm authoring modules from
16
- `packages/elm-ssr/elm-src/ElmSsr/*.elm` into
17
- `<root>/.elm-ssr/src/ElmSsr/`.
18
- 4. Runs `elm make` to compile the page program + one combined island bundle
19
- (`Elm.<App>.Islands.<Name>` per island).
20
-
21
- Outputs land in `generated/<app-name>/` (gitignored).
22
-
23
- ### `elm-ssr compress`
24
-
25
- Same pipeline as `build`, but additionally gzips the generated bundles so the
26
- edge can serve `Content-Encoding: gzip` directly.
27
-
28
- ### `elm-ssr dev`
29
-
30
- ```sh
31
- elm-ssr dev
32
- ```
33
-
34
- Runs `build` then `wrangler dev`. Use this for local Cloudflare-flavoured
35
- development.
36
-
37
- ### `elm-ssr new <name>`
38
-
39
- ```sh
40
- elm-ssr new my-app
41
- ```
42
-
43
- Scaffolds a new app under `examples/<name>/` and registers it in
44
- `elm-ssr.config.json`. Generates `Routes/Index.elm`, `Routes/Counter.elm`,
45
- `Routes/NotFound.elm`, `Islands/Counter.elm`, `View/Shared.elm`, plus the
46
- TypeScript `runtime.ts`, `worker.ts`, `styles.ts`, and `elm.json`.
47
-
48
- Name must match `^[a-z0-9-]+$` (lowercase letters, digits, dashes).
49
-
50
- ### `elm-ssr migrate <up|down|status>`
51
-
52
- SQL-file migration runner. See [docs/migrations.md](migrations.md) for
53
- behaviour. Quick reference:
54
-
55
- ```sh
56
- elm-ssr migrate up # apply pending
57
- elm-ssr migrate down # revert most recent
58
- elm-ssr migrate down --count 3
59
- elm-ssr migrate status # applied + pending
60
-
61
- elm-ssr migrate up --db postgres://user:pass@host:5432/db
62
- elm-ssr migrate up --db sqlite://./app.db
63
- elm-ssr migrate up --db ./app.db # bare path = SQLite
64
-
65
- elm-ssr migrate up --dir ./db/migrations # default ./migrations
66
- elm-ssr migrate up --table schema_history # default __elm_ssr_migrations
67
- ```
68
-
69
- Reads `DATABASE_URL` from the environment if `--db` is omitted.
70
-
71
- ### `elm-ssr routes`
72
-
73
- Prints each configured app with its module + routes directory.
74
-
75
- ```sh
76
- $ elm-ssr routes
77
- basic: root=examples/basic module=Example.Basic routes=src/Example/Basic/Routes
78
- ```
79
-
80
- ### `elm-ssr info`
81
-
82
- Prints the workspace package name + the list of configured app names.
83
-
84
- ### `elm-ssr help`
85
-
86
- Default when no command is given. Lists everything.
87
-
88
- ## Global flag
89
-
90
- `--root <path>` overrides where the CLI looks for `elm-ssr.config.json` and
91
- treats as the workspace root. Default: current working directory.
92
-
93
- ## Configuration
94
-
95
- `elm-ssr.config.json` at the workspace root lists your apps:
96
-
97
- ```jsonc
98
- {
99
- "apps": [
100
- { "name": "basic", "root": "examples/basic", "module": "Example.Basic" },
101
- { "name": "crypto-dashboard","root": "examples/crypto-dashboard","module": "CryptoDashboard" }
102
- ]
103
- }
104
- ```
105
-
106
- - `name` — short identifier; used in `generated/<name>/` paths.
107
- - `root` — the app's directory (relative to the workspace root).
108
- - `module` — the root Elm namespace (period-separated). `Example.Basic`
109
- expects `src/Example/Basic/Routes/`, `src/Example/Basic/Islands/`, etc.
110
-
111
- ## Source
112
-
113
- - Entry point: [packages/elm-ssr/bin/elm-ssr.mjs](../packages/elm-ssr/bin/elm-ssr.mjs)
114
- - Build: [packages/elm-ssr/lib/build.mjs](../packages/elm-ssr/lib/build.mjs)
115
- - Scaffold: [packages/elm-ssr/lib/scaffold.mjs](../packages/elm-ssr/lib/scaffold.mjs)
116
- - Migrate: [packages/elm-ssr/lib/migrate.mjs](../packages/elm-ssr/lib/migrate.mjs)
117
- - Workspace config: [packages/elm-ssr/lib/workspace.mjs](../packages/elm-ssr/lib/workspace.mjs)
package/docs/effects.md DELETED
@@ -1,91 +0,0 @@
1
- # Effects
2
-
3
- Effects are the **single, backend-neutral vocabulary** that loaders and
4
- actions speak. Elm describes what it needs (`{ kind, payload }`); the Worker's
5
- configured adapter executes it against a real backend (KV/D1, Redis/Postgres,
6
- in-memory map, …). This is what lets the same Elm code run on Cloudflare and
7
- locally without change.
8
-
9
- ## The vocabulary
10
-
11
- All effects are available on `Loader`. Actions reuse them via
12
- `Action.fromLoader`.
13
-
14
- | Elm function | `kind` string | What it does |
15
- | ------------ | ------------- | ------------ |
16
- | `Loader.fetchJson { url, decoder }` | `fetchJson` | HTTP GET, JSON-decoded. |
17
- | `Loader.cacheGet { key, decoder }` | `cacheGet` | Cache read; returns `Maybe a`. |
18
- | `Loader.cachePut { key, value, ttlSeconds }` | `cachePut` | Cache write with optional TTL. |
19
- | `Loader.query { sql, params, decoder }` | `query` | SQL select, decode rows. |
20
- | `Loader.queryOne { sql, params, decoder }` | `queryOne` | First row only (`Maybe a`). |
21
- | `Loader.execute { sql, params }` | `execute` | INSERT/UPDATE/DELETE; returns `{ rowsAffected }`. |
22
- | `Loader.env name` | `env` | Read an env var / binding name. |
23
- | `Loader.getCookie name` | `cookie` | Read a request cookie (parsed from the `Cookie` header). Returns `Maybe String`. |
24
- | `Loader.session decoder` | `session` | Read the current session payload (requires `sessionMiddleware`). See [sessions](sessions.md). |
25
- | `Loader.csrfToken` | `csrfToken` | Read the current CSRF token (requires `sessionMiddleware`). See [sessions](sessions.md). |
26
- | `Loader.setSession value` | `setSession` | Replace the session payload. Mark dirty for middleware to persist. See [sessions](sessions.md). |
27
- | `Loader.clearSession` | `clearSession` | Destroy the session. Mark for middleware to delete + clear cookie. See [sessions](sessions.md). |
28
- | `Loader.enqueue { task, payload }` | `enqueue` | Fire-and-forget background work. See [tasks](tasks.md). |
29
-
30
- To **write** a cookie on the response, use `Action.setCookie` /
31
- `Action.clearCookie` / `Action.sessionCookie` from inside an `Action` — see
32
- [Loaders and Actions](loaders-and-actions.md#cookies).
33
-
34
- ## Failure modes
35
-
36
- - `fetchJson` non-2xx → `502 fetchJson received <status> from <url>`.
37
- - `fetchJson` decode failure → `502 Loader response did not match decoder: …`.
38
- - `cacheGet` / `query…` decode failure → `502` with the decode error.
39
- - Missing backend (e.g. no SQL adapter wired in `inMemoryEffects`) →
40
- `502 The "sql" handler is not configured…`.
41
-
42
- Failures are turned into HTTP responses (5xx page or JSON for `/api/`).
43
-
44
- ## How a `kind` resolves to a backend
45
-
46
- The Worker uses an `EffectRunner` you supply (or the default if you supply
47
- none). Adapters are composable functions of type
48
- `EffectRunner -> EffectRunner` that intercept the kinds they care about and
49
- forward everything else.
50
-
51
- ```ts
52
- import { inMemoryEffects, cloudflareEffects } from "elm-ssr/effects";
53
- import { withCache, redisCache, postgresSql } from "elm-ssr/backends";
54
- import { withTasks } from "elm-ssr/tasks";
55
-
56
- // Cloudflare: KV (cacheGet/Put), D1 (query/queryOne/execute), env, cookie, fetchJson.
57
- const effects = withTasks(
58
- cloudflareEffects({ cacheBinding: "CACHE", dbBinding: "DB" }),
59
- { sendEmail, warmCache }
60
- );
61
-
62
- // Local: in-memory cache + env + fetchJson, Redis cache, Postgres SQL.
63
- const effects = withTasks(
64
- withCache(
65
- inMemoryEffects({ sql: postgresSql(pg), env: { /* … */ } }),
66
- redisCache(redis)
67
- ),
68
- { sendEmail, warmCache }
69
- );
70
- ```
71
-
72
- See [Backends](backends.md) for the adapter catalogue and [Tasks and
73
- queues](tasks.md) for `enqueue`.
74
-
75
- ## Defaults if you skip `effects`
76
-
77
- The runtime falls back to `defaultEffectRunner`. It handles only:
78
- - `fetchJson` — real `fetch`.
79
- - `cookie` — parsed from `request.headers["cookie"]`.
80
-
81
- Everything else returns `{ ok: false, error: "Effect \"<kind>\" has no
82
- configured backend…" }`, which surfaces as a 502 to the caller. So a Loader
83
- that just calls `fetchJson` works with zero config; one that touches the cache
84
- or DB needs an adapter.
85
-
86
- ## What next
87
-
88
- - [Backends](backends.md) — assemble an adapter that runs these effects.
89
- - [Tasks and queues](tasks.md) — what `enqueue` actually does.
90
- - Source: [packages/elm-ssr/src/effects.ts](../packages/elm-ssr/src/effects.ts),
91
- [packages/elm-ssr/elm-src/ElmSsr/Loader.elm](../packages/elm-ssr/elm-src/ElmSsr/Loader.elm).
@@ -1,94 +0,0 @@
1
- # Getting started
2
-
3
- elm-ssr is one package: `elm-ssr`. It ships the CLI, the Worker runtime, the
4
- Elm authoring modules, and the SQL migration runner. Use it on Cloudflare
5
- Workers in production or on Bun locally for dev.
6
-
7
- ## Install
8
-
9
- ```sh
10
- bun add elm-ssr
11
- ```
12
-
13
- Make sure Bun ≥ 1.3 (`engines.bun` in the package).
14
-
15
- ## Scaffold a new app
16
-
17
- In a fresh workspace, create `elm-ssr.config.json`:
18
-
19
- ```jsonc
20
- {
21
- "apps": []
22
- }
23
- ```
24
-
25
- Then:
26
-
27
- ```sh
28
- bun elm-ssr new my-app
29
- ```
30
-
31
- This creates `examples/my-app/` (or whatever you name it) with:
32
- - `elm.json` — Elm package manifest
33
- - `runtime.ts` — wires the Worker (`createWorkerApp`)
34
- - `worker.ts` — entry point (`export default worker`)
35
- - `styles.ts` — inlined stylesheet
36
- - `src/<Namespace>/Routes/Index.elm`, `Counter.elm`, `NotFound.elm`
37
- - `src/<Namespace>/Islands/Counter.elm`
38
- - `src/<Namespace>/View/Shared.elm`
39
-
40
- It also adds the app to `elm-ssr.config.json`.
41
-
42
- ## Build
43
-
44
- ```sh
45
- bun elm-ssr build
46
- ```
47
-
48
- For each configured app the CLI:
49
- 1. Scans `src/<Namespace>/Routes/` and `src/<Namespace>/Islands/`.
50
- 2. Generates `<app-root>/.elm-ssr/Main.elm` (the router, with file-based
51
- routes + dynamic segments).
52
- 3. Syncs the Elm authoring modules (`ElmSsr/*.elm`) into
53
- `<app-root>/.elm-ssr/src/ElmSsr/`.
54
- 4. Runs `elm make` to compile the page program and one combined island bundle
55
- (`Elm.<App>.Islands.<Name>` per island).
56
-
57
- Outputs land in `generated/<app-name>/` (gitignored).
58
-
59
- ## Run locally
60
-
61
- ```sh
62
- bun elm-ssr dev
63
- ```
64
-
65
- That runs `build` then `wrangler dev`. The same Worker handler is used both on
66
- Bun (via wrangler) and on Cloudflare in production.
67
-
68
- ## Project layout
69
-
70
- ```
71
- elm-ssr.config.json # Lists your apps
72
- package.json # Workspaces, scripts
73
- examples/
74
- my-app/
75
- elm.json
76
- runtime.ts # createWorkerApp(...)
77
- worker.ts # export default worker
78
- src/MyApp/
79
- Routes/
80
- Index.elm
81
- Islands/
82
- Counter.elm
83
- View/
84
- Shared.elm
85
- migrations/ # Optional: SQL files for `elm-ssr migrate`
86
- generated/ # Build output (gitignored)
87
- ```
88
-
89
- ## What next
90
-
91
- - [Routing](routing.md) — adding pages with file-based routes.
92
- - [Loaders and Actions](loaders-and-actions.md) — fetching data and handling
93
- form submissions.
94
- - [Islands](islands.md) — adding interactive bits to otherwise-static pages.
package/docs/islands.md DELETED
@@ -1,197 +0,0 @@
1
- # Islands
2
-
3
- An island is a normal `Browser.element` Elm program that mounts client-side
4
- into an SSR-rendered page. It uses **stock** `elm/html`, `elm/svg`, `elm/http`,
5
- `Html.Keyed`, etc. — there is no custom virtual-DOM patcher, no fake
6
- hydration. The server emits a marker element with encoded flags + optional
7
- fallback markup; the browser runtime finds the marker and runs the island's
8
- `main`.
9
-
10
- This keeps islands fully compatible with the broader Elm package ecosystem.
11
-
12
- ## Authoring an island
13
-
14
- Islands live under `src/<App>/Islands/`. The build picks them up
15
- automatically. Each module should expose `embed` (called from pages) plus
16
- `main` (mounted client-side):
17
-
18
- ```elm
19
- module Demo.Islands.Counter exposing (embed, main)
20
-
21
- import Browser
22
- import ElmSsr.Island as Island
23
- import ElmSsr.Html as SsrHtml
24
- import ElmSsr.Html.Attributes as SsrAttributes
25
- import Html exposing (Html, button, div, span, text)
26
- import Html.Attributes exposing (class)
27
- import Html.Events exposing (onClick)
28
- import Json.Encode as Encode
29
-
30
-
31
- -- SSR-side: page calls `Counter.embed { start = 0 }`.
32
- embed : Flags -> Island.Node msg
33
- embed =
34
- Island.embed "Counter"
35
- { encodeFlags = \f -> Encode.object [ ( "start", Encode.int f.start ) ]
36
- , fallback = \f -> [ SsrHtml.text (String.fromInt f.start) ]
37
- , id = Nothing
38
- }
39
-
40
-
41
- -- Client-side: stock Browser.element with stock elm/html.
42
- type alias Flags =
43
- { start : Int }
44
-
45
-
46
- type alias Model =
47
- { count : Int }
48
-
49
-
50
- type Msg
51
- = Increment
52
- | Decrement
53
-
54
-
55
- main : Program Flags Model Msg
56
- main =
57
- Browser.element
58
- { init = \flags -> ( { count = flags.start }, Cmd.none )
59
- , update = update
60
- , view = view
61
- , subscriptions = \_ -> Sub.none
62
- }
63
-
64
-
65
- update : Msg -> Model -> ( Model, Cmd Msg )
66
- update msg model =
67
- case msg of
68
- Increment -> ( { model | count = model.count + 1 }, Cmd.none )
69
- Decrement -> ( { model | count = model.count - 1 }, Cmd.none )
70
-
71
-
72
- view : Model -> Html Msg
73
- view model =
74
- div [ class "counter" ]
75
- [ button [ onClick Decrement ] [ text "-" ]
76
- , span [] [ text (String.fromInt model.count) ]
77
- , button [ onClick Increment ] [ text "+" ]
78
- ]
79
- ```
80
-
81
- A page embeds the island the same way it would render anything else:
82
-
83
- ```elm
84
- import Demo.Islands.Counter as Counter
85
-
86
- view : Document Never
87
- view =
88
- Page.page
89
- { title = "Counter"
90
- , head = []
91
- , body = [ Counter.embed { start = 0 } ]
92
- }
93
- ```
94
-
95
- There is **no `Generated.Islands` re-export.** Authors import island modules
96
- directly. Codegen is reserved for things the author never touches
97
- (`Main.elm`, the islands client program, the manifest).
98
-
99
- ## `Island.embed` and `Config`
100
-
101
- ```elm
102
- type alias Config flags pageMsg =
103
- { encodeFlags : flags -> Encode.Value
104
- , fallback : flags -> List (Node pageMsg)
105
- , id : Maybe String
106
- }
107
-
108
- embed : String -> Config flags pageMsg -> flags -> Node pageMsg
109
- ```
110
-
111
- - `name` must match the generated bundle key (the module's basename, e.g.
112
- `"Counter"` for `Islands/Counter.elm`).
113
- - `encodeFlags` serializes the island's flags into the SSR markup; the client
114
- runtime decodes them and feeds them to `init`.
115
- - `fallback` is SSR-only markup shown before the client bundle mounts — show
116
- initial server-rendered state, a spinner, or nothing.
117
- - `id` lets the island **persist across SPA navigation** (see below).
118
-
119
- ## Pages use `ElmSsr.Html`, islands use `elm/html`
120
-
121
- This is the load-bearing convention in this repo:
122
-
123
- - A **page** returns `Document Never` and is serialised on the server to
124
- HTML. It uses `ElmSsr.Html` because Cloudflare Workers has no DOM, and
125
- `elm/html`'s virtual-DOM kernel can't run server-side.
126
- - An **island** is a normal browser Elm program. Its `view : Model -> Html Msg`
127
- uses `Html exposing (...)` from `elm/html`. `ElmSsr.Html` only shows up
128
- inside the island module for the `fallback` markup (which is part of the
129
- page tree, not the island's runtime).
130
-
131
- Don't "fix" an island view to use `ElmSsr.Html` — that's the pre-pivot
132
- architecture that was deleted.
133
-
134
- ## Cross-island state
135
-
136
- Islands are isolated from each other. To communicate, use
137
- `ElmSsr.Island.Shared`, which provides a `window` CustomEvent bus.
138
-
139
- ```elm
140
- import ElmSsr.Island.Shared as Shared
141
-
142
-
143
- -- Send to all other islands:
144
- update msg model =
145
- ( newModel, Shared.broadcast "cart:add" (encodeItem item) )
146
-
147
-
148
- -- Listen for broadcasts. A broadcaster also hears its own broadcast —
149
- -- filter by tag in update:
150
- subscriptions _ =
151
- Shared.listen GotGlobalEvent
152
-
153
-
154
- update msg model =
155
- case msg of
156
- GotGlobalEvent { tag, payload } ->
157
- if tag == "cart:add" then ... else (model, Cmd.none)
158
- ```
159
-
160
- `GlobalEvent` is `{ tag : String, payload : Value }`.
161
-
162
- ## Persistence across SPA navigation
163
-
164
- When a user clicks an in-app link, the client runtime fetches the new page
165
- via `/api/render`, swaps `#elm-ssr-root` innerHTML, re-boots islands, and
166
- syncs the `<head>`. By default, every island re-initializes from its new
167
- flags.
168
-
169
- To **keep an island alive** across navigation, give it an `id`:
170
-
171
- ```elm
172
- Island.embed "MiniCart"
173
- { encodeFlags = encodeFlags
174
- , fallback = ...
175
- , id = Just "mini-cart" -- ← persist
176
- }
177
- ```
178
-
179
- The runtime transfers the live marker (with its full subtree and Elm runtime
180
- state) into the new page if a marker with the same `id` is present.
181
- Non-persistent islands are torn down — their `window` broadcast listeners are
182
- removed, but Elm has no program teardown, so subscriptions to timers/ports
183
- continue running until the page reloads. Keep that in mind for long-lived
184
- non-persistent islands.
185
-
186
- ## What next
187
-
188
- - [Routing](routing.md) — where the page that embeds the island lives.
189
- - [examples/crypto-dashboard/](../examples/crypto-dashboard/) — islands using
190
- `elm/svg`, `elm/http`, `Html.Keyed`, 15s `Time.every` refresh, and the
191
- cross-island bus.
192
-
193
- ## Source
194
-
195
- - [packages/elm-ssr/elm-src/ElmSsr/Island.elm](../packages/elm-ssr/elm-src/ElmSsr/Island.elm)
196
- - [packages/elm-ssr/elm-src/ElmSsr/Island/Shared.elm](../packages/elm-ssr/elm-src/ElmSsr/Island/Shared.elm)
197
- - [packages/elm-ssr/src/client-runtime/islands.ts](../packages/elm-ssr/src/client-runtime/islands.ts)