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.
- package/README.md +10 -1
- package/codegen/{elm-pages-codegen.js → elm-pages-codegen.cjs} +2420 -1592
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateData.elmi +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateData.elmo +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateDataTest.elmo +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/i.dat +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/o.dat +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm.json +1 -1
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +1326 -121
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Runner.elm.js +15215 -13007
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_supervisor.js +1 -1
- package/generator/dead-code-review/elm.json +8 -6
- package/generator/dead-code-review/src/Pages/Review/DeadCodeEliminateData.elm +189 -17
- package/generator/dead-code-review/tests/Pages/Review/DeadCodeEliminateDataTest.elm +255 -21
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/i.dat +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/o.dat +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm.json +1 -1
- package/generator/review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +1326 -121
- package/generator/review/elm-stuff/tests-0.19.1/js/Runner.elm.js +14620 -12636
- package/generator/review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
- package/generator/review/elm-stuff/tests-0.19.1/js/node_supervisor.js +1 -1
- package/generator/review/elm.json +8 -8
- package/generator/src/RouteBuilder.elm +66 -52
- package/generator/src/SharedTemplate.elm +3 -2
- package/generator/src/SiteConfig.elm +3 -2
- package/generator/src/basepath-middleware.js +3 -3
- package/generator/src/build.js +122 -86
- package/generator/src/cli.js +247 -51
- package/generator/src/codegen.js +29 -27
- package/generator/src/compatibility-key.js +1 -0
- package/generator/src/compile-elm.js +20 -22
- package/generator/src/config.js +39 -0
- package/generator/src/copy-dir.js +2 -2
- package/generator/src/dev-server.js +119 -110
- package/generator/src/dir-helpers.js +9 -26
- package/generator/src/elm-codegen.js +5 -4
- package/generator/src/elm-file-constants.js +2 -3
- package/generator/src/error-formatter.js +12 -11
- package/generator/src/file-helpers.js +3 -4
- package/generator/src/generate-template-module-connector.js +14 -21
- package/generator/src/init.js +8 -7
- package/generator/src/pre-render-html.js +41 -28
- package/generator/src/render-test.js +109 -0
- package/generator/src/render-worker.js +26 -28
- package/generator/src/render.js +322 -142
- package/generator/src/request-cache.js +200 -162
- package/generator/src/rewrite-client-elm-json.js +5 -5
- package/generator/src/rewrite-elm-json.js +7 -7
- package/generator/src/route-codegen-helpers.js +16 -31
- package/generator/src/seo-renderer.js +12 -7
- package/generator/src/vite-utils.js +77 -0
- package/generator/static-code/hmr.js +16 -2
- package/generator/template/app/Api.elm +3 -3
- package/generator/template/app/Route/Index.elm +3 -3
- package/generator/template/app/Shared.elm +3 -3
- package/generator/template/app/Site.elm +9 -4
- package/package.json +21 -21
- package/src/ApiRoute.elm +199 -61
- package/src/BackendTask/Custom.elm +214 -0
- package/src/BackendTask/Env.elm +90 -0
- package/src/{DataSource → BackendTask}/File.elm +128 -43
- package/src/{DataSource → BackendTask}/Glob.elm +136 -125
- package/src/BackendTask/Http.elm +673 -0
- package/src/{DataSource → BackendTask}/Internal/Glob.elm +1 -1
- package/src/BackendTask/Internal/Request.elm +28 -0
- package/src/BackendTask/Random.elm +79 -0
- package/src/BackendTask/Time.elm +47 -0
- package/src/BackendTask.elm +537 -0
- package/src/FatalError.elm +89 -0
- package/src/Form/Field.elm +1 -1
- package/src/Form.elm +72 -92
- package/src/Head/Seo.elm +4 -4
- package/src/Head.elm +237 -7
- package/src/HtmlPrinter.elm +7 -3
- package/src/Internal/ApiRoute.elm +7 -5
- package/src/PageServerResponse.elm +6 -1
- package/src/Pages/Generate.elm +775 -132
- package/src/Pages/GeneratorProgramConfig.elm +15 -0
- package/src/Pages/Internal/FatalError.elm +5 -0
- package/src/Pages/Internal/Form.elm +21 -1
- package/src/Pages/Internal/Platform/Cli.elm +479 -747
- package/src/Pages/Internal/Platform/Cli.elm.bak +1276 -0
- package/src/Pages/Internal/Platform/CompatibilityKey.elm +6 -0
- package/src/Pages/Internal/Platform/Effect.elm +1 -2
- package/src/Pages/Internal/Platform/GeneratorApplication.elm +373 -0
- package/src/Pages/Internal/Platform/StaticResponses.elm +73 -270
- package/src/Pages/Internal/Platform/ToJsPayload.elm +4 -7
- package/src/Pages/Internal/Platform.elm +54 -53
- package/src/Pages/Internal/Script.elm +17 -0
- package/src/Pages/Internal/StaticHttpBody.elm +35 -1
- package/src/Pages/Manifest.elm +29 -4
- package/src/Pages/ProgramConfig.elm +12 -8
- package/src/Pages/Script.elm +109 -0
- package/src/Pages/SiteConfig.elm +3 -2
- package/src/Pages/StaticHttp/Request.elm +2 -2
- package/src/Pages/StaticHttpRequest.elm +23 -98
- package/src/RequestsAndPending.elm +8 -19
- package/src/Result/Extra.elm +21 -0
- package/src/Server/Request.elm +43 -34
- package/src/Server/Session.elm +166 -100
- package/src/Server/SetCookie.elm +89 -31
- package/src/DataSource/Env.elm +0 -38
- package/src/DataSource/Http.elm +0 -446
- package/src/DataSource/Internal/Request.elm +0 -20
- package/src/DataSource/Port.elm +0 -90
- package/src/DataSource.elm +0 -538
package/src/ApiRoute.elm
CHANGED
|
@@ -1,32 +1,164 @@
|
|
|
1
1
|
module ApiRoute exposing
|
|
2
|
-
(
|
|
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,
|
|
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
|
|
12
|
-
the
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
+
## Pre-Rendering
|
|
20
20
|
|
|
21
|
+
A pre-rendered ApiRoute is just a generated file. For example:
|
|
21
22
|
|
|
22
|
-
|
|
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
|
-
|
|
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,
|
|
171
|
+
@docs toJson, getBuildTimeRoutes, getGlobalHeadTagsBackendTask
|
|
40
172
|
|
|
41
173
|
-}
|
|
42
174
|
|
|
43
|
-
import
|
|
44
|
-
import
|
|
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
|
-
|
|
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 ->
|
|
198
|
+
|> preRender (\constructor -> BackendTask.succeed [ constructor ])
|
|
65
199
|
|
|
66
200
|
|
|
67
201
|
{-| -}
|
|
68
|
-
serverRender : ApiRouteBuilder (Server.Request.Parser (
|
|
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
|
-
(\
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|>
|
|
234
|
+
|> BackendTask.succeed
|
|
92
235
|
|
|
93
236
|
Nothing ->
|
|
94
237
|
Server.Response.plainText "No matching request handler"
|
|
95
238
|
|> Server.Response.withStatusCode 400
|
|
96
|
-
|>
|
|
239
|
+
|> BackendTask.succeed
|
|
97
240
|
)
|
|
98
241
|
)
|
|
99
|
-
|> Maybe.map (
|
|
242
|
+
|> Maybe.map (BackendTask.map (Server.Response.toJson >> Just))
|
|
100
243
|
|> Maybe.withDefault
|
|
101
|
-
(
|
|
102
|
-
, buildTimeRoutes =
|
|
244
|
+
(BackendTask.succeed Nothing)
|
|
245
|
+
, buildTimeRoutes = BackendTask.succeed []
|
|
103
246
|
, handleRoute =
|
|
104
247
|
\path ->
|
|
105
|
-
|
|
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 ->
|
|
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__ :
|
|
266
|
+
buildTimeRoutes__ : BackendTask FatalError (List String)
|
|
124
267
|
buildTimeRoutes__ =
|
|
125
268
|
buildUrls (constructor [])
|
|
126
|
-
|>
|
|
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 (
|
|
276
|
+
|> Maybe.map (BackendTask.map (Server.Response.toJson >> Just))
|
|
134
277
|
|> Maybe.withDefault
|
|
135
|
-
(
|
|
278
|
+
(BackendTask.succeed Nothing)
|
|
136
279
|
, buildTimeRoutes = buildTimeRoutes__
|
|
137
280
|
, handleRoute =
|
|
138
281
|
\path ->
|
|
139
|
-
|
|
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 ->
|
|
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__ :
|
|
308
|
+
buildTimeRoutes__ : BackendTask FatalError (List String)
|
|
166
309
|
buildTimeRoutes__ =
|
|
167
310
|
buildUrls (constructor [])
|
|
168
|
-
|>
|
|
311
|
+
|> BackendTask.map (List.map toString)
|
|
169
312
|
|
|
170
|
-
preBuiltMatches :
|
|
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 :
|
|
326
|
+
routeFound : BackendTask FatalError Bool
|
|
184
327
|
routeFound =
|
|
185
328
|
preBuiltMatches
|
|
186
|
-
|>
|
|
329
|
+
|> BackendTask.map (List.member matches)
|
|
187
330
|
in
|
|
188
331
|
routeFound
|
|
189
|
-
|>
|
|
332
|
+
|> BackendTask.andThen
|
|
190
333
|
(\found ->
|
|
191
334
|
if found then
|
|
192
335
|
Internal.ApiRoute.tryMatch path fullHandler
|
|
193
|
-
|> Maybe.map (
|
|
194
|
-
|> Maybe.withDefault (
|
|
336
|
+
|> Maybe.map (BackendTask.map (encodeStaticFileBody >> Just))
|
|
337
|
+
|> Maybe.withDefault (BackendTask.succeed Nothing)
|
|
195
338
|
|
|
196
339
|
else
|
|
197
|
-
|
|
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
|
-
|>
|
|
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 ->
|
|
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 :
|
|
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
|
-
|
|
304
|
-
|
|
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
|