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
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Routes.NotFound exposing (page, action)
|
|
2
|
-
|
|
3
|
-
import ElmSsr.Action as Action exposing (Action)
|
|
4
|
-
import ElmSsr.Document exposing (Document)
|
|
5
|
-
import ElmSsr.Html exposing (a, article, h1, p, text)
|
|
6
|
-
import ElmSsr.Html.Attributes exposing (class, href)
|
|
7
|
-
import ElmSsr.Loader as Loader exposing (Loader)
|
|
8
|
-
import ElmSsr.Page as Page
|
|
9
|
-
import ElmSsr.Route exposing (Request)
|
|
10
|
-
import Example.Basic.View.Shared as Shared
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
page : Request -> Loader (Document Never)
|
|
14
|
-
page _ =
|
|
15
|
-
Loader.succeed view
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
action : Request -> Action (Document Never)
|
|
19
|
-
action _ =
|
|
20
|
-
Action.fail 405 "Method not allowed"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
view : Document Never
|
|
24
|
-
view =
|
|
25
|
-
Page.notFound
|
|
26
|
-
{ title = "Not Found | elm-ssr"
|
|
27
|
-
, head = Shared.baseHead
|
|
28
|
-
, body =
|
|
29
|
-
[ Shared.layout
|
|
30
|
-
[ article [ class "panel" ]
|
|
31
|
-
[ h1 [] [ text "404" ]
|
|
32
|
-
, p [] [ text "This route is not registered in the example app." ]
|
|
33
|
-
, a [ class "link", href "/" ] [ text "Back to home" ]
|
|
34
|
-
]
|
|
35
|
-
]
|
|
36
|
-
]
|
|
37
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Routes.Profile exposing (action, page)
|
|
2
|
-
|
|
3
|
-
{-| File-based routing: GET /profile reads the current session via
|
|
4
|
-
`Loader.session`; POST /profile?op=login sets it via `Loader.setSession`
|
|
5
|
-
(lifted into the action with `Action.fromLoader`); POST /profile?op=logout
|
|
6
|
-
calls `Loader.clearSession`. Demonstrates the full session+CSRF round-trip.
|
|
7
|
-
|
|
8
|
-
The TS side wires `sessionMiddleware` + `csrfMiddleware` +
|
|
9
|
-
`sessionEffects(runner)` in runtime.ts; this Elm module never touches the
|
|
10
|
-
underlying cookie itself.
|
|
11
|
-
-}
|
|
12
|
-
|
|
13
|
-
import ElmSsr.Action as Action exposing (Action)
|
|
14
|
-
import ElmSsr.Document exposing (Document)
|
|
15
|
-
import ElmSsr.Html exposing (button, form, h1, input, label, p, section, span, text)
|
|
16
|
-
import ElmSsr.Html.Attributes as Attr
|
|
17
|
-
import ElmSsr.Loader as Loader exposing (Loader)
|
|
18
|
-
import ElmSsr.Route as Route exposing (Request)
|
|
19
|
-
import Example.Basic.View.Shared as Shared
|
|
20
|
-
import Json.Decode as Decode
|
|
21
|
-
import Json.Encode as Encode
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
type alias Profile =
|
|
25
|
-
{ username : String }
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
profileDecoder : Decode.Decoder Profile
|
|
29
|
-
profileDecoder =
|
|
30
|
-
Decode.map Profile (Decode.field "username" Decode.string)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
encodeProfile : Profile -> Encode.Value
|
|
34
|
-
encodeProfile profile =
|
|
35
|
-
Encode.object [ ( "username", Encode.string profile.username ) ]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
page : Request -> Loader (Document Never)
|
|
39
|
-
page _ =
|
|
40
|
-
Loader.map2 view (Loader.session profileDecoder) Loader.csrfToken
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
action : Request -> Action (Document Never)
|
|
44
|
-
action request =
|
|
45
|
-
case Route.query "op" request of
|
|
46
|
-
Just "logout" ->
|
|
47
|
-
Action.fromLoader Loader.clearSession
|
|
48
|
-
|> Action.andThen (\_ -> Action.redirect "/profile")
|
|
49
|
-
|
|
50
|
-
_ ->
|
|
51
|
-
case Maybe.map String.trim (Route.formValue "username" request) of
|
|
52
|
-
Just username ->
|
|
53
|
-
if String.isEmpty username then
|
|
54
|
-
Action.fail 422 "Username is required."
|
|
55
|
-
|
|
56
|
-
else
|
|
57
|
-
Action.fromLoader (Loader.setSession (encodeProfile { username = username }))
|
|
58
|
-
|> Action.andThen (\_ -> Action.redirect "/profile")
|
|
59
|
-
|
|
60
|
-
Nothing ->
|
|
61
|
-
Action.fail 422 "Username is required."
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
view : Maybe Profile -> Maybe String -> Document Never
|
|
65
|
-
view profile token =
|
|
66
|
-
Shared.pageDocument "Profile"
|
|
67
|
-
[ section [ Attr.class "panel" ]
|
|
68
|
-
(case profile of
|
|
69
|
-
Just { username } ->
|
|
70
|
-
[ h1 [] [ text "Signed in" ]
|
|
71
|
-
, p [] [ text "Hello, ", span [ Attr.class "value" ] [ text username ], text "." ]
|
|
72
|
-
, form
|
|
73
|
-
[ Attr.method "post", Attr.action "/profile?op=logout" ]
|
|
74
|
-
(csrfField token
|
|
75
|
-
++ [ button [ Attr.type_ "submit", Attr.class "button" ] [ text "Sign out" ] ]
|
|
76
|
-
)
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
Nothing ->
|
|
80
|
-
[ h1 [] [ text "Sign in" ]
|
|
81
|
-
, p [] [ text "No session yet." ]
|
|
82
|
-
, form
|
|
83
|
-
[ Attr.method "post", Attr.action "/profile", Attr.class "form" ]
|
|
84
|
-
(csrfField token
|
|
85
|
-
++ [ label [ Attr.for "username" ] [ text "Username" ]
|
|
86
|
-
, input
|
|
87
|
-
[ Attr.id "username"
|
|
88
|
-
, Attr.name "username"
|
|
89
|
-
, Attr.type_ "text"
|
|
90
|
-
, Attr.class "input"
|
|
91
|
-
]
|
|
92
|
-
, button [ Attr.type_ "submit", Attr.class "button" ] [ text "Sign in" ]
|
|
93
|
-
]
|
|
94
|
-
)
|
|
95
|
-
]
|
|
96
|
-
)
|
|
97
|
-
]
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
csrfField : Maybe String -> List (ElmSsr.Html.Node msg)
|
|
101
|
-
csrfField token =
|
|
102
|
-
case token of
|
|
103
|
-
Just value ->
|
|
104
|
-
[ input
|
|
105
|
-
[ Attr.type_ "hidden"
|
|
106
|
-
, Attr.name "_csrf"
|
|
107
|
-
, Attr.value value
|
|
108
|
-
]
|
|
109
|
-
]
|
|
110
|
-
|
|
111
|
-
Nothing ->
|
|
112
|
-
[]
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Routes.Session exposing (action, page)
|
|
2
|
-
|
|
3
|
-
{-| File-based routing: GET /session reads the "session" cookie and shows who
|
|
4
|
-
you are; POST /session?op=login sets a hardened session cookie via
|
|
5
|
-
`Action.sessionCookie`; POST /session?op=logout clears it. Demonstrates both
|
|
6
|
-
sides of the cookie API end-to-end with no client JS.
|
|
7
|
-
-}
|
|
8
|
-
|
|
9
|
-
import ElmSsr.Action as Action exposing (Action)
|
|
10
|
-
import ElmSsr.Document exposing (Document)
|
|
11
|
-
import ElmSsr.Html exposing (button, form, h1, input, label, p, section, span, text)
|
|
12
|
-
import ElmSsr.Html.Attributes as Attr
|
|
13
|
-
import ElmSsr.Loader as Loader exposing (Loader)
|
|
14
|
-
import ElmSsr.Route as Route exposing (Request)
|
|
15
|
-
import Example.Basic.View.Shared as Shared
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
page : Request -> Loader (Document Never)
|
|
19
|
-
page _ =
|
|
20
|
-
Loader.getCookie "session"
|
|
21
|
-
|> Loader.map view
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
action : Request -> Action (Document Never)
|
|
25
|
-
action request =
|
|
26
|
-
case Route.query "op" request of
|
|
27
|
-
Just "logout" ->
|
|
28
|
-
Action.redirect "/session"
|
|
29
|
-
|> Action.clearCookie { name = "session", path = Just "/", domain = Nothing }
|
|
30
|
-
|
|
31
|
-
_ ->
|
|
32
|
-
case Maybe.map String.trim (Route.formValue "username" request) of
|
|
33
|
-
Just username ->
|
|
34
|
-
if String.isEmpty username then
|
|
35
|
-
Action.fail 422 "Username is required."
|
|
36
|
-
|
|
37
|
-
else
|
|
38
|
-
-- For the example, the username *is* the session id. A real app
|
|
39
|
-
-- would mint a server-side session token, store it (KV/D1/Redis),
|
|
40
|
-
-- and put the token in the cookie.
|
|
41
|
-
Action.redirect "/session"
|
|
42
|
-
|> Action.setCookie (devSessionCookie "session" username)
|
|
43
|
-
|
|
44
|
-
Nothing ->
|
|
45
|
-
Action.fail 422 "Username is required."
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{-| The example runs over plain HTTP in tests + `wrangler dev`, so we mint a
|
|
49
|
-
session cookie with `Secure=False`. Production code uses `Action.sessionCookie`
|
|
50
|
-
unchanged.
|
|
51
|
-
-}
|
|
52
|
-
devSessionCookie : String -> String -> Action.Cookie
|
|
53
|
-
devSessionCookie name value =
|
|
54
|
-
let
|
|
55
|
-
secure =
|
|
56
|
-
Action.sessionCookie name value
|
|
57
|
-
in
|
|
58
|
-
{ secure | secure = False }
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
view : Maybe String -> Document Never
|
|
62
|
-
view session =
|
|
63
|
-
Shared.pageDocument "Session"
|
|
64
|
-
[ section [ Attr.class "panel" ]
|
|
65
|
-
(case session of
|
|
66
|
-
Just user ->
|
|
67
|
-
[ h1 [] [ text "Signed in" ]
|
|
68
|
-
, p [] [ text "Welcome back, ", span [ Attr.class "value" ] [ text user ], text "." ]
|
|
69
|
-
, form [ Attr.method "post", Attr.action "/session?op=logout" ]
|
|
70
|
-
[ button [ Attr.type_ "submit", Attr.class "button" ] [ text "Sign out" ] ]
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
Nothing ->
|
|
74
|
-
[ h1 [] [ text "Sign in" ]
|
|
75
|
-
, p [] [ text "No session cookie was sent." ]
|
|
76
|
-
, form [ Attr.method "post", Attr.action "/session", Attr.class "form" ]
|
|
77
|
-
[ label [ Attr.for "username" ] [ text "Username" ]
|
|
78
|
-
, input
|
|
79
|
-
[ Attr.id "username"
|
|
80
|
-
, Attr.name "username"
|
|
81
|
-
, Attr.type_ "text"
|
|
82
|
-
, Attr.class "input"
|
|
83
|
-
, Attr.attr "autocomplete" "username"
|
|
84
|
-
]
|
|
85
|
-
, button [ Attr.type_ "submit", Attr.class "button" ] [ text "Sign in" ]
|
|
86
|
-
]
|
|
87
|
-
]
|
|
88
|
-
)
|
|
89
|
-
]
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Routes.Status exposing (action, page)
|
|
2
|
-
|
|
3
|
-
import ElmSsr.Action as Action exposing (Action)
|
|
4
|
-
import ElmSsr.Document exposing (Document)
|
|
5
|
-
import ElmSsr.Html exposing (Node, h1, li, p, section, span, text, ul)
|
|
6
|
-
import ElmSsr.Html.Attributes exposing (class)
|
|
7
|
-
import ElmSsr.Loader as Loader exposing (Loader)
|
|
8
|
-
import ElmSsr.Route exposing (Request)
|
|
9
|
-
import Example.Basic.View.Shared as Shared
|
|
10
|
-
import Json.Decode as Decode
|
|
11
|
-
import Json.Encode as Encode
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type alias Status =
|
|
15
|
-
{ uptime : String
|
|
16
|
-
, region : String
|
|
17
|
-
, builds : Int
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
page : Request -> Loader (Document Never)
|
|
22
|
-
page _ =
|
|
23
|
-
Loader.map2 view cachedStatus (Loader.env "GREETING")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{-| Read the status from the cache; on a miss, fetch it and populate the cache
|
|
27
|
-
(with a 60s TTL). Backend-neutral: cache is KV on Cloudflare, Redis/Map locally.
|
|
28
|
-
-}
|
|
29
|
-
cachedStatus : Loader Status
|
|
30
|
-
cachedStatus =
|
|
31
|
-
Loader.cacheGet { key = "status", decoder = decoder }
|
|
32
|
-
|> Loader.andThen
|
|
33
|
-
(\cached ->
|
|
34
|
-
case cached of
|
|
35
|
-
Just status ->
|
|
36
|
-
Loader.succeed status
|
|
37
|
-
|
|
38
|
-
Nothing ->
|
|
39
|
-
Loader.fetchJson { url = "app://status", decoder = decoder }
|
|
40
|
-
|> Loader.andThen
|
|
41
|
-
(\status ->
|
|
42
|
-
Loader.cachePut { key = "status", value = encode status, ttlSeconds = Just 60 }
|
|
43
|
-
|> Loader.map (\_ -> status)
|
|
44
|
-
)
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
action : Request -> Action (Document Never)
|
|
49
|
-
action _ =
|
|
50
|
-
Action.fail 405 "Method not allowed"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
decoder : Decode.Decoder Status
|
|
54
|
-
decoder =
|
|
55
|
-
Decode.map3 Status
|
|
56
|
-
(Decode.field "uptime" Decode.string)
|
|
57
|
-
(Decode.field "region" Decode.string)
|
|
58
|
-
(Decode.field "builds" Decode.int)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
encode : Status -> Encode.Value
|
|
62
|
-
encode status =
|
|
63
|
-
Encode.object
|
|
64
|
-
[ ( "uptime", Encode.string status.uptime )
|
|
65
|
-
, ( "region", Encode.string status.region )
|
|
66
|
-
, ( "builds", Encode.int status.builds )
|
|
67
|
-
]
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
view : Status -> Maybe String -> Document Never
|
|
71
|
-
view data greeting =
|
|
72
|
-
Shared.pageDocument "Edge Status"
|
|
73
|
-
[ statusSection data greeting
|
|
74
|
-
, Shared.featureSection
|
|
75
|
-
]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
statusSection : Status -> Maybe String -> Node msg
|
|
79
|
-
statusSection data greeting =
|
|
80
|
-
section [ class "panel" ]
|
|
81
|
-
[ span [ class "eyebrow" ] [ text "Loader page (server only)" ]
|
|
82
|
-
, h1 [] [ text "Edge status" ]
|
|
83
|
-
, p [] [ text "This page has no Model and no Msg. A Loader read the cache (filling it from a fetch on a miss) and read an env value on the server, then rendered once with no client runtime." ]
|
|
84
|
-
, ul [ class "list" ]
|
|
85
|
-
[ li [] [ text ("Uptime: " ++ data.uptime) ]
|
|
86
|
-
, li [] [ text ("Region: " ++ data.region) ]
|
|
87
|
-
, li [] [ text ("Builds: " ++ String.fromInt data.builds) ]
|
|
88
|
-
, li [] [ text ("Env GREETING: " ++ Maybe.withDefault "—" greeting) ]
|
|
89
|
-
]
|
|
90
|
-
]
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
module Example.Basic.View.Shared exposing (baseHead, featureSection, layout, pageDocument)
|
|
2
|
-
|
|
3
|
-
import ElmSsr.Document exposing (Document)
|
|
4
|
-
import ElmSsr.Html exposing (Node, a, article, div, h2, h3, header, main_, nav, p, section, span, text)
|
|
5
|
-
import ElmSsr.Html.Attributes exposing (class, href)
|
|
6
|
-
import ElmSsr.Page as Page
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
pageDocument : String -> List (Node msg) -> Document msg
|
|
10
|
-
pageDocument pageTitle sections =
|
|
11
|
-
Page.page
|
|
12
|
-
{ title = pageTitle ++ " | elm-ssr"
|
|
13
|
-
, head = baseHead
|
|
14
|
-
, body = [ layout sections ]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
baseHead : List (Node msg)
|
|
19
|
-
baseHead =
|
|
20
|
-
[ Page.metaCharset "utf-8"
|
|
21
|
-
, Page.metaViewport "width=device-width, initial-scale=1"
|
|
22
|
-
, Page.metaName "description" "Elm SSR library prototype with client-side updates."
|
|
23
|
-
, Page.stylesheet "/styles.css"
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
layout : List (Node msg) -> Node msg
|
|
28
|
-
layout sections =
|
|
29
|
-
div [ class "shell" ]
|
|
30
|
-
[ header [ class "topbar" ]
|
|
31
|
-
[ div [ class "brand" ]
|
|
32
|
-
[ span [ class "brand-badge" ] [ text "elm-ssr" ]
|
|
33
|
-
, h2 [ class "brand-title" ] [ text "Library Example" ]
|
|
34
|
-
]
|
|
35
|
-
, nav [ class "nav" ]
|
|
36
|
-
[ a [ class "nav-link", href "/" ] [ text "Home" ]
|
|
37
|
-
, a [ class "nav-link", href "/status" ] [ text "Status" ]
|
|
38
|
-
, a [ class "nav-link", href "/counter" ] [ text "Counter" ]
|
|
39
|
-
]
|
|
40
|
-
]
|
|
41
|
-
, main_ [ class "content" ] sections
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
featureSection : Node msg
|
|
46
|
-
featureSection =
|
|
47
|
-
section [ class "grid" ]
|
|
48
|
-
[ article [ class "panel" ]
|
|
49
|
-
[ h3 [] [ text "Page" ]
|
|
50
|
-
, p [] [ text "Route + Loader + view. Stateless, no client runtime, Document Never enforces it." ]
|
|
51
|
-
]
|
|
52
|
-
, article [ class "panel" ]
|
|
53
|
-
[ h3 [] [ text "Loader" ]
|
|
54
|
-
, p [] [ text "Describe the data a route needs. The Worker runs the IO and feeds results back, fully typed." ]
|
|
55
|
-
]
|
|
56
|
-
, article [ class "panel" ]
|
|
57
|
-
[ h3 [] [ text "Interactive" ]
|
|
58
|
-
, p [] [ text "Opt into the MVU loop only where you need it. Events patch the server-rendered DOM in place." ]
|
|
59
|
-
]
|
|
60
|
-
]
|
package/examples/basic/styles.ts
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
export const stylesheet = `
|
|
2
|
-
:root {
|
|
3
|
-
color-scheme: light;
|
|
4
|
-
--bg: #f4efe7;
|
|
5
|
-
--panel: rgba(255, 251, 244, 0.9);
|
|
6
|
-
--ink: #1d1b19;
|
|
7
|
-
--muted: #5a554f;
|
|
8
|
-
--accent: #155848;
|
|
9
|
-
--accent-soft: #dcebe5;
|
|
10
|
-
--line: rgba(29, 27, 25, 0.08);
|
|
11
|
-
--shadow: 0 18px 60px rgba(21, 88, 72, 0.14);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
* {
|
|
15
|
-
box-sizing: border-box;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
html,
|
|
19
|
-
body {
|
|
20
|
-
margin: 0;
|
|
21
|
-
min-height: 100%;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
body {
|
|
25
|
-
background:
|
|
26
|
-
radial-gradient(circle at top left, rgba(21, 88, 72, 0.12), transparent 34%),
|
|
27
|
-
linear-gradient(180deg, #fbf8f2 0%, var(--bg) 100%);
|
|
28
|
-
color: var(--ink);
|
|
29
|
-
font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
a {
|
|
33
|
-
color: inherit;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.shell {
|
|
37
|
-
width: min(1120px, calc(100vw - 32px));
|
|
38
|
-
margin: 0 auto;
|
|
39
|
-
padding: 24px 0 64px;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.topbar {
|
|
43
|
-
display: flex;
|
|
44
|
-
align-items: center;
|
|
45
|
-
justify-content: space-between;
|
|
46
|
-
gap: 18px;
|
|
47
|
-
padding: 10px 0 24px;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.brand {
|
|
51
|
-
display: flex;
|
|
52
|
-
align-items: center;
|
|
53
|
-
gap: 14px;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.brand-badge {
|
|
57
|
-
border-radius: 999px;
|
|
58
|
-
padding: 6px 10px;
|
|
59
|
-
background: var(--accent-soft);
|
|
60
|
-
color: var(--accent);
|
|
61
|
-
font-size: 12px;
|
|
62
|
-
font-weight: 700;
|
|
63
|
-
letter-spacing: 0.08em;
|
|
64
|
-
text-transform: uppercase;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.brand-title {
|
|
68
|
-
margin: 0;
|
|
69
|
-
font-size: 1.1rem;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.nav {
|
|
73
|
-
display: flex;
|
|
74
|
-
flex-wrap: wrap;
|
|
75
|
-
gap: 10px;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.nav-link,
|
|
79
|
-
.button-link,
|
|
80
|
-
.link {
|
|
81
|
-
text-decoration: none;
|
|
82
|
-
font-weight: 700;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.nav-link,
|
|
86
|
-
.button-link {
|
|
87
|
-
border-radius: 999px;
|
|
88
|
-
border: 1px solid var(--line);
|
|
89
|
-
background: rgba(255, 255, 255, 0.72);
|
|
90
|
-
padding: 10px 14px;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.button-link.primary,
|
|
94
|
-
.counter-button.primary {
|
|
95
|
-
background: var(--accent);
|
|
96
|
-
border-color: var(--accent);
|
|
97
|
-
color: white;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.content {
|
|
101
|
-
display: grid;
|
|
102
|
-
gap: 24px;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.panel {
|
|
106
|
-
border: 1px solid var(--line);
|
|
107
|
-
border-radius: 26px;
|
|
108
|
-
background: var(--panel);
|
|
109
|
-
padding: 24px;
|
|
110
|
-
box-shadow: var(--shadow);
|
|
111
|
-
backdrop-filter: blur(12px);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.hero {
|
|
115
|
-
padding: 32px;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.hero h1,
|
|
119
|
-
.counter-panel h1 {
|
|
120
|
-
margin: 0 0 12px;
|
|
121
|
-
font-size: clamp(2.7rem, 7vw, 5rem);
|
|
122
|
-
line-height: 0.94;
|
|
123
|
-
letter-spacing: -0.045em;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.eyebrow {
|
|
127
|
-
display: inline-block;
|
|
128
|
-
margin-bottom: 12px;
|
|
129
|
-
color: var(--accent);
|
|
130
|
-
font-size: 0.9rem;
|
|
131
|
-
font-weight: 700;
|
|
132
|
-
letter-spacing: 0.08em;
|
|
133
|
-
text-transform: uppercase;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.panel p,
|
|
137
|
-
.list {
|
|
138
|
-
color: var(--muted);
|
|
139
|
-
font-size: 1.04rem;
|
|
140
|
-
line-height: 1.7;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.hero-actions,
|
|
144
|
-
.counter-actions {
|
|
145
|
-
display: flex;
|
|
146
|
-
flex-wrap: wrap;
|
|
147
|
-
gap: 12px;
|
|
148
|
-
margin-top: 24px;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.grid {
|
|
152
|
-
display: grid;
|
|
153
|
-
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
154
|
-
gap: 18px;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.counter-value {
|
|
158
|
-
margin-top: 24px;
|
|
159
|
-
font-size: clamp(4rem, 14vw, 7rem);
|
|
160
|
-
line-height: 1;
|
|
161
|
-
letter-spacing: -0.06em;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.counter-button {
|
|
165
|
-
min-width: 88px;
|
|
166
|
-
border-radius: 18px;
|
|
167
|
-
border: 1px solid var(--line);
|
|
168
|
-
background: white;
|
|
169
|
-
color: var(--ink);
|
|
170
|
-
padding: 14px 18px;
|
|
171
|
-
font: inherit;
|
|
172
|
-
font-weight: 700;
|
|
173
|
-
cursor: pointer;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.counter-code {
|
|
177
|
-
display: block;
|
|
178
|
-
margin-top: 20px;
|
|
179
|
-
color: var(--accent);
|
|
180
|
-
white-space: pre-wrap;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.list {
|
|
184
|
-
margin: 0;
|
|
185
|
-
padding-left: 18px;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
@media (max-width: 720px) {
|
|
189
|
-
.shell {
|
|
190
|
-
width: min(100vw - 20px, 1120px);
|
|
191
|
-
padding-top: 16px;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
.topbar {
|
|
195
|
-
align-items: flex-start;
|
|
196
|
-
flex-direction: column;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.hero,
|
|
200
|
-
.panel {
|
|
201
|
-
padding: 20px;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
`;
|
package/examples/basic/worker.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"type": "application",
|
|
3
|
-
"source-directories": [
|
|
4
|
-
".elm-ssr",
|
|
5
|
-
"src",
|
|
6
|
-
".elm-ssr/src"
|
|
7
|
-
],
|
|
8
|
-
"elm-version": "0.19.1",
|
|
9
|
-
"dependencies": {
|
|
10
|
-
"direct": {
|
|
11
|
-
"elm/browser": "1.0.2",
|
|
12
|
-
"elm/core": "1.0.5",
|
|
13
|
-
"elm/html": "1.0.0",
|
|
14
|
-
"elm/http": "2.0.0",
|
|
15
|
-
"elm/json": "1.1.3",
|
|
16
|
-
"elm/svg": "1.0.1",
|
|
17
|
-
"elm/time": "1.0.0",
|
|
18
|
-
"elm/url": "1.0.0"
|
|
19
|
-
},
|
|
20
|
-
"indirect": {
|
|
21
|
-
"elm/bytes": "1.0.8",
|
|
22
|
-
"elm/file": "1.0.5",
|
|
23
|
-
"elm/virtual-dom": "1.0.3"
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
"test-dependencies": {
|
|
27
|
-
"direct": {},
|
|
28
|
-
"indirect": {}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@elm-ssr/example-crypto-dashboard",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"description": "Crypto dashboard example: Tailwind + elm/svg + elm/http islands with 15s live refresh and cross-island event bus.",
|
|
7
|
-
"dependencies": {
|
|
8
|
-
"elm-ssr": "workspace:*"
|
|
9
|
-
}
|
|
10
|
-
}
|