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,110 +0,0 @@
1
- module Example.Basic.Islands.Counter exposing
2
- ( embed
3
- , Flags, Model, Msg
4
- , encodeFlags
5
- , init, main, subscriptions, update, view
6
- )
7
-
8
- import Browser
9
- import ElmSsr.Html as SsrHtml exposing (Node)
10
- import ElmSsr.Html.Attributes as SsrAttributes
11
- import ElmSsr.Island as Island
12
- import ElmSsr.Island.Shared as Shared
13
- import Html exposing (Html, button, code, div, span, text)
14
- import Html.Attributes exposing (class, type_)
15
- import Html.Events exposing (onClick)
16
- import Json.Encode as Encode
17
-
18
-
19
- type alias Flags =
20
- { start : Int
21
- }
22
-
23
-
24
- type alias Model =
25
- { count : Int
26
- }
27
-
28
-
29
- type Msg
30
- = Increment
31
- | Decrement
32
- | Reset
33
-
34
-
35
- embed : Flags -> Node msg
36
- embed =
37
- Island.embed "Counter"
38
- { encodeFlags = encodeFlags
39
- , fallback = fallback
40
- , id = Just "global-counter"
41
- }
42
-
43
-
44
- encodeFlags : Flags -> Encode.Value
45
- encodeFlags flags =
46
- Encode.object [ ( "start", Encode.int flags.start ) ]
47
-
48
-
49
- fallback : Flags -> List (Node msg)
50
- fallback flags =
51
- [ SsrHtml.div [ SsrAttributes.class "counter-island fallback" ]
52
- [ SsrHtml.div [ SsrAttributes.class "counter-actions" ]
53
- [ SsrHtml.span [ SsrAttributes.class "counter-value" ] [ SsrHtml.text (String.fromInt flags.start) ] ]
54
- , SsrHtml.code [ SsrAttributes.class "counter-code" ] [ SsrHtml.text "Loading counter island…" ]
55
- ]
56
- ]
57
-
58
-
59
- main : Program Flags Model Msg
60
- main =
61
- Browser.element
62
- { init = init
63
- , update = update
64
- , view = view
65
- , subscriptions = subscriptions
66
- }
67
-
68
-
69
- init : Flags -> ( Model, Cmd Msg )
70
- init flags =
71
- ( { count = flags.start }, Cmd.none )
72
-
73
-
74
- update : Msg -> Model -> ( Model, Cmd Msg )
75
- update msg model =
76
- case msg of
77
- Increment ->
78
- let
79
- newCount =
80
- model.count + 1
81
- in
82
- ( { model | count = newCount }, Shared.broadcast "count-changed" (Encode.int newCount) )
83
-
84
- Decrement ->
85
- let
86
- newCount =
87
- model.count - 1
88
- in
89
- ( { model | count = newCount }, Shared.broadcast "count-changed" (Encode.int newCount) )
90
-
91
- Reset ->
92
- ( { model | count = 0 }, Shared.broadcast "count-changed" (Encode.int 0) )
93
-
94
-
95
- subscriptions : Model -> Sub Msg
96
- subscriptions _ =
97
- Sub.none
98
-
99
-
100
- view : Model -> Html Msg
101
- view model =
102
- div [ class "counter-island" ]
103
- [ div [ class "counter-actions" ]
104
- [ button [ class "counter-button", type_ "button", onClick Decrement ] [ text "-" ]
105
- , span [ class "counter-value" ] [ text (String.fromInt model.count) ]
106
- , button [ class "counter-button primary", type_ "button", onClick Increment ] [ text "+" ]
107
- , button [ class "counter-button", type_ "button", onClick Reset ] [ text "reset" ]
108
- ]
109
- , code [ class "counter-code" ] [ text "Browser.element island using standard elm/html" ]
110
- ]
@@ -1,67 +0,0 @@
1
- module Example.Basic.Islands.Observer exposing (embed, main)
2
-
3
- import Browser
4
- import ElmSsr.Html as SsrHtml exposing (Node)
5
- import ElmSsr.Html.Attributes as SsrAttributes
6
- import ElmSsr.Island as Island
7
- import ElmSsr.Island.Shared as Shared
8
- import Html exposing (Html, div, span, text)
9
- import Html.Attributes exposing (class)
10
- import Json.Decode as Decode
11
- import Json.Encode as Encode
12
-
13
-
14
- type alias Flags =
15
- {}
16
-
17
-
18
- type alias Model =
19
- { lastCount : Int
20
- }
21
-
22
-
23
- type Msg
24
- = OnGlobalEvent Shared.GlobalEvent
25
-
26
-
27
- embed : Flags -> Node msg
28
- embed =
29
- Island.embed "Observer"
30
- { encodeFlags = \_ -> Encode.null
31
- , fallback = \_ -> [ SsrHtml.text "Waiting for updates..." ]
32
- , id = Just "global-observer"
33
- }
34
-
35
-
36
- main : Program Flags Model Msg
37
- main =
38
- Browser.element
39
- { init = \_ -> ( { lastCount = 0 }, Cmd.none )
40
- , update = update
41
- , view = view
42
- , subscriptions = \_ -> Shared.listen OnGlobalEvent
43
- }
44
-
45
-
46
- update : Msg -> Model -> ( Model, Cmd Msg )
47
- update msg model =
48
- case msg of
49
- OnGlobalEvent event ->
50
- if event.tag == "count-changed" then
51
- case Decode.decodeValue Decode.int event.payload of
52
- Ok count ->
53
- ( { model | lastCount = count }, Cmd.none )
54
-
55
- Err _ ->
56
- ( model, Cmd.none )
57
-
58
- else
59
- ( model, Cmd.none )
60
-
61
-
62
- view : Model -> Html Msg
63
- view model =
64
- div [ class "observer-island" ]
65
- [ span [ class "eyebrow" ] [ text "Observer Island" ]
66
- , div [] [ text ("Last count from bus: " ++ String.fromInt model.lastCount) ]
67
- ]
@@ -1,151 +0,0 @@
1
- module Example.Basic.Islands.Tasks exposing
2
- ( embed
3
- , Flags, Model, Msg
4
- , encodeFlags
5
- , init, main, subscriptions, update, view
6
- )
7
-
8
- import Browser
9
- import ElmSsr.Html as SsrHtml exposing (Node)
10
- import ElmSsr.Html.Attributes as SsrAttributes
11
- import ElmSsr.Island as Island
12
- import Html exposing (Html, button, div, input, li, span, text, ul)
13
- import Html.Attributes exposing (class, placeholder, type_, value)
14
- import Html.Events exposing (onClick, onInput)
15
- import Html.Keyed as Keyed
16
- import Json.Encode as Encode
17
-
18
-
19
- type alias Flags =
20
- { items : List String
21
- }
22
-
23
-
24
- type alias Item =
25
- { id : Int
26
- , label : String
27
- , note : String
28
- }
29
-
30
-
31
- type alias Model =
32
- { items : List Item
33
- }
34
-
35
-
36
- type Msg
37
- = Remove Int
38
- | MoveUp Int
39
- | UpdateNote Int String
40
-
41
-
42
- embed : Flags -> Node msg
43
- embed =
44
- Island.embed "Tasks"
45
- { encodeFlags = encodeFlags
46
- , fallback = fallback
47
- , id = Nothing
48
- }
49
-
50
-
51
- encodeFlags : Flags -> Encode.Value
52
- encodeFlags flags =
53
- Encode.object [ ( "items", Encode.list Encode.string flags.items ) ]
54
-
55
-
56
- fallback : Flags -> List (Node msg)
57
- fallback flags =
58
- [ SsrHtml.div [ SsrAttributes.class "tasks-island fallback" ]
59
- [ SsrHtml.ul [ SsrAttributes.class "tasks-list" ]
60
- (List.map
61
- (\label ->
62
- SsrHtml.li [ SsrAttributes.class "task-item" ]
63
- [ SsrHtml.span [ SsrAttributes.class "task-label" ] [ SsrHtml.text label ] ]
64
- )
65
- flags.items
66
- )
67
- ]
68
- ]
69
-
70
-
71
- main : Program Flags Model Msg
72
- main =
73
- Browser.element
74
- { init = init
75
- , update = update
76
- , view = view
77
- , subscriptions = subscriptions
78
- }
79
-
80
-
81
- init : Flags -> ( Model, Cmd Msg )
82
- init flags =
83
- ( { items = List.indexedMap (\index label -> { id = index, label = label, note = "" }) flags.items }
84
- , Cmd.none
85
- )
86
-
87
-
88
- update : Msg -> Model -> ( Model, Cmd Msg )
89
- update msg model =
90
- case msg of
91
- Remove id ->
92
- ( { model | items = List.filter (\item -> item.id /= id) model.items }, Cmd.none )
93
-
94
- MoveUp id ->
95
- ( { model | items = moveUp id model.items }, Cmd.none )
96
-
97
- UpdateNote id note ->
98
- ( { model | items = List.map (updateNote id note) model.items }, Cmd.none )
99
-
100
-
101
- updateNote : Int -> String -> Item -> Item
102
- updateNote id note item =
103
- if item.id == id then
104
- { item | note = note }
105
-
106
- else
107
- item
108
-
109
-
110
- moveUp : Int -> List Item -> List Item
111
- moveUp id items =
112
- case items of
113
- first :: second :: rest ->
114
- if second.id == id then
115
- second :: first :: rest
116
-
117
- else
118
- first :: moveUp id (second :: rest)
119
-
120
- _ ->
121
- items
122
-
123
-
124
- subscriptions : Model -> Sub Msg
125
- subscriptions _ =
126
- Sub.none
127
-
128
-
129
- view : Model -> Html Msg
130
- view model =
131
- div [ class "tasks-island" ]
132
- [ Keyed.ul [ class "tasks-list" ] (List.map viewItem model.items) ]
133
-
134
-
135
- viewItem : Item -> ( String, Html Msg )
136
- viewItem item =
137
- ( String.fromInt item.id
138
- , li [ class "task-item" ]
139
- [ span [ class "task-label" ] [ text item.label ]
140
- , input
141
- [ class "task-note"
142
- , type_ "text"
143
- , placeholder "note (kept across moves)"
144
- , value item.note
145
- , onInput (UpdateNote item.id)
146
- ]
147
- []
148
- , button [ class "task-up", type_ "button", onClick (MoveUp item.id) ] [ text "↑" ]
149
- , button [ class "task-remove", type_ "button", onClick (Remove item.id) ] [ text "remove" ]
150
- ]
151
- )
@@ -1,87 +0,0 @@
1
- module Example.Basic.Routes.Chart exposing (action, page)
2
-
3
- -- File-based routing: GET /chart. A fully static page whose chart is inline SVG
4
- -- produced by ElmSsr.Svg and serialized on the server — no client JavaScript.
5
-
6
- import ElmSsr.Action as Action exposing (Action)
7
- import ElmSsr.Document exposing (Document)
8
- import ElmSsr.Html exposing (Node, h1, p, section, span, text)
9
- import ElmSsr.Html.Attributes exposing (class)
10
- import ElmSsr.Loader as Loader exposing (Loader)
11
- import ElmSsr.Route exposing (Request)
12
- import ElmSsr.Svg as Svg exposing (Svg)
13
- import ElmSsr.Svg.Attributes as SvgAttr
14
- import Example.Basic.View.Shared as Shared
15
-
16
-
17
- page : Request -> Loader (Document Never)
18
- page _ =
19
- Loader.succeed view
20
-
21
-
22
- action : Request -> Action (Document Never)
23
- action _ =
24
- Action.fail 405 "Method not allowed"
25
-
26
-
27
- view : Document Never
28
- view =
29
- Shared.pageDocument "SVG Chart"
30
- [ section [ class "panel" ]
31
- [ span [ class "eyebrow" ] [ text "Server-rendered SVG" ]
32
- , h1 [] [ text "Inline SVG, rendered on the edge" ]
33
- , p [] [ text "This chart is built with ElmSsr.Svg and serialized server-side. It ships zero client JavaScript." ]
34
- , sparkline
35
- ]
36
- ]
37
-
38
-
39
- sparkline : Svg msg
40
- sparkline =
41
- Svg.svg
42
- [ SvgAttr.viewBox "0 0 200 100"
43
- , SvgAttr.width "200"
44
- , SvgAttr.height "100"
45
- , SvgAttr.class "sparkline"
46
- ]
47
- [ Svg.defs []
48
- [ Svg.linearGradient
49
- [ SvgAttr.id "fill"
50
- , SvgAttr.x1 "0"
51
- , SvgAttr.y1 "0"
52
- , SvgAttr.x2 "0"
53
- , SvgAttr.y2 "1"
54
- ]
55
- [ Svg.stop [ SvgAttr.offset "0%", SvgAttr.stopColor "#6366f1" ] []
56
- , Svg.stop [ SvgAttr.offset "100%", SvgAttr.stopColor "#0ea5e9" ] []
57
- ]
58
- ]
59
- , Svg.rect
60
- [ SvgAttr.x "0"
61
- , SvgAttr.y "0"
62
- , SvgAttr.width "200"
63
- , SvgAttr.height "100"
64
- , SvgAttr.rx "8"
65
- , SvgAttr.fill "url(#fill)"
66
- ]
67
- []
68
- , Svg.polyline
69
- [ SvgAttr.points "0,80 40,60 80,70 120,30 160,45 200,10"
70
- , SvgAttr.fill "none"
71
- , SvgAttr.stroke "#ffffff"
72
- , SvgAttr.strokeWidth "3"
73
- , SvgAttr.strokeLinecap "round"
74
- , SvgAttr.strokeLinejoin "round"
75
- ]
76
- []
77
- , Svg.g [ SvgAttr.transform "translate(0,0)" ]
78
- [ Svg.circle [ SvgAttr.cx "120", SvgAttr.cy "30", SvgAttr.r "4", SvgAttr.fill "#ffffff" ] [] ]
79
- , Svg.text_
80
- [ SvgAttr.x "8"
81
- , SvgAttr.y "20"
82
- , SvgAttr.fontSize "12"
83
- , SvgAttr.textAnchor "start"
84
- , SvgAttr.fill "#ffffff"
85
- ]
86
- [ Svg.text "7-day trend & momentum" ]
87
- ]
@@ -1,42 +0,0 @@
1
- module Example.Basic.Routes.Counter exposing (page, action)
2
-
3
- import ElmSsr.Action as Action exposing (Action)
4
- import ElmSsr.Document exposing (Document)
5
- import ElmSsr.Html exposing (h1, p, section, span, text)
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.Islands.Counter as Counter
10
- import Example.Basic.Islands.Observer as Observer
11
- import Example.Basic.Islands.Tasks as Tasks
12
- import Example.Basic.View.Shared as Shared
13
-
14
-
15
- page : Request -> Loader (Document Never)
16
- page _ =
17
- Loader.succeed view
18
-
19
-
20
- action : Request -> Action (Document Never)
21
- action _ =
22
- Action.fail 405 "Method not allowed"
23
-
24
-
25
- view : Document Never
26
- view =
27
- Shared.pageDocument "Interactive Counter"
28
- [ section [ class "panel counter-panel" ]
29
- [ span [ class "eyebrow" ] [ text "Island" ]
30
- , h1 [] [ text "Counter route" ]
31
- , p [] [ text "This page is static. Only the counter below is an interactive island; clicking it patches just its subtree, not the page." ]
32
- , Counter.embed { start = 0 }
33
- , Observer.embed {}
34
- ]
35
- , section [ class "panel tasks-panel" ]
36
- [ span [ class "eyebrow" ] [ text "Keyed island" ]
37
- , h1 [] [ text "Keyed list" ]
38
- , p [] [ text "Reorder or remove rows: each row is keyed, so the surviving DOM nodes (and any note you type) are preserved rather than rebuilt." ]
39
- , Tasks.embed { items = [ "Write the RFC", "Spike the runtime", "Cover it with tests" ] }
40
- ]
41
- , Shared.featureSection
42
- ]
@@ -1,76 +0,0 @@
1
- module Example.Basic.Routes.Echo exposing (action, page)
2
-
3
- -- File-based routing: GET /echo renders a form; POST /echo runs the action.
4
- -- The action validates, performs a server effect, then redirects (PRG) — so the
5
- -- form works with no client JavaScript at all.
6
-
7
- import ElmSsr.Action as Action exposing (Action)
8
- import ElmSsr.Document exposing (Document)
9
- import ElmSsr.Html exposing (Node, a, button, form, h1, input, label, p, section, span, text)
10
- import ElmSsr.Html.Attributes as Attr
11
- import ElmSsr.Loader as Loader exposing (Loader)
12
- import ElmSsr.Route as Route exposing (Request)
13
- import Example.Basic.View.Shared as Shared
14
- import Json.Decode as Decode
15
-
16
-
17
- page : Request -> Loader (Document Never)
18
- page request =
19
- Loader.succeed (view request)
20
-
21
-
22
- action : Request -> Action (Document Never)
23
- action request =
24
- case Maybe.map String.trim (Route.formValue "message" request) of
25
- Just message ->
26
- if String.isEmpty message then
27
- Action.fail 422 "Message is required."
28
-
29
- else
30
- -- Run a server effect (confirm the edge region), then redirect.
31
- Action.fromLoader confirmRegion
32
- |> Action.andThen (\region -> Action.redirect ("/echo?status=received&region=" ++ region))
33
-
34
- Nothing ->
35
- Action.fail 422 "Message is required."
36
-
37
-
38
- confirmRegion : Loader String
39
- confirmRegion =
40
- Loader.fetchJson
41
- { url = "app://status"
42
- , decoder = Decode.field "region" Decode.string
43
- }
44
-
45
-
46
- view : Request -> Document Never
47
- view request =
48
- Shared.pageDocument "Form Action"
49
- [ section [ Attr.class "panel" ]
50
- (case Route.query "status" request of
51
- Just "received" ->
52
- [ span [ Attr.class "eyebrow" ] [ text "Action complete" ]
53
- , h1 [] [ text "Message received" ]
54
- , p [] [ text ("Saved on the server (region: " ++ (Route.query "region" request |> Maybe.withDefault "unknown") ++ "). This page arrived via a POST → action → redirect, no client JavaScript.") ]
55
- , p [] [ a [ Attr.class "button-link", Attr.href "/echo" ] [ text "Send another" ] ]
56
- ]
57
-
58
- _ ->
59
- [ span [ Attr.class "eyebrow" ] [ text "Forms without JS" ]
60
- , h1 [] [ text "Server action" ]
61
- , p [] [ text "Submitting this form POSTs to the route's action, which validates, runs a server effect, then redirects. It works with JavaScript disabled." ]
62
- , echoForm
63
- ]
64
- )
65
- ]
66
-
67
-
68
- echoForm : Node msg
69
- echoForm =
70
- form [ Attr.class "echo-form", Attr.method "post", Attr.action "/echo" ]
71
- [ label [ Attr.class "field" ]
72
- [ span [] [ text "Message" ]
73
- , input [ Attr.type_ "text", Attr.name "message", Attr.placeholder "Say something", Attr.required True ]
74
- ]
75
- , button [ Attr.type_ "submit", Attr.class "button-link primary" ] [ text "Send" ]
76
- ]
@@ -1,37 +0,0 @@
1
- module Example.Basic.Routes.Greet.Name_ exposing (page, action)
2
-
3
- import ElmSsr.Action as Action exposing (Action)
4
- import ElmSsr.Document exposing (Document)
5
- import ElmSsr.Html exposing (Node, code, h1, p, section, span, text)
6
- import ElmSsr.Html.Attributes exposing (class)
7
- import ElmSsr.Loader as Loader exposing (Loader)
8
- import ElmSsr.Route as Route exposing (Request)
9
- import Example.Basic.View.Shared as Shared
10
-
11
-
12
- page : Request -> Loader (Document Never)
13
- page request =
14
- Loader.succeed (view (Route.param "name" request |> Maybe.withDefault "stranger"))
15
-
16
-
17
- action : Request -> Action (Document Never)
18
- action _ =
19
- Action.fail 405 "Method not allowed"
20
-
21
-
22
- view : String -> Document Never
23
- view name =
24
- Shared.pageDocument ("Hello " ++ name)
25
- [ greeting name
26
- , Shared.featureSection
27
- ]
28
-
29
-
30
- greeting : String -> Node msg
31
- greeting name =
32
- section [ class "panel" ]
33
- [ span [ class "eyebrow" ] [ text "Dynamic route" ]
34
- , h1 [] [ text ("Hello, " ++ name ++ "!") ]
35
- , p [] [ text "The name above was captured from the URL by the file Routes/Greet/Name_.elm." ]
36
- , code [ class "counter-code" ] [ text "Route.param \"name\" request" ]
37
- ]
@@ -1,86 +0,0 @@
1
- module Example.Basic.Routes.Guestbook exposing (action, page)
2
-
3
- -- File-based routing: GET /guestbook lists entries (Loader.query); POST inserts
4
- -- one (Action via Loader.execute) then redirects. SQL is backend-neutral — D1 on
5
- -- Cloudflare, SQLite/Postgres locally — the Elm code is identical.
6
-
7
- import ElmSsr.Action as Action exposing (Action)
8
- import ElmSsr.Document exposing (Document)
9
- import ElmSsr.Html exposing (Node, button, form, h1, input, li, p, section, span, text, ul)
10
- import ElmSsr.Html.Attributes as Attr
11
- import ElmSsr.Loader as Loader exposing (Loader)
12
- import ElmSsr.Route as Route exposing (Request)
13
- import Example.Basic.View.Shared as Shared
14
- import Json.Decode as Decode
15
- import Json.Encode as Encode
16
-
17
-
18
- page : Request -> Loader (Document Never)
19
- page _ =
20
- Loader.map view recentEntries
21
-
22
-
23
- recentEntries : Loader (List String)
24
- recentEntries =
25
- Loader.query
26
- { sql = "SELECT message FROM entries ORDER BY id DESC LIMIT 10"
27
- , params = []
28
- , decoder = Decode.field "message" Decode.string
29
- }
30
-
31
-
32
- action : Request -> Action (Document Never)
33
- action request =
34
- case Maybe.map String.trim (Route.formValue "message" request) of
35
- Just message ->
36
- if String.isEmpty message then
37
- Action.fail 422 "Message is required."
38
-
39
- else
40
- Action.fromLoader (saveAndAudit message)
41
- |> Action.andThen (\_ -> Action.redirect "/guestbook")
42
-
43
- Nothing ->
44
- Action.fail 422 "Message is required."
45
-
46
-
47
- saveAndAudit : String -> Loader ()
48
- saveAndAudit message =
49
- Loader.execute
50
- { sql = "INSERT INTO entries (message) VALUES (?)"
51
- , params = [ Encode.string message ]
52
- }
53
- |> Loader.andThen
54
- (\_ ->
55
- -- Fire-and-forget: the audit runs after the response is sent.
56
- Loader.enqueue
57
- { task = "auditEntry"
58
- , payload = Encode.object [ ( "message", Encode.string message ) ]
59
- }
60
- )
61
-
62
-
63
- view : List String -> Document Never
64
- view entries =
65
- Shared.pageDocument "Guestbook"
66
- [ section [ Attr.class "panel" ]
67
- [ span [ Attr.class "eyebrow" ] [ text "SQL via the effect runner" ]
68
- , h1 [] [ text "Guestbook" ]
69
- , p [] [ text "Entries are read with Loader.query; the form inserts with Loader.execute inside an Action. The SQL backend is swappable without touching this code." ]
70
- , entryForm
71
- , ul [ Attr.class "list" ] (List.map entryItem entries)
72
- ]
73
- ]
74
-
75
-
76
- entryForm : Node msg
77
- entryForm =
78
- form [ Attr.class "echo-form", Attr.method "post", Attr.action "/guestbook" ]
79
- [ input [ Attr.type_ "text", Attr.name "message", Attr.placeholder "Sign the guestbook", Attr.required True ]
80
- , button [ Attr.type_ "submit", Attr.class "button-link primary" ] [ text "Sign" ]
81
- ]
82
-
83
-
84
- entryItem : String -> Node msg
85
- entryItem message =
86
- li [] [ text message ]
@@ -1,41 +0,0 @@
1
- module Example.Basic.Routes.Index exposing (page, action)
2
-
3
- import ElmSsr.Action as Action exposing (Action)
4
- import ElmSsr.Document exposing (Document)
5
- import ElmSsr.Html exposing (Node, a, div, h1, p, section, span, text)
6
- import ElmSsr.Html.Attributes exposing (class, href)
7
- import ElmSsr.Loader as Loader exposing (Loader)
8
- import ElmSsr.Route exposing (Request)
9
- import Example.Basic.View.Shared as Shared
10
-
11
-
12
- page : Request -> Loader (Document Never)
13
- page _ =
14
- Loader.succeed view
15
-
16
-
17
- action : Request -> Action (Document Never)
18
- action _ =
19
- Action.fail 405 "Method not allowed"
20
-
21
-
22
- view : Document Never
23
- view =
24
- Shared.pageDocument "Elm SSR Library"
25
- [ heroSection
26
- , Shared.featureSection
27
- ]
28
-
29
-
30
- heroSection : Node msg
31
- heroSection =
32
- section [ class "hero panel" ]
33
- [ span [ class "eyebrow" ] [ text "Pages, loaders, Browser.element islands" ]
34
- , h1 [] [ text "Write Elm once, render on the edge, update in the browser." ]
35
- , p []
36
- [ text "Stateless pages load data and render once with zero client JavaScript. Interactive UI lives in standard Browser.element islands mounted inside the page." ]
37
- , div [ class "hero-actions" ]
38
- [ a [ class "button-link primary", href "/counter" ] [ text "Open island demo" ]
39
- , a [ class "button-link", href "/status" ] [ text "See a loader page" ]
40
- ]
41
- ]