elm-pages 3.0.0-beta.2 → 3.0.0-beta.20

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 (109) hide show
  1. package/README.md +10 -1
  2. package/codegen/{elm-pages-codegen.js → elm-pages-codegen.cjs} +2420 -1592
  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/i.dat +0 -0
  8. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/o.dat +0 -0
  9. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm.json +1 -1
  10. package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +1326 -121
  11. package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Runner.elm.js +15215 -13007
  12. package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
  13. package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_supervisor.js +1 -1
  14. package/generator/dead-code-review/elm.json +8 -6
  15. package/generator/dead-code-review/src/Pages/Review/DeadCodeEliminateData.elm +189 -17
  16. package/generator/dead-code-review/tests/Pages/Review/DeadCodeEliminateDataTest.elm +255 -21
  17. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
  18. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/i.dat +0 -0
  19. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/o.dat +0 -0
  20. package/generator/review/elm-stuff/tests-0.19.1/elm.json +1 -1
  21. package/generator/review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +1326 -121
  22. package/generator/review/elm-stuff/tests-0.19.1/js/Runner.elm.js +14620 -12636
  23. package/generator/review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
  24. package/generator/review/elm-stuff/tests-0.19.1/js/node_supervisor.js +1 -1
  25. package/generator/review/elm.json +8 -8
  26. package/generator/src/RouteBuilder.elm +66 -52
  27. package/generator/src/SharedTemplate.elm +3 -2
  28. package/generator/src/SiteConfig.elm +3 -2
  29. package/generator/src/basepath-middleware.js +3 -3
  30. package/generator/src/build.js +122 -86
  31. package/generator/src/cli.js +247 -51
  32. package/generator/src/codegen.js +29 -27
  33. package/generator/src/compatibility-key.js +1 -0
  34. package/generator/src/compile-elm.js +20 -22
  35. package/generator/src/config.js +39 -0
  36. package/generator/src/copy-dir.js +2 -2
  37. package/generator/src/dev-server.js +119 -110
  38. package/generator/src/dir-helpers.js +9 -26
  39. package/generator/src/elm-codegen.js +5 -4
  40. package/generator/src/elm-file-constants.js +2 -3
  41. package/generator/src/error-formatter.js +12 -11
  42. package/generator/src/file-helpers.js +3 -4
  43. package/generator/src/generate-template-module-connector.js +14 -21
  44. package/generator/src/init.js +8 -7
  45. package/generator/src/pre-render-html.js +41 -28
  46. package/generator/src/render-test.js +109 -0
  47. package/generator/src/render-worker.js +26 -28
  48. package/generator/src/render.js +322 -142
  49. package/generator/src/request-cache.js +200 -162
  50. package/generator/src/rewrite-client-elm-json.js +5 -5
  51. package/generator/src/rewrite-elm-json.js +7 -7
  52. package/generator/src/route-codegen-helpers.js +16 -31
  53. package/generator/src/seo-renderer.js +12 -7
  54. package/generator/src/vite-utils.js +77 -0
  55. package/generator/static-code/hmr.js +16 -2
  56. package/generator/template/app/Api.elm +3 -3
  57. package/generator/template/app/Route/Index.elm +3 -3
  58. package/generator/template/app/Shared.elm +3 -3
  59. package/generator/template/app/Site.elm +9 -4
  60. package/package.json +21 -21
  61. package/src/ApiRoute.elm +199 -61
  62. package/src/BackendTask/Custom.elm +214 -0
  63. package/src/BackendTask/Env.elm +90 -0
  64. package/src/{DataSource → BackendTask}/File.elm +128 -43
  65. package/src/{DataSource → BackendTask}/Glob.elm +136 -125
  66. package/src/BackendTask/Http.elm +673 -0
  67. package/src/{DataSource → BackendTask}/Internal/Glob.elm +1 -1
  68. package/src/BackendTask/Internal/Request.elm +28 -0
  69. package/src/BackendTask/Random.elm +79 -0
  70. package/src/BackendTask/Time.elm +47 -0
  71. package/src/BackendTask.elm +537 -0
  72. package/src/FatalError.elm +89 -0
  73. package/src/Form/Field.elm +1 -1
  74. package/src/Form.elm +72 -92
  75. package/src/Head/Seo.elm +4 -4
  76. package/src/Head.elm +237 -7
  77. package/src/HtmlPrinter.elm +7 -3
  78. package/src/Internal/ApiRoute.elm +7 -5
  79. package/src/PageServerResponse.elm +6 -1
  80. package/src/Pages/Generate.elm +775 -132
  81. package/src/Pages/GeneratorProgramConfig.elm +15 -0
  82. package/src/Pages/Internal/FatalError.elm +5 -0
  83. package/src/Pages/Internal/Form.elm +21 -1
  84. package/src/Pages/Internal/Platform/Cli.elm +479 -747
  85. package/src/Pages/Internal/Platform/Cli.elm.bak +1276 -0
  86. package/src/Pages/Internal/Platform/CompatibilityKey.elm +6 -0
  87. package/src/Pages/Internal/Platform/Effect.elm +1 -2
  88. package/src/Pages/Internal/Platform/GeneratorApplication.elm +373 -0
  89. package/src/Pages/Internal/Platform/StaticResponses.elm +73 -270
  90. package/src/Pages/Internal/Platform/ToJsPayload.elm +4 -7
  91. package/src/Pages/Internal/Platform.elm +54 -53
  92. package/src/Pages/Internal/Script.elm +17 -0
  93. package/src/Pages/Internal/StaticHttpBody.elm +35 -1
  94. package/src/Pages/Manifest.elm +29 -4
  95. package/src/Pages/ProgramConfig.elm +12 -8
  96. package/src/Pages/Script.elm +109 -0
  97. package/src/Pages/SiteConfig.elm +3 -2
  98. package/src/Pages/StaticHttp/Request.elm +2 -2
  99. package/src/Pages/StaticHttpRequest.elm +23 -98
  100. package/src/RequestsAndPending.elm +8 -19
  101. package/src/Result/Extra.elm +21 -0
  102. package/src/Server/Request.elm +43 -34
  103. package/src/Server/Session.elm +166 -100
  104. package/src/Server/SetCookie.elm +89 -31
  105. package/src/DataSource/Env.elm +0 -38
  106. package/src/DataSource/Http.elm +0 -446
  107. package/src/DataSource/Internal/Request.elm +0 -20
  108. package/src/DataSource/Port.elm +0 -90
  109. package/src/DataSource.elm +0 -538
package/src/ApiRoute.elm CHANGED
@@ -1,32 +1,164 @@
1
1
  module ApiRoute exposing
2
- ( ApiRoute, ApiRouteBuilder, Response
2
+ ( single, preRender
3
+ , serverRender
4
+ , preRenderWithFallback
5
+ , ApiRoute, ApiRouteBuilder, Response
3
6
  , capture, literal, slash, succeed
4
- , single, preRender
5
- , preRenderWithFallback, serverRender
6
7
  , withGlobalHeadTags
7
- , toJson, getBuildTimeRoutes, getGlobalHeadTagsDataSource
8
+ , toJson, getBuildTimeRoutes, getGlobalHeadTagsBackendTask
8
9
  )
9
10
 
10
11
  {-| ApiRoute's are defined in `src/Api.elm` and are a way to generate files, like RSS feeds, sitemaps, or any text-based file that you output with an Elm function! You get access
11
- to a DataSource so you can pull in HTTP data, etc. Because ApiRoutes don't hydrate into Elm apps (like pages in elm-pages do), you can pull in as much data as you want in
12
- the DataSource for your ApiRoutes, and it won't effect the payload size. Instead, the size of an ApiRoute is just the content you output for that route.
12
+ to a BackendTask so you can pull in HTTP data, etc. Because ApiRoutes don't hydrate into Elm apps (like pages in elm-pages do), you can pull in as much data as you want in
13
+ the BackendTask for your ApiRoutes, and it won't effect the payload size. Instead, the size of an ApiRoute is just the content you output for that route.
13
14
 
14
- In a future release, ApiRoutes may be able to run at request-time in a serverless function, allowing you to use pure Elm code to create dynamic APIs, and even pulling in data from
15
- DataSources dynamically.
15
+ Similar to your elm-pages Route Modules, ApiRoute's can be either server-rendered or pre-rendered. Let's compare the differences between pre-rendered and server-rendered ApiRoutes, and the different
16
+ use cases they support.
16
17
 
17
- @docs ApiRoute, ApiRouteBuilder, Response
18
18
 
19
- @docs capture, literal, slash, succeed
19
+ ## Pre-Rendering
20
20
 
21
+ A pre-rendered ApiRoute is just a generated file. For example:
21
22
 
22
- ## Pre-Rendering
23
+ - [An RSS feed](https://github.com/dillonkearns/elm-pages/blob/131f7b750cdefb2ba7a34a06be06dfbfafc79a86/examples/docs/app/Api.elm#L77-L84) ([Output file](https://elm-pages.com/blog/feed.xml))
24
+ - [A calendar feed in the ical format](https://github.com/dillonkearns/incrementalelm.com/blob/d4934d899d06232dc66dcf9f4b5eccc74bbc60d3/src/Api.elm#L51-L60) ([Output file](https://incrementalelm.com/live.ics))
25
+ - A redirect file for a hosting provider like Netlify
26
+
27
+ You could even generate a JavaScript file, an Elm file, or any file with a String body! It's really just a way to generate files, which are typically used to serve files to a user or Browser, but you execute them, copy them, etc. The only limit is your imagination!
28
+ The beauty is that you have a way to 1) pull in type-safe data using BackendTask's, and 2) write those files, and all in pure Elm!
23
29
 
24
30
  @docs single, preRender
25
31
 
26
32
 
27
33
  ## Server Rendering
28
34
 
29
- @docs preRenderWithFallback, serverRender
35
+ You could use server-rendered ApiRoutes to do a lot of similar things, the main difference being that it will be served up through a URL and generated on-demand when that URL is requested.
36
+ So for example, for an RSS feed or ical calendar feed like in the pre-rendered examples, you could build the same routes, but you would be pulling in the list of posts or calendar events on-demand rather
37
+ than upfront at build-time. That means you can hit your database and serve up always-up-to-date data.
38
+
39
+ Not only that, but your server-rendered ApiRoutes have access to the incoming HTTP request payload just like your server-rendered Route Modules do. Just as with server-rendered Route Modules,
40
+ a server-rendered ApiRoute accesses the incoming HTTP request through a [Server.Request.Parser](Server-Request). Consider the use cases that this opens up:
41
+
42
+ - Serve up protected assets. For example, gated content, like a paid subscriber feed for a podcast that checks authentication information in a query parameter to authenticate that a user has an active paid subscription before serving up the Pro RSS feed.
43
+ - Serve up user-specific content, either through a cookie or other means of authentication
44
+ - Look at the [accepted content-type in the request headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) and use that to choose a response format, like XML or JSON ([full example](https://github.com/dillonkearns/elm-pages/blob/131f7b750cdefb2ba7a34a06be06dfbfafc79a86/examples/end-to-end/app/Api.elm#L76-L107)).
45
+ - Look at the [accepted language in the request headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) and use that to choose a language for the response data.
46
+
47
+ @docs serverRender
48
+
49
+ You can also do a hybrid approach using `preRenderWithFallback`. This allows you to pre-render a set of routes at build-time, but build additional routes that weren't rendered at build-time on the fly on the server.
50
+ Conceptually, this is just a delayed version of a pre-rendered route. Because of that, you _do not_ have access to the incoming HTTP request (no `Server.Request.Parser` like in server-rendered ApiRoute's).
51
+ The strategy used to build these routes will differ depending on your hosting provider and the elm-pages adapter you have setup, but generally ApiRoute's that use `preRenderWithFallback` will be cached on the server
52
+ so within a certain time interval (or in the case of [Netlify's DPR](https://www.netlify.com/blog/2021/04/14/distributed-persistent-rendering-a-new-jamstack-approach-for-faster-builds/), until a new build is done)
53
+ that asset will be served up if that URL was already served up by the server.
54
+
55
+ @docs preRenderWithFallback
56
+
57
+
58
+ ## Defining ApiRoute's
59
+
60
+ You define your ApiRoute's in `app/Api.elm`. Here's a simple example:
61
+
62
+ module Api exposing (routes)
63
+
64
+ import ApiRoute
65
+ import BackendTask exposing (BackendTask)
66
+ import Server.Request
67
+
68
+ routes :
69
+ BackendTask (List Route)
70
+ -> (Maybe { indent : Int, newLines : Bool } -> Html Never -> String)
71
+ -> List (ApiRoute.ApiRoute ApiRoute.Response)
72
+ routes getStaticRoutes htmlToString =
73
+ [ preRenderedExample
74
+ , requestPrinterExample
75
+ ]
76
+
77
+ {-| Generates the following files when you
78
+ run `elm-pages build`:
79
+
80
+ - `dist/users/1.json`
81
+ - `dist/users/2.json`
82
+ - `dist/users/3.json`
83
+
84
+ When you host it, these static assets will
85
+ be served at `/users/1.json`, etc.
86
+
87
+ -}
88
+ preRenderedExample : ApiRoute.ApiRoute ApiRoute.Response
89
+ preRenderedExample =
90
+ ApiRoute.succeed
91
+ (\userId ->
92
+ BackendTask.succeed
93
+ (Json.Encode.object
94
+ [ ( "id", Json.Encode.string userId )
95
+ , ( "name", "Data for user " ++ userId |> Json.Encode.string )
96
+ ]
97
+ |> Json.Encode.encode 2
98
+ )
99
+ )
100
+ |> ApiRoute.literal "users"
101
+ |> ApiRoute.slash
102
+ |> ApiRoute.capture
103
+ |> ApiRoute.literal ".json"
104
+ |> ApiRoute.preRender
105
+ (\route ->
106
+ BackendTask.succeed
107
+ [ route "1"
108
+ , route "2"
109
+ , route "3"
110
+ ]
111
+ )
112
+
113
+ {-| This returns a JSON response that prints information about the incoming
114
+ HTTP request. In practice you'd want to do something useful with that data,
115
+ and use more of the high-level helpers from the Server.Request API.
116
+ -}
117
+ requestPrinterExample : ApiRoute ApiRoute.Response
118
+ requestPrinterExample =
119
+ ApiRoute.succeed
120
+ (Server.Request.map4
121
+ (\rawBody method cookies queryParams ->
122
+ Encode.object
123
+ [ ( "rawBody"
124
+ , rawBody
125
+ |> Maybe.map Encode.string
126
+ |> Maybe.withDefault Encode.null
127
+ )
128
+ , ( "method"
129
+ , method
130
+ |> Server.Request.methodToString
131
+ |> Encode.string
132
+ )
133
+ , ( "cookies"
134
+ , cookies
135
+ |> Encode.dict
136
+ identity
137
+ Encode.string
138
+ )
139
+ , ( "queryParams"
140
+ , queryParams
141
+ |> Encode.dict
142
+ identity
143
+ (Encode.list Encode.string)
144
+ )
145
+ ]
146
+ |> Response.json
147
+ |> BackendTask.succeed
148
+ )
149
+ Server.Request.rawBody
150
+ Server.Request.method
151
+ Server.Request.allCookies
152
+ Server.Request.queryParams
153
+ )
154
+ |> ApiRoute.literal "api"
155
+ |> ApiRoute.slash
156
+ |> ApiRoute.literal "request-test"
157
+ |> ApiRoute.serverRender
158
+
159
+ @docs ApiRoute, ApiRouteBuilder, Response
160
+
161
+ @docs capture, literal, slash, succeed
30
162
 
31
163
 
32
164
  ## Including Head Tags
@@ -36,12 +168,12 @@ DataSources dynamically.
36
168
 
37
169
  ## Internals
38
170
 
39
- @docs toJson, getBuildTimeRoutes, getGlobalHeadTagsDataSource
171
+ @docs toJson, getBuildTimeRoutes, getGlobalHeadTagsBackendTask
40
172
 
41
173
  -}
42
174
 
43
- import DataSource exposing (DataSource)
44
- import DataSource.Http
175
+ import BackendTask exposing (BackendTask)
176
+ import FatalError exposing (FatalError)
45
177
  import Head
46
178
  import Internal.ApiRoute exposing (ApiRoute(..), ApiRouteBuilder(..))
47
179
  import Json.Decode as Decode
@@ -57,27 +189,38 @@ type alias ApiRoute response =
57
189
  Internal.ApiRoute.ApiRoute response
58
190
 
59
191
 
60
- {-| -}
61
- single : ApiRouteBuilder (DataSource String) (List String) -> ApiRoute Response
192
+ {-| Same as [`preRender`](#preRender), but for an ApiRoute that has no dynamic segments. This is just a bit simpler because
193
+ since there are no dynamic segments, you don't need to provide a BackendTask with the list of dynamic segments to pre-render because there is only a single possible route.
194
+ -}
195
+ single : ApiRouteBuilder (BackendTask FatalError String) (List String) -> ApiRoute Response
62
196
  single handler =
63
197
  handler
64
- |> preRender (\constructor -> DataSource.succeed [ constructor ])
198
+ |> preRender (\constructor -> BackendTask.succeed [ constructor ])
65
199
 
66
200
 
67
201
  {-| -}
68
- serverRender : ApiRouteBuilder (Server.Request.Parser (DataSource (Server.Response.Response Never Never))) constructor -> ApiRoute Response
202
+ serverRender : ApiRouteBuilder (Server.Request.Parser (BackendTask FatalError (Server.Response.Response Never Never))) constructor -> ApiRoute Response
69
203
  serverRender ((ApiRouteBuilder patterns pattern _ _ _) as fullHandler) =
70
204
  ApiRoute
71
205
  { regex = Regex.fromString ("^" ++ pattern ++ "$") |> Maybe.withDefault Regex.never
72
206
  , matchesToResponse =
73
- \path ->
207
+ \serverRequest path ->
74
208
  Internal.ApiRoute.tryMatch path fullHandler
75
209
  |> Maybe.map
76
- (\toDataSource ->
77
- DataSource.Http.get
78
- "$$elm-pages$$headers"
79
- (toDataSource |> Server.Request.getDecoder |> Decode.map Just)
80
- |> DataSource.andThen
210
+ (\toBackendTask ->
211
+ Server.Request.getDecoder toBackendTask
212
+ |> (\decoder ->
213
+ Decode.decodeValue decoder serverRequest
214
+ |> Result.mapError Decode.errorToString
215
+ |> BackendTask.fromResult
216
+ |> BackendTask.map Just
217
+ )
218
+ |> BackendTask.onError
219
+ (\stringError ->
220
+ -- TODO make error with title and better context/formatting
221
+ FatalError.fromString stringError |> BackendTask.fail
222
+ )
223
+ |> BackendTask.andThen
81
224
  (\rendered ->
82
225
  case rendered of
83
226
  Just (Ok okRendered) ->
@@ -88,21 +231,21 @@ serverRender ((ApiRouteBuilder patterns pattern _ _ _) as fullHandler) =
88
231
  |> Server.Request.errorsToString
89
232
  |> Server.Response.plainText
90
233
  |> Server.Response.withStatusCode 400
91
- |> DataSource.succeed
234
+ |> BackendTask.succeed
92
235
 
93
236
  Nothing ->
94
237
  Server.Response.plainText "No matching request handler"
95
238
  |> Server.Response.withStatusCode 400
96
- |> DataSource.succeed
239
+ |> BackendTask.succeed
97
240
  )
98
241
  )
99
- |> Maybe.map (DataSource.map (Server.Response.toJson >> Just))
242
+ |> Maybe.map (BackendTask.map (Server.Response.toJson >> Just))
100
243
  |> Maybe.withDefault
101
- (DataSource.succeed Nothing)
102
- , buildTimeRoutes = DataSource.succeed []
244
+ (BackendTask.succeed Nothing)
245
+ , buildTimeRoutes = BackendTask.succeed []
103
246
  , handleRoute =
104
247
  \path ->
105
- DataSource.succeed
248
+ BackendTask.succeed
106
249
  (case Internal.ApiRoute.tryMatch path fullHandler of
107
250
  Just _ ->
108
251
  True
@@ -117,26 +260,26 @@ serverRender ((ApiRouteBuilder patterns pattern _ _ _) as fullHandler) =
117
260
 
118
261
 
119
262
  {-| -}
120
- preRenderWithFallback : (constructor -> DataSource (List (List String))) -> ApiRouteBuilder (DataSource (Server.Response.Response Never Never)) constructor -> ApiRoute Response
263
+ preRenderWithFallback : (constructor -> BackendTask FatalError (List (List String))) -> ApiRouteBuilder (BackendTask FatalError (Server.Response.Response Never Never)) constructor -> ApiRoute Response
121
264
  preRenderWithFallback buildUrls ((ApiRouteBuilder patterns pattern _ toString constructor) as fullHandler) =
122
265
  let
123
- buildTimeRoutes__ : DataSource (List String)
266
+ buildTimeRoutes__ : BackendTask FatalError (List String)
124
267
  buildTimeRoutes__ =
125
268
  buildUrls (constructor [])
126
- |> DataSource.map (List.map toString)
269
+ |> BackendTask.map (List.map toString)
127
270
  in
128
271
  ApiRoute
129
272
  { regex = Regex.fromString ("^" ++ pattern ++ "$") |> Maybe.withDefault Regex.never
130
273
  , matchesToResponse =
131
- \path ->
274
+ \_ path ->
132
275
  Internal.ApiRoute.tryMatch path fullHandler
133
- |> Maybe.map (DataSource.map (Server.Response.toJson >> Just))
276
+ |> Maybe.map (BackendTask.map (Server.Response.toJson >> Just))
134
277
  |> Maybe.withDefault
135
- (DataSource.succeed Nothing)
278
+ (BackendTask.succeed Nothing)
136
279
  , buildTimeRoutes = buildTimeRoutes__
137
280
  , handleRoute =
138
281
  \path ->
139
- DataSource.succeed
282
+ BackendTask.succeed
140
283
  (case Internal.ApiRoute.tryMatch path fullHandler of
141
284
  Just _ ->
142
285
  True
@@ -159,42 +302,42 @@ encodeStaticFileBody fileBody =
159
302
 
160
303
 
161
304
  {-| -}
162
- preRender : (constructor -> DataSource (List (List String))) -> ApiRouteBuilder (DataSource String) constructor -> ApiRoute Response
305
+ preRender : (constructor -> BackendTask FatalError (List (List String))) -> ApiRouteBuilder (BackendTask FatalError String) constructor -> ApiRoute Response
163
306
  preRender buildUrls ((ApiRouteBuilder patterns pattern _ toString constructor) as fullHandler) =
164
307
  let
165
- buildTimeRoutes__ : DataSource (List String)
308
+ buildTimeRoutes__ : BackendTask FatalError (List String)
166
309
  buildTimeRoutes__ =
167
310
  buildUrls (constructor [])
168
- |> DataSource.map (List.map toString)
311
+ |> BackendTask.map (List.map toString)
169
312
 
170
- preBuiltMatches : DataSource (List (List String))
313
+ preBuiltMatches : BackendTask FatalError (List (List String))
171
314
  preBuiltMatches =
172
315
  buildUrls (constructor [])
173
316
  in
174
317
  ApiRoute
175
318
  { regex = Regex.fromString ("^" ++ pattern ++ "$") |> Maybe.withDefault Regex.never
176
319
  , matchesToResponse =
177
- \path ->
320
+ \_ path ->
178
321
  let
179
322
  matches : List String
180
323
  matches =
181
324
  Internal.ApiRoute.pathToMatches path fullHandler
182
325
 
183
- routeFound : DataSource Bool
326
+ routeFound : BackendTask FatalError Bool
184
327
  routeFound =
185
328
  preBuiltMatches
186
- |> DataSource.map (List.member matches)
329
+ |> BackendTask.map (List.member matches)
187
330
  in
188
331
  routeFound
189
- |> DataSource.andThen
332
+ |> BackendTask.andThen
190
333
  (\found ->
191
334
  if found then
192
335
  Internal.ApiRoute.tryMatch path fullHandler
193
- |> Maybe.map (DataSource.map (encodeStaticFileBody >> Just))
194
- |> Maybe.withDefault (DataSource.succeed Nothing)
336
+ |> Maybe.map (BackendTask.map (encodeStaticFileBody >> Just))
337
+ |> Maybe.withDefault (BackendTask.succeed Nothing)
195
338
 
196
339
  else
197
- DataSource.succeed Nothing
340
+ BackendTask.succeed Nothing
198
341
  )
199
342
  , buildTimeRoutes = buildTimeRoutes__
200
343
  , handleRoute =
@@ -205,7 +348,7 @@ preRender buildUrls ((ApiRouteBuilder patterns pattern _ toString constructor) a
205
348
  Internal.ApiRoute.pathToMatches path fullHandler
206
349
  in
207
350
  preBuiltMatches
208
- |> DataSource.map (List.member matches)
351
+ |> BackendTask.map (List.member matches)
209
352
  , pattern = patterns
210
353
  , kind = "prerender"
211
354
  , globalHeadTags = Nothing
@@ -238,7 +381,8 @@ toJson ((ApiRoute { kind }) as apiRoute) =
238
381
  ]
239
382
 
240
383
 
241
- {-| -}
384
+ {-| A literal String segment of a route.
385
+ -}
242
386
  literal : String -> ApiRouteBuilder a constructor -> ApiRouteBuilder a constructor
243
387
  literal segment (ApiRouteBuilder patterns pattern handler toString constructor) =
244
388
  ApiRouteBuilder
@@ -287,25 +431,19 @@ capture (ApiRouteBuilder patterns pattern previousHandler toString constructor)
287
431
 
288
432
  {-| For internal use by generated code. Not so useful in user-land.
289
433
  -}
290
- getBuildTimeRoutes : ApiRoute response -> DataSource (List String)
434
+ getBuildTimeRoutes : ApiRoute response -> BackendTask FatalError (List String)
291
435
  getBuildTimeRoutes (ApiRoute handler) =
292
436
  handler.buildTimeRoutes
293
437
 
294
438
 
295
439
  {-| Include head tags on every page's HTML.
296
440
  -}
297
- withGlobalHeadTags : DataSource (List Head.Tag) -> ApiRoute response -> ApiRoute response
441
+ withGlobalHeadTags : BackendTask FatalError (List Head.Tag) -> ApiRoute response -> ApiRoute response
298
442
  withGlobalHeadTags globalHeadTags (ApiRoute handler) =
299
443
  ApiRoute { handler | globalHeadTags = Just globalHeadTags }
300
444
 
301
445
 
302
446
  {-| -}
303
- getGlobalHeadTagsDataSource : ApiRoute response -> Maybe (DataSource (List Head.Tag))
304
- getGlobalHeadTagsDataSource (ApiRoute handler) =
447
+ getGlobalHeadTagsBackendTask : ApiRoute response -> Maybe (BackendTask FatalError (List Head.Tag))
448
+ getGlobalHeadTagsBackendTask (ApiRoute handler) =
305
449
  handler.globalHeadTags
306
-
307
-
308
-
309
- --captureRest : ApiRouteBuilder (List String -> a) b -> ApiRouteBuilder a b
310
- --captureRest previousHandler =
311
- -- Debug.todo ""
@@ -0,0 +1,214 @@
1
+ module BackendTask.Custom exposing
2
+ ( run
3
+ , Error(..)
4
+ )
5
+
6
+ {-| In a vanilla Elm application, ports let you either send or receive JSON data between your Elm application and the JavaScript context in the user's browser at runtime.
7
+
8
+ With `BackendTask.Custom`, you send and receive JSON to JavaScript running in NodeJS. As with any `BackendTask`, Custom BackendTask's are either run at build-time (for pre-rendered routes) or at request-time (for server-rendered routes). See [`BackendTask`](BackendTask) for more about the
9
+ lifecycle of `BackendTask`'s.
10
+
11
+ This means that you can call shell scripts, run NPM packages that are installed, or anything else you could do with NodeJS to perform custom side-effects, get some data, or both.
12
+
13
+ A `BackendTask.Custom` will call an async JavaScript function with the given name from the definition in a file called `custom-backend-task.js` in your project's root directory. The function receives the input JSON value, and the Decoder is used to decode the return value of the async function.
14
+
15
+ @docs run
16
+
17
+ Here is the Elm code and corresponding JavaScript definition for getting an environment variable (or an `FatalError BackendTask.Custom.Error` if it isn't found). In this example,
18
+ we're using `BackendTask.allowFatal` to let the framework treat that as an unexpected exception, but we could also handle the possible failures of the `FatalError` (see [`FatalError`](FatalError)).
19
+
20
+ import BackendTask exposing (BackendTask)
21
+ import BackendTask.Custom
22
+ import Json.Encode
23
+ import OptimizedDecoder as Decode
24
+
25
+ data : BackendTask FatalError String
26
+ data =
27
+ BackendTask.Custom.run "environmentVariable"
28
+ (Json.Encode.string "EDITOR")
29
+ Decode.string
30
+ |> BackendTask.allowFatal
31
+
32
+ -- will resolve to "VIM" if you run `EDITOR=vim elm-pages dev`
33
+
34
+ ```javascript
35
+ // custom-backend-task.js
36
+
37
+ module.exports =
38
+ /**
39
+ * @param { unknown } fromElm
40
+ * @returns { Promise<unknown> }
41
+ */
42
+ {
43
+ environmentVariable: async function (name) {
44
+ const result = process.env[name];
45
+ if (result) {
46
+ return result;
47
+ } else {
48
+ throw `No environment variable called ${name}
49
+
50
+ Available:
51
+
52
+ ${Object.keys(process.env).join("\n")}
53
+ `;
54
+ }
55
+ },
56
+ }
57
+ ```
58
+
59
+
60
+ ## Performance
61
+
62
+ As with any JavaScript or NodeJS code, avoid doing blocking IO operations. For example, avoid using `fs.readFileSync`, because blocking IO can slow down your elm-pages builds and dev server. `elm-pages` performs all `BackendTask`'s in parallel whenever possible.
63
+ So if you do `BackendTask.map2 Tuple.pair myHttpBackendTask myCustomBackendTask`, it will resolve those two in parallel. NodeJS performs best when you take advantage of its ability to do non-blocking I/O (file reads, HTTP requests, etc.). If you use `BackendTask.andThen`,
64
+ it will need to resolve them in sequence rather than in parallel, but it's still best to avoid blocking IO operations in your Custom BackendTask definitions.
65
+
66
+
67
+ ## Error Handling
68
+
69
+ There are a few different things that can go wrong when running a custom-backend-task. These possible errors are captured in the `BackendTask.Custom.Error` type.
70
+
71
+ @docs Error
72
+
73
+ Any time you throw a JavaScript exception from a BackendTask.Custom definition, it will give you a `CustomBackendTaskException`. It's usually easier to add a `try`/`catch` in your JavaScript code in `custom-backend-task.js`
74
+ to handle possible errors, but you can throw a JSON value and handle it in Elm in the `CustomBackendTaskException` call error.
75
+
76
+ -}
77
+
78
+ import BackendTask
79
+ import BackendTask.Http
80
+ import BackendTask.Internal.Request
81
+ import FatalError exposing (FatalError)
82
+ import Json.Decode as Decode exposing (Decoder)
83
+ import Json.Encode as Encode
84
+ import TerminalText
85
+
86
+
87
+ {-| -}
88
+ run :
89
+ String
90
+ -> Encode.Value
91
+ -> Decoder b
92
+ -> BackendTask.BackendTask { fatal : FatalError, recoverable : Error } b
93
+ run portName input decoder =
94
+ BackendTask.Internal.Request.request
95
+ { name = "port"
96
+ , body =
97
+ Encode.object
98
+ [ ( "input", input )
99
+ , ( "portName", Encode.string portName )
100
+ ]
101
+ |> BackendTask.Http.jsonBody
102
+ , expect =
103
+ Decode.oneOf
104
+ [ Decode.field "elm-pages-internal-error" Decode.string
105
+ |> Decode.andThen
106
+ (\errorKind ->
107
+ if errorKind == "CustomBackendTaskNotDefined" then
108
+ FatalError.recoverable
109
+ { title = "Custom BackendTask Error"
110
+ , body =
111
+ [ TerminalText.text "Something went wrong in a call to BackendTask.Custom.run. I expected to find a port named `"
112
+ , TerminalText.yellow portName
113
+ , TerminalText.text "` but I couldn't find it. Is the function exported in your custom-backend-task file?"
114
+ ]
115
+ |> TerminalText.toString
116
+ }
117
+ (CustomBackendTaskNotDefined { name = portName })
118
+ |> Decode.succeed
119
+
120
+ else if errorKind == "ExportIsNotFunction" then
121
+ Decode.field "error" Decode.string
122
+ |> Decode.maybe
123
+ |> Decode.map (Maybe.withDefault "")
124
+ |> Decode.map
125
+ (\incorrectType ->
126
+ FatalError.recoverable
127
+ { title = "Custom BackendTask Error"
128
+ , body =
129
+ [ TerminalText.text "Something went wrong in a call to BackendTask.Custom.run. I found an export called `"
130
+ , TerminalText.yellow portName
131
+ , TerminalText.text "` but I expected its type to be function, but instead its type was: "
132
+ , TerminalText.red incorrectType
133
+ ]
134
+ |> TerminalText.toString
135
+ }
136
+ ExportIsNotFunction
137
+ )
138
+
139
+ else if errorKind == "MissingCustomBackendTaskFile" then
140
+ FatalError.recoverable
141
+ { title = "Custom BackendTask Error"
142
+ , body =
143
+ [ TerminalText.text "Something went wrong in a call to BackendTask.Custom.run. I couldn't find your custom-backend-task file. Be sure to create a 'custom-backend-task.ts' or 'custom-backend-task.js' file."
144
+ ]
145
+ |> TerminalText.toString
146
+ }
147
+ MissingCustomBackendTaskFile
148
+ |> Decode.succeed
149
+
150
+ else if errorKind == "ErrorInCustomBackendTaskFile" then
151
+ Decode.field "error" Decode.string
152
+ |> Decode.maybe
153
+ |> Decode.map (Maybe.withDefault "")
154
+ |> Decode.map
155
+ (\errorMessage ->
156
+ FatalError.recoverable
157
+ { title = "Custom BackendTask Error"
158
+ , body =
159
+ [ TerminalText.text "Something went wrong in a call to BackendTask.Custom.run. I couldn't import the port definitions file, because of this exception:\n\n"
160
+ , TerminalText.red errorMessage
161
+ , TerminalText.text "\n\nAre there syntax errors or exceptions thrown during import?"
162
+ ]
163
+ |> TerminalText.toString
164
+ }
165
+ ErrorInCustomBackendTaskFile
166
+ )
167
+
168
+ else if errorKind == "CustomBackendTaskException" then
169
+ Decode.field "error" Decode.value
170
+ |> Decode.maybe
171
+ |> Decode.map (Maybe.withDefault Encode.null)
172
+ |> Decode.map
173
+ (\portCallError ->
174
+ FatalError.recoverable
175
+ { title = "Custom BackendTask Error"
176
+ , body =
177
+ [ TerminalText.text "Something went wrong in a call to BackendTask.Custom.run. I was able to import the port definitions file, but when running it I encountered this exception:\n\n"
178
+ , TerminalText.red (Encode.encode 2 portCallError)
179
+ , TerminalText.text "\n\nYou could add a `try`/`catch` in your `custom-backend-task` JavaScript code to handle that error."
180
+ ]
181
+ |> TerminalText.toString
182
+ }
183
+ (CustomBackendTaskException portCallError)
184
+ )
185
+
186
+ else
187
+ FatalError.recoverable
188
+ { title = "Custom BackendTask Error"
189
+ , body =
190
+ [ TerminalText.text "Something went wrong in a call to BackendTask.Custom.run. I expected to find a port named `"
191
+ , TerminalText.yellow portName
192
+ , TerminalText.text "`."
193
+ ]
194
+ |> TerminalText.toString
195
+ }
196
+ ErrorInCustomBackendTaskFile
197
+ |> Decode.succeed
198
+ )
199
+ |> Decode.map Err
200
+ , decoder |> Decode.map Ok
201
+ ]
202
+ |> BackendTask.Http.expectJson
203
+ }
204
+ |> BackendTask.andThen BackendTask.fromResult
205
+
206
+
207
+ {-| -}
208
+ type Error
209
+ = Error
210
+ | ErrorInCustomBackendTaskFile
211
+ | MissingCustomBackendTaskFile
212
+ | CustomBackendTaskNotDefined { name : String }
213
+ | CustomBackendTaskException Decode.Value
214
+ | ExportIsNotFunction