elm-pages 3.0.0-beta.1 → 3.0.0-beta.10

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 (49) hide show
  1. package/README.md +10 -1
  2. package/codegen/elm-pages-codegen.js +803 -284
  3. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateData.elmi +0 -0
  4. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateData.elmo +0 -0
  5. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateDataTest.elmo +0 -0
  6. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
  7. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/o.dat +0 -0
  8. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm.json +1 -1
  9. package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Runner.elm.js +285 -101
  10. package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
  11. package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_supervisor.js +1 -1
  12. package/generator/dead-code-review/src/Pages/Review/DeadCodeEliminateData.elm +140 -17
  13. package/generator/dead-code-review/tests/Pages/Review/DeadCodeEliminateDataTest.elm +218 -0
  14. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
  15. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/o.dat +0 -0
  16. package/generator/review/elm-stuff/tests-0.19.1/elm.json +1 -1
  17. package/generator/review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
  18. package/generator/review/elm-stuff/tests-0.19.1/js/node_supervisor.js +1 -1
  19. package/generator/src/SharedTemplate.elm +1 -1
  20. package/generator/src/build.js +75 -42
  21. package/generator/src/compatibility-key.js +1 -0
  22. package/generator/src/config.js +41 -0
  23. package/generator/src/dev-server.js +36 -56
  24. package/generator/src/generate-template-module-connector.js +0 -28
  25. package/generator/src/pre-render-html.js +31 -17
  26. package/generator/src/render.js +2 -0
  27. package/generator/src/seo-renderer.js +11 -4
  28. package/generator/src/vite-utils.js +78 -0
  29. package/generator/template/app/Api.elm +1 -1
  30. package/generator/template/app/Site.elm +6 -1
  31. package/package.json +2 -3
  32. package/src/ApiRoute.elm +0 -3
  33. package/src/DataSource/File.elm +1 -1
  34. package/src/DataSource/Internal/Request.elm +0 -5
  35. package/src/DataSource.elm +39 -31
  36. package/src/Form/Field.elm +1 -1
  37. package/src/Form.elm +1 -1
  38. package/src/Head/Seo.elm +16 -27
  39. package/src/Head.elm +126 -0
  40. package/src/HtmlPrinter.elm +7 -3
  41. package/src/Pages/Generate.elm +544 -102
  42. package/src/Pages/Internal/NotFoundReason.elm +3 -2
  43. package/src/Pages/Internal/Platform/Cli.elm +91 -27
  44. package/src/Pages/Internal/Platform/Cli.elm.bak +1276 -0
  45. package/src/Pages/Internal/Platform/CompatibilityKey.elm +6 -0
  46. package/src/Pages/Internal/Platform.elm +34 -27
  47. package/src/Pages/ProgramConfig.elm +6 -3
  48. package/src/Server/Session.elm +149 -83
  49. package/src/Server/SetCookie.elm +89 -31
@@ -0,0 +1,6 @@
1
+ module Pages.Internal.Platform.CompatibilityKey exposing (currentCompatibilityKey)
2
+
3
+
4
+ currentCompatibilityKey : Int
5
+ currentCompatibilityKey =
6
+ 1
@@ -58,7 +58,7 @@ type alias Program userModel userMsg pageData actionData sharedData errorPage =
58
58
  mainView :
59
59
  ProgramConfig userMsg userModel route pageData actionData sharedData effect (Msg userMsg pageData actionData sharedData errorPage) errorPage
60
60
  -> Model userModel pageData actionData sharedData
61
- -> { title : String, body : Html (Pages.Msg.Msg userMsg) }
61
+ -> { title : String, body : List (Html (Pages.Msg.Msg userMsg)) }
62
62
  mainView config model =
63
63
  case model.notFound of
64
64
  Just info ->
@@ -95,7 +95,7 @@ mainView config model =
95
95
  Err error ->
96
96
  { title = "Page Data Error"
97
97
  , body =
98
- Html.div [] [ Html.text error ]
98
+ [ Html.div [] [ Html.text error ] ]
99
99
  }
100
100
 
101
101
 
@@ -124,9 +124,9 @@ view config model =
124
124
  { title = title
125
125
  , body =
126
126
  [ onViewChangeElement model.url
127
- , body |> Html.map UserMsg
128
127
  , AriaLiveAnnouncer.view model.ariaNavigationAnnouncement
129
128
  ]
129
+ ++ List.map (Html.map UserMsg) body
130
130
  }
131
131
 
132
132
 
@@ -423,9 +423,7 @@ update config appMsg model =
423
423
  )
424
424
 
425
425
  else
426
- ( { model
427
- | url = url
428
- }
426
+ ( model
429
427
  , NoEffect
430
428
  )
431
429
  -- TODO is it reasonable to always re-fetch route data if you re-navigate to the current route? Might be a good
@@ -605,27 +603,32 @@ update config appMsg model =
605
603
  , actionData = newActionData
606
604
  }
607
605
 
608
- ( userModel, _ ) =
606
+ ( userModel, userEffect ) =
609
607
  -- TODO if urlWithoutRedirectResolution is different from the url with redirect resolution, then
610
608
  -- instead of calling update, call pushUrl (I think?)
611
609
  -- TODO include user Cmd
612
- config.update model.pageFormState
613
- (model.inFlightFetchers |> toFetcherState)
614
- (model.transition |> Maybe.map Tuple.second)
615
- newSharedData
616
- newPageData
617
- model.key
618
- (config.onPageChange
619
- { protocol = model.url.protocol
620
- , host = model.url.host
621
- , port_ = model.url.port_
622
- , path = urlPathToPath urlWithoutRedirectResolution
623
- , query = urlWithoutRedirectResolution.query
624
- , fragment = urlWithoutRedirectResolution.fragment
625
- , metadata = config.urlToRoute urlWithoutRedirectResolution
626
- }
627
- )
628
- previousPageData.userModel
610
+ if stayingOnSamePath then
611
+ ( previousPageData.userModel, NoEffect )
612
+
613
+ else
614
+ config.update model.pageFormState
615
+ (model.inFlightFetchers |> toFetcherState)
616
+ (model.transition |> Maybe.map Tuple.second)
617
+ newSharedData
618
+ newPageData
619
+ model.key
620
+ (config.onPageChange
621
+ { protocol = model.url.protocol
622
+ , host = model.url.host
623
+ , port_ = model.url.port_
624
+ , path = urlPathToPath urlWithoutRedirectResolution
625
+ , query = urlWithoutRedirectResolution.query
626
+ , fragment = urlWithoutRedirectResolution.fragment
627
+ , metadata = config.urlToRoute urlWithoutRedirectResolution
628
+ }
629
+ )
630
+ previousPageData.userModel
631
+ |> Tuple.mapSecond UserCmd
629
632
 
630
633
  updatedModel : Model userModel pageData actionData sharedData
631
634
  updatedModel =
@@ -656,10 +659,13 @@ update config appMsg model =
656
659
  , currentPath = newUrl.path
657
660
  }
658
661
  , if not stayingOnSamePath && scrollToTopWhenDone then
659
- ScrollToTop
662
+ Batch
663
+ [ ScrollToTop
664
+ , userEffect
665
+ ]
660
666
 
661
667
  else
662
- NoEffect
668
+ userEffect
663
669
  )
664
670
  |> (case maybeUserMsg of
665
671
  Just userMsg ->
@@ -1392,7 +1398,8 @@ loadDataAndUpdateUrl ( newPageData, newSharedData, newActionData ) maybeUserMsg
1392
1398
  , actionData = newActionData
1393
1399
  }
1394
1400
 
1395
- ( userModel, _ ) =
1401
+ -- TODO use userEffect here?
1402
+ ( userModel, userEffect ) =
1396
1403
  -- TODO if urlWithoutRedirectResolution is different from the url with redirect resolution, then
1397
1404
  -- instead of calling update, call pushUrl (I think?)
1398
1405
  -- TODO include user Cmd
@@ -65,7 +65,7 @@ type alias ProgramConfig userMsg userModel route pageData actionData sharedData
65
65
  -> pageData
66
66
  -> Maybe actionData
67
67
  ->
68
- { view : userModel -> { title : String, body : Html (Pages.Msg.Msg userMsg) }
68
+ { view : userModel -> { title : String, body : List (Html (Pages.Msg.Msg userMsg)) }
69
69
  , head : List Head.Tag
70
70
  }
71
71
  , handleRoute : route -> DataSource (Maybe NotFoundReason)
@@ -88,7 +88,10 @@ type alias ProgramConfig userMsg userModel route pageData actionData sharedData
88
88
  }
89
89
  -> userMsg
90
90
  , apiRoutes :
91
- (Html Never -> String)
91
+ (Maybe { indent : Int, newLines : Bool }
92
+ -> Html Never
93
+ -> String
94
+ )
92
95
  -> List (ApiRoute.ApiRoute ApiRoute.Response)
93
96
  , pathPatterns : List RoutePattern
94
97
  , basePath : List String
@@ -98,7 +101,7 @@ type alias ProgramConfig userMsg userModel route pageData actionData sharedData
98
101
  , encodeResponse : ResponseSketch pageData actionData sharedData -> Bytes.Encode.Encoder
99
102
  , encodeAction : actionData -> Bytes.Encode.Encoder
100
103
  , decodeResponse : Bytes.Decode.Decoder (ResponseSketch pageData actionData sharedData)
101
- , globalHeadTags : Maybe ((Html Never -> String) -> DataSource (List Head.Tag))
104
+ , globalHeadTags : Maybe ((Maybe { indent : Int, newLines : Bool } -> Html Never -> String) -> DataSource (List Head.Tag))
102
105
  , cmdToEffect : Cmd userMsg -> effect
103
106
  , perform :
104
107
  { fetchRouteData :
@@ -1,8 +1,109 @@
1
- module Server.Session exposing (Decoder, NotLoadedReason(..), Session(..), Value(..), clearFlashCookies, empty, expectSession, flashPrefix, get, insert, remove, setValues, succeed, unwrap, update, withFlash, withSession)
1
+ module Server.Session exposing
2
+ ( withSession
3
+ , NotLoadedReason(..)
4
+ , Session, empty, get, insert, remove, update, withFlash
5
+ )
6
+
7
+ {-| You can manage server state with HTTP cookies using this Server.Session API. Server-rendered pages define a `Server.Request.Parser`
8
+ to choose which requests to respond to and how to extract structured data from the incoming request.
9
+
10
+
11
+ ## Using Sessions in a Request.Parser
12
+
13
+ Using these functions, you can store and read session data in cookies to maintain state between requests.
14
+ For example, TODO:
15
+
16
+ action : RouteParams -> Request.Parser (DataSource (Response ActionData ErrorPage))
17
+ action routeParams =
18
+ MySession.withSession
19
+ (Request.formDataWithServerValidation (form |> Form.initCombinedServer identity))
20
+ (\nameResultData session ->
21
+ nameResultData
22
+ |> DataSource.map
23
+ (\nameResult ->
24
+ case nameResult of
25
+ Err errors ->
26
+ ( session
27
+ |> Result.withDefault Nothing
28
+ |> Maybe.withDefault Session.empty
29
+ , Response.render
30
+ { errors = errors
31
+ }
32
+ )
33
+
34
+ Ok ( _, name ) ->
35
+ ( session
36
+ |> Result.withDefault Nothing
37
+ |> Maybe.withDefault Session.empty
38
+ |> Session.insert "name" name
39
+ |> Session.withFlash "message" ("Welcome " ++ name ++ "!")
40
+ , Route.redirectTo Route.Greet
41
+ )
42
+ )
43
+ )
44
+
45
+ The elm-pages framework will manage signing these cookies using the `secrets : DataSource (List String)` you pass in.
46
+ That means that the values you set in your session will be directly visible to anyone who has access to the cookie
47
+ (so don't directly store sensitive data in your session). Since the session cookie is signed using the secret you provide,
48
+ the cookie will be invalidated if it is tampered with because it won't match when elm-pages verifies that it has been
49
+ signed with your secrets. Of course you need to provide secure secrets and treat your secrets with care.
50
+
51
+
52
+ ### Rotating Secrets
53
+
54
+ The first String in `secrets : DataSource (List String)` will be used to sign sessions, while the remaining String's will
55
+ still be used to attempt to "unsign" the cookies. So if you have a single secret:
56
+
57
+ Session.withSession
58
+ { name = "mysession"
59
+ , secrets =
60
+ DataSource.map List.singleton
61
+ (Env.expect "SESSION_SECRET2022-09-01")
62
+ , options = cookieOptions
63
+ }
64
+
65
+ Then you add a second secret
66
+
67
+ Session.withSession
68
+ { name = "mysession"
69
+ , secrets =
70
+ DataSource.map2
71
+ (\newSecret oldSecret -> [ newSecret, oldSecret ])
72
+ (Env.expect "SESSION_SECRET2022-12-01")
73
+ (Env.expect "SESSION_SECRET2022-09-01")
74
+ , options = cookieOptions
75
+ }
2
76
 
3
- {-|
77
+ The new secret (`2022-12-01`) will be used to sign all requests. This API always re-signs using the newest secret in the list
78
+ whenever a new request comes in (even if the Session key-value pairs are unchanged), so these cookies get "refreshed" with the latest
79
+ signing secret when a new request comes in.
4
80
 
5
- @docs Decoder, NotLoadedReason, Session, Value, clearFlashCookies, empty, expectSession, flashPrefix, get, insert, remove, setValues, succeed, unwrap, update, withFlash, withSession
81
+ However, incoming requests with a cookie signed using the old secret (`2022-09-01`) will still successfully be unsigned
82
+ because they are still in the rotation (and then subsequently "refreshed" and signed using the new secret).
83
+
84
+ This allows you to rotate your session secrets (for security purposes). When a secret goes out of the rotation,
85
+ it will invalidate all cookies signed with that. For example, if we remove our old secret from the rotation:
86
+
87
+ Session.withSession
88
+ { name = "mysession"
89
+ , secrets =
90
+ DataSource.map List.singleton
91
+ (Env.expect "SESSION_SECRET2022-12-01")
92
+ , options = cookieOptions
93
+ }
94
+
95
+ And then a user makes a request but had a session signed with our old secret (`2022-09-01`), the session will be invalid
96
+ (so `withSession` would parse the session for that request as `Nothing`). It's standard for cookies to have an expiration date,
97
+ so there's nothing wrong with an old session expiring (and the browser will eventually delete old cookies), just be aware of that when rotating secrets.
98
+
99
+ @docs withSession
100
+
101
+ @docs NotLoadedReason
102
+
103
+
104
+ ## Creating and Updating Sessions
105
+
106
+ @docs Session, empty, get, insert, remove, update, withFlash
6
107
 
7
108
  -}
8
109
 
@@ -10,10 +111,9 @@ import DataSource exposing (DataSource)
10
111
  import DataSource.Http
11
112
  import DataSource.Internal.Request
12
113
  import Dict exposing (Dict)
13
- import Dict.Extra
14
114
  import Json.Decode
15
115
  import Json.Encode
16
- import Server.Request as Request exposing (Parser)
116
+ import Server.Request
17
117
  import Server.Response exposing (Response)
18
118
  import Server.SetCookie as SetCookie
19
119
 
@@ -23,11 +123,6 @@ type Session
23
123
  = Session (Dict String Value)
24
124
 
25
125
 
26
- {-| -}
27
- type alias Decoder decoded =
28
- Json.Decode.Decoder decoded
29
-
30
-
31
126
  {-| -}
32
127
  type Value
33
128
  = Persistent String
@@ -113,20 +208,13 @@ empty =
113
208
 
114
209
  {-| -}
115
210
  type NotLoadedReason
116
- = NoCookies
117
- | MissingHeaders
211
+ = NoSessionCookie
212
+ | InvalidSessionCookie
118
213
 
119
214
 
120
215
  {-| -}
121
- succeed : constructor -> Decoder constructor
122
- succeed constructor =
123
- constructor
124
- |> Json.Decode.succeed
125
-
126
-
127
- {-| -}
128
- setValues : Session -> Json.Encode.Value
129
- setValues (Session session) =
216
+ encodeNonExpiringPairs : Session -> Json.Encode.Value
217
+ encodeNonExpiringPairs (Session session) =
130
218
  session
131
219
  |> Dict.toList
132
220
  |> List.filterMap
@@ -151,71 +239,56 @@ flashPrefix =
151
239
  "__flash__"
152
240
 
153
241
 
154
- {-| -}
155
- clearFlashCookies : Dict String String -> Dict String String
156
- clearFlashCookies dict =
157
- Dict.Extra.removeWhen
158
- (\key _ ->
159
- key |> String.startsWith flashPrefix
160
- )
161
- dict
162
-
163
-
164
- {-| -}
165
- expectSession :
166
- { name : String
167
- , secrets : DataSource (List String)
168
- , sameSite : String
169
- }
170
- -> Parser request
171
- -> (request -> Result () Session -> DataSource ( Session, Response data errorPage ))
172
- -> Parser (DataSource (Response data errorPage))
173
- expectSession config userRequest toRequest =
174
- Request.map2
175
- (\sessionCookie userRequestData ->
176
- sessionCookie
177
- |> decryptCookie config
178
- |> DataSource.andThen
179
- (encodeSessionUpdate config toRequest userRequestData)
180
- )
181
- (Request.expectCookie config.name)
182
- userRequest
183
-
184
-
185
242
  {-| -}
186
243
  withSession :
187
244
  { name : String
188
245
  , secrets : DataSource (List String)
189
- , sameSite : String
246
+ , options : SetCookie.Options
190
247
  }
191
- -> Parser request
192
- -> (request -> Result () (Maybe Session) -> DataSource ( Session, Response data errorPage ))
193
- -> Parser (DataSource (Response data errorPage))
194
- withSession config userRequest toRequest =
195
- Request.map2
248
+ -> (request -> Result NotLoadedReason Session -> DataSource ( Session, Response data errorPage ))
249
+ -> Server.Request.Parser request
250
+ -> Server.Request.Parser (DataSource (Response data errorPage))
251
+ withSession config toRequest userRequest =
252
+ Server.Request.map2
196
253
  (\maybeSessionCookie userRequestData ->
197
254
  let
198
- decrypted : DataSource (Result () (Maybe Session))
199
- decrypted =
255
+ unsigned : DataSource (Result NotLoadedReason Session)
256
+ unsigned =
200
257
  case maybeSessionCookie of
201
258
  Just sessionCookie ->
202
259
  sessionCookie
203
- |> decryptCookie config
204
- |> DataSource.map (Result.map Just)
260
+ |> unsignCookie config
261
+ |> DataSource.map
262
+ (\unsignResult ->
263
+ case unsignResult of
264
+ Ok decoded ->
265
+ Ok decoded
266
+
267
+ Err () ->
268
+ Err InvalidSessionCookie
269
+ )
205
270
 
206
271
  Nothing ->
207
- Ok Nothing
272
+ Err NoSessionCookie
208
273
  |> DataSource.succeed
209
274
  in
210
- decrypted
275
+ unsigned
211
276
  |> DataSource.andThen
212
277
  (encodeSessionUpdate config toRequest userRequestData)
213
278
  )
214
- (Request.cookie config.name)
279
+ (Server.Request.cookie config.name)
215
280
  userRequest
216
281
 
217
282
 
218
- encodeSessionUpdate : { a | name : String, secrets : DataSource (List String) } -> (c -> d -> DataSource ( Session, Response data errorPage )) -> c -> d -> DataSource (Response data errorPage)
283
+ encodeSessionUpdate :
284
+ { name : String
285
+ , secrets : DataSource (List String)
286
+ , options : SetCookie.Options
287
+ }
288
+ -> (c -> d -> DataSource ( Session, Response data errorPage ))
289
+ -> c
290
+ -> d
291
+ -> DataSource (Response data errorPage)
219
292
  encodeSessionUpdate config toRequest userRequestData sessionResult =
220
293
  sessionResult
221
294
  |> toRequest userRequestData
@@ -225,25 +298,18 @@ encodeSessionUpdate config toRequest userRequestData sessionResult =
225
298
  (\encoded ->
226
299
  response
227
300
  |> Server.Response.withSetCookieHeader
228
- (SetCookie.setCookie config.name encoded
229
- |> SetCookie.httpOnly
230
- |> SetCookie.withPath "/"
231
- -- TODO set expiration time
232
- -- TODO do I need to encrypt the session expiration as part of it
233
- -- TODO should I update the expiration time every time?
234
- --|> SetCookie.withExpiration (Time.millisToPosix 100000000000)
235
- )
301
+ (SetCookie.setCookie config.name encoded config.options)
236
302
  )
237
- (encrypt config.secrets
238
- (setValues sessionUpdate)
303
+ (sign config.secrets
304
+ (encodeNonExpiringPairs sessionUpdate)
239
305
  )
240
306
  )
241
307
 
242
308
 
243
- decryptCookie : { a | secrets : DataSource (List String) } -> String -> DataSource (Result () Session)
244
- decryptCookie config sessionCookie =
309
+ unsignCookie : { a | secrets : DataSource (List String) } -> String -> DataSource (Result () Session)
310
+ unsignCookie config sessionCookie =
245
311
  sessionCookie
246
- |> decrypt config.secrets (Json.Decode.dict Json.Decode.string)
312
+ |> unsign config.secrets (Json.Decode.dict Json.Decode.string)
247
313
  |> DataSource.map
248
314
  (Result.map
249
315
  (\dict ->
@@ -265,8 +331,8 @@ decryptCookie config sessionCookie =
265
331
  )
266
332
 
267
333
 
268
- encrypt : DataSource (List String) -> Json.Encode.Value -> DataSource String
269
- encrypt getSecrets input =
334
+ sign : DataSource (List String) -> Json.Encode.Value -> DataSource String
335
+ sign getSecrets input =
270
336
  getSecrets
271
337
  |> DataSource.andThen
272
338
  (\secrets ->
@@ -293,8 +359,8 @@ encrypt getSecrets input =
293
359
  )
294
360
 
295
361
 
296
- decrypt : DataSource (List String) -> Json.Decode.Decoder a -> String -> DataSource (Result () a)
297
- decrypt getSecrets decoder input =
362
+ unsign : DataSource (List String) -> Json.Decode.Decoder a -> String -> DataSource (Result () a)
363
+ unsign getSecrets decoder input =
298
364
  getSecrets
299
365
  |> DataSource.andThen
300
366
  (\secrets ->