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.
- package/README.md +48 -342
- package/elm-src/ElmSsr/Island/Sse.elm +151 -0
- package/package.json +53 -24
- package/{packages/elm-ssr/src → src}/client-runtime/islands.ts +113 -15
- package/src/sse.ts +162 -0
- package/AGENTS.md +0 -289
- package/CHANGELOG.md +0 -87
- package/LICENSE +0 -21
- package/bun.lock +0 -259
- package/docker-compose.yml +0 -33
- package/docs/README.md +0 -51
- package/docs/backends.md +0 -146
- package/docs/cli.md +0 -117
- package/docs/effects.md +0 -91
- package/docs/getting-started.md +0 -94
- package/docs/islands.md +0 -197
- package/docs/loaders-and-actions.md +0 -241
- package/docs/middleware.md +0 -93
- package/docs/migrations.md +0 -143
- package/docs/routing.md +0 -108
- package/docs/sessions.md +0 -218
- package/docs/tasks.md +0 -149
- package/docs/testing.md +0 -84
- package/elm-ssr.config.json +0 -14
- package/examples/basic/elm.json +0 -27
- package/examples/basic/migrations/0001_guestbook.down.sql +0 -1
- package/examples/basic/migrations/0001_guestbook.sql +0 -10
- package/examples/basic/package.json +0 -10
- package/examples/basic/runtime.ts +0 -148
- package/examples/basic/src/Example/Basic/Islands/Counter.elm +0 -110
- package/examples/basic/src/Example/Basic/Islands/Observer.elm +0 -67
- package/examples/basic/src/Example/Basic/Islands/Tasks.elm +0 -151
- package/examples/basic/src/Example/Basic/Routes/Chart.elm +0 -87
- package/examples/basic/src/Example/Basic/Routes/Counter.elm +0 -42
- package/examples/basic/src/Example/Basic/Routes/Echo.elm +0 -76
- package/examples/basic/src/Example/Basic/Routes/Greet/Name_.elm +0 -37
- package/examples/basic/src/Example/Basic/Routes/Guestbook.elm +0 -86
- package/examples/basic/src/Example/Basic/Routes/Index.elm +0 -41
- package/examples/basic/src/Example/Basic/Routes/NotFound.elm +0 -37
- package/examples/basic/src/Example/Basic/Routes/Profile.elm +0 -112
- package/examples/basic/src/Example/Basic/Routes/Session.elm +0 -89
- package/examples/basic/src/Example/Basic/Routes/Status.elm +0 -90
- package/examples/basic/src/Example/Basic/View/Shared.elm +0 -60
- package/examples/basic/styles.ts +0 -204
- package/examples/basic/worker.ts +0 -3
- package/examples/crypto-dashboard/elm.json +0 -30
- package/examples/crypto-dashboard/package.json +0 -10
- package/examples/crypto-dashboard/runtime.ts +0 -97
- package/examples/crypto-dashboard/src/CryptoDashboard/Islands/MarketOverview.elm +0 -204
- package/examples/crypto-dashboard/src/CryptoDashboard/Islands/PriceChart.elm +0 -200
- package/examples/crypto-dashboard/src/CryptoDashboard/Routes/Index.elm +0 -67
- package/examples/crypto-dashboard/src/CryptoDashboard/Routes/NotFound.elm +0 -30
- package/examples/crypto-dashboard/src/CryptoDashboard/View/Shared.elm +0 -39
- package/examples/crypto-dashboard/styles.ts +0 -23
- package/examples/crypto-dashboard/worker.ts +0 -3
- package/llms.txt +0 -69
- package/packages/elm-ssr/README.md +0 -67
- package/packages/elm-ssr/package.json +0 -61
- package/scripts/benchmark.mjs +0 -60
- package/test/action.test.ts +0 -81
- package/test/adapters.test.ts +0 -173
- package/test/advanced-robustness.test.ts +0 -75
- package/test/app.test.ts +0 -209
- package/test/browser-island.test.ts +0 -184
- package/test/cli-migrate.test.ts +0 -97
- package/test/cli.test.ts +0 -94
- package/test/cookies.test.ts +0 -156
- package/test/crypto-dashboard.test.ts +0 -35
- package/test/effects.test.ts +0 -117
- package/test/http.test.ts +0 -50
- package/test/integration/redis-postgres.test.ts +0 -174
- package/test/island-runtime.test.ts +0 -214
- package/test/middleware.test.ts +0 -134
- package/test/migrations.test.ts +0 -244
- package/test/profile.test.ts +0 -159
- package/test/robustness.test.ts +0 -135
- package/test/serialize.test.ts +0 -92
- package/test/sessions.test.ts +0 -429
- package/test/svg.test.ts +0 -65
- package/tsconfig.json +0 -20
- package/wrangler.jsonc +0 -11
- /package/{packages/elm-ssr/bin → bin}/elm-ssr.mjs +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Action.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document/Encode.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document/Events.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html/Attributes.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html/Events.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Island/Shared.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Island.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Loader.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Page.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Route.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Runtime.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Svg/Attributes.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Svg.elm +0 -0
- /package/{packages/elm-ssr/lib → lib}/build.mjs +0 -0
- /package/{packages/elm-ssr/lib → lib}/migrate.mjs +0 -0
- /package/{packages/elm-ssr/lib → lib}/scaffold.mjs +0 -0
- /package/{packages/elm-ssr/lib → lib}/workspace.mjs +0 -0
- /package/{packages/elm-ssr/src → src}/app.ts +0 -0
- /package/{packages/elm-ssr/src → src}/backends.ts +0 -0
- /package/{packages/elm-ssr/src → src}/effects.ts +0 -0
- /package/{packages/elm-ssr/src → src}/http.ts +0 -0
- /package/{packages/elm-ssr/src → src}/middleware.ts +0 -0
- /package/{packages/elm-ssr/src → src}/migrations.ts +0 -0
- /package/{packages/elm-ssr/src → src}/protocol.ts +0 -0
- /package/{packages/elm-ssr/src → src}/render.ts +0 -0
- /package/{packages/elm-ssr/src → src}/request-handler.ts +0 -0
- /package/{packages/elm-ssr/src → src}/response-headers.ts +0 -0
- /package/{packages/elm-ssr/src → src}/serialize.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/crypto.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/effects.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/index.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/middleware.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/store.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/types.ts +0 -0
- /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).
|
package/docs/getting-started.md
DELETED
|
@@ -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)
|