elm-pages 3.0.7 β 3.0.9
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 +3 -3
- 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/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/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/js/node_runner.js +1 -1
- package/generator/review/elm-stuff/tests-0.19.1/js/node_supervisor.js +1 -1
- package/generator/src/build.js +1 -2
- package/generator/src/compatibility-key.js +1 -1
- package/generator/src/dev-server.js +1 -1
- package/generator/src/generate-template-module-connector.js +2 -2
- package/generator/src/vite-utils.js +1 -1
- package/generator/static-code/elm-pages.js +12 -9
- package/package.json +2 -2
- package/src/ApiRoute.elm +2 -1
- package/src/BackendTask/Custom.elm +12 -15
- package/src/BackendTask/File.elm +2 -0
- package/src/BackendTask/Glob.elm +34 -31
- package/src/BackendTask.elm +32 -7
- package/src/List/Chunks.elm +51 -0
- package/src/Pages/Internal/Platform.elm +17 -8
- package/src/Server/Session.elm +9 -6
package/README.md
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
- [elm-pages Docs Site](https://elm-pages.com/docs)
|
|
13
13
|
- [elm-pages site showcase](https://elm-pages.com/showcase/)
|
|
14
|
-
- [elm-pages Elm API Docs](https://package.elm-lang.org/packages/dillonkearns/elm-pages/10.0.
|
|
14
|
+
- [elm-pages Elm API Docs](https://package.elm-lang.org/packages/dillonkearns/elm-pages/10.0.2/)
|
|
15
15
|
- [Quick start repo](https://github.com/dillonkearns/elm-pages-starter) [(live site hosted here)](https://elm-pages-starter.netlify.com)
|
|
16
16
|
- [Introducing `elm-pages` blog post](https://elm-pages.com/blog/introducing-elm-pages)
|
|
17
|
-
- [`examples` folder](https://github.com/dillonkearns/elm-pages/blob/master/examples/) (includes https://elm-pages.com site source)
|
|
17
|
+
- [`examples` folder](https://github.com/dillonkearns/elm-pages/blob/master/examples/) (includes https://elm-pages.com site source) Use `git clone --recurse-submodules https://github.com/dillonkearns/elm-pages.git` so that there aren't missing files when you try to build the examples.
|
|
18
18
|
|
|
19
19
|
## Compatibility Key
|
|
20
20
|
|
|
@@ -34,7 +34,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|
|
34
34
|
<tr>
|
|
35
35
|
<td align="center"><a href="https://github.com/danmarcab"><img src="https://avatars2.githubusercontent.com/u/1517969?v=4" width="100px;" alt=""/><br /><sub><b>Daniel MarΓn</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=danmarcab" title="Code">π»</a></td>
|
|
36
36
|
<td align="center"><a href="https://citric.id"><img src="https://avatars1.githubusercontent.com/u/296665?v=4" width="100px;" alt=""/><br /><sub><b>Steven Vandevelde</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=icidasset" title="Code">π»</a></td>
|
|
37
|
-
<td align="center"><a href="https://github.com/
|
|
37
|
+
<td align="center"><a href="https://github.com/j-maas"><img src="https://avatars0.githubusercontent.com/u/11377826?v=4" width="100px;" alt=""/><br /><sub><b>Johannes Maas</b></sub></a><br /><a href="#userTesting-j-maas" title="User Testing">π</a> <a href="https://github.com/dillonkearns/elm-pages/commits?author=j-maas" title="Code">π»</a></td>
|
|
38
38
|
<td align="center"><a href="https://github.com/vViktorPL"><img src="https://avatars1.githubusercontent.com/u/2961541?v=4" width="100px;" alt=""/><br /><sub><b>Wiktor Toporek</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=vViktorPL" title="Code">π»</a></td>
|
|
39
39
|
<td align="center"><a href="https://sunrisemovement.com"><img src="https://avatars1.githubusercontent.com/u/1508245?v=4" width="100px;" alt=""/><br /><sub><b>Luke Westby</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=lukewestby" title="Code">π»</a></td>
|
|
40
40
|
</tr>
|
|
Binary file
|
|
@@ -75,7 +75,7 @@ console.elmlog = (str) => logs.push(str + "\n");
|
|
|
75
75
|
const { Elm } = require("./Runner.elm.js");
|
|
76
76
|
|
|
77
77
|
// Start the Elm app
|
|
78
|
-
const flags = { initialSeed:
|
|
78
|
+
const flags = { initialSeed: 1491170149, fuzzRuns: 100, filter: null };
|
|
79
79
|
const app = Elm.Runner.init({ flags: flags });
|
|
80
80
|
|
|
81
81
|
// Record the timing at which we received the last "runTest" message
|
|
Binary file
|
|
@@ -75,7 +75,7 @@ console.elmlog = (str) => logs.push(str + "\n");
|
|
|
75
75
|
const { Elm } = require("./Runner.elm.js");
|
|
76
76
|
|
|
77
77
|
// Start the Elm app
|
|
78
|
-
const flags = { initialSeed:
|
|
78
|
+
const flags = { initialSeed: 3733738384, fuzzRuns: 100, filter: null };
|
|
79
79
|
const app = Elm.Runner.init({ flags: flags });
|
|
80
80
|
|
|
81
81
|
// Record the timing at which we received the last "runTest" message
|
package/generator/src/build.js
CHANGED
|
@@ -73,7 +73,7 @@ export async function run(options) {
|
|
|
73
73
|
// This is a temporary hack to avoid this warning. elm-pages manages compiling the Elm code without Vite's involvement, so it is external to Vite.
|
|
74
74
|
// There is a pending issue to allow having external scripts in Vite, once this issue is fixed we can remove this hack:
|
|
75
75
|
// https://github.com/vitejs/vite/issues/3533
|
|
76
|
-
if (!messages[0]
|
|
76
|
+
if (messages && messages[0] && !messages[0].startsWith(`<script src="/elm.js">`)) {
|
|
77
77
|
console.info(...messages);
|
|
78
78
|
}
|
|
79
79
|
};
|
|
@@ -221,7 +221,6 @@ export async function render(request) {
|
|
|
221
221
|
addWatcher,
|
|
222
222
|
false
|
|
223
223
|
);
|
|
224
|
-
console.dir(response);
|
|
225
224
|
if (response.kind === "bytes") {
|
|
226
225
|
return {
|
|
227
226
|
body: response.contentDatPayload.buffer,
|
|
@@ -37,7 +37,7 @@ const __dirname = path.dirname(__filename);
|
|
|
37
37
|
*/
|
|
38
38
|
export async function start(options) {
|
|
39
39
|
console.error = function (...messages) {
|
|
40
|
-
if (!messages[0]
|
|
40
|
+
if (messages && messages[0] && !messages[0].startsWith("Failed to load url")) {
|
|
41
41
|
console.info(...messages);
|
|
42
42
|
}
|
|
43
43
|
};
|
|
@@ -3,7 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import { default as mm } from "micromatch";
|
|
4
4
|
import * as routeHelpers from "./route-codegen-helpers.js";
|
|
5
5
|
import { restoreColorSafe } from "./error-formatter.js";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @param {string} basePath
|
|
@@ -66,7 +66,7 @@ export async function generateTemplateModuleConnector(basePath, phase) {
|
|
|
66
66
|
async function runElmCodegenCli(templates, basePath, phase) {
|
|
67
67
|
const __filename = fileURLToPath(import.meta.url);
|
|
68
68
|
const __dirname = path.dirname(__filename);
|
|
69
|
-
const filePath = path.join(__dirname, `../../codegen/elm-pages-codegen.cjs`);
|
|
69
|
+
const filePath = pathToFileURL(path.join(__dirname, `../../codegen/elm-pages-codegen.cjs`)).href;
|
|
70
70
|
|
|
71
71
|
const promise = new Promise(async (resolve, reject) => {
|
|
72
72
|
const elmPagesCodegen = (await import(filePath)).default.Elm.Generate;
|
|
@@ -103,14 +103,17 @@ function find_anchor(node) {
|
|
|
103
103
|
return /** @type {HTMLAnchorElement} */ (node);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
formData
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
// only run in modern browsers to prevent exception: https://github.com/dillonkearns/elm-pages/issues/427
|
|
107
|
+
if ("SubmitEvent" in window) {
|
|
108
|
+
Object.defineProperty(SubmitEvent.prototype, "fields", {
|
|
109
|
+
get: function fields() {
|
|
110
|
+
let formData = new FormData(this.currentTarget);
|
|
111
|
+
if (this.submitter && this.submitter.name) {
|
|
112
|
+
formData.append(this.submitter.name, this.submitter.value);
|
|
113
|
+
}
|
|
114
|
+
return [...formData.entries()];
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
115
118
|
|
|
116
119
|
setup();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elm-pages",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0.
|
|
4
|
+
"version": "3.0.9",
|
|
5
5
|
"homepage": "https://elm-pages.com",
|
|
6
6
|
"moduleResolution": "node",
|
|
7
7
|
"description": "Type-safe static sites, written in pure elm with your own custom elm-markup syntax.",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@types/micromatch": "^4.0.2",
|
|
56
56
|
"@types/node": "^20.1.0",
|
|
57
57
|
"@types/serve-static": "^1.15.1",
|
|
58
|
-
"cypress": "^
|
|
58
|
+
"cypress": "^13.3.0",
|
|
59
59
|
"elm-codegen": "^0.3.0",
|
|
60
60
|
"elm-optimize-level-2": "^0.3.5",
|
|
61
61
|
"elm-review": "^2.10.2",
|
package/src/ApiRoute.elm
CHANGED
|
@@ -65,10 +65,11 @@ You define your ApiRoute's in `app/Api.elm`. Here's a simple example:
|
|
|
65
65
|
|
|
66
66
|
import ApiRoute
|
|
67
67
|
import BackendTask exposing (BackendTask)
|
|
68
|
+
import FatalError exposing (FatalError)
|
|
68
69
|
import Server.Request
|
|
69
70
|
|
|
70
71
|
routes :
|
|
71
|
-
BackendTask (List Route)
|
|
72
|
+
BackendTask FatalError (List Route)
|
|
72
73
|
-> (Maybe { indent : Int, newLines : Bool } -> Html Never -> String)
|
|
73
74
|
-> List (ApiRoute.ApiRoute ApiRoute.Response)
|
|
74
75
|
routes getStaticRoutes htmlToString =
|
|
@@ -35,26 +35,23 @@ we're using `BackendTask.allowFatal` to let the framework treat that as an unexp
|
|
|
35
35
|
```javascript
|
|
36
36
|
// custom-backend-task.js
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} else {
|
|
49
|
-
throw `No environment variable called ${name}
|
|
38
|
+
/**
|
|
39
|
+
* @param { string } fromElm
|
|
40
|
+
* @returns { Promise<string> }
|
|
41
|
+
*/
|
|
42
|
+
export async function environmentVariable(name) {
|
|
43
|
+
const result = process.env[name];
|
|
44
|
+
if (result) {
|
|
45
|
+
return result;
|
|
46
|
+
} else {
|
|
47
|
+
throw `No environment variable called ${name}
|
|
50
48
|
|
|
51
49
|
Available:
|
|
52
50
|
|
|
53
51
|
${Object.keys(process.env).join("\n")}
|
|
54
52
|
`;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
58
55
|
```
|
|
59
56
|
|
|
60
57
|
|
package/src/BackendTask/File.elm
CHANGED
|
@@ -167,6 +167,7 @@ bodyWithFrontmatter frontmatterDecoder filePath =
|
|
|
167
167
|
{ title = "BackendTask.File Decoder Error"
|
|
168
168
|
, body =
|
|
169
169
|
"I encountered a Json Decoder error from a call to BackendTask.File.bodyWithFrontmatter.\n\n"
|
|
170
|
+
++ ("I was trying to process `" ++ filePath ++ "`.\n\n")
|
|
170
171
|
++ Decode.errorToString frontmatterDecodeError
|
|
171
172
|
}
|
|
172
173
|
|> FatalError.build
|
|
@@ -257,6 +258,7 @@ onlyFrontmatter frontmatterDecoder filePath =
|
|
|
257
258
|
{ title = "BackendTask.File Decoder Error"
|
|
258
259
|
, body =
|
|
259
260
|
"I encountered a Json Decoder error from a call to BackendTask.File.onlyFrontmatter.\n\n"
|
|
261
|
+
++ ("I was trying to process `" ++ filePath ++ "`.\n\n")
|
|
260
262
|
++ Decode.errorToString frontmatterDecodeError
|
|
261
263
|
}
|
|
262
264
|
|> FatalError.build
|
package/src/BackendTask/Glob.elm
CHANGED
|
@@ -28,7 +28,7 @@ With the `BackendTask.Glob` API, you could get all of those files like so:
|
|
|
28
28
|
|
|
29
29
|
import BackendTask exposing (BackendTask)
|
|
30
30
|
|
|
31
|
-
blogPostsGlob : BackendTask (List String)
|
|
31
|
+
blogPostsGlob : BackendTask error (List String)
|
|
32
32
|
blogPostsGlob =
|
|
33
33
|
Glob.succeed (\slug -> slug)
|
|
34
34
|
|> Glob.match (Glob.literal "content/blog/")
|
|
@@ -69,7 +69,7 @@ There will be one argument for every `capture` in your pipeline, whereas `match`
|
|
|
69
69
|
import BackendTask exposing (BackendTask)
|
|
70
70
|
import BackendTask.Glob as Glob
|
|
71
71
|
|
|
72
|
-
blogPostsGlob : BackendTask (List String)
|
|
72
|
+
blogPostsGlob : BackendTask error (List String)
|
|
73
73
|
blogPostsGlob =
|
|
74
74
|
Glob.succeed (\slug -> slug)
|
|
75
75
|
-- no argument from this, but we will only
|
|
@@ -97,7 +97,7 @@ Let's try our blogPostsGlob from before, but change every `match` to `capture`.
|
|
|
97
97
|
import BackendTask exposing (BackendTask)
|
|
98
98
|
|
|
99
99
|
blogPostsGlob :
|
|
100
|
-
BackendTask
|
|
100
|
+
BackendTask error
|
|
101
101
|
(List
|
|
102
102
|
{ filePath : String
|
|
103
103
|
, slug : String
|
|
@@ -155,20 +155,22 @@ This is my first post!
|
|
|
155
155
|
Then we could read that title for our blog post list page using our `blogPosts` `BackendTask` that we defined above.
|
|
156
156
|
|
|
157
157
|
import BackendTask.File
|
|
158
|
+
import FatalError exposing (FatalError)
|
|
158
159
|
import Json.Decode as Decode exposing (Decoder)
|
|
159
160
|
|
|
160
|
-
titles : BackendTask (List BlogPost)
|
|
161
|
+
titles : BackendTask FatalError (List BlogPost)
|
|
161
162
|
titles =
|
|
162
163
|
blogPosts
|
|
163
164
|
|> BackendTask.map
|
|
164
165
|
(List.map
|
|
165
166
|
(\blogPost ->
|
|
166
|
-
BackendTask.File.
|
|
167
|
+
BackendTask.File.onlyFrontmatter
|
|
168
|
+
blogFrontmatterDecoder
|
|
167
169
|
blogPost.filePath
|
|
168
|
-
(BackendTask.File.frontmatter blogFrontmatterDecoder)
|
|
169
170
|
)
|
|
170
171
|
)
|
|
171
172
|
|> BackendTask.resolve
|
|
173
|
+
|> BackendTask.allowFatal
|
|
172
174
|
|
|
173
175
|
type alias BlogPost =
|
|
174
176
|
{ title : String }
|
|
@@ -249,7 +251,7 @@ could use
|
|
|
249
251
|
import BackendTask exposing (BackendTask)
|
|
250
252
|
import BackendTask.Glob as Glob
|
|
251
253
|
|
|
252
|
-
blogPostsGlob : BackendTask (List String)
|
|
254
|
+
blogPostsGlob : BackendTask error (List String)
|
|
253
255
|
blogPostsGlob =
|
|
254
256
|
Glob.succeed (\slug -> slug)
|
|
255
257
|
|> Glob.match (Glob.literal "content/blog/")
|
|
@@ -258,14 +260,14 @@ could use
|
|
|
258
260
|
|> Glob.toBackendTask
|
|
259
261
|
|
|
260
262
|
If you want to validate file formats, you can combine that with some `BackendTask` helpers to turn a `Glob (Result String value)` into
|
|
261
|
-
a `BackendTask (List value)`.
|
|
263
|
+
a `BackendTask FatalError (List value)`.
|
|
262
264
|
|
|
263
265
|
For example, you could take a date and parse it.
|
|
264
266
|
|
|
265
267
|
import BackendTask exposing (BackendTask)
|
|
266
268
|
import BackendTask.Glob as Glob
|
|
267
269
|
|
|
268
|
-
example : BackendTask (List ( String, String ))
|
|
270
|
+
example : BackendTask FatalError (List ( String, String ))
|
|
269
271
|
example =
|
|
270
272
|
Glob.succeed
|
|
271
273
|
(\dateResult slug ->
|
|
@@ -281,14 +283,14 @@ For example, you could take a date and parse it.
|
|
|
281
283
|
|> BackendTask.map (List.map BackendTask.fromResult)
|
|
282
284
|
|> BackendTask.resolve
|
|
283
285
|
|
|
284
|
-
expectDateFormat : List String -> Result
|
|
286
|
+
expectDateFormat : List String -> Result FatalError String
|
|
285
287
|
expectDateFormat dateParts =
|
|
286
288
|
case dateParts of
|
|
287
289
|
[ year, month, date ] ->
|
|
288
290
|
Ok (String.join "-" [ year, month, date ])
|
|
289
291
|
|
|
290
292
|
_ ->
|
|
291
|
-
Err "Unexpected date format, expected yyyy/mm/dd folder structure."
|
|
293
|
+
Err <| FatalError.fromString "Unexpected date format, expected yyyy/mm/dd folder structure."
|
|
292
294
|
|
|
293
295
|
-}
|
|
294
296
|
map : (a -> b) -> Glob a -> Glob b
|
|
@@ -322,7 +324,7 @@ fullFilePath =
|
|
|
322
324
|
import BackendTask.Glob as Glob
|
|
323
325
|
|
|
324
326
|
blogPosts :
|
|
325
|
-
BackendTask
|
|
327
|
+
BackendTask error
|
|
326
328
|
(List
|
|
327
329
|
{ filePath : String
|
|
328
330
|
, slug : String
|
|
@@ -368,11 +370,11 @@ match 0 or more path parts like, see `recursiveWildcard`.
|
|
|
368
370
|
, slug : String
|
|
369
371
|
}
|
|
370
372
|
|
|
371
|
-
example : BackendTask (List BlogPost)
|
|
373
|
+
example : BackendTask error (List BlogPost)
|
|
372
374
|
example =
|
|
373
375
|
Glob.succeed BlogPost
|
|
374
376
|
|> Glob.match (Glob.literal "blog/")
|
|
375
|
-
|> Glob.
|
|
377
|
+
|> Glob.capture Glob.wildcard
|
|
376
378
|
|> Glob.match (Glob.literal "-")
|
|
377
379
|
|> Glob.capture Glob.wildcard
|
|
378
380
|
|> Glob.match (Glob.literal "-")
|
|
@@ -391,7 +393,7 @@ match 0 or more path parts like, see `recursiveWildcard`.
|
|
|
391
393
|
|
|
392
394
|
That will match to:
|
|
393
395
|
|
|
394
|
-
results : BackendTask (List BlogPost)
|
|
396
|
+
results : BackendTask error (List BlogPost)
|
|
395
397
|
results =
|
|
396
398
|
BackendTask.succeed
|
|
397
399
|
[ { year = "2021"
|
|
@@ -443,7 +445,7 @@ Leading 0's are ignored.
|
|
|
443
445
|
import BackendTask exposing (BackendTask)
|
|
444
446
|
import BackendTask.Glob as Glob
|
|
445
447
|
|
|
446
|
-
slides : BackendTask (List Int)
|
|
448
|
+
slides : BackendTask error (List Int)
|
|
447
449
|
slides =
|
|
448
450
|
Glob.succeed identity
|
|
449
451
|
|> Glob.match (Glob.literal "slide-")
|
|
@@ -472,7 +474,7 @@ With files
|
|
|
472
474
|
|
|
473
475
|
Yields
|
|
474
476
|
|
|
475
|
-
matches : BackendTask (List Int)
|
|
477
|
+
matches : BackendTask error (List Int)
|
|
476
478
|
matches =
|
|
477
479
|
BackendTask.succeed
|
|
478
480
|
[ 1
|
|
@@ -513,7 +515,7 @@ This is the elm-pages equivalent of `**/*.txt` in standard shell syntax:
|
|
|
513
515
|
import BackendTask exposing (BackendTask)
|
|
514
516
|
import BackendTask.Glob as Glob
|
|
515
517
|
|
|
516
|
-
example : BackendTask (List ( List String, String ))
|
|
518
|
+
example : BackendTask error (List ( List String, String ))
|
|
517
519
|
example =
|
|
518
520
|
Glob.succeed Tuple.pair
|
|
519
521
|
|> Glob.match (Glob.literal "articles/")
|
|
@@ -537,7 +539,7 @@ With these files:
|
|
|
537
539
|
|
|
538
540
|
We would get the following matches:
|
|
539
541
|
|
|
540
|
-
matches : BackendTask (List ( List String, String ))
|
|
542
|
+
matches : BackendTask error (List ( List String, String ))
|
|
541
543
|
matches =
|
|
542
544
|
BackendTask.succeed
|
|
543
545
|
[ ( [ "archive", "1977", "06", "10" ], "apple-2-announced" )
|
|
@@ -552,14 +554,14 @@ And also note that it matches 0 path parts into an empty list.
|
|
|
552
554
|
If we didn't include the `wildcard` after the `recursiveWildcard`, then we would only get
|
|
553
555
|
a single level of matches because it is followed by a file extension.
|
|
554
556
|
|
|
555
|
-
example : BackendTask (List String)
|
|
557
|
+
example : BackendTask error (List String)
|
|
556
558
|
example =
|
|
557
559
|
Glob.succeed identity
|
|
558
560
|
|> Glob.match (Glob.literal "articles/")
|
|
559
561
|
|> Glob.capture Glob.recursiveWildcard
|
|
560
562
|
|> Glob.match (Glob.literal ".txt")
|
|
561
563
|
|
|
562
|
-
matches : BackendTask (List String)
|
|
564
|
+
matches : BackendTask error (List String)
|
|
563
565
|
matches =
|
|
564
566
|
BackendTask.succeed
|
|
565
567
|
[ "google-io-2021-recap"
|
|
@@ -677,7 +679,7 @@ Exactly the same as `match` except it also captures the matched sub-pattern.
|
|
|
677
679
|
, slug : String
|
|
678
680
|
}
|
|
679
681
|
|
|
680
|
-
archives : BackendTask ArchivesArticle
|
|
682
|
+
archives : BackendTask error ArchivesArticle
|
|
681
683
|
archives =
|
|
682
684
|
Glob.succeed ArchivesArticle
|
|
683
685
|
|> Glob.match (Glob.literal "archive/")
|
|
@@ -693,7 +695,7 @@ Exactly the same as `match` except it also captures the matched sub-pattern.
|
|
|
693
695
|
|
|
694
696
|
The file `archive/1977/06/10/apple-2-released.md` will give us this match:
|
|
695
697
|
|
|
696
|
-
matches : List ArchivesArticle
|
|
698
|
+
matches : List error ArchivesArticle
|
|
697
699
|
matches =
|
|
698
700
|
BackendTask.succeed
|
|
699
701
|
[ { year = 1977
|
|
@@ -744,7 +746,7 @@ capture (Glob matcherPattern apply1) (Glob pattern apply2) =
|
|
|
744
746
|
, extension : String
|
|
745
747
|
}
|
|
746
748
|
|
|
747
|
-
dataFiles : BackendTask (List DataFile)
|
|
749
|
+
dataFiles : BackendTask error (List DataFile)
|
|
748
750
|
dataFiles =
|
|
749
751
|
Glob.succeed DataFile
|
|
750
752
|
|> Glob.match (Glob.literal "my-data/")
|
|
@@ -768,7 +770,7 @@ If we have the following files
|
|
|
768
770
|
|
|
769
771
|
That gives us
|
|
770
772
|
|
|
771
|
-
results : BackendTask (List DataFile)
|
|
773
|
+
results : BackendTask error (List DataFile)
|
|
772
774
|
results =
|
|
773
775
|
BackendTask.succeed
|
|
774
776
|
[ { name = "authors"
|
|
@@ -781,7 +783,7 @@ That gives us
|
|
|
781
783
|
|
|
782
784
|
You could also match an optional file path segment using `oneOf`.
|
|
783
785
|
|
|
784
|
-
rootFilesMd : BackendTask (List String)
|
|
786
|
+
rootFilesMd : BackendTask error (List String)
|
|
785
787
|
rootFilesMd =
|
|
786
788
|
Glob.succeed (\slug -> slug)
|
|
787
789
|
|> Glob.match (Glob.literal "blog/")
|
|
@@ -806,7 +808,7 @@ With these files:
|
|
|
806
808
|
|
|
807
809
|
This would give us:
|
|
808
810
|
|
|
809
|
-
results : BackendTask (List String)
|
|
811
|
+
results : BackendTask error (List String)
|
|
810
812
|
results =
|
|
811
813
|
BackendTask.succeed
|
|
812
814
|
[ "first-post"
|
|
@@ -965,7 +967,7 @@ encodeOptions options =
|
|
|
965
967
|
|
|
966
968
|
import BackendTask.Glob as Glob exposing (OnlyFolders, defaultOptions)
|
|
967
969
|
|
|
968
|
-
matchingFiles : Glob a -> BackendTask (List a)
|
|
970
|
+
matchingFiles : Glob a -> BackendTask error (List a)
|
|
969
971
|
matchingFiles glob =
|
|
970
972
|
glob
|
|
971
973
|
|> Glob.toBackendTaskWithOptions { defaultOptions | include = OnlyFolders }
|
|
@@ -1010,12 +1012,12 @@ For example, maybe you can have
|
|
|
1010
1012
|
import BackendTask exposing (BackendTask)
|
|
1011
1013
|
import BackendTask.Glob as Glob
|
|
1012
1014
|
|
|
1013
|
-
findBlogBySlug : String -> BackendTask String
|
|
1015
|
+
findBlogBySlug : String -> BackendTask FatalError String
|
|
1014
1016
|
findBlogBySlug slug =
|
|
1015
1017
|
Glob.succeed identity
|
|
1016
1018
|
|> Glob.captureFilePath
|
|
1017
1019
|
|> Glob.match (Glob.literal "blog/")
|
|
1018
|
-
|> Glob.
|
|
1020
|
+
|> Glob.match (Glob.literal slug)
|
|
1019
1021
|
|> Glob.match
|
|
1020
1022
|
(Glob.oneOf
|
|
1021
1023
|
( ( "", () )
|
|
@@ -1024,6 +1026,7 @@ For example, maybe you can have
|
|
|
1024
1026
|
)
|
|
1025
1027
|
|> Glob.match (Glob.literal ".md")
|
|
1026
1028
|
|> Glob.expectUniqueMatch
|
|
1029
|
+
|> BackendTask.allowFatal
|
|
1027
1030
|
|
|
1028
1031
|
If we used `findBlogBySlug "first-post"` with these files:
|
|
1029
1032
|
|
|
@@ -1035,7 +1038,7 @@ If we used `findBlogBySlug "first-post"` with these files:
|
|
|
1035
1038
|
|
|
1036
1039
|
This would give us:
|
|
1037
1040
|
|
|
1038
|
-
results : BackendTask String
|
|
1041
|
+
results : BackendTask FatalError String
|
|
1039
1042
|
results =
|
|
1040
1043
|
BackendTask.succeed "blog/first-post/index.md"
|
|
1041
1044
|
|
package/src/BackendTask.elm
CHANGED
|
@@ -88,6 +88,7 @@ Any place in your `elm-pages` app where the framework lets you pass in a value o
|
|
|
88
88
|
|
|
89
89
|
import FatalError exposing (FatalError)
|
|
90
90
|
import Json.Encode
|
|
91
|
+
import List.Chunks
|
|
91
92
|
import Pages.StaticHttpRequest exposing (RawRequest(..))
|
|
92
93
|
|
|
93
94
|
|
|
@@ -140,6 +141,7 @@ resolve =
|
|
|
140
141
|
{-| Turn a list of `BackendTask`s into a single one.
|
|
141
142
|
|
|
142
143
|
import BackendTask
|
|
144
|
+
import FatalError exposing (FatalError)
|
|
143
145
|
import Json.Decode as Decode exposing (Decoder)
|
|
144
146
|
|
|
145
147
|
type alias Pokemon =
|
|
@@ -147,7 +149,7 @@ resolve =
|
|
|
147
149
|
, sprite : String
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
pokemonDetailRequest : BackendTask (List Pokemon)
|
|
152
|
+
pokemonDetailRequest : BackendTask FatalError (List Pokemon)
|
|
151
153
|
pokemonDetailRequest =
|
|
152
154
|
BackendTask.Http.getJson
|
|
153
155
|
"https://pokeapi.co/api/v2/pokemon/?limit=3"
|
|
@@ -169,10 +171,31 @@ resolve =
|
|
|
169
171
|
)
|
|
170
172
|
)
|
|
171
173
|
|> BackendTask.andThen BackendTask.combine
|
|
174
|
+
|> BackendTask.allowFatal
|
|
172
175
|
|
|
173
176
|
-}
|
|
174
177
|
combine : List (BackendTask error value) -> BackendTask error (List value)
|
|
175
178
|
combine items =
|
|
179
|
+
items
|
|
180
|
+
|> List.Chunks.chunk 100
|
|
181
|
+
|> List.map combineHelp
|
|
182
|
+
|> List.Chunks.chunk 100
|
|
183
|
+
|> List.map combineHelp
|
|
184
|
+
|> List.Chunks.chunk 100
|
|
185
|
+
|> List.map combineHelp
|
|
186
|
+
|> combineHelp
|
|
187
|
+
|> map (List.concat >> List.concat >> List.concat)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
{-| `combineHelp` on its own will overflow the stack with larger lists of tasks
|
|
191
|
+
|
|
192
|
+
dividing the tasks into smaller nested chunks and recombining makes `combine` stack safe
|
|
193
|
+
|
|
194
|
+
There's probably a way of doing this without the Lists but it's a neat trick to safely combine lots of tasks!
|
|
195
|
+
|
|
196
|
+
-}
|
|
197
|
+
combineHelp : List (BackendTask error value) -> BackendTask error (List value)
|
|
198
|
+
combineHelp items =
|
|
176
199
|
List.foldl (map2 (::)) (succeed []) items |> map List.reverse
|
|
177
200
|
|
|
178
201
|
|
|
@@ -190,11 +213,11 @@ combine items =
|
|
|
190
213
|
}
|
|
191
214
|
)
|
|
192
215
|
(get
|
|
193
|
-
|
|
216
|
+
"https://api.github.com/repos/dillonkearns/elm-pages"
|
|
194
217
|
(Decode.field "stargazers_count" Decode.int)
|
|
195
218
|
)
|
|
196
219
|
(get
|
|
197
|
-
|
|
220
|
+
"https://api.github.com/repos/dillonkearns/elm-markdown"
|
|
198
221
|
(Decode.field "stargazers_count" Decode.int)
|
|
199
222
|
)
|
|
200
223
|
|
|
@@ -239,17 +262,19 @@ map2 fn request1 request2 =
|
|
|
239
262
|
from the previous response to build up the URL, headers, etc. that you send to the subsequent request.
|
|
240
263
|
|
|
241
264
|
import BackendTask
|
|
265
|
+
import FatalError exposing (FatalError)
|
|
242
266
|
import Json.Decode as Decode exposing (Decoder)
|
|
243
267
|
|
|
244
|
-
licenseData : BackendTask String
|
|
268
|
+
licenseData : BackendTask FatalError String
|
|
245
269
|
licenseData =
|
|
246
|
-
BackendTask.Http.
|
|
247
|
-
|
|
270
|
+
BackendTask.Http.getJson
|
|
271
|
+
"https://api.github.com/repos/dillonkearns/elm-pages"
|
|
248
272
|
(Decode.at [ "license", "url" ] Decode.string)
|
|
249
273
|
|> BackendTask.andThen
|
|
250
274
|
(\licenseUrl ->
|
|
251
|
-
BackendTask.Http.
|
|
275
|
+
BackendTask.Http.getJson licenseUrl (Decode.field "description" Decode.string)
|
|
252
276
|
)
|
|
277
|
+
|> BackendTask.allowFatal
|
|
253
278
|
|
|
254
279
|
-}
|
|
255
280
|
andThen : (a -> BackendTask error b) -> BackendTask error a -> BackendTask error b
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module List.Chunks exposing (chunk)
|
|
2
|
+
|
|
3
|
+
import Array exposing (Array)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
{-| Adapted from <https://package.elm-lang.org/packages/krisajenkins/elm-exts/latest/Exts-List>
|
|
7
|
+
|
|
8
|
+
Split a list into chunks of length `n`.
|
|
9
|
+
|
|
10
|
+
Be aware that the last sub-list may be smaller than `n`-items long.
|
|
11
|
+
|
|
12
|
+
For example `chunk 3 [1..10] => [[1,2,3], [4,5,6], [7,8,9], [10]]`
|
|
13
|
+
|
|
14
|
+
-}
|
|
15
|
+
chunk : Int -> List a -> List (List a)
|
|
16
|
+
chunk n xs =
|
|
17
|
+
if n < 1 then
|
|
18
|
+
List.singleton xs
|
|
19
|
+
|
|
20
|
+
else
|
|
21
|
+
evaluate (chunkInternal n xs Array.empty)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
chunkInternal : Int -> List a -> Array (List a) -> Trampoline (List (List a))
|
|
25
|
+
chunkInternal n xs acc =
|
|
26
|
+
-- elm-review: known-unoptimized-recursion
|
|
27
|
+
if List.isEmpty xs then
|
|
28
|
+
Done (Array.toList acc)
|
|
29
|
+
|
|
30
|
+
else
|
|
31
|
+
Jump
|
|
32
|
+
(\_ ->
|
|
33
|
+
chunkInternal n
|
|
34
|
+
(List.drop n xs)
|
|
35
|
+
(Array.push (List.take n xs) acc)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
type Trampoline a
|
|
40
|
+
= Done a
|
|
41
|
+
| Jump (() -> Trampoline a)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
evaluate : Trampoline a -> a
|
|
45
|
+
evaluate trampoline =
|
|
46
|
+
case trampoline of
|
|
47
|
+
Done value ->
|
|
48
|
+
value
|
|
49
|
+
|
|
50
|
+
Jump f ->
|
|
51
|
+
evaluate (f ())
|
|
@@ -392,7 +392,7 @@ update config appMsg model =
|
|
|
392
392
|
let
|
|
393
393
|
navigatingToSamePage : Bool
|
|
394
394
|
navigatingToSamePage =
|
|
395
|
-
url.path == model.url.path
|
|
395
|
+
url.path == model.url.path && url.query == model.url.query && url.fragment /= Nothing
|
|
396
396
|
in
|
|
397
397
|
if navigatingToSamePage then
|
|
398
398
|
-- this is a workaround for an issue with anchor fragment navigation
|
|
@@ -403,7 +403,7 @@ update config appMsg model =
|
|
|
403
403
|
|
|
404
404
|
else
|
|
405
405
|
( model
|
|
406
|
-
, BrowserPushUrl url
|
|
406
|
+
, BrowserPushUrl (Url.toString url)
|
|
407
407
|
)
|
|
408
408
|
|
|
409
409
|
Browser.External href ->
|
|
@@ -425,12 +425,21 @@ update config appMsg model =
|
|
|
425
425
|
|
|
426
426
|
Nothing ->
|
|
427
427
|
if model.url.path == url.path && model.url.query == url.query then
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
428
|
+
if url.fragment == Nothing then
|
|
429
|
+
( { model
|
|
430
|
+
| -- update the URL in case query params or fragment changed
|
|
431
|
+
url = url
|
|
432
|
+
}
|
|
433
|
+
, ScrollToTop
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
else
|
|
437
|
+
( { model
|
|
438
|
+
| -- update the URL in case query params or fragment changed
|
|
439
|
+
url = url
|
|
440
|
+
}
|
|
441
|
+
, NoEffect
|
|
442
|
+
)
|
|
434
443
|
|
|
435
444
|
else
|
|
436
445
|
( model
|
package/src/Server/Session.elm
CHANGED
|
@@ -27,7 +27,7 @@ Using these functions, you can store and read session data in cookies to maintai
|
|
|
27
27
|
type alias Data =
|
|
28
28
|
{ darkMode : Bool }
|
|
29
29
|
|
|
30
|
-
data : RouteParams -> Request -> BackendTask (Response Data ErrorPage)
|
|
30
|
+
data : RouteParams -> Request -> BackendTask FatalError (Response Data ErrorPage)
|
|
31
31
|
data routeParams request =
|
|
32
32
|
request
|
|
33
33
|
|> Session.withSession
|
|
@@ -42,12 +42,15 @@ Using these functions, you can store and read session data in cookies to maintai
|
|
|
42
42
|
(session |> Session.get "mode" |> Maybe.withDefault "light")
|
|
43
43
|
== "dark"
|
|
44
44
|
in
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
BackendTask.succeed
|
|
46
|
+
( session
|
|
47
|
+
, Response.render
|
|
48
|
+
{ darkMode = darkMode
|
|
49
|
+
}
|
|
50
|
+
)
|
|
48
51
|
)
|
|
49
52
|
|
|
50
|
-
The elm-pages framework will manage signing these cookies using the `secrets : BackendTask (List String)` you pass in.
|
|
53
|
+
The elm-pages framework will manage signing these cookies using the `secrets : BackendTask FatalError (List String)` you pass in.
|
|
51
54
|
That means that the values you set in your session will be directly visible to anyone who has access to the cookie
|
|
52
55
|
(so don't directly store sensitive data in your session). Since the session cookie is signed using the secret you provide,
|
|
53
56
|
the cookie will be invalidated if it is tampered with because it won't match when elm-pages verifies that it has been
|
|
@@ -56,7 +59,7 @@ signed with your secrets. Of course you need to provide secure secrets and treat
|
|
|
56
59
|
|
|
57
60
|
### Rotating Secrets
|
|
58
61
|
|
|
59
|
-
The first String in `secrets : BackendTask (List String)` will be used to sign sessions, while the remaining String's will
|
|
62
|
+
The first String in `secrets : BackendTask FatalError (List String)` will be used to sign sessions, while the remaining String's will
|
|
60
63
|
still be used to attempt to "unsign" the cookies. So if you have a single secret:
|
|
61
64
|
|
|
62
65
|
Session.withSession
|