mirror of
https://github.com/dillonkearns/elm-pages-v3-beta.git
synced 2024-11-24 06:54:03 +03:00
Merge branch 'master' into phantom-builder
This commit is contained in:
commit
3a61933d81
@ -40,6 +40,15 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "lukewestby",
|
||||||
|
"name": "Luke Westby",
|
||||||
|
"avatar_url": "https://avatars1.githubusercontent.com/u/1508245?v=4",
|
||||||
|
"profile": "https://sunrisemovement.com",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
@ -47,5 +56,6 @@
|
|||||||
"projectOwner": "dillonkearns",
|
"projectOwner": "dillonkearns",
|
||||||
"repoType": "github",
|
"repoType": "github",
|
||||||
"repoHost": "https://github.com",
|
"repoHost": "https://github.com",
|
||||||
"skipCi": true
|
"skipCi": true,
|
||||||
|
"commitConvention": "none"
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [1.2.12] - 2020-03-28
|
## [1.3.0] - 2020-03-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- You can now host your `elm-pages` site in a sub-directory. For example, you could host it at mysite.com/blog, where the top-level mysite.com/ is hosting a different app.
|
- You can now host your `elm-pages` site in a sub-directory. For example, you could host it at mysite.com/blog, where the top-level mysite.com/ is hosting a different app.
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
# `elm-pages` [![Netlify Status](https://api.netlify.com/api/v1/badges/8ee4a674-4f37-4f16-b99e-607c0a02ee75/deploy-status)](https://app.netlify.com/sites/elm-pages/deploys) [![Build Status](https://github.com/dillonkearns/elm-pages/workflows/Elm%20CI/badge.svg)](https://github.com/dillonkearns/elm-pages/actions?query=branch%3Amaster) [![npm](https://img.shields.io/npm/v/elm-pages.svg)](https://npmjs.com/package/elm-pages) [![Elm package](https://img.shields.io/elm-package/v/dillonkearns/elm-pages.svg)](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/)
|
# `elm-pages` [![Netlify Status](https://api.netlify.com/api/v1/badges/8ee4a674-4f37-4f16-b99e-607c0a02ee75/deploy-status)](https://app.netlify.com/sites/elm-pages/deploys) [![Build Status](https://github.com/dillonkearns/elm-pages/workflows/Elm%20CI/badge.svg)](https://github.com/dillonkearns/elm-pages/actions?query=branch%3Amaster) [![npm](https://img.shields.io/npm/v/elm-pages.svg)](https://npmjs.com/package/elm-pages) [![Elm package](https://img.shields.io/elm-package/v/dillonkearns/elm-pages.svg)](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/)
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||||
|
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-)
|
||||||
[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-)
|
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/dillonkearns/elm-pages-starter)
|
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/dillonkearns/elm-pages-starter)
|
||||||
@ -165,12 +163,12 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<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>
|
<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>
|
||||||
<td align="center"><a href="https://github.com/Y0hy0h"><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-Y0hy0h" title="User Testing">📓</a></td>
|
<td align="center"><a href="https://github.com/Y0hy0h"><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-Y0hy0h" title="User Testing">📓</a></td>
|
||||||
<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>
|
<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>
|
||||||
|
<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>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- markdownlint-enable -->
|
<!-- markdownlint-enable -->
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||||
|
5
elm.json
5
elm.json
@ -7,6 +7,8 @@
|
|||||||
"exposed-modules": [
|
"exposed-modules": [
|
||||||
"Head",
|
"Head",
|
||||||
"Head.Seo",
|
"Head.Seo",
|
||||||
|
"OptimizedDecoder",
|
||||||
|
"OptimizedDecoder.Pipeline",
|
||||||
"Pages.ImagePath",
|
"Pages.ImagePath",
|
||||||
"Pages.PagePath",
|
"Pages.PagePath",
|
||||||
"Pages.StaticHttp",
|
"Pages.StaticHttp",
|
||||||
@ -31,7 +33,6 @@
|
|||||||
"elm-community/list-extra": "8.2.2 <= v < 9.0.0",
|
"elm-community/list-extra": "8.2.2 <= v < 9.0.0",
|
||||||
"elm-community/result-extra": "2.2.1 <= v < 3.0.0",
|
"elm-community/result-extra": "2.2.1 <= v < 3.0.0",
|
||||||
"lukewestby/elm-string-interpolate": "1.0.4 <= v < 2.0.0",
|
"lukewestby/elm-string-interpolate": "1.0.4 <= v < 2.0.0",
|
||||||
"mdgriffith/elm-markup": "3.0.1 <= v < 4.0.0",
|
|
||||||
"mgold/elm-nonempty-list": "4.0.2 <= v < 5.0.0",
|
"mgold/elm-nonempty-list": "4.0.2 <= v < 5.0.0",
|
||||||
"miniBill/elm-codec": "1.2.0 <= v < 2.0.0",
|
"miniBill/elm-codec": "1.2.0 <= v < 2.0.0",
|
||||||
"noahzgordon/elm-color-extra": "1.0.2 <= v < 2.0.0",
|
"noahzgordon/elm-color-extra": "1.0.2 <= v < 2.0.0",
|
||||||
@ -43,4 +44,4 @@
|
|||||||
"elm-explorations/test": "1.2.2 <= v < 2.0.0",
|
"elm-explorations/test": "1.2.2 <= v < 2.0.0",
|
||||||
"jgrenat/elm-html-test-runner": "1.0.3 <= v < 2.0.0"
|
"jgrenat/elm-html-test-runner": "1.0.3 <= v < 2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@
|
|||||||
"type": "blog",
|
"type": "blog",
|
||||||
"author": "Dillon Kearns",
|
"author": "Dillon Kearns",
|
||||||
"title": "A is for API - Introducing Static HTTP Requests",
|
"title": "A is for API - Introducing Static HTTP Requests",
|
||||||
"description": "",
|
"description": "The new StaticHttp API lets you fetch data when your site is built. That lets you remove loading spinners, and even access environment variables.",
|
||||||
"image": "images/article-covers/static-http.jpg",
|
"image": "images/article-covers/static-http.jpg",
|
||||||
"published": "2019-12-10",
|
"published": "2019-12-10",
|
||||||
}
|
}
|
||||||
|
7
examples/docs/content/docs/core-concepts.md
Normal file
7
examples/docs/content/docs/core-concepts.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Core Concepts
|
||||||
|
type: doc
|
||||||
|
---
|
||||||
|
## StaticHttp
|
||||||
|
|
||||||
|
Gives you a way to pull in data during the build step. This data changes every time you run a build. You won't see a loading spinner or error with this data in your built production site. You might get a build error that you can fix.
|
@ -31,7 +31,6 @@
|
|||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"justinmimbs/date": "3.2.0",
|
"justinmimbs/date": "3.2.0",
|
||||||
"lukewestby/elm-string-interpolate": "1.0.4",
|
"lukewestby/elm-string-interpolate": "1.0.4",
|
||||||
"mdgriffith/elm-markup": "3.0.1",
|
|
||||||
"mdgriffith/elm-ui": "1.1.5",
|
"mdgriffith/elm-ui": "1.1.5",
|
||||||
"miniBill/elm-codec": "1.2.0",
|
"miniBill/elm-codec": "1.2.0",
|
||||||
"noahzgordon/elm-color-extra": "1.0.2",
|
"noahzgordon/elm-color-extra": "1.0.2",
|
||||||
@ -59,4 +58,4 @@
|
|||||||
},
|
},
|
||||||
"indirect": {}
|
"indirect": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,10 +17,12 @@ import Head.Seo as Seo
|
|||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
import Html.Attributes as Attr
|
import Html.Attributes as Attr
|
||||||
import Index
|
import Index
|
||||||
import Json.Decode.Exploration as D
|
import Json.Decode as Decode exposing (Decoder)
|
||||||
|
import Json.Encode
|
||||||
import MarkdownRenderer
|
import MarkdownRenderer
|
||||||
import Metadata exposing (Metadata)
|
import Metadata exposing (Metadata)
|
||||||
import MySitemap
|
import MySitemap
|
||||||
|
import OptimizedDecoder as D
|
||||||
import Pages exposing (images, pages)
|
import Pages exposing (images, pages)
|
||||||
import Pages.Directory as Directory exposing (Directory)
|
import Pages.Directory as Directory exposing (Directory)
|
||||||
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
||||||
@ -34,6 +36,7 @@ import Rss
|
|||||||
import RssPlugin
|
import RssPlugin
|
||||||
import Secrets
|
import Secrets
|
||||||
import Showcase
|
import Showcase
|
||||||
|
import StructuredData
|
||||||
|
|
||||||
|
|
||||||
manifest : Manifest.Config Pages.PathKey
|
manifest : Manifest.Config Pages.PathKey
|
||||||
@ -202,7 +205,7 @@ view siteMetadata page =
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|> wrapBody stars page model
|
|> wrapBody stars page model
|
||||||
, head = head page.frontmatter
|
, head = head page.path page.frontmatter
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
(StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
|
(StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
|
||||||
@ -219,49 +222,11 @@ view siteMetadata page =
|
|||||||
\model viewForPage ->
|
\model viewForPage ->
|
||||||
pageView stars model siteMetadata page viewForPage
|
pageView stars model siteMetadata page viewForPage
|
||||||
|> wrapBody stars page model
|
|> wrapBody stars page model
|
||||||
, head = head page.frontmatter
|
, head = head page.path page.frontmatter
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--let
|
|
||||||
-- viewFn =
|
|
||||||
-- case page.frontmatter of
|
|
||||||
-- Metadata.Page metadata ->
|
|
||||||
-- StaticHttp.map3
|
|
||||||
-- (\elmPagesStars elmPagesStarterStars netlifyStars ->
|
|
||||||
-- { view =
|
|
||||||
-- \model viewForPage ->
|
|
||||||
-- { title = metadata.title
|
|
||||||
-- , body =
|
|
||||||
-- "elm-pages ⭐️'s: "
|
|
||||||
-- ++ String.fromInt elmPagesStars
|
|
||||||
-- ++ "\n\nelm-pages-starter ⭐️'s: "
|
|
||||||
-- ++ String.fromInt elmPagesStarterStars
|
|
||||||
-- ++ "\n\nelm-markdown ⭐️'s: "
|
|
||||||
-- ++ String.fromInt netlifyStars
|
|
||||||
-- |> Element.text
|
|
||||||
-- |> wrapBody
|
|
||||||
-- }
|
|
||||||
-- , head = head page.frontmatter
|
|
||||||
-- }
|
|
||||||
-- )
|
|
||||||
-- (StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
|
|
||||||
-- (D.field "stargazers_count" D.int)
|
|
||||||
-- )
|
|
||||||
-- (StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages-starter")
|
|
||||||
-- (D.field "stargazers_count" D.int)
|
|
||||||
-- )
|
|
||||||
-- (StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-markdown")
|
|
||||||
-- (D.field "stargazers_count" D.int)
|
|
||||||
-- )
|
|
||||||
--
|
|
||||||
-- _ ->
|
|
||||||
-- StaticHttp.withData "https://api.github.com/repos/dillonkearns/elm-pages"
|
|
||||||
-- (Decode.field "stargazers_count" Decode.int)
|
|
||||||
|
|
||||||
|
|
||||||
pageView :
|
pageView :
|
||||||
Int
|
Int
|
||||||
-> Model
|
-> Model
|
||||||
@ -535,8 +500,8 @@ highlightableLink currentPath linkDirectory displayName =
|
|||||||
<https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names>
|
<https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names>
|
||||||
<https://ogp.me/>
|
<https://ogp.me/>
|
||||||
-}
|
-}
|
||||||
head : Metadata -> List (Head.Tag Pages.PathKey)
|
head : PagePath Pages.PathKey -> Metadata -> List (Head.Tag Pages.PathKey)
|
||||||
head metadata =
|
head currentPath metadata =
|
||||||
case metadata of
|
case metadata of
|
||||||
Metadata.Page meta ->
|
Metadata.Page meta ->
|
||||||
Seo.summary
|
Seo.summary
|
||||||
@ -571,26 +536,45 @@ head metadata =
|
|||||||
|> Seo.website
|
|> Seo.website
|
||||||
|
|
||||||
Metadata.Article meta ->
|
Metadata.Article meta ->
|
||||||
Seo.summaryLarge
|
Head.structuredData
|
||||||
{ canonicalUrlOverride = Nothing
|
(StructuredData.article
|
||||||
, siteName = "elm-pages"
|
{ title = meta.title
|
||||||
, image =
|
, description = meta.description
|
||||||
{ url = meta.image
|
, author = StructuredData.person { name = meta.author.name }
|
||||||
, alt = meta.description
|
, publisher = StructuredData.person { name = "Dillon Kearns" }
|
||||||
, dimensions = Nothing
|
, url = canonicalSiteUrl ++ "/" ++ PagePath.toString currentPath
|
||||||
, mimeType = Nothing
|
, imageUrl = canonicalSiteUrl ++ "/" ++ ImagePath.toString meta.image
|
||||||
}
|
, datePublished = Date.toIsoString meta.published
|
||||||
, description = meta.description
|
, mainEntityOfPage =
|
||||||
, locale = Nothing
|
StructuredData.softwareSourceCode
|
||||||
, title = meta.title
|
{ codeRepositoryUrl = "https://github.com/dillonkearns/elm-pages"
|
||||||
}
|
, description = "A statically typed site generator for Elm."
|
||||||
|> Seo.article
|
, author = "Dillon Kearns"
|
||||||
{ tags = []
|
, programmingLanguage = StructuredData.elmLang
|
||||||
, section = Nothing
|
}
|
||||||
, publishedTime = Just (Date.toIsoString meta.published)
|
|
||||||
, modifiedTime = Nothing
|
|
||||||
, expirationTime = Nothing
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
:: (Seo.summaryLarge
|
||||||
|
{ canonicalUrlOverride = Nothing
|
||||||
|
, siteName = "elm-pages"
|
||||||
|
, image =
|
||||||
|
{ url = meta.image
|
||||||
|
, alt = meta.description
|
||||||
|
, dimensions = Nothing
|
||||||
|
, mimeType = Nothing
|
||||||
|
}
|
||||||
|
, description = meta.description
|
||||||
|
, locale = Nothing
|
||||||
|
, title = meta.title
|
||||||
|
}
|
||||||
|
|> Seo.article
|
||||||
|
{ tags = []
|
||||||
|
, section = Nothing
|
||||||
|
, publishedTime = Just (Date.toIsoString meta.published)
|
||||||
|
, modifiedTime = Nothing
|
||||||
|
, expirationTime = Nothing
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Metadata.Author meta ->
|
Metadata.Author meta ->
|
||||||
let
|
let
|
||||||
|
@ -91,7 +91,7 @@ decoder =
|
|||||||
|> Decode.map Article
|
|> Decode.map Article
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
Decode.fail <| "Unexpected page type " ++ pageType
|
Decode.fail <| "Unexpected page \"type\" " ++ pageType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import Element
|
|||||||
import Element.Border
|
import Element.Border
|
||||||
import Element.Font
|
import Element.Font
|
||||||
import FontAwesome
|
import FontAwesome
|
||||||
import Json.Decode.Exploration as Decode
|
import OptimizedDecoder as Decode
|
||||||
import Pages.Secrets as Secrets
|
import Pages.Secrets as Secrets
|
||||||
import Pages.StaticHttp as StaticHttp
|
import Pages.StaticHttp as StaticHttp
|
||||||
import Palette
|
import Palette
|
||||||
|
@ -27,7 +27,6 @@
|
|||||||
"elm-explorations/markdown": "1.0.0",
|
"elm-explorations/markdown": "1.0.0",
|
||||||
"justinmimbs/date": "3.1.2",
|
"justinmimbs/date": "3.1.2",
|
||||||
"lukewestby/elm-string-interpolate": "1.0.4",
|
"lukewestby/elm-string-interpolate": "1.0.4",
|
||||||
"mdgriffith/elm-markup": "3.0.1",
|
|
||||||
"mdgriffith/elm-ui": "1.1.5",
|
"mdgriffith/elm-ui": "1.1.5",
|
||||||
"mgold/elm-nonempty-list": "4.0.2",
|
"mgold/elm-nonempty-list": "4.0.2",
|
||||||
"miniBill/elm-codec": "1.2.0",
|
"miniBill/elm-codec": "1.2.0",
|
||||||
@ -51,4 +50,4 @@
|
|||||||
},
|
},
|
||||||
"indirect": {}
|
"indirect": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -95,20 +95,13 @@ import Dict exposing (Dict)
|
|||||||
|
|
||||||
content : { markdown : List ( List String, { frontMatter : String, body : Maybe String } ), markup : List ( List String, String ) }
|
content : { markdown : List ( List String, { frontMatter : String, body : Maybe String } ), markup : List ( List String, String ) }
|
||||||
content =
|
content =
|
||||||
{ markdown = markdown, markup = markup }
|
{ markdown = markdown }
|
||||||
|
|
||||||
|
|
||||||
markdown : List ( List String, { frontMatter : String, body : Maybe String } )
|
markdown : List ( List String, { frontMatter : String, body : Maybe String } )
|
||||||
markdown =
|
markdown =
|
||||||
[ {1}
|
[ {1}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
markup : List ( List String, String )
|
|
||||||
markup =
|
|
||||||
[
|
|
||||||
{0}
|
|
||||||
]
|
|
||||||
"""
|
"""
|
||||||
[ List.map generatePage content |> String.join "\n ,"
|
[ List.map generatePage content |> String.join "\n ,"
|
||||||
, List.map generateMarkdownPage markdownContent |> String.join "\n ,"
|
, List.map generateMarkdownPage markdownContent |> String.join "\n ,"
|
||||||
|
@ -2,6 +2,7 @@ const path = require("path");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const globby = require("globby");
|
const globby = require("globby");
|
||||||
const parseFrontmatter = require("./frontmatter.js");
|
const parseFrontmatter = require("./frontmatter.js");
|
||||||
|
const webpack = require('webpack')
|
||||||
|
|
||||||
function unpackFile(filePath) {
|
function unpackFile(filePath) {
|
||||||
const { content, data } = parseFrontmatter(
|
const { content, data } = parseFrontmatter(
|
||||||
@ -15,52 +16,74 @@ function unpackFile(filePath) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
baseRoute,
|
baseRoute,
|
||||||
content
|
content,
|
||||||
|
filePath
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = class AddFilesPlugin {
|
module.exports = class AddFilesPlugin {
|
||||||
constructor(data, filesToGenerate) {
|
apply(/** @type {webpack.Compiler} */ compiler) {
|
||||||
this.pagesWithRequests = data;
|
|
||||||
this.filesToGenerate = filesToGenerate;
|
|
||||||
}
|
|
||||||
apply(compiler) {
|
|
||||||
compiler.hooks.emit.tap("AddFilesPlugin", compilation => {
|
|
||||||
const files = globby
|
|
||||||
.sync(["content/**/*.*", "!content/**/*.emu"], {})
|
|
||||||
.map(unpackFile);
|
|
||||||
|
|
||||||
files.forEach(file => {
|
(global.mode === "dev" ? compiler.hooks.emit : compiler.hooks.make).tapAsync("AddFilesPlugin", (compilation, callback) => {
|
||||||
// Couldn't find this documented in the webpack docs,
|
|
||||||
// but I found the example code for it here:
|
|
||||||
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
|
|
||||||
|
|
||||||
let route = file.baseRoute.replace(/\/$/, '');
|
|
||||||
const staticRequests = this.pagesWithRequests[route];
|
|
||||||
|
|
||||||
const filename = path.join(file.baseRoute, "content.json");
|
const files = globby.sync("content").map(unpackFile);
|
||||||
compilation.fileDependencies.add(filename);
|
|
||||||
const rawContents = JSON.stringify({
|
|
||||||
body: file.content,
|
|
||||||
staticData: staticRequests || {}
|
|
||||||
});
|
|
||||||
|
|
||||||
compilation.assets[filename] = {
|
|
||||||
source: () => rawContents,
|
|
||||||
size: () => rawContents.length
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
(this.filesToGenerate || []).forEach(file => {
|
let staticRequestData = {}
|
||||||
// Couldn't find this documented in the webpack docs,
|
global.pagesWithRequests.then(payload => {
|
||||||
// but I found the example code for it here:
|
|
||||||
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
|
|
||||||
compilation.assets[file.path] = {
|
|
||||||
source: () => file.content,
|
|
||||||
size: () => file.content.length
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if (payload.type === 'error') {
|
||||||
|
compilation.errors.push(new Error(payload.message))
|
||||||
|
} else if (payload.errors && payload.errors.length > 0) {
|
||||||
|
compilation.errors.push(new Error(payload.errors[0]))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
staticRequestData = payload.pages
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
// Couldn't find this documented in the webpack docs,
|
||||||
|
// but I found the example code for it here:
|
||||||
|
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
|
||||||
|
|
||||||
|
let route = file.baseRoute.replace(/\/$/, '');
|
||||||
|
const staticRequests = staticRequestData[route];
|
||||||
|
|
||||||
|
const filename = path.join(file.baseRoute, "content.json");
|
||||||
|
if (compilation.contextDependencies) {
|
||||||
|
compilation.contextDependencies.add('content')
|
||||||
|
}
|
||||||
|
// compilation.fileDependencies.add(filename);
|
||||||
|
if (compilation.fileDependencies) {
|
||||||
|
compilation.fileDependencies.add(path.resolve(file.filePath));
|
||||||
|
}
|
||||||
|
const rawContents = JSON.stringify({
|
||||||
|
body: file.content,
|
||||||
|
staticData: staticRequests || {}
|
||||||
|
});
|
||||||
|
|
||||||
|
compilation.assets[filename] = {
|
||||||
|
source: () => rawContents,
|
||||||
|
size: () => rawContents.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
(global.filesToGenerate || []).forEach(file => {
|
||||||
|
// Couldn't find this documented in the webpack docs,
|
||||||
|
// but I found the example code for it here:
|
||||||
|
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
|
||||||
|
compilation.assets[file.path] = {
|
||||||
|
source: () => file.content,
|
||||||
|
size: () => file.content.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
callback()
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,36 @@
|
|||||||
const { compileToString } = require("../node-elm-compiler/index.js");
|
const { compileToStringSync } = require("../node-elm-compiler/index.js");
|
||||||
XMLHttpRequest = require("xhr2");
|
XMLHttpRequest = require("xhr2");
|
||||||
|
|
||||||
module.exports = runElm;
|
module.exports = runElm;
|
||||||
function runElm(/** @type string */ mode, /** @type any */ callback) {
|
function runElm(/** @type string */ mode) {
|
||||||
const elmBaseDirectory = "./elm-stuff/elm-pages";
|
return new Promise((resolve, reject) => {
|
||||||
const mainElmFile = "../../src/Main.elm";
|
const elmBaseDirectory = "./elm-stuff/elm-pages";
|
||||||
const startingDir = process.cwd();
|
const mainElmFile = "../../src/Main.elm";
|
||||||
process.chdir(elmBaseDirectory);
|
const startingDir = process.cwd();
|
||||||
compileToString([mainElmFile], {}).then(function(data) {
|
process.chdir(elmBaseDirectory);
|
||||||
(function() {
|
const data = compileToStringSync([mainElmFile], {});
|
||||||
|
process.chdir(startingDir);
|
||||||
|
(function () {
|
||||||
const warnOriginal = console.warn;
|
const warnOriginal = console.warn;
|
||||||
console.warn = function() {};
|
console.warn = function () { };
|
||||||
eval(data.toString());
|
eval(data.toString());
|
||||||
const app = Elm.Main.init({
|
const app = Elm.Main.init({
|
||||||
flags: { secrets: process.env, mode }
|
flags: { secrets: process.env, mode, staticHttpCache: global.staticHttpCache }
|
||||||
});
|
});
|
||||||
|
|
||||||
app.ports.toJsPort.subscribe(payload => {
|
app.ports.toJsPort.subscribe(payload => {
|
||||||
process.chdir(startingDir);
|
|
||||||
|
|
||||||
if (payload.tag === "Success") {
|
if (payload.tag === "Success") {
|
||||||
callback(payload.args[0]);
|
global.staticHttpCache = payload.args[0].staticHttpCache;
|
||||||
|
resolve(payload.args[0])
|
||||||
} else {
|
} else {
|
||||||
console.log(payload.args[0]);
|
reject(payload.args[0])
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
delete Elm;
|
delete Elm;
|
||||||
console.warn = warnOriginal;
|
console.warn = warnOriginal;
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const middleware = require("webpack-dev-middleware");
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const HTMLWebpackPlugin = require("html-webpack-plugin");
|
const HTMLWebpackPlugin = require("html-webpack-plugin");
|
||||||
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
|
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
|
||||||
@ -14,14 +13,27 @@ const imageminMozjpeg = require("imagemin-mozjpeg");
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const ClosurePlugin = require("closure-webpack-plugin");
|
const ClosurePlugin = require("closure-webpack-plugin");
|
||||||
const readline = require("readline");
|
const readline = require("readline");
|
||||||
|
const webpackDevMiddleware = require("webpack-dev-middleware");
|
||||||
|
const PluginGenerateElmPagesBuild = require('./plugin-generate-elm-pages-build')
|
||||||
|
|
||||||
|
const hotReloadIndicatorStyle = `
|
||||||
|
<style>
|
||||||
|
@keyframes lds-default {
|
||||||
|
0%, 20%, 80%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`
|
||||||
|
|
||||||
module.exports = { start, run };
|
module.exports = { start, run };
|
||||||
function start({ routes, debug, customPort, manifestConfig, routesWithRequests, filesToGenerate }) {
|
function start({ routes, debug, customPort, manifestConfig }) {
|
||||||
const config = webpackOptions(false, routes, {
|
const config = webpackOptions(false, routes, {
|
||||||
debug,
|
debug,
|
||||||
manifestConfig,
|
manifestConfig
|
||||||
routesWithRequests,
|
|
||||||
filesToGenerate
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const compiler = webpack(config);
|
const compiler = webpack(config);
|
||||||
@ -39,27 +51,31 @@ function start({ routes, debug, customPort, manifestConfig, routesWithRequests,
|
|||||||
app.use('/images', express.static(path.resolve(process.cwd(), "./images")));
|
app.use('/images', express.static(path.resolve(process.cwd(), "./images")));
|
||||||
|
|
||||||
|
|
||||||
app.use(require("webpack-dev-middleware")(compiler, options));
|
app.use(webpackDevMiddleware(compiler, options));
|
||||||
app.use(require("webpack-hot-middleware")(compiler, {
|
app.use(require("webpack-hot-middleware")(compiler, {
|
||||||
log: console.log, path: '/__webpack_hmr'
|
log: console.log, path: '/__webpack_hmr'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
app.use("*", function(req, res, next) {
|
app.get('/elm-pages-dev-server-options', function (req, res) {
|
||||||
|
res.json({ elmDebugger: debug });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use("*", function (req, res, next) {
|
||||||
// don't know why this works, but it does
|
// don't know why this works, but it does
|
||||||
// see: https://github.com/jantimon/html-webpack-plugin/issues/145#issuecomment-170554832
|
// see: https://github.com/jantimon/html-webpack-plugin/issues/145#issuecomment-170554832
|
||||||
const filename = path.join(compiler.outputPath, "index.html");
|
const filename = path.join(compiler.outputPath, "index.html");
|
||||||
const route = req.originalUrl.replace(/(\w)\/$/, "$1").replace(/^\//, "");
|
const route = req.originalUrl.replace(/(\w)\/$/, "$1").replace(/^\//, "");
|
||||||
const isPage = routes.includes(route);
|
const isPage = routes.includes(route);
|
||||||
|
|
||||||
compiler.outputFileSystem.readFile(filename, function(err, result) {
|
compiler.outputFileSystem.readFile(filename, function (err, result) {
|
||||||
const contents = isPage
|
|
||||||
? replaceBaseAndLinks(result.toString(), route)
|
|
||||||
: result
|
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contents = isPage
|
||||||
|
? replaceBaseAndLinks(result.toString(), route)
|
||||||
|
: result
|
||||||
|
|
||||||
res.set("content-type", "text/html");
|
res.set("content-type", "text/html");
|
||||||
res.send(contents);
|
res.send(contents);
|
||||||
res.end();
|
res.end();
|
||||||
@ -74,20 +90,18 @@ function start({ routes, debug, customPort, manifestConfig, routesWithRequests,
|
|||||||
// app.use(express.static(__dirname + "/path-to-static-folder"));
|
// app.use(express.static(__dirname + "/path-to-static-folder"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function run({ routes, manifestConfig, routesWithRequests, filesToGenerate }, callback) {
|
function run({ routes, manifestConfig }) {
|
||||||
webpack(
|
webpack(
|
||||||
webpackOptions(true, routes, {
|
webpackOptions(true, routes, {
|
||||||
debug: false,
|
debug: false,
|
||||||
manifestConfig,
|
manifestConfig,
|
||||||
routesWithRequests,
|
|
||||||
filesToGenerate
|
|
||||||
})
|
})
|
||||||
).run((err, stats) => {
|
).run((err, stats) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} else {
|
} else {
|
||||||
callback();
|
// done
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@ -128,12 +142,13 @@ function printProgress(progress, message) {
|
|||||||
function webpackOptions(
|
function webpackOptions(
|
||||||
production,
|
production,
|
||||||
routes,
|
routes,
|
||||||
{ debug, manifestConfig, routesWithRequests, filesToGenerate }
|
{ debug, manifestConfig }
|
||||||
) {
|
) {
|
||||||
const common = {
|
const common = {
|
||||||
mode: production ? "production" : "development",
|
mode: production ? "production" : "development",
|
||||||
plugins: [
|
plugins: [
|
||||||
new AddFilesPlugin(routesWithRequests, filesToGenerate),
|
new PluginGenerateElmPagesBuild(),
|
||||||
|
new AddFilesPlugin(),
|
||||||
new CopyPlugin([
|
new CopyPlugin([
|
||||||
{
|
{
|
||||||
from: "static/**/*",
|
from: "static/**/*",
|
||||||
@ -167,7 +182,33 @@ function webpackOptions(
|
|||||||
|
|
||||||
new HTMLWebpackPlugin({
|
new HTMLWebpackPlugin({
|
||||||
inject: "head",
|
inject: "head",
|
||||||
template: path.resolve(__dirname, "template.html")
|
templateContent: `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<link rel="preload" href="content.json" as="fetch" crossorigin />
|
||||||
|
|
||||||
|
<base href="/" />
|
||||||
|
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
navigator.serviceWorker.register("service-worker.js");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("No service worker registered.");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
${production ? '' : hotReloadIndicatorStyle}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body></body>
|
||||||
|
|
||||||
|
|
||||||
|
</html>`
|
||||||
}),
|
}),
|
||||||
new ScriptExtHtmlWebpackPlugin({
|
new ScriptExtHtmlWebpackPlugin({
|
||||||
preload: /\.js$/,
|
preload: /\.js$/,
|
||||||
@ -234,10 +275,10 @@ function webpackOptions(
|
|||||||
/assets\//
|
/assets\//
|
||||||
],
|
],
|
||||||
swDest: "service-worker.js"
|
swDest: "service-worker.js"
|
||||||
})
|
}),
|
||||||
// comment this out to do performance profiling
|
// comment this out to do performance profiling
|
||||||
// (drag-and-drop `events.json` file into Chrome performance tab)
|
// (drag-and-drop `events.json` file into Chrome performance tab)
|
||||||
// , new webpack.debug.ProfilingPlugin()
|
// new webpack.debug.ProfilingPlugin()
|
||||||
],
|
],
|
||||||
output: {},
|
output: {},
|
||||||
resolve: {
|
resolve: {
|
||||||
@ -249,7 +290,7 @@ function webpackOptions(
|
|||||||
// process.cwd prefixed node_modules above).
|
// process.cwd prefixed node_modules above).
|
||||||
path.resolve(path.dirname(require.resolve('webpack')), '../../'),
|
path.resolve(path.dirname(require.resolve('webpack')), '../../'),
|
||||||
|
|
||||||
],
|
],
|
||||||
extensions: [".js", ".elm", ".scss", ".png", ".html"]
|
extensions: [".js", ".elm", ".scss", ".png", ".html"]
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
@ -323,7 +364,7 @@ function webpackOptions(
|
|||||||
renderer: new PrerenderSPAPlugin.PuppeteerRenderer({
|
renderer: new PrerenderSPAPlugin.PuppeteerRenderer({
|
||||||
renderAfterDocumentEvent: "prerender-trigger",
|
renderAfterDocumentEvent: "prerender-trigger",
|
||||||
headless: true,
|
headless: true,
|
||||||
devtools: false
|
devtools: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
postProcess: renderedRoute => {
|
postProcess: renderedRoute => {
|
||||||
@ -354,14 +395,14 @@ function webpackOptions(
|
|||||||
} else {
|
} else {
|
||||||
return merge(common, {
|
return merge(common, {
|
||||||
entry: [
|
entry: [
|
||||||
require.resolve("webpack-hot-middleware/client"),
|
hmrClientPath(),
|
||||||
"./index.js",
|
"./index.js",
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.NamedModulesPlugin(),
|
new webpack.NamedModulesPlugin(),
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
// Prevents compilation errors causing the hot loader to lose state
|
// Prevents compilation errors causing the hot loader to lose state
|
||||||
new webpack.NoEmitOnErrorsPlugin(),
|
new webpack.NoEmitOnErrorsPlugin(),
|
||||||
new webpack.HotModuleReplacementPlugin()
|
|
||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
@ -385,6 +426,42 @@ function webpackOptions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hmrClientPath() {
|
||||||
|
var ansiColors = {
|
||||||
|
reset: ['ffffff', 'transparent'], // [FOREGROUD_COLOR, BACKGROUND_COLOR]
|
||||||
|
black: '000',
|
||||||
|
red: 'c91b00',
|
||||||
|
green: '00c200',
|
||||||
|
yellow: 'c7c400',
|
||||||
|
blue: '0225c7',
|
||||||
|
magenta: 'c930c7',
|
||||||
|
cyan: '00c5c7',
|
||||||
|
lightgrey: 'f0f0f0',
|
||||||
|
darkgrey: '888'
|
||||||
|
};
|
||||||
|
var overlayStyles = {
|
||||||
|
// options from https://github.com/webpack-contrib/webpack-hot-middleware/blob/master/client-overlay.js
|
||||||
|
|
||||||
|
background: 'rgba(0,0,0,0.90)',
|
||||||
|
color: '#e8e8e8',
|
||||||
|
lineHeight: '1.6',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
fontFamily: 'Menlo, Consolas, monospace',
|
||||||
|
fontSize: '16px',
|
||||||
|
// position: 'fixed',
|
||||||
|
// zIndex: 9999,
|
||||||
|
// padding: '10px',
|
||||||
|
// left: 0,
|
||||||
|
// right: 0,
|
||||||
|
// top: 0,
|
||||||
|
// bottom: 0,
|
||||||
|
// overflow: 'auto',
|
||||||
|
// dir: 'ltr',
|
||||||
|
// textAlign: 'left',
|
||||||
|
};
|
||||||
|
return `${require.resolve("webpack-hot-middleware/client")}?ansiColors=${encodeURIComponent(JSON.stringify(ansiColors))}&overlayStyles=${encodeURIComponent(JSON.stringify(overlayStyles))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function cleanRoute(route) {
|
function cleanRoute(route) {
|
||||||
return route.replace(/(^\/|\/$)/, "")
|
return route.replace(/(^\/|\/$)/, "")
|
||||||
@ -395,10 +472,10 @@ function pathToRoot(cleanedRoute) {
|
|||||||
return cleanedRoute === ""
|
return cleanedRoute === ""
|
||||||
? cleanedRoute
|
? cleanedRoute
|
||||||
: cleanedRoute
|
: cleanedRoute
|
||||||
.split("/")
|
.split("/")
|
||||||
.map(_ => "..")
|
.map(_ => "..")
|
||||||
.join("/")
|
.join("/")
|
||||||
.replace(/\.$/, "./")
|
.replace(/\.$/, "./")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
generateRawContent = require("./generate-raw-content.js");
|
generateRawContent = require("./generate-raw-content.js");
|
||||||
const exposingList =
|
const exposingList =
|
||||||
"(PathKey, allPages, allImages, internals, images, isValidRoute, pages, builtAt)";
|
"(PathKey, allPages, allImages, internals, images, isValidRoute, pages, builtAt)";
|
||||||
|
|
||||||
function staticRouteStuff(staticRoutes) {
|
function staticRouteStuff(staticRoutes) {
|
||||||
return `
|
return `
|
||||||
|
|
||||||
|
|
||||||
${staticRoutes.allRoutes}
|
${staticRoutes.allRoutes}
|
||||||
@ -41,8 +41,8 @@ isValidRoute route =
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function elmPagesUiFile(staticRoutes, markdownContent, markupContent) {
|
function elmPagesUiFile(staticRoutes, markdownContent) {
|
||||||
return `port module Pages exposing ${exposingList}
|
return `port module Pages exposing ${exposingList}
|
||||||
|
|
||||||
import Color exposing (Color)
|
import Color exposing (Color)
|
||||||
import Pages.Internal
|
import Pages.Internal
|
||||||
@ -50,7 +50,6 @@ import Head
|
|||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
import Json.Decode
|
import Json.Decode
|
||||||
import Json.Encode
|
import Json.Encode
|
||||||
import Mark
|
|
||||||
import Pages.Platform
|
import Pages.Platform
|
||||||
import Pages.Manifest exposing (DisplayMode, Orientation)
|
import Pages.Manifest exposing (DisplayMode, Orientation)
|
||||||
import Pages.Manifest.Category as Category exposing (Category)
|
import Pages.Manifest.Category as Category exposing (Category)
|
||||||
@ -92,23 +91,26 @@ directoryWithoutIndex path =
|
|||||||
|
|
||||||
port toJsPort : Json.Encode.Value -> Cmd msg
|
port toJsPort : Json.Encode.Value -> Cmd msg
|
||||||
|
|
||||||
|
port fromJsPort : (Json.Decode.Value -> msg) -> Sub msg
|
||||||
|
|
||||||
|
|
||||||
internals : Pages.Internal.Internal PathKey
|
internals : Pages.Internal.Internal PathKey
|
||||||
internals =
|
internals =
|
||||||
{ applicationType = Pages.Internal.Browser
|
{ applicationType = Pages.Internal.Browser
|
||||||
, toJsPort = toJsPort
|
, toJsPort = toJsPort
|
||||||
|
, fromJsPort = fromJsPort identity
|
||||||
, content = content
|
, content = content
|
||||||
, pathKey = PathKey
|
, pathKey = PathKey
|
||||||
}
|
}
|
||||||
|
|
||||||
${staticRouteStuff(staticRoutes)}
|
${staticRouteStuff(staticRoutes)}
|
||||||
|
|
||||||
${generateRawContent(markdownContent, markupContent, false)}
|
${generateRawContent(markdownContent, false)}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function elmPagesCliFile(staticRoutes, markdownContent, markupContent) {
|
function elmPagesCliFile(staticRoutes, markdownContent) {
|
||||||
return `port module Pages exposing ${exposingList}
|
return `port module Pages exposing ${exposingList}
|
||||||
|
|
||||||
import Color exposing (Color)
|
import Color exposing (Color)
|
||||||
import Pages.Internal
|
import Pages.Internal
|
||||||
@ -116,7 +118,6 @@ import Head
|
|||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
import Json.Decode
|
import Json.Decode
|
||||||
import Json.Encode
|
import Json.Encode
|
||||||
import Mark
|
|
||||||
import Pages.Platform
|
import Pages.Platform
|
||||||
import Pages.Manifest exposing (DisplayMode, Orientation)
|
import Pages.Manifest exposing (DisplayMode, Orientation)
|
||||||
import Pages.Manifest.Category as Category exposing (Category)
|
import Pages.Manifest.Category as Category exposing (Category)
|
||||||
@ -159,10 +160,14 @@ directoryWithoutIndex path =
|
|||||||
port toJsPort : Json.Encode.Value -> Cmd msg
|
port toJsPort : Json.Encode.Value -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
|
port fromJsPort : (Json.Decode.Value -> msg) -> Sub msg
|
||||||
|
|
||||||
|
|
||||||
internals : Pages.Internal.Internal PathKey
|
internals : Pages.Internal.Internal PathKey
|
||||||
internals =
|
internals =
|
||||||
{ applicationType = Pages.Internal.Cli
|
{ applicationType = Pages.Internal.Cli
|
||||||
, toJsPort = toJsPort
|
, toJsPort = toJsPort
|
||||||
|
, fromJsPort = fromJsPort identity
|
||||||
, content = content
|
, content = content
|
||||||
, pathKey = PathKey
|
, pathKey = PathKey
|
||||||
}
|
}
|
||||||
@ -170,7 +175,7 @@ internals =
|
|||||||
|
|
||||||
${staticRouteStuff(staticRoutes)}
|
${staticRouteStuff(staticRoutes)}
|
||||||
|
|
||||||
${generateRawContent(markdownContent, markupContent, true)}
|
${generateRawContent(markdownContent, true)}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
module.exports = { elmPagesUiFile, elmPagesCliFile };
|
module.exports = { elmPagesUiFile, elmPagesCliFile };
|
||||||
|
@ -5,67 +5,38 @@ const { version } = require("../../package.json");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const globby = require("globby");
|
const globby = require("globby");
|
||||||
const develop = require("./develop.js");
|
const develop = require("./develop.js");
|
||||||
const chokidar = require("chokidar");
|
|
||||||
const doCliStuff = require("./generate-elm-stuff.js");
|
|
||||||
const { elmPagesUiFile } = require("./elm-file-constants.js");
|
|
||||||
const generateRecords = require("./generate-records.js");
|
|
||||||
const parseFrontmatter = require("./frontmatter.js");
|
const parseFrontmatter = require("./frontmatter.js");
|
||||||
const path = require("path");
|
const generateRecords = require("./generate-records.js");
|
||||||
const { ensureDirSync, deleteIfExists } = require('./file-helpers.js')
|
const doCliStuff = require("./generate-elm-stuff.js");
|
||||||
global.builtAt = new Date();
|
global.builtAt = new Date();
|
||||||
|
global.staticHttpCache = {};
|
||||||
const contentGlobPath = "content/**/*.emu";
|
|
||||||
|
|
||||||
let watcher = null;
|
|
||||||
let devServerRunning = false;
|
|
||||||
|
|
||||||
function unpackFile(path) {
|
function unpackFile(path) {
|
||||||
return { path, contents: fs.readFileSync(path).toString() };
|
return { path, contents: fs.readFileSync(path).toString() };
|
||||||
}
|
}
|
||||||
|
|
||||||
function unpackMarkup(path) {
|
|
||||||
const separated = parseFrontmatter(path, fs.readFileSync(path).toString());
|
|
||||||
return {
|
|
||||||
path,
|
|
||||||
metadata: separated.matter,
|
|
||||||
body: separated.content,
|
|
||||||
extension: "emu"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMarkdown(path, fileContents) {
|
function parseMarkdown(path, fileContents) {
|
||||||
const { content, data } = parseFrontmatter(path, fileContents);
|
const { content, data } = parseFrontmatter(path, fileContents);
|
||||||
return {
|
return {
|
||||||
path,
|
path,
|
||||||
metadata: JSON.stringify(data),
|
metadata: JSON.stringify(data),
|
||||||
body: content,
|
body: content
|
||||||
extension: "md"
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
console.log("Running elm-pages...");
|
|
||||||
const content = globby.sync([contentGlobPath], {}).map(unpackMarkup);
|
|
||||||
const staticRoutes = generateRecords();
|
|
||||||
|
|
||||||
const markdownContent = globby
|
const markdownContent = globby
|
||||||
.sync(["content/**/*.*", "!content/**/*.emu"], {})
|
.sync(["content/**/*.*"], {})
|
||||||
.map(unpackFile)
|
.map(unpackFile)
|
||||||
.map(({ path, contents }) => {
|
.map(({ path, contents }) => {
|
||||||
return parseMarkdown(path, contents);
|
return parseMarkdown(path, contents);
|
||||||
});
|
});
|
||||||
|
|
||||||
const images = globby
|
|
||||||
.sync("images/**/*", {})
|
|
||||||
.filter(imagePath => !fs.lstatSync(imagePath).isDirectory());
|
|
||||||
|
|
||||||
let app = Elm.Main.init({
|
let app = Elm.Main.init({
|
||||||
flags: {
|
flags: {
|
||||||
argv: process.argv,
|
argv: process.argv,
|
||||||
versionMessage: version,
|
versionMessage: version,
|
||||||
content,
|
|
||||||
markdownContent,
|
|
||||||
images
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,89 +50,51 @@ function run() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.ports.writeFile.subscribe(contents => {
|
app.ports.writeFile.subscribe(cliOptions => {
|
||||||
const routes = toRoutes(markdownContent.concat(content));
|
|
||||||
|
|
||||||
|
const markdownContent = globby
|
||||||
|
.sync(["content/**/*.*"], {})
|
||||||
|
.map(unpackFile)
|
||||||
|
.map(({ path, contents }) => {
|
||||||
|
return parseMarkdown(path, contents);
|
||||||
|
});
|
||||||
|
const routes = toRoutes(markdownContent);
|
||||||
|
|
||||||
|
global.mode = cliOptions.watch ? "dev" : "prod"
|
||||||
|
const staticRoutes = generateRecords();
|
||||||
|
|
||||||
doCliStuff(
|
doCliStuff(
|
||||||
contents.watch ? "dev" : "prod",
|
global.mode,
|
||||||
staticRoutes,
|
staticRoutes,
|
||||||
markdownContent,
|
markdownContent
|
||||||
content,
|
).then((payload) => {
|
||||||
function(payload) {
|
if (cliOptions.watch) {
|
||||||
if (contents.watch) {
|
develop.start({
|
||||||
startWatchIfNeeded();
|
routes,
|
||||||
if (!devServerRunning) {
|
debug: cliOptions.debug,
|
||||||
devServerRunning = true;
|
customPort: cliOptions.customPort,
|
||||||
develop.start({
|
manifestConfig: payload.manifest,
|
||||||
routes,
|
|
||||||
debug: contents.debug,
|
|
||||||
manifestConfig: payload.manifest,
|
|
||||||
routesWithRequests: payload.pages,
|
|
||||||
filesToGenerate: payload.filesToGenerate,
|
|
||||||
customPort: contents.customPort
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (payload.errors && payload.errors.length > 0) {
|
|
||||||
printErrorsAndExit(payload.errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
develop.run(
|
});
|
||||||
{
|
} else {
|
||||||
routes,
|
develop.run({
|
||||||
manifestConfig: payload.manifest,
|
routes,
|
||||||
routesWithRequests: payload.pages,
|
debug: cliOptions.debug,
|
||||||
filesToGenerate: payload.filesToGenerate
|
customPort: cliOptions.customPort,
|
||||||
},
|
manifestConfig: payload.manifest,
|
||||||
() => {}
|
});
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ensureDirSync("./gen");
|
})
|
||||||
|
|
||||||
// prevent compilation errors if migrating from previous elm-pages version
|
|
||||||
deleteIfExists("./gen/Pages/ContentCache.elm");
|
|
||||||
deleteIfExists("./gen/Pages/Platform.elm");
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
"./gen/Pages.elm",
|
|
||||||
elmPagesUiFile(staticRoutes, markdownContent, content)
|
|
||||||
);
|
|
||||||
console.log("elm-pages DONE");
|
|
||||||
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|
||||||
function printErrorsAndExit(errors) {
|
|
||||||
console.error(
|
|
||||||
"Found errors. Exiting. Fix your content or parsers and re-run, or run in dev mode with `elm-pages develop`."
|
|
||||||
);
|
|
||||||
console.error(errors.join("\n\n"));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function startWatchIfNeeded() {
|
|
||||||
if (!watcher) {
|
|
||||||
console.log("Watching...");
|
|
||||||
watcher = chokidar
|
|
||||||
.watch(["content/**/*.*"], {
|
|
||||||
awaitWriteFinish: {
|
|
||||||
stabilityThreshold: 500
|
|
||||||
},
|
|
||||||
ignoreInitial: true
|
|
||||||
})
|
|
||||||
.on("all", function(event, filePath) {
|
|
||||||
console.log(`Rerunning for ${filePath}...`);
|
|
||||||
run();
|
|
||||||
console.log("Done!");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toRoutes(entries) {
|
function toRoutes(entries) {
|
||||||
return entries.map(toRoute);
|
return entries.map(toRoute);
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,5 @@
|
|||||||
const path = require("path");
|
|
||||||
const matter = require("gray-matter");
|
const matter = require("gray-matter");
|
||||||
|
|
||||||
module.exports = function parseFrontmatter(filePath, fileContents) {
|
module.exports = function parseFrontmatter(filePath, fileContents) {
|
||||||
return path.extname(filePath) === ".emu"
|
return matter(fileContents);
|
||||||
? matter(fileContents, markupFrontmatterOptions)
|
|
||||||
: matter(fileContents);
|
|
||||||
};
|
|
||||||
|
|
||||||
const markupFrontmatterOptions = {
|
|
||||||
language: "markup",
|
|
||||||
engines: {
|
|
||||||
markup: {
|
|
||||||
parse: function(string) {
|
|
||||||
return string;
|
|
||||||
},
|
|
||||||
|
|
||||||
stringify: function(string) {
|
|
||||||
return string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -1,35 +1,57 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const runElm = require("./compile-elm.js");
|
const runElm = require("./compile-elm.js");
|
||||||
const copyModifiedElmJson = require("./rewrite-elm-json.js");
|
const copyModifiedElmJson = require("./rewrite-elm-json.js");
|
||||||
const { elmPagesCliFile } = require("./elm-file-constants.js");
|
const { elmPagesCliFile, elmPagesUiFile } = require("./elm-file-constants.js");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { ensureDirSync, deleteIfExists } = require('./file-helpers.js')
|
const { ensureDirSync, deleteIfExists } = require('./file-helpers.js')
|
||||||
|
let wasEqualBefore = false
|
||||||
|
|
||||||
|
|
||||||
module.exports = function run(
|
module.exports = function run(
|
||||||
mode,
|
mode,
|
||||||
staticRoutes,
|
staticRoutes,
|
||||||
markdownContent,
|
markdownContent
|
||||||
markupContent,
|
|
||||||
callback
|
|
||||||
) {
|
) {
|
||||||
ensureDirSync("./elm-stuff");
|
ensureDirSync("./elm-stuff");
|
||||||
|
ensureDirSync("./gen");
|
||||||
ensureDirSync("./elm-stuff/elm-pages");
|
ensureDirSync("./elm-stuff/elm-pages");
|
||||||
|
|
||||||
// prevent compilation errors if migrating from previous elm-pages version
|
// prevent compilation errors if migrating from previous elm-pages version
|
||||||
deleteIfExists("./elm-stuff/elm-pages/Pages/ContentCache.elm");
|
deleteIfExists("./elm-stuff/elm-pages/Pages/ContentCache.elm");
|
||||||
deleteIfExists("./elm-stuff/elm-pages/Pages/Platform.elm");
|
deleteIfExists("./elm-stuff/elm-pages/Pages/Platform.elm");
|
||||||
|
|
||||||
|
|
||||||
|
const uiFileContent = elmPagesUiFile(staticRoutes, markdownContent)
|
||||||
|
|
||||||
|
// TODO should just write it once, but webpack doesn't seem to pick up the changes
|
||||||
|
// so this wasEqualBefore code causes it to get written twice to make sure the changes come through for HMR
|
||||||
|
if (wasEqualBefore) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
"./gen/Pages.elm",
|
||||||
|
uiFileContent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (global.previousUiFileContent === uiFileContent) {
|
||||||
|
wasEqualBefore = false
|
||||||
|
} else {
|
||||||
|
wasEqualBefore = true
|
||||||
|
fs.writeFileSync(
|
||||||
|
"./gen/Pages.elm",
|
||||||
|
uiFileContent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
global.previousUiFileContent = uiFileContent
|
||||||
|
|
||||||
// write `Pages.elm` with cli interface
|
// write `Pages.elm` with cli interface
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
"./elm-stuff/elm-pages/Pages.elm",
|
"./elm-stuff/elm-pages/Pages.elm",
|
||||||
elmPagesCliFile(staticRoutes, markdownContent, markupContent)
|
elmPagesCliFile(staticRoutes, markdownContent)
|
||||||
);
|
);
|
||||||
|
|
||||||
// write modified elm.json to elm-stuff/elm-pages/
|
// write modified elm.json to elm-stuff/elm-pages/
|
||||||
copyModifiedElmJson();
|
copyModifiedElmJson();
|
||||||
|
|
||||||
// run Main.elm from elm-stuff/elm-pages with `runElm`
|
// run Main.elm from elm-stuff/elm-pages with `runElm`
|
||||||
runElm(mode, callback);
|
return runElm(mode);
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
module.exports = function(markdown, markup, includeBody) {
|
module.exports = function (markdown, includeBody) {
|
||||||
return `content : List ( List String, { extension: String, frontMatter : String, body : Maybe String } )
|
return `content : List ( List String, { extension: String, frontMatter : String, body : Maybe String } )
|
||||||
content =
|
content =
|
||||||
[ ${markdown.concat(markup).map(entry => toEntry(entry, includeBody))}
|
[ ${markdown.map(entry => toEntry(entry, includeBody))}
|
||||||
]`;
|
]`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const matter = require("gray-matter");
|
|
||||||
const dir = "content/";
|
const dir = "content/";
|
||||||
const glob = require("glob");
|
const glob = require("glob");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
@ -138,14 +137,14 @@ function allImageAssetNames() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function toPascalCase(str) {
|
function toPascalCase(str) {
|
||||||
var pascal = str.replace(/(\-\w)/g, function(m) {
|
var pascal = str.replace(/(\-\w)/g, function (m) {
|
||||||
return m[1].toUpperCase();
|
return m[1].toUpperCase();
|
||||||
});
|
});
|
||||||
return pascal.charAt(0).toUpperCase() + pascal.slice(1);
|
return pascal.charAt(0).toUpperCase() + pascal.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toCamelCase(str) {
|
function toCamelCase(str) {
|
||||||
var pascal = str.replace(/(\-\w)/g, function(m) {
|
var pascal = str.replace(/(\-\w)/g, function (m) {
|
||||||
return m[1].toUpperCase();
|
return m[1].toUpperCase();
|
||||||
});
|
});
|
||||||
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
||||||
@ -180,14 +179,14 @@ function formatRecord(directoryPath, rec, asType, level) {
|
|||||||
} else {
|
} else {
|
||||||
keyVals.push(
|
keyVals.push(
|
||||||
key +
|
key +
|
||||||
" =\n" +
|
" =\n" +
|
||||||
formatRecord(directoryPath.concat(key), val, asType, level + 1)
|
formatRecord(directoryPath.concat(key), val, asType, level + 1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyVals.push(
|
keyVals.push(
|
||||||
`directory = ${
|
`directory = ${
|
||||||
keys.includes("index") ? "directoryWithIndex" : "directoryWithoutIndex"
|
keys.includes("index") ? "directoryWithIndex" : "directoryWithoutIndex"
|
||||||
} [${directoryPath.map(pathFragment => `"${pathFragment}"`).join(", ")}]`
|
} [${directoryPath.map(pathFragment => `"${pathFragment}"`).join(", ")}]`
|
||||||
);
|
);
|
||||||
const indentationDelimiter = `\n${indentation}, `;
|
const indentationDelimiter = `\n${indentation}, `;
|
||||||
|
62
generator/src/plugin-generate-elm-pages-build.js
Normal file
62
generator/src/plugin-generate-elm-pages-build.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const doCliStuff = require("./generate-elm-stuff.js");
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const parseFrontmatter = require("./frontmatter.js");
|
||||||
|
const generateRecords = require("./generate-records.js");
|
||||||
|
const globby = require("globby");
|
||||||
|
|
||||||
|
module.exports = class PluginGenerateElmPagesBuild {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(/** @type {webpack.Compiler} */ compiler) {
|
||||||
|
compiler.hooks.beforeCompile.tap('PluginGenerateElmPagesBuild', (compilation) => {
|
||||||
|
const staticRoutes = generateRecords();
|
||||||
|
|
||||||
|
const markdownContent = globby
|
||||||
|
.sync(["content/**/*.*"], {})
|
||||||
|
.map(unpackFile)
|
||||||
|
.map(({ path, contents }) => {
|
||||||
|
return parseMarkdown(path, contents);
|
||||||
|
});
|
||||||
|
|
||||||
|
let resolvePageRequests;
|
||||||
|
let rejectPageRequests;
|
||||||
|
global.pagesWithRequests = new Promise(function (resolve, reject) {
|
||||||
|
resolvePageRequests = resolve;
|
||||||
|
rejectPageRequests = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
doCliStuff(
|
||||||
|
global.mode,
|
||||||
|
staticRoutes,
|
||||||
|
markdownContent
|
||||||
|
).then((payload) => {
|
||||||
|
// console.log('PROMISE RESOLVED doCliStuff');
|
||||||
|
|
||||||
|
resolvePageRequests(payload);
|
||||||
|
global.filesToGenerate = payload.filesToGenerate;
|
||||||
|
|
||||||
|
}).catch(function (errorPayload) {
|
||||||
|
resolvePageRequests({ type: 'error', message: errorPayload });
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function unpackFile(path) {
|
||||||
|
return { path, contents: fs.readFileSync(path).toString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMarkdown(path, fileContents) {
|
||||||
|
const { content, data } = parseFrontmatter(path, fileContents);
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
metadata: JSON.stringify(data),
|
||||||
|
body: content,
|
||||||
|
};
|
||||||
|
}
|
@ -1,23 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<link rel="preload" href="content.json" as="fetch" crossorigin />
|
|
||||||
|
|
||||||
<base href="/" />
|
|
||||||
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
|
|
||||||
<script>
|
|
||||||
if ("serviceWorker" in navigator) {
|
|
||||||
window.addEventListener("load", () => {
|
|
||||||
navigator.serviceWorker.register("service-worker.js");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("No service worker registered.");
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body></body>
|
|
||||||
</html>
|
|
153
index.js
153
index.js
@ -11,9 +11,9 @@ module.exports = function pagesInit(
|
|||||||
prefetchedPages = [window.location.pathname];
|
prefetchedPages = [window.location.pathname];
|
||||||
initialLocationHash = document.location.hash.replace(/^#/, "");
|
initialLocationHash = document.location.hash.replace(/^#/, "");
|
||||||
|
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
document.addEventListener("DOMContentLoaded", _ => {
|
document.addEventListener("DOMContentLoaded", _ => {
|
||||||
new MutationObserver(function() {
|
new MutationObserver(function () {
|
||||||
elmViewRendered = true;
|
elmViewRendered = true;
|
||||||
if (headTagsAdded) {
|
if (headTagsAdded) {
|
||||||
document.dispatchEvent(new Event("prerender-trigger"));
|
document.dispatchEvent(new Event("prerender-trigger"));
|
||||||
@ -32,48 +32,98 @@ function loadContentAndInitializeApp(/** @type { init: any } */ mainElmModule)
|
|||||||
const isPrerendering = navigator.userAgent.indexOf("Headless") >= 0
|
const isPrerendering = navigator.userAgent.indexOf("Headless") >= 0
|
||||||
const path = window.location.pathname.replace(/(\w)$/, "$1/")
|
const path = window.location.pathname.replace(/(\w)$/, "$1/")
|
||||||
|
|
||||||
return httpGet(`${window.location.origin}${path}content.json`).then(function(/** @type JSON */ contentJson) {
|
return Promise.all([
|
||||||
|
getConfig(),
|
||||||
|
httpGet(`${window.location.origin}${path}content.json`)]).then(function (/** @type {[DevServerConfig?, JSON]} */[devServerConfig, contentJson]) {
|
||||||
|
console.log('devServerConfig', devServerConfig);
|
||||||
|
|
||||||
const app = mainElmModule.init({
|
const app = mainElmModule.init({
|
||||||
flags: {
|
flags: {
|
||||||
secrets: null,
|
secrets: null,
|
||||||
baseUrl: isPrerendering
|
baseUrl: isPrerendering
|
||||||
? window.location.origin
|
? window.location.origin
|
||||||
: document.baseURI,
|
: document.baseURI,
|
||||||
isPrerendering: isPrerendering,
|
isPrerendering: isPrerendering,
|
||||||
contentJson
|
isDevServer: !!module.hot,
|
||||||
}
|
isElmDebugMode: devServerConfig ? devServerConfig.elmDebugger : false,
|
||||||
});
|
contentJson,
|
||||||
|
}
|
||||||
app.ports.toJsPort.subscribe((
|
|
||||||
/** @type { { head: HeadTag[], allRoutes: string[] } } */ fromElm
|
|
||||||
) => {
|
|
||||||
appendTag({
|
|
||||||
name: "meta",
|
|
||||||
attributes: [
|
|
||||||
["name", "generator"],
|
|
||||||
["content", `elm-pages v${elmPagesVersion}`]
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.allRoutes = fromElm.allRoutes.map(route => new URL(route, document.baseURI).href);
|
app.ports.toJsPort.subscribe((
|
||||||
|
/** @type { { head: SeoTag[], allRoutes: string[] } } */ fromElm
|
||||||
if (navigator.userAgent.indexOf("Headless") >= 0) {
|
) => {
|
||||||
fromElm.head.forEach(headTag => {
|
appendTag({
|
||||||
appendTag(headTag);
|
type: 'head',
|
||||||
|
name: "meta",
|
||||||
|
attributes: [
|
||||||
|
["name", "generator"],
|
||||||
|
["content", `elm-pages v${elmPagesVersion}`]
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.allRoutes = fromElm.allRoutes.map(route => new URL(route, document.baseURI).href);
|
||||||
|
|
||||||
|
if (navigator.userAgent.indexOf("Headless") >= 0) {
|
||||||
|
fromElm.head.forEach(headTag => {
|
||||||
|
if (headTag.type === 'head') {
|
||||||
|
appendTag(headTag);
|
||||||
|
} else if (headTag.type === 'json-ld') {
|
||||||
|
appendJsonLdTag(headTag);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown tag type #{headTag}`)
|
||||||
|
}
|
||||||
|
});
|
||||||
headTagsAdded = true;
|
headTagsAdded = true;
|
||||||
if (elmViewRendered) {
|
if (elmViewRendered) {
|
||||||
document.dispatchEvent(new Event("prerender-trigger"));
|
document.dispatchEvent(new Event("prerender-trigger"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setupLinkPrefetching();
|
setupLinkPrefetching();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
|
||||||
|
// found this trick in the next.js source code
|
||||||
|
// https://github.com/zeit/next.js/blob/886037b1bac4bdbfeb689b032c1612750fb593f7/packages/next/client/dev/error-overlay/eventsource.js
|
||||||
|
// https://github.com/zeit/next.js/blob/886037b1bac4bdbfeb689b032c1612750fb593f7/packages/next/client/dev/dev-build-watcher.js
|
||||||
|
// more details about this API at https://www.html5rocks.com/en/tutorials/eventsource/basics/
|
||||||
|
let source = new window.EventSource('/__webpack_hmr')
|
||||||
|
// source.addEventListener('open', () => { console.log('open!!!!!') })
|
||||||
|
source.addEventListener('message', (e) => {
|
||||||
|
// console.log('message!!!!!', e)
|
||||||
|
// console.log(e.data.action)
|
||||||
|
// console.log('ACTION', e.data.action);
|
||||||
|
// if (e.data && e.data.action)
|
||||||
|
|
||||||
|
if (event.data === '\uD83D\uDC93') {
|
||||||
|
// heartbeat
|
||||||
|
} else {
|
||||||
|
const obj = JSON.parse(event.data)
|
||||||
|
// console.log('obj.action', obj.action);
|
||||||
|
|
||||||
|
if (obj.action === 'building') {
|
||||||
|
app.ports.fromJsPort.send({ thingy: 'hmr-check' });
|
||||||
|
} else if (obj.action === 'built') {
|
||||||
|
// console.log('httpGet start');
|
||||||
|
|
||||||
|
let currentPath = window.location.pathname.replace(/(\w)$/, "$1/")
|
||||||
|
httpGet(`${window.location.origin}${currentPath}content.json`).then(function (/** @type JSON */ contentJson) {
|
||||||
|
// console.log('httpGet received');
|
||||||
|
|
||||||
|
app.ports.fromJsPort.send({ contentJson: contentJson });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return app
|
||||||
});
|
});
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupLinkPrefetching() {
|
function setupLinkPrefetching() {
|
||||||
@ -132,7 +182,7 @@ function setupLinkPrefetchingHelp(
|
|||||||
const links = document.querySelectorAll("a");
|
const links = document.querySelectorAll("a");
|
||||||
links.forEach(link => {
|
links.forEach(link => {
|
||||||
// console.log(link.pathname);
|
// console.log(link.pathname);
|
||||||
link.addEventListener("mouseenter", function(event) {
|
link.addEventListener("mouseenter", function (event) {
|
||||||
if (
|
if (
|
||||||
event &&
|
event &&
|
||||||
event.target &&
|
event.target &&
|
||||||
@ -166,7 +216,9 @@ function prefetchIfNeeded(/** @type {HTMLAnchorElement} */ target) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @typedef {{ name: string; attributes: string[][]; }} HeadTag */
|
/** @typedef {HeadTag | JsonLdTag} SeoTag */
|
||||||
|
|
||||||
|
/** @typedef {{ name: string; attributes: string[][]; type: 'head' }} HeadTag */
|
||||||
function appendTag(/** @type {HeadTag} */ tagDetails) {
|
function appendTag(/** @type {HeadTag} */ tagDetails) {
|
||||||
const meta = document.createElement(tagDetails.name);
|
const meta = document.createElement(tagDetails.name);
|
||||||
tagDetails.attributes.forEach(([name, value]) => {
|
tagDetails.attributes.forEach(([name, value]) => {
|
||||||
@ -175,15 +227,36 @@ function appendTag(/** @type {HeadTag} */ tagDetails) {
|
|||||||
document.getElementsByTagName("head")[0].appendChild(meta);
|
document.getElementsByTagName("head")[0].appendChild(meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @typedef {{ contents: Object; type: 'json-ld' }} JsonLdTag */
|
||||||
|
function appendJsonLdTag(/** @type {JsonLdTag} */ tagDetails) {
|
||||||
|
let jsonLdScript = document.createElement('script');
|
||||||
|
jsonLdScript.type = "application/ld+json";
|
||||||
|
jsonLdScript.innerHTML = JSON.stringify(tagDetails.contents);
|
||||||
|
document.getElementsByTagName("head")[0].appendChild(jsonLdScript);
|
||||||
|
}
|
||||||
|
|
||||||
function httpGet(/** @type string */ theUrl) {
|
function httpGet(/** @type string */ theUrl) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
const xmlHttp = new XMLHttpRequest();
|
const xmlHttp = new XMLHttpRequest();
|
||||||
xmlHttp.onreadystatechange = function() {
|
xmlHttp.onreadystatechange = function () {
|
||||||
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
|
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
|
||||||
resolve(JSON.parse(xmlHttp.responseText));
|
resolve(JSON.parse(xmlHttp.responseText));
|
||||||
}
|
}
|
||||||
xmlHttp.onerror = reject;
|
xmlHttp.onerror = reject;
|
||||||
xmlHttp.open("GET", theUrl, true); // true for asynchronous
|
xmlHttp.open("GET", theUrl, true); // true for asynchronous
|
||||||
xmlHttp.send(null);
|
xmlHttp.send(null);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns { Promise<DevServerConfig?>}
|
||||||
|
*/
|
||||||
|
function getConfig() {
|
||||||
|
if (module.hot) {
|
||||||
|
return httpGet(`/elm-pages-dev-server-options`)
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @typedef { { elmDebugger : boolean } } DevServerConfig */
|
||||||
|
5026
package-lock.json
generated
5026
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -22,9 +22,9 @@
|
|||||||
"@babel/core": "^7.5.5",
|
"@babel/core": "^7.5.5",
|
||||||
"@babel/preset-env": "^7.5.5",
|
"@babel/preset-env": "^7.5.5",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"chokidar": "^2.1.5",
|
|
||||||
"closure-webpack-plugin": "^2.0.1",
|
"closure-webpack-plugin": "^2.0.1",
|
||||||
"copy-webpack-plugin": "^5.0.4",
|
"copy-webpack-plugin": "^5.0.4",
|
||||||
|
"cross-spawn": "6.0.5",
|
||||||
"css-loader": "^3.2.0",
|
"css-loader": "^3.2.0",
|
||||||
"elm": "^0.19.1-3",
|
"elm": "^0.19.1-3",
|
||||||
"elm-hot-webpack-loader": "^1.1.2",
|
"elm-hot-webpack-loader": "^1.1.2",
|
||||||
@ -32,30 +32,29 @@
|
|||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"favicons-webpack-plugin": "^3.0.0",
|
"favicons-webpack-plugin": "^3.0.0",
|
||||||
"file-loader": "^4.2.0",
|
"file-loader": "^4.2.0",
|
||||||
|
"find-elm-dependencies": "2.0.2",
|
||||||
"globby": "^10.0.1",
|
"globby": "^10.0.1",
|
||||||
"google-closure-compiler": "^20190909.0.0",
|
"google-closure-compiler": "^20190909.0.0",
|
||||||
"gray-matter": "^4.0.2",
|
"gray-matter": "^4.0.2",
|
||||||
"html-webpack-plugin": "^4.0.0-beta.11",
|
"html-webpack-plugin": "^4.2.0",
|
||||||
"imagemin-mozjpeg": "^8.0.0",
|
"imagemin-mozjpeg": "^8.0.0",
|
||||||
"imagemin-webpack-plugin": "^2.4.2",
|
"imagemin-webpack-plugin": "^2.4.2",
|
||||||
|
"lodash": "4.17.15",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.12.0",
|
||||||
"prerender-spa-plugin": "^3.4.0",
|
"prerender-spa-plugin": "^3.4.0",
|
||||||
|
"raw-loader": "^4.0.0",
|
||||||
"sass-loader": "^8.0.0",
|
"sass-loader": "^8.0.0",
|
||||||
"script-ext-html-webpack-plugin": "^2.1.4",
|
"script-ext-html-webpack-plugin": "^2.1.4",
|
||||||
"style-loader": "^1.0.0",
|
"style-loader": "^1.0.0",
|
||||||
"webpack": "^4.41.5",
|
"temp": "^0.9.0",
|
||||||
|
"webpack": "4.42.1",
|
||||||
"webpack-dev-middleware": "^3.7.0",
|
"webpack-dev-middleware": "^3.7.0",
|
||||||
"webpack-hot-middleware": "^2.25.0",
|
"webpack-hot-middleware": "^2.25.0",
|
||||||
"webpack-merge": "^4.2.1",
|
"webpack-merge": "^4.2.1",
|
||||||
"workbox-webpack-plugin": "^4.3.1",
|
"workbox-webpack-plugin": "^4.3.1",
|
||||||
"xhr2": "^0.2.0",
|
"xhr2": "^0.2.0"
|
||||||
"cross-spawn": "6.0.5",
|
|
||||||
"find-elm-dependencies": "2.0.2",
|
|
||||||
"lodash": "4.17.15",
|
|
||||||
"temp": "^0.9.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chokidar": "^2.1.3",
|
|
||||||
"@types/express": "^4.17.0",
|
"@types/express": "^4.17.0",
|
||||||
"@types/node": "^12.7.7",
|
"@types/node": "^12.7.7",
|
||||||
"@types/webpack": "^4.32.1",
|
"@types/webpack": "^4.32.1",
|
||||||
|
120
src/Head.elm
120
src/Head.elm
@ -1,6 +1,7 @@
|
|||||||
module Head exposing
|
module Head exposing
|
||||||
( Tag, metaName, metaProperty
|
( Tag, metaName, metaProperty
|
||||||
, rssLink, sitemapLink
|
, rssLink, sitemapLink
|
||||||
|
, structuredData
|
||||||
, AttributeValue
|
, AttributeValue
|
||||||
, currentPageFullUrl, fullImageUrl, fullPageUrl, raw
|
, currentPageFullUrl, fullImageUrl, fullPageUrl, raw
|
||||||
, toJson, canonicalLink
|
, toJson, canonicalLink
|
||||||
@ -19,6 +20,11 @@ writing a plugin package to extend `elm-pages`.
|
|||||||
@docs rssLink, sitemapLink
|
@docs rssLink, sitemapLink
|
||||||
|
|
||||||
|
|
||||||
|
## Structured Data
|
||||||
|
|
||||||
|
@docs structuredData
|
||||||
|
|
||||||
|
|
||||||
## `AttributeValue`s
|
## `AttributeValue`s
|
||||||
|
|
||||||
@docs AttributeValue
|
@docs AttributeValue
|
||||||
@ -42,6 +48,7 @@ through the `head` function.
|
|||||||
-}
|
-}
|
||||||
type Tag pathKey
|
type Tag pathKey
|
||||||
= Tag (Details pathKey)
|
= Tag (Details pathKey)
|
||||||
|
| StructuredData Json.Encode.Value
|
||||||
|
|
||||||
|
|
||||||
type alias Details pathKey =
|
type alias Details pathKey =
|
||||||
@ -50,6 +57,100 @@ type alias Details pathKey =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| You can learn more about structured data in [Google's intro to structured data](https://developers.google.com/search/docs/guides/intro-structured-data).
|
||||||
|
|
||||||
|
When you add a `structuredData` item to one of your pages in `elm-pages`, it will add `json-ld` data to your document that looks like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context":"http://schema.org/",
|
||||||
|
"@type":"Article",
|
||||||
|
"headline":"Extensible Markdown Parsing in Pure Elm",
|
||||||
|
"description":"Introducing a new parser that extends your palette with no additional syntax",
|
||||||
|
"image":"https://elm-pages.com/images/article-covers/extensible-markdown-parsing.jpg",
|
||||||
|
"author":{
|
||||||
|
"@type":"Person",
|
||||||
|
"name":"Dillon Kearns"
|
||||||
|
},
|
||||||
|
"publisher":{
|
||||||
|
"@type":"Person",
|
||||||
|
"name":"Dillon Kearns"
|
||||||
|
},
|
||||||
|
"url":"https://elm-pages.com/blog/extensible-markdown-parsing-in-elm",
|
||||||
|
"datePublished":"2019-10-08",
|
||||||
|
"mainEntityOfPage":{
|
||||||
|
"@type":"SoftwareSourceCode",
|
||||||
|
"codeRepository":"https://github.com/dillonkearns/elm-pages",
|
||||||
|
"description":"A statically typed site generator for Elm.",
|
||||||
|
"author":"Dillon Kearns",
|
||||||
|
"programmingLanguage":{
|
||||||
|
"@type":"ComputerLanguage",
|
||||||
|
"url":"http://elm-lang.org/",
|
||||||
|
"name":"Elm",
|
||||||
|
"image":"http://elm-lang.org/",
|
||||||
|
"identifier":"http://elm-lang.org/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
To get that data, you would write this in your `elm-pages` head tags:
|
||||||
|
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
{-| <https://schema.org/Article>
|
||||||
|
-}
|
||||||
|
encodeArticle :
|
||||||
|
{ title : String
|
||||||
|
, description : String
|
||||||
|
, author : StructuredData { authorMemberOf | personOrOrganization : () } authorPossibleFields
|
||||||
|
, publisher : StructuredData { publisherMemberOf | personOrOrganization : () } publisherPossibleFields
|
||||||
|
, url : String
|
||||||
|
, imageUrl : String
|
||||||
|
, datePublished : String
|
||||||
|
, mainEntityOfPage : Encode.Value
|
||||||
|
}
|
||||||
|
-> Head.Tag pathKey
|
||||||
|
encodeArticle info =
|
||||||
|
Encode.object
|
||||||
|
[ ( "@context", Encode.string "http://schema.org/" )
|
||||||
|
, ( "@type", Encode.string "Article" )
|
||||||
|
, ( "headline", Encode.string info.title )
|
||||||
|
, ( "description", Encode.string info.description )
|
||||||
|
, ( "image", Encode.string info.imageUrl )
|
||||||
|
, ( "author", encode info.author )
|
||||||
|
, ( "publisher", encode info.publisher )
|
||||||
|
, ( "url", Encode.string info.url )
|
||||||
|
, ( "datePublished", Encode.string info.datePublished )
|
||||||
|
, ( "mainEntityOfPage", info.mainEntityOfPage )
|
||||||
|
]
|
||||||
|
|> Head.structuredData
|
||||||
|
|
||||||
|
Take a look at this [Google Search Gallery](https://developers.google.com/search/docs/guides/search-gallery)
|
||||||
|
to see some examples of how structured data can be used by search engines to give rich search results. It can help boost
|
||||||
|
your rankings, get better engagement for your content, and also make your content more accessible. For example,
|
||||||
|
voice assistant devices can make use of structured data. If you're hosting a conference and want to make the event
|
||||||
|
date and location easy for attendees to find, this can make that information more accessible.
|
||||||
|
|
||||||
|
For the current version of API, you'll need to make sure that the format is correct and contains the required and recommended
|
||||||
|
structure.
|
||||||
|
|
||||||
|
Check out <https://schema.org> for a comprehensive listing of possible data types and fields. And take a look at
|
||||||
|
Google's [Structured Data Testing Tool](https://search.google.com/structured-data/testing-tool)
|
||||||
|
too make sure that your structured data is valid and includes the recommended values.
|
||||||
|
|
||||||
|
In the future, `elm-pages` will likely support a typed API, but schema.org is a massive spec, and changes frequently.
|
||||||
|
And there are multiple sources of information on the possible and recommended structure. So it will take some time
|
||||||
|
for the right API design to evolve. In the meantime, this allows you to make use of this for SEO purposes.
|
||||||
|
|
||||||
|
-}
|
||||||
|
structuredData : Json.Encode.Value -> Tag pathKey
|
||||||
|
structuredData value =
|
||||||
|
StructuredData value
|
||||||
|
|
||||||
|
|
||||||
{-| Create a raw `AttributeValue` (as opposed to some kind of absolute URL).
|
{-| Create a raw `AttributeValue` (as opposed to some kind of absolute URL).
|
||||||
-}
|
-}
|
||||||
raw : String -> AttributeValue pathKey
|
raw : String -> AttributeValue pathKey
|
||||||
@ -196,11 +297,20 @@ node name attributes =
|
|||||||
code will run this for you to generate your `manifest.json` file automatically!
|
code will run this for you to generate your `manifest.json` file automatically!
|
||||||
-}
|
-}
|
||||||
toJson : String -> String -> Tag pathKey -> Json.Encode.Value
|
toJson : String -> String -> Tag pathKey -> Json.Encode.Value
|
||||||
toJson canonicalSiteUrl currentPagePath (Tag tag) =
|
toJson canonicalSiteUrl currentPagePath tag =
|
||||||
Json.Encode.object
|
case tag of
|
||||||
[ ( "name", Json.Encode.string tag.name )
|
Tag headTag ->
|
||||||
, ( "attributes", Json.Encode.list (encodeProperty canonicalSiteUrl currentPagePath) tag.attributes )
|
Json.Encode.object
|
||||||
]
|
[ ( "name", Json.Encode.string headTag.name )
|
||||||
|
, ( "attributes", Json.Encode.list (encodeProperty canonicalSiteUrl currentPagePath) headTag.attributes )
|
||||||
|
, ( "type", Json.Encode.string "head" )
|
||||||
|
]
|
||||||
|
|
||||||
|
StructuredData value ->
|
||||||
|
Json.Encode.object
|
||||||
|
[ ( "contents", value )
|
||||||
|
, ( "type", Json.Encode.string "json-ld" )
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
encodeProperty : String -> String -> ( String, AttributeValue pathKey ) -> Json.Encode.Value
|
encodeProperty : String -> String -> ( String, AttributeValue pathKey ) -> Json.Encode.Value
|
||||||
|
18
src/Internal/OptimizedDecoder.elm
Normal file
18
src/Internal/OptimizedDecoder.elm
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module Internal.OptimizedDecoder exposing (OptimizedDecoder(..), jd, jde)
|
||||||
|
|
||||||
|
import Json.Decode
|
||||||
|
import Json.Decode.Exploration
|
||||||
|
|
||||||
|
|
||||||
|
type OptimizedDecoder a
|
||||||
|
= OptimizedDecoder (Json.Decode.Decoder a) (Json.Decode.Exploration.Decoder a)
|
||||||
|
|
||||||
|
|
||||||
|
jd : OptimizedDecoder a -> Json.Decode.Decoder a
|
||||||
|
jd (OptimizedDecoder jd_ jde_) =
|
||||||
|
jd_
|
||||||
|
|
||||||
|
|
||||||
|
jde : OptimizedDecoder a -> Json.Decode.Exploration.Decoder a
|
||||||
|
jde (OptimizedDecoder jd_ jde_) =
|
||||||
|
jde_
|
769
src/OptimizedDecoder.elm
Normal file
769
src/OptimizedDecoder.elm
Normal file
@ -0,0 +1,769 @@
|
|||||||
|
module OptimizedDecoder exposing
|
||||||
|
( Error, errorToString
|
||||||
|
, Decoder, string, bool, int, float, Value
|
||||||
|
, nullable, list, array, dict, keyValuePairs
|
||||||
|
, field, at, index
|
||||||
|
, maybe, oneOf
|
||||||
|
, lazy, value, null, succeed, fail, andThen
|
||||||
|
, map, map2, map3, map4, map5, map6, map7, map8, andMap
|
||||||
|
, decodeString, decodeValue, decoder
|
||||||
|
)
|
||||||
|
|
||||||
|
{-| This module allows you to build decoders that `elm-pages` can optimize for you in your `StaticHttp` requests.
|
||||||
|
It does this by stripping of unused fields during the CLI build step. When it runs in production, it will
|
||||||
|
just run a plain `elm/json` decoder, so you're fetching and decoding the stripped-down data, but without any
|
||||||
|
performance penalty.
|
||||||
|
|
||||||
|
For convenience, this library also includes a `Json.Decode.Exploration.Pipeline`
|
||||||
|
module which is largely a copy of [`NoRedInk/elm-decode-pipeline`][edp].
|
||||||
|
|
||||||
|
[edp]: http://package.elm-lang.org/packages/NoRedInk/elm-decode-pipeline/latest
|
||||||
|
|
||||||
|
|
||||||
|
## Dealing with warnings and errors
|
||||||
|
|
||||||
|
@docs Error, errorToString
|
||||||
|
|
||||||
|
|
||||||
|
# Primitives
|
||||||
|
|
||||||
|
@docs Decoder, string, bool, int, float, Value
|
||||||
|
|
||||||
|
|
||||||
|
# Data Structures
|
||||||
|
|
||||||
|
@docs nullable, list, array, dict, keyValuePairs
|
||||||
|
|
||||||
|
|
||||||
|
# Object Primitives
|
||||||
|
|
||||||
|
@docs field, at, index
|
||||||
|
|
||||||
|
|
||||||
|
# Inconsistent Structure
|
||||||
|
|
||||||
|
@docs maybe, oneOf
|
||||||
|
|
||||||
|
|
||||||
|
# Fancy Decoding
|
||||||
|
|
||||||
|
@docs lazy, value, null, succeed, fail, andThen
|
||||||
|
|
||||||
|
|
||||||
|
# Mapping
|
||||||
|
|
||||||
|
**Note:** If you run out of map functions, take a look at [the pipeline module][pipe]
|
||||||
|
which makes it easier to handle large objects.
|
||||||
|
|
||||||
|
[pipe]: http://package.elm-lang.org/packages/zwilias/json-decode-exploration/latest/Json-Decode-Exploration-Pipeline
|
||||||
|
|
||||||
|
@docs map, map2, map3, map4, map5, map6, map7, map8, andMap
|
||||||
|
|
||||||
|
|
||||||
|
# Directly Running Decoders
|
||||||
|
|
||||||
|
Usually you'll be passing your decoders to
|
||||||
|
|
||||||
|
@docs decodeString, decodeValue, decoder
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Array exposing (Array)
|
||||||
|
import Dict exposing (Dict)
|
||||||
|
import Internal.OptimizedDecoder exposing (OptimizedDecoder(..))
|
||||||
|
import Json.Decode as JD
|
||||||
|
import Json.Decode.Exploration as JDE
|
||||||
|
|
||||||
|
|
||||||
|
{-| A decoder that will be optimized in your production bundle.
|
||||||
|
-}
|
||||||
|
type alias Decoder a =
|
||||||
|
OptimizedDecoder a
|
||||||
|
|
||||||
|
|
||||||
|
{-| A simple type alias for `Json.Decode.Value`.
|
||||||
|
-}
|
||||||
|
type alias Value =
|
||||||
|
JD.Value
|
||||||
|
|
||||||
|
|
||||||
|
{-| A simple type alias for `Json.Decode.Error`.
|
||||||
|
-}
|
||||||
|
type alias Error =
|
||||||
|
JD.Error
|
||||||
|
|
||||||
|
|
||||||
|
{-| A simple wrapper for `Json.Decode.errorToString`.
|
||||||
|
-}
|
||||||
|
errorToString : JD.Error -> String
|
||||||
|
errorToString =
|
||||||
|
JD.errorToString
|
||||||
|
|
||||||
|
|
||||||
|
{-| Usually you'll want to directly pass your `OptimizedDecoder` to `StaticHttp` or other `elm-pages` APIs.
|
||||||
|
But if you want to re-use your decoder somewhere else, it may be useful to turn it into a plain `elm/json` decoder.
|
||||||
|
-}
|
||||||
|
decoder : Decoder a -> JD.Decoder a
|
||||||
|
decoder (OptimizedDecoder jd jde) =
|
||||||
|
jd
|
||||||
|
|
||||||
|
|
||||||
|
{-| A simple wrapper for `Json.Decode.errorToString`.
|
||||||
|
|
||||||
|
This will directly call the raw `elm/json` decoder that is stored under the hood.
|
||||||
|
|
||||||
|
-}
|
||||||
|
decodeString : Decoder a -> String -> Result Error a
|
||||||
|
decodeString (OptimizedDecoder jd jde) =
|
||||||
|
JD.decodeString jd
|
||||||
|
|
||||||
|
|
||||||
|
{-| A simple wrapper for `Json.Decode.errorToString`.
|
||||||
|
|
||||||
|
This will directly call the raw `elm/json` decoder that is stored under the hood.
|
||||||
|
|
||||||
|
-}
|
||||||
|
decodeValue : Decoder a -> Value -> Result Error a
|
||||||
|
decodeValue (OptimizedDecoder jd jde) =
|
||||||
|
JD.decodeValue jd
|
||||||
|
|
||||||
|
|
||||||
|
{-| A decoder that will ignore the actual JSON and succeed with the provided
|
||||||
|
value. Note that this may still fail when dealing with an invalid JSON string.
|
||||||
|
|
||||||
|
If a value in the JSON ends up being ignored because of this, this will cause a
|
||||||
|
warning.
|
||||||
|
|
||||||
|
import List.Nonempty exposing (Nonempty(..))
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
|
||||||
|
""" null """
|
||||||
|
|> decodeString (value |> andThen (\_ -> succeed "hello world"))
|
||||||
|
--> Success "hello world"
|
||||||
|
|
||||||
|
|
||||||
|
""" null """
|
||||||
|
|> decodeString (succeed "hello world")
|
||||||
|
--> WithWarnings
|
||||||
|
--> (Nonempty (Here <| UnusedValue Encode.null) [])
|
||||||
|
--> "hello world"
|
||||||
|
|
||||||
|
|
||||||
|
""" foo """
|
||||||
|
|> decodeString (succeed "hello world")
|
||||||
|
--> BadJson
|
||||||
|
|
||||||
|
-}
|
||||||
|
succeed : a -> Decoder a
|
||||||
|
succeed a =
|
||||||
|
OptimizedDecoder (JD.succeed a) (JDE.succeed a)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Ignore the json and fail with a provided message.
|
||||||
|
|
||||||
|
import List.Nonempty exposing (Nonempty(..))
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
""" "hello" """
|
||||||
|
|> decodeString (fail "failure")
|
||||||
|
--> Errors (Nonempty (Here <| Failure "failure" (Just <| Encode.string "hello")) [])
|
||||||
|
|
||||||
|
-}
|
||||||
|
fail : String -> Decoder a
|
||||||
|
fail message =
|
||||||
|
OptimizedDecoder (JD.fail message) (JDE.fail message)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a string.
|
||||||
|
|
||||||
|
import List.Nonempty exposing (Nonempty(..))
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
|
||||||
|
""" "hello world" """
|
||||||
|
|> decodeString string
|
||||||
|
--> Success "hello world"
|
||||||
|
|
||||||
|
|
||||||
|
""" 123 """
|
||||||
|
|> decodeString string
|
||||||
|
--> Errors (Nonempty (Here <| Expected TString (Encode.int 123)) [])
|
||||||
|
|
||||||
|
-}
|
||||||
|
string : Decoder String
|
||||||
|
string =
|
||||||
|
OptimizedDecoder JD.string JDE.string
|
||||||
|
|
||||||
|
|
||||||
|
{-| Extract a piece without actually decoding it.
|
||||||
|
|
||||||
|
If a structure is decoded as a `value`, everything _in_ the structure will be
|
||||||
|
considered as having been used and will not appear in `UnusedValue` warnings.
|
||||||
|
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
|
||||||
|
""" [ 123, "world" ] """
|
||||||
|
|> decodeString value
|
||||||
|
--> Success (Encode.list identity [ Encode.int 123, Encode.string "world" ])
|
||||||
|
|
||||||
|
-}
|
||||||
|
value : Decoder Value
|
||||||
|
value =
|
||||||
|
OptimizedDecoder JD.value JDE.value
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a number into a `Float`.
|
||||||
|
|
||||||
|
import List.Nonempty exposing (Nonempty(..))
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
|
||||||
|
""" 12.34 """
|
||||||
|
|> decodeString float
|
||||||
|
--> Success 12.34
|
||||||
|
|
||||||
|
|
||||||
|
""" 12 """
|
||||||
|
|> decodeString float
|
||||||
|
--> Success 12
|
||||||
|
|
||||||
|
|
||||||
|
""" null """
|
||||||
|
|> decodeString float
|
||||||
|
--> Errors (Nonempty (Here <| Expected TNumber Encode.null) [])
|
||||||
|
|
||||||
|
-}
|
||||||
|
float : Decoder Float
|
||||||
|
float =
|
||||||
|
OptimizedDecoder JD.float JDE.float
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a number into an `Int`.
|
||||||
|
|
||||||
|
import List.Nonempty exposing (Nonempty(..))
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
|
||||||
|
""" 123 """
|
||||||
|
|> decodeString int
|
||||||
|
--> Success 123
|
||||||
|
|
||||||
|
|
||||||
|
""" 0.1 """
|
||||||
|
|> decodeString int
|
||||||
|
--> Errors <|
|
||||||
|
--> Nonempty
|
||||||
|
--> (Here <| Expected TInt (Encode.float 0.1))
|
||||||
|
--> []
|
||||||
|
|
||||||
|
-}
|
||||||
|
int : Decoder Int
|
||||||
|
int =
|
||||||
|
OptimizedDecoder JD.int JDE.int
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a boolean value.
|
||||||
|
|
||||||
|
""" [ true, false ] """
|
||||||
|
|> decodeString (list bool)
|
||||||
|
--> Success [ True, False ]
|
||||||
|
|
||||||
|
-}
|
||||||
|
bool : Decoder Bool
|
||||||
|
bool =
|
||||||
|
OptimizedDecoder JD.bool JDE.bool
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a `null` and succeed with some value.
|
||||||
|
|
||||||
|
""" null """
|
||||||
|
|> decodeString (null "it was null")
|
||||||
|
--> Success "it was null"
|
||||||
|
|
||||||
|
Note that `undefined` and `null` are not the same thing. This cannot be used to
|
||||||
|
verify that a field is _missing_, only that it is explicitly set to `null`.
|
||||||
|
|
||||||
|
""" { "foo": null } """
|
||||||
|
|> decodeString (field "foo" (null ()))
|
||||||
|
--> Success ()
|
||||||
|
|
||||||
|
|
||||||
|
import List.Nonempty exposing (Nonempty(..))
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
|
||||||
|
""" { } """
|
||||||
|
|> decodeString (field "foo" (null ()))
|
||||||
|
--> Errors <|
|
||||||
|
--> Nonempty
|
||||||
|
--> (Here <| Expected (TObjectField "foo") (Encode.object []))
|
||||||
|
--> []
|
||||||
|
|
||||||
|
-}
|
||||||
|
null : a -> Decoder a
|
||||||
|
null val =
|
||||||
|
OptimizedDecoder (JD.null val) (JDE.null val)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a list of values, decoding each entry with the provided decoder.
|
||||||
|
|
||||||
|
import List.Nonempty exposing (Nonempty(..))
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
|
||||||
|
""" [ "foo", "bar" ] """
|
||||||
|
|> decodeString (list string)
|
||||||
|
--> Success [ "foo", "bar" ]
|
||||||
|
|
||||||
|
|
||||||
|
""" [ "foo", null ] """
|
||||||
|
|> decodeString (list string)
|
||||||
|
--> Errors <|
|
||||||
|
--> Nonempty
|
||||||
|
--> (AtIndex 1 <|
|
||||||
|
--> Nonempty (Here <| Expected TString Encode.null) []
|
||||||
|
--> )
|
||||||
|
--> []
|
||||||
|
|
||||||
|
-}
|
||||||
|
list : Decoder a -> Decoder (List a)
|
||||||
|
list (OptimizedDecoder jd jde) =
|
||||||
|
OptimizedDecoder (JD.list jd) (JDE.list jde)
|
||||||
|
|
||||||
|
|
||||||
|
{-| _Convenience function._ Decode a JSON array into an Elm `Array`.
|
||||||
|
|
||||||
|
import Array
|
||||||
|
|
||||||
|
""" [ 1, 2, 3 ] """
|
||||||
|
|> decodeString (array int)
|
||||||
|
--> Success <| Array.fromList [ 1, 2, 3 ]
|
||||||
|
|
||||||
|
-}
|
||||||
|
array : Decoder a -> Decoder (Array a)
|
||||||
|
array (OptimizedDecoder jd jde) =
|
||||||
|
OptimizedDecoder (JD.array jd) (JDE.array jde)
|
||||||
|
|
||||||
|
|
||||||
|
{-| _Convenience function._ Decode a JSON object into an Elm `Dict String`.
|
||||||
|
|
||||||
|
import Dict
|
||||||
|
|
||||||
|
|
||||||
|
""" { "foo": "bar", "bar": "hi there" } """
|
||||||
|
|> decodeString (dict string)
|
||||||
|
--> Success <| Dict.fromList
|
||||||
|
--> [ ( "bar", "hi there" )
|
||||||
|
--> , ( "foo", "bar" )
|
||||||
|
--> ]
|
||||||
|
|
||||||
|
-}
|
||||||
|
dict : Decoder v -> Decoder (Dict String v)
|
||||||
|
dict (OptimizedDecoder jd jde) =
|
||||||
|
OptimizedDecoder (JD.dict jd) (JDE.dict jde)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a specific index using a specified `Decoder`.
|
||||||
|
|
||||||
|
import List.Nonempty exposing (Nonempty(..))
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
|
||||||
|
""" [ "hello", 123 ] """
|
||||||
|
|> decodeString (map2 Tuple.pair (index 0 string) (index 1 int))
|
||||||
|
--> Success ( "hello", 123 )
|
||||||
|
|
||||||
|
|
||||||
|
""" [ "hello", "there" ] """
|
||||||
|
|> decodeString (index 1 string)
|
||||||
|
--> WithWarnings (Nonempty (AtIndex 0 (Nonempty (Here (UnusedValue (Encode.string "hello"))) [])) [])
|
||||||
|
--> "there"
|
||||||
|
|
||||||
|
-}
|
||||||
|
index : Int -> Decoder a -> Decoder a
|
||||||
|
index idx (OptimizedDecoder jd jde) =
|
||||||
|
OptimizedDecoder (JD.index idx jd) (JDE.index idx jde)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a JSON object into a list of key-value pairs. The decoder you provide
|
||||||
|
will be used to decode the values.
|
||||||
|
|
||||||
|
""" { "foo": "bar", "hello": "world" } """
|
||||||
|
|> decodeString (keyValuePairs string)
|
||||||
|
--> Success [ ( "foo", "bar" ), ( "hello", "world" ) ]
|
||||||
|
|
||||||
|
-}
|
||||||
|
keyValuePairs : Decoder a -> Decoder (List ( String, a ))
|
||||||
|
keyValuePairs (OptimizedDecoder jd jde) =
|
||||||
|
OptimizedDecoder (JD.keyValuePairs jd) (JDE.keyValuePairs jde)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode the content of a field using a provided decoder.
|
||||||
|
|
||||||
|
import List.Nonempty as Nonempty
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
""" { "foo": "bar" } """
|
||||||
|
|> decodeString (field "foo" string)
|
||||||
|
--> Success "bar"
|
||||||
|
|
||||||
|
|
||||||
|
""" [ { "foo": "bar" }, { "foo": "baz", "hello": "world" } ] """
|
||||||
|
|> decodeString (list (field "foo" string))
|
||||||
|
--> WithWarnings expectedWarnings [ "bar", "baz" ]
|
||||||
|
|
||||||
|
|
||||||
|
expectedWarnings : Warnings
|
||||||
|
expectedWarnings =
|
||||||
|
UnusedField "hello"
|
||||||
|
|> Here
|
||||||
|
|> Nonempty.fromElement
|
||||||
|
|> AtIndex 1
|
||||||
|
|> Nonempty.fromElement
|
||||||
|
|
||||||
|
-}
|
||||||
|
field : String -> Decoder a -> Decoder a
|
||||||
|
field fieldName (OptimizedDecoder jd jde) =
|
||||||
|
OptimizedDecoder (JD.field fieldName jd) (JDE.field fieldName jde)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decodes a value at a certain path, using a provided decoder. Essentially,
|
||||||
|
writing `at [ "a", "b", "c" ] string` is sugar over writing
|
||||||
|
`field "a" (field "b" (field "c" string))`}.
|
||||||
|
|
||||||
|
""" { "a": { "b": { "c": "hi there" } } } """
|
||||||
|
|> decodeString (at [ "a", "b", "c" ] string)
|
||||||
|
--> Success "hi there"
|
||||||
|
|
||||||
|
-}
|
||||||
|
at : List String -> Decoder a -> Decoder a
|
||||||
|
at fields (OptimizedDecoder jd jde) =
|
||||||
|
OptimizedDecoder (JD.at fields jd) (JDE.at fields jde)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Choosing
|
||||||
|
|
||||||
|
|
||||||
|
{-| Tries a bunch of decoders. The first one to not fail will be the one used.
|
||||||
|
|
||||||
|
If all fail, the errors are collected into a `BadOneOf`.
|
||||||
|
|
||||||
|
import List.Nonempty as Nonempty
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
""" [ 12, "whatever" ] """
|
||||||
|
|> decodeString (list <| oneOf [ map String.fromInt int, string ])
|
||||||
|
--> Success [ "12", "whatever" ]
|
||||||
|
|
||||||
|
|
||||||
|
""" null """
|
||||||
|
|> decodeString (oneOf [ string, map String.fromInt int ])
|
||||||
|
--> Errors <| Nonempty.fromElement <| Here <| BadOneOf
|
||||||
|
--> [ Nonempty.fromElement <| Here <| Expected TString Encode.null
|
||||||
|
--> , Nonempty.fromElement <| Here <| Expected TInt Encode.null
|
||||||
|
--> ]
|
||||||
|
|
||||||
|
-}
|
||||||
|
oneOf : List (Decoder a) -> Decoder a
|
||||||
|
oneOf decoders =
|
||||||
|
let
|
||||||
|
jds =
|
||||||
|
List.map
|
||||||
|
(\(OptimizedDecoder jd jde) ->
|
||||||
|
jd
|
||||||
|
)
|
||||||
|
decoders
|
||||||
|
|
||||||
|
jdes =
|
||||||
|
List.map
|
||||||
|
(\(OptimizedDecoder jd jde) ->
|
||||||
|
jde
|
||||||
|
)
|
||||||
|
decoders
|
||||||
|
in
|
||||||
|
OptimizedDecoder (JD.oneOf jds) (JDE.oneOf jdes)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decodes successfully and wraps with a `Just`, handling failure by succeeding
|
||||||
|
with `Nothing`.
|
||||||
|
|
||||||
|
import List.Nonempty as Nonempty
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
|
||||||
|
""" [ "foo", 12 ] """
|
||||||
|
|> decodeString (list <| maybe string)
|
||||||
|
--> WithWarnings expectedWarnings [ Just "foo", Nothing ]
|
||||||
|
|
||||||
|
|
||||||
|
expectedWarnings : Warnings
|
||||||
|
expectedWarnings =
|
||||||
|
UnusedValue (Encode.int 12)
|
||||||
|
|> Here
|
||||||
|
|> Nonempty.fromElement
|
||||||
|
|> AtIndex 1
|
||||||
|
|> Nonempty.fromElement
|
||||||
|
|
||||||
|
-}
|
||||||
|
maybe : Decoder a -> Decoder (Maybe a)
|
||||||
|
maybe (OptimizedDecoder jd jde) =
|
||||||
|
OptimizedDecoder (JD.maybe jd) (JDE.maybe jde)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decodes successfully and wraps with a `Just`. If the values is `null`
|
||||||
|
succeeds with `Nothing`.
|
||||||
|
|
||||||
|
""" [ { "foo": "bar" }, { "foo": null } ] """
|
||||||
|
|> decodeString (list <| field "foo" <| nullable string)
|
||||||
|
--> Success [ Just "bar", Nothing ]
|
||||||
|
|
||||||
|
-}
|
||||||
|
nullable : Decoder a -> Decoder (Maybe a)
|
||||||
|
nullable (OptimizedDecoder jd jde) =
|
||||||
|
OptimizedDecoder (JD.nullable jd) (JDE.nullable jde)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
{-| Required when using (mutually) recursive decoders.
|
||||||
|
-}
|
||||||
|
lazy : (() -> Decoder a) -> Decoder a
|
||||||
|
lazy toDecoder =
|
||||||
|
let
|
||||||
|
jd : JD.Decoder a
|
||||||
|
jd =
|
||||||
|
(\() ->
|
||||||
|
case toDecoder () of
|
||||||
|
OptimizedDecoder jd_ jde_ ->
|
||||||
|
jd_
|
||||||
|
)
|
||||||
|
|> JD.lazy
|
||||||
|
|
||||||
|
jde : JDE.Decoder a
|
||||||
|
jde =
|
||||||
|
(\() ->
|
||||||
|
case toDecoder () of
|
||||||
|
OptimizedDecoder jd_ jde_ ->
|
||||||
|
jde_
|
||||||
|
)
|
||||||
|
|> JDE.lazy
|
||||||
|
in
|
||||||
|
OptimizedDecoder
|
||||||
|
jd
|
||||||
|
jde
|
||||||
|
|
||||||
|
|
||||||
|
{-| Useful for checking a value in the JSON matches the value you expect it to
|
||||||
|
have. If it does, succeeds with the second decoder. If it doesn't it fails.
|
||||||
|
|
||||||
|
This can be used to decode union types:
|
||||||
|
|
||||||
|
type Pet = Cat | Dog | Rabbit
|
||||||
|
|
||||||
|
petDecoder : Decoder Pet
|
||||||
|
petDecoder =
|
||||||
|
oneOf
|
||||||
|
[ check string "cat" <| succeed Cat
|
||||||
|
, check string "dog" <| succeed Dog
|
||||||
|
, check string "rabbit" <| succeed Rabbit
|
||||||
|
]
|
||||||
|
|
||||||
|
""" [ "dog", "rabbit", "cat" ] """
|
||||||
|
|> decodeString (list petDecoder)
|
||||||
|
--> Success [ Dog, Rabbit, Cat ]
|
||||||
|
|
||||||
|
-}
|
||||||
|
check : Decoder a -> a -> Decoder b -> Decoder b
|
||||||
|
check checkDecoder expectedVal actualDecoder =
|
||||||
|
checkDecoder
|
||||||
|
|> andThen
|
||||||
|
(\actual ->
|
||||||
|
if actual == expectedVal then
|
||||||
|
actualDecoder
|
||||||
|
|
||||||
|
else
|
||||||
|
fail "Verification failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Mapping and chaining
|
||||||
|
|
||||||
|
|
||||||
|
{-| Useful for transforming decoders.
|
||||||
|
|
||||||
|
""" "foo" """
|
||||||
|
|> decodeString (map String.toUpper string)
|
||||||
|
--> Success "FOO"
|
||||||
|
|
||||||
|
-}
|
||||||
|
map : (a -> b) -> Decoder a -> Decoder b
|
||||||
|
map f (OptimizedDecoder jd jde) =
|
||||||
|
OptimizedDecoder (JD.map f jd) (JDE.map f jde)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Chain decoders where one decoder depends on the value of another decoder.
|
||||||
|
-}
|
||||||
|
andThen : (a -> Decoder b) -> Decoder a -> Decoder b
|
||||||
|
andThen toDecoderB (OptimizedDecoder jd jde) =
|
||||||
|
OptimizedDecoder
|
||||||
|
(JD.andThen (toDecoderB >> Internal.OptimizedDecoder.jd) jd)
|
||||||
|
(JDE.andThen (toDecoderB >> Internal.OptimizedDecoder.jde) jde)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Combine 2 decoders.
|
||||||
|
-}
|
||||||
|
map2 : (a -> b -> c) -> Decoder a -> Decoder b -> Decoder c
|
||||||
|
map2 f (OptimizedDecoder jdA jdeA) (OptimizedDecoder jdB jdeB) =
|
||||||
|
OptimizedDecoder
|
||||||
|
(JD.map2 f jdA jdB)
|
||||||
|
(JDE.map2 f jdeA jdeB)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode an argument and provide it to a function in a decoder.
|
||||||
|
|
||||||
|
decoder : Decoder String
|
||||||
|
decoder =
|
||||||
|
succeed (String.repeat)
|
||||||
|
|> andMap (field "count" int)
|
||||||
|
|> andMap (field "val" string)
|
||||||
|
|
||||||
|
|
||||||
|
""" { "val": "hi", "count": 3 } """
|
||||||
|
|> decodeString decoder
|
||||||
|
--> Success "hihihi"
|
||||||
|
|
||||||
|
-}
|
||||||
|
andMap : Decoder a -> Decoder (a -> b) -> Decoder b
|
||||||
|
andMap =
|
||||||
|
map2 (|>)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Combine 3 decoders.
|
||||||
|
-}
|
||||||
|
map3 :
|
||||||
|
(a -> b -> c -> d)
|
||||||
|
-> Decoder a
|
||||||
|
-> Decoder b
|
||||||
|
-> Decoder c
|
||||||
|
-> Decoder d
|
||||||
|
map3 f decoderA decoderB decoderC =
|
||||||
|
map f decoderA
|
||||||
|
|> andMap decoderB
|
||||||
|
|> andMap decoderC
|
||||||
|
|
||||||
|
|
||||||
|
{-| Combine 4 decoders.
|
||||||
|
-}
|
||||||
|
map4 :
|
||||||
|
(a -> b -> c -> d -> e)
|
||||||
|
-> Decoder a
|
||||||
|
-> Decoder b
|
||||||
|
-> Decoder c
|
||||||
|
-> Decoder d
|
||||||
|
-> Decoder e
|
||||||
|
map4 f decoderA decoderB decoderC decoderD =
|
||||||
|
map f decoderA
|
||||||
|
|> andMap decoderB
|
||||||
|
|> andMap decoderC
|
||||||
|
|> andMap decoderD
|
||||||
|
|
||||||
|
|
||||||
|
{-| Combine 5 decoders.
|
||||||
|
-}
|
||||||
|
map5 :
|
||||||
|
(a -> b -> c -> d -> e -> f)
|
||||||
|
-> Decoder a
|
||||||
|
-> Decoder b
|
||||||
|
-> Decoder c
|
||||||
|
-> Decoder d
|
||||||
|
-> Decoder e
|
||||||
|
-> Decoder f
|
||||||
|
map5 f decoderA decoderB decoderC decoderD decoderE =
|
||||||
|
map f decoderA
|
||||||
|
|> andMap decoderB
|
||||||
|
|> andMap decoderC
|
||||||
|
|> andMap decoderD
|
||||||
|
|> andMap decoderE
|
||||||
|
|
||||||
|
|
||||||
|
{-| Combine 6 decoders.
|
||||||
|
-}
|
||||||
|
map6 :
|
||||||
|
(a -> b -> c -> d -> e -> f -> g)
|
||||||
|
-> Decoder a
|
||||||
|
-> Decoder b
|
||||||
|
-> Decoder c
|
||||||
|
-> Decoder d
|
||||||
|
-> Decoder e
|
||||||
|
-> Decoder f
|
||||||
|
-> Decoder g
|
||||||
|
map6 f decoderA decoderB decoderC decoderD decoderE decoderF =
|
||||||
|
map f decoderA
|
||||||
|
|> andMap decoderB
|
||||||
|
|> andMap decoderC
|
||||||
|
|> andMap decoderD
|
||||||
|
|> andMap decoderE
|
||||||
|
|> andMap decoderF
|
||||||
|
|
||||||
|
|
||||||
|
{-| Combine 7 decoders.
|
||||||
|
-}
|
||||||
|
map7 :
|
||||||
|
(a -> b -> c -> d -> e -> f -> g -> h)
|
||||||
|
-> Decoder a
|
||||||
|
-> Decoder b
|
||||||
|
-> Decoder c
|
||||||
|
-> Decoder d
|
||||||
|
-> Decoder e
|
||||||
|
-> Decoder f
|
||||||
|
-> Decoder g
|
||||||
|
-> Decoder h
|
||||||
|
map7 f decoderA decoderB decoderC decoderD decoderE decoderF decoderG =
|
||||||
|
map f decoderA
|
||||||
|
|> andMap decoderB
|
||||||
|
|> andMap decoderC
|
||||||
|
|> andMap decoderD
|
||||||
|
|> andMap decoderE
|
||||||
|
|> andMap decoderF
|
||||||
|
|> andMap decoderG
|
||||||
|
|
||||||
|
|
||||||
|
{-| Combine 8 decoders.
|
||||||
|
-}
|
||||||
|
map8 :
|
||||||
|
(a -> b -> c -> d -> e -> f -> g -> h -> i)
|
||||||
|
-> Decoder a
|
||||||
|
-> Decoder b
|
||||||
|
-> Decoder c
|
||||||
|
-> Decoder d
|
||||||
|
-> Decoder e
|
||||||
|
-> Decoder f
|
||||||
|
-> Decoder g
|
||||||
|
-> Decoder h
|
||||||
|
-> Decoder i
|
||||||
|
map8 f decoderA decoderB decoderC decoderD decoderE decoderF decoderG decoderH =
|
||||||
|
map f decoderA
|
||||||
|
|> andMap decoderB
|
||||||
|
|> andMap decoderC
|
||||||
|
|> andMap decoderD
|
||||||
|
|> andMap decoderE
|
||||||
|
|> andMap decoderF
|
||||||
|
|> andMap decoderG
|
||||||
|
|> andMap decoderH
|
333
src/OptimizedDecoder/Pipeline.elm
Normal file
333
src/OptimizedDecoder/Pipeline.elm
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
module OptimizedDecoder.Pipeline exposing
|
||||||
|
( required, requiredAt, optional, optionalAt, hardcoded, custom
|
||||||
|
, decode, resolve
|
||||||
|
)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
|
||||||
|
# Json.Decode.Pipeline
|
||||||
|
|
||||||
|
Use the `(|>)` operator to build JSON decoders.
|
||||||
|
|
||||||
|
|
||||||
|
## Decoding fields
|
||||||
|
|
||||||
|
@docs required, requiredAt, optional, optionalAt, hardcoded, custom
|
||||||
|
|
||||||
|
|
||||||
|
## Beginning and ending pipelines
|
||||||
|
|
||||||
|
@docs decode, resolve
|
||||||
|
|
||||||
|
|
||||||
|
### Verified docs
|
||||||
|
|
||||||
|
The examples all expect imports set up like this:
|
||||||
|
|
||||||
|
import Json.Decode.Exploration exposing (..)
|
||||||
|
import Json.Decode.Exploration.Pipeline exposing (..)
|
||||||
|
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||||
|
import Json.Encode as Encode
|
||||||
|
import List.Nonempty as Nonempty
|
||||||
|
|
||||||
|
For automated verification of these examples, this import is also required.
|
||||||
|
Please ignore it.
|
||||||
|
|
||||||
|
import DocVerificationHelpers exposing (User)
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import OptimizedDecoder as Decode exposing (Decoder)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a required field.
|
||||||
|
|
||||||
|
import Json.Decode.Exploration exposing (..)
|
||||||
|
|
||||||
|
type alias User =
|
||||||
|
{ id : Int
|
||||||
|
, name : String
|
||||||
|
, email : String
|
||||||
|
}
|
||||||
|
|
||||||
|
userDecoder : Decoder User
|
||||||
|
userDecoder =
|
||||||
|
decode User
|
||||||
|
|> required "id" int
|
||||||
|
|> required "name" string
|
||||||
|
|> required "email" string
|
||||||
|
|
||||||
|
""" {"id": 123, "email": "sam@example.com", "name": "Sam"} """
|
||||||
|
|> decodeString userDecoder
|
||||||
|
--> Success { id = 123, name = "Sam", email = "sam@example.com" }
|
||||||
|
|
||||||
|
-}
|
||||||
|
required : String -> Decoder a -> Decoder (a -> b) -> Decoder b
|
||||||
|
required key valDecoder decoder =
|
||||||
|
decoder |> Decode.andMap (Decode.field key valDecoder)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a required nested field.
|
||||||
|
|
||||||
|
import Json.Decode.Exploration exposing (..)
|
||||||
|
|
||||||
|
type alias User =
|
||||||
|
{ id : Int
|
||||||
|
, name : String
|
||||||
|
, email : String
|
||||||
|
}
|
||||||
|
|
||||||
|
userDecoder : Decoder User
|
||||||
|
userDecoder =
|
||||||
|
decode User
|
||||||
|
|> required "id" int
|
||||||
|
|> requiredAt [ "profile", "name" ] string
|
||||||
|
|> required "email" string
|
||||||
|
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"email": "sam@example.com",
|
||||||
|
"profile": { "name": "Sam" }
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|> decodeString userDecoder
|
||||||
|
--> Success { id = 123, name = "Sam", email = "sam@example.com" }
|
||||||
|
|
||||||
|
-}
|
||||||
|
requiredAt : List String -> Decoder a -> Decoder (a -> b) -> Decoder b
|
||||||
|
requiredAt path valDecoder decoder =
|
||||||
|
decoder |> Decode.andMap (Decode.at path valDecoder)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a field that may be missing or have a null value. If the field is
|
||||||
|
missing, then it decodes as the `fallback` value. If the field is present,
|
||||||
|
then `valDecoder` is used to decode its value. If `valDecoder` fails on a
|
||||||
|
`null` value, then the `fallback` is used as if the field were missing
|
||||||
|
entirely.
|
||||||
|
|
||||||
|
import Json.Decode.Exploration exposing (..)
|
||||||
|
|
||||||
|
type alias User =
|
||||||
|
{ id : Int
|
||||||
|
, name : String
|
||||||
|
, email : String
|
||||||
|
}
|
||||||
|
|
||||||
|
userDecoder : Decoder User
|
||||||
|
userDecoder =
|
||||||
|
decode User
|
||||||
|
|> required "id" int
|
||||||
|
|> optional "name" string "blah"
|
||||||
|
|> required "email" string
|
||||||
|
|
||||||
|
""" { "id": 123, "email": "sam@example.com" } """
|
||||||
|
|> decodeString userDecoder
|
||||||
|
--> Success { id = 123, name = "blah", email = "sam@example.com" }
|
||||||
|
|
||||||
|
Because `valDecoder` is given an opportunity to decode `null` values before
|
||||||
|
resorting to the `fallback`, you can distinguish between missing and `null`
|
||||||
|
values if you need to:
|
||||||
|
|
||||||
|
userDecoder2 =
|
||||||
|
decode User
|
||||||
|
|> required "id" int
|
||||||
|
|> optional "name" (oneOf [ string, null "NULL" ]) "MISSING"
|
||||||
|
|> required "email" string
|
||||||
|
|
||||||
|
Note also that this behaves _slightly_ different than the stock pipeline
|
||||||
|
package.
|
||||||
|
|
||||||
|
In the stock pipeline package, running the following decoder with an array as
|
||||||
|
the input would _succeed_.
|
||||||
|
|
||||||
|
fooDecoder =
|
||||||
|
decode identity
|
||||||
|
|> optional "foo" (maybe string) Nothing
|
||||||
|
|
||||||
|
In this package, such a decoder will error out instead, saying that it expected
|
||||||
|
the input to be an object. The _key_ `"foo"` is optional, but it really does
|
||||||
|
have to be an object before we even consider trying your decoder or returning
|
||||||
|
the fallback.
|
||||||
|
|
||||||
|
-}
|
||||||
|
optional : String -> Decoder a -> a -> Decoder (a -> b) -> Decoder b
|
||||||
|
optional key valDecoder fallback decoder =
|
||||||
|
-- source: https://github.com/NoRedInk/elm-json-decode-pipeline/blob/d9c10a2b388176569fe3e88ef0e2b6fc19d9beeb/src/Json/Decode/Pipeline.elm#L113
|
||||||
|
custom (optionalDecoder (Decode.field key Decode.value) valDecoder fallback) decoder
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode an optional nested field.
|
||||||
|
-}
|
||||||
|
optionalAt : List String -> Decoder a -> a -> Decoder (a -> b) -> Decoder b
|
||||||
|
optionalAt path valDecoder fallback decoder =
|
||||||
|
custom (optionalDecoder (Decode.at path Decode.value) valDecoder fallback) decoder
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- source: https://github.com/NoRedInk/elm-json-decode-pipeline/blob/d9c10a2b388176569fe3e88ef0e2b6fc19d9beeb/src/Json/Decode/Pipeline.elm#L116-L148
|
||||||
|
|
||||||
|
|
||||||
|
optionalDecoder : Decode.Decoder Decode.Value -> Decoder a -> a -> Decoder a
|
||||||
|
optionalDecoder pathDecoder valDecoder fallback =
|
||||||
|
let
|
||||||
|
nullOr decoder =
|
||||||
|
Decode.oneOf [ decoder, Decode.null fallback ]
|
||||||
|
|
||||||
|
handleResult input =
|
||||||
|
case Decode.decodeValue pathDecoder input of
|
||||||
|
Ok rawValue ->
|
||||||
|
-- The field was present, so now let's try to decode that value.
|
||||||
|
-- (If it was present but fails to decode, this should and will fail!)
|
||||||
|
case Decode.decodeValue (nullOr valDecoder) rawValue of
|
||||||
|
Ok finalResult ->
|
||||||
|
Decode.succeed finalResult
|
||||||
|
|
||||||
|
Err finalErr ->
|
||||||
|
-- TODO is there some way to preserve the structure
|
||||||
|
-- of the original error instead of using toString here?
|
||||||
|
Decode.fail (Decode.errorToString finalErr)
|
||||||
|
|
||||||
|
Err _ ->
|
||||||
|
-- The field was not present, so use the fallback.
|
||||||
|
Decode.succeed fallback
|
||||||
|
in
|
||||||
|
Decode.value
|
||||||
|
|> Decode.andThen handleResult
|
||||||
|
|
||||||
|
|
||||||
|
{-| Rather than decoding anything, use a fixed value for the next step in the
|
||||||
|
pipeline. `harcoded` does not look at the JSON at all.
|
||||||
|
|
||||||
|
import Json.Decode.Exploration exposing (..)
|
||||||
|
|
||||||
|
|
||||||
|
type alias User =
|
||||||
|
{ id : Int
|
||||||
|
, name : String
|
||||||
|
, email : String
|
||||||
|
}
|
||||||
|
|
||||||
|
userDecoder : Decoder User
|
||||||
|
userDecoder =
|
||||||
|
decode User
|
||||||
|
|> required "id" int
|
||||||
|
|> hardcoded "Alex"
|
||||||
|
|> required "email" string
|
||||||
|
|
||||||
|
""" { "id": 123, "email": "sam@example.com" } """
|
||||||
|
|> decodeString userDecoder
|
||||||
|
--> Success { id = 123, name = "Alex", email = "sam@example.com" }
|
||||||
|
|
||||||
|
-}
|
||||||
|
hardcoded : a -> Decoder (a -> b) -> Decoder b
|
||||||
|
hardcoded =
|
||||||
|
Decode.andMap << Decode.succeed
|
||||||
|
|
||||||
|
|
||||||
|
{-| Run the given decoder and feed its result into the pipeline at this point.
|
||||||
|
|
||||||
|
Consider this example.
|
||||||
|
|
||||||
|
import Json.Decode.Exploration exposing (..)
|
||||||
|
|
||||||
|
|
||||||
|
type alias User =
|
||||||
|
{ id : Int
|
||||||
|
, name : String
|
||||||
|
, email : String
|
||||||
|
}
|
||||||
|
|
||||||
|
userDecoder : Decoder User
|
||||||
|
userDecoder =
|
||||||
|
decode User
|
||||||
|
|> required "id" int
|
||||||
|
|> custom (at [ "profile", "name" ] string)
|
||||||
|
|> required "email" string
|
||||||
|
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"email": "sam@example.com",
|
||||||
|
"profile": {"name": "Sam"}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|> decodeString userDecoder
|
||||||
|
--> Success { id = 123, name = "Sam", email = "sam@example.com" }
|
||||||
|
|
||||||
|
-}
|
||||||
|
custom : Decoder a -> Decoder (a -> b) -> Decoder b
|
||||||
|
custom =
|
||||||
|
Decode.andMap
|
||||||
|
|
||||||
|
|
||||||
|
{-| Convert a `Decoder (Result x a)` into a `Decoder a`. Useful when you want
|
||||||
|
to perform some custom processing just before completing the decoding operation.
|
||||||
|
|
||||||
|
import Json.Decode.Exploration exposing (..)
|
||||||
|
|
||||||
|
type alias User =
|
||||||
|
{ id : Int
|
||||||
|
, name : String
|
||||||
|
, email : String
|
||||||
|
}
|
||||||
|
|
||||||
|
userDecoder : Decoder User
|
||||||
|
userDecoder =
|
||||||
|
let
|
||||||
|
-- toDecoder gets run *after* all the
|
||||||
|
-- (|> required ...) steps are done.
|
||||||
|
toDecoder : Int -> String -> String -> Int -> Decoder User
|
||||||
|
toDecoder id name email version =
|
||||||
|
if version >= 2 then
|
||||||
|
succeed (User id name email)
|
||||||
|
else
|
||||||
|
fail "This JSON is from a deprecated source. Please upgrade!"
|
||||||
|
in
|
||||||
|
decode toDecoder
|
||||||
|
|> required "id" int
|
||||||
|
|> required "name" string
|
||||||
|
|> required "email" string
|
||||||
|
|> required "version" int
|
||||||
|
-- version is part of toDecoder,
|
||||||
|
-- but it is not a part of User
|
||||||
|
|> resolve
|
||||||
|
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"name": "Sam",
|
||||||
|
"email": "sam@example.com",
|
||||||
|
"version": 3
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|> decodeString userDecoder
|
||||||
|
--> Success { id = 123, name = "Sam", email = "sam@example.com" }
|
||||||
|
|
||||||
|
-}
|
||||||
|
resolve : Decoder (Decoder a) -> Decoder a
|
||||||
|
resolve =
|
||||||
|
Decode.andThen identity
|
||||||
|
|
||||||
|
|
||||||
|
{-| Begin a decoding pipeline. This is a synonym for [Json.Decode.succeed](http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#succeed),
|
||||||
|
intended to make things read more clearly.
|
||||||
|
|
||||||
|
type alias User =
|
||||||
|
{ id : Int
|
||||||
|
, email : String
|
||||||
|
, name : String
|
||||||
|
}
|
||||||
|
|
||||||
|
userDecoder : Decoder User
|
||||||
|
userDecoder =
|
||||||
|
decode User
|
||||||
|
|> required "id" int
|
||||||
|
|> required "email" string
|
||||||
|
|> optional "name" string ""
|
||||||
|
|
||||||
|
-}
|
||||||
|
decode : a -> Decoder a
|
||||||
|
decode =
|
||||||
|
Decode.succeed
|
@ -21,16 +21,12 @@ import Html exposing (Html)
|
|||||||
import Html.Attributes as Attr
|
import Html.Attributes as Attr
|
||||||
import Http
|
import Http
|
||||||
import Json.Decode as Decode
|
import Json.Decode as Decode
|
||||||
import Mark
|
|
||||||
import Mark.Error
|
|
||||||
import Pages.Document as Document exposing (Document)
|
import Pages.Document as Document exposing (Document)
|
||||||
import Pages.Internal.String as String
|
import Pages.Internal.String as String
|
||||||
import Pages.PagePath as PagePath exposing (PagePath)
|
import Pages.PagePath as PagePath exposing (PagePath)
|
||||||
import Result.Extra
|
|
||||||
import Task exposing (Task)
|
import Task exposing (Task)
|
||||||
import TerminalText as Terminal
|
import TerminalText as Terminal
|
||||||
import Url exposing (Url)
|
import Url exposing (Url)
|
||||||
import Url.Builder
|
|
||||||
|
|
||||||
|
|
||||||
type alias Content =
|
type alias Content =
|
||||||
@ -283,16 +279,6 @@ type alias Page metadata view pathKey =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
renderErrors : ( List String, List Mark.Error.Error ) -> Html msg
|
|
||||||
renderErrors ( path, errors ) =
|
|
||||||
Html.div []
|
|
||||||
[ Html.text (String.join "/" path)
|
|
||||||
, errors
|
|
||||||
|> List.map (Mark.Error.toHtml Mark.Error.Light)
|
|
||||||
|> Html.div []
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
combineTupleResults :
|
combineTupleResults :
|
||||||
List ( List String, Result error success )
|
List ( List String, Result error success )
|
||||||
-> Result (List error) (List ( List String, success ))
|
-> Result (List error) (List ( List String, success ))
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
module Pages.Document exposing
|
module Pages.Document exposing
|
||||||
( Document, DocumentHandler
|
( Document, DocumentHandler
|
||||||
, parser, markupParser
|
, parser
|
||||||
, fromList, get
|
, fromList, get
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ Hello!!!
|
|||||||
)
|
)
|
||||||
|
|
||||||
@docs Document, DocumentHandler
|
@docs Document, DocumentHandler
|
||||||
@docs parser, markupParser
|
@docs parser
|
||||||
|
|
||||||
|
|
||||||
## Functions for use by generated code
|
## Functions for use by generated code
|
||||||
@ -92,8 +92,6 @@ Hello!!!
|
|||||||
import Dict exposing (Dict)
|
import Dict exposing (Dict)
|
||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
import Json.Decode
|
import Json.Decode
|
||||||
import Mark
|
|
||||||
import Mark.Error
|
|
||||||
|
|
||||||
|
|
||||||
{-| Represents all of the `DocumentHandler`s. You register a handler for each
|
{-| Represents all of the `DocumentHandler`s. You register a handler for each
|
||||||
@ -157,54 +155,3 @@ parser { extension, body, metadata } =
|
|||||||
|> Result.mapError Json.Decode.errorToString
|
|> Result.mapError Json.Decode.errorToString
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
{-| Register an [`elm-markup`](https://github.com/mdgriffith/elm-markup/)
|
|
||||||
parser for your `.emu` files.
|
|
||||||
-}
|
|
||||||
markupParser :
|
|
||||||
Mark.Document metadata
|
|
||||||
-> Mark.Document view
|
|
||||||
-> ( String, DocumentHandler metadata view )
|
|
||||||
markupParser metadataParser markBodyParser =
|
|
||||||
( "emu"
|
|
||||||
, DocumentHandler
|
|
||||||
{ contentParser = renderMarkup markBodyParser
|
|
||||||
, frontmatterParser =
|
|
||||||
\frontMatter ->
|
|
||||||
Mark.compile metadataParser
|
|
||||||
frontMatter
|
|
||||||
|> (\outcome ->
|
|
||||||
case outcome of
|
|
||||||
Mark.Success parsedMetadata ->
|
|
||||||
Ok parsedMetadata
|
|
||||||
|
|
||||||
Mark.Failure failure ->
|
|
||||||
Err "Failure"
|
|
||||||
|
|
||||||
Mark.Almost failure ->
|
|
||||||
Err "Almost failure"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
renderMarkup : Mark.Document view -> String -> Result String view
|
|
||||||
renderMarkup markBodyParser markupBody =
|
|
||||||
Mark.compile
|
|
||||||
markBodyParser
|
|
||||||
(markupBody |> String.trimLeft)
|
|
||||||
|> (\outcome ->
|
|
||||||
case outcome of
|
|
||||||
Mark.Success renderedView ->
|
|
||||||
Ok renderedView
|
|
||||||
|
|
||||||
Mark.Failure failures ->
|
|
||||||
failures
|
|
||||||
|> List.map Mark.Error.toString
|
|
||||||
|> String.join "\n"
|
|
||||||
|> Err
|
|
||||||
|
|
||||||
Mark.Almost failure ->
|
|
||||||
Err "TODO almost failure"
|
|
||||||
)
|
|
||||||
|
@ -13,11 +13,12 @@ that is in the generated `Pages` module (see <Pages.Platform>).
|
|||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
import Json.Decode
|
||||||
import Json.Encode
|
import Json.Encode
|
||||||
import Pages.Internal.Platform
|
import Pages.Internal.Platform
|
||||||
|
|
||||||
|
|
||||||
{-| Internal detial to track whether to run the CLI step or the runtime step in the browser.
|
{-| Internal detail to track whether to run the CLI step or the runtime step in the browser.
|
||||||
-}
|
-}
|
||||||
type ApplicationType
|
type ApplicationType
|
||||||
= Browser
|
= Browser
|
||||||
@ -31,4 +32,5 @@ type alias Internal pathKey =
|
|||||||
, content : Pages.Internal.Platform.Content
|
, content : Pages.Internal.Platform.Content
|
||||||
, pathKey : pathKey
|
, pathKey : pathKey
|
||||||
, toJsPort : Json.Encode.Value -> Cmd Never
|
, toJsPort : Json.Encode.Value -> Cmd Never
|
||||||
|
, fromJsPort : Sub Json.Decode.Value
|
||||||
}
|
}
|
||||||
|
6
src/Pages/Internal/ApplicationType.elm
Normal file
6
src/Pages/Internal/ApplicationType.elm
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module Pages.Internal.ApplicationType exposing (ApplicationType(..))
|
||||||
|
|
||||||
|
|
||||||
|
type ApplicationType
|
||||||
|
= Browser
|
||||||
|
| Cli
|
108
src/Pages/Internal/HotReloadLoadingIndicator.elm
Normal file
108
src/Pages/Internal/HotReloadLoadingIndicator.elm
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
module Pages.Internal.HotReloadLoadingIndicator exposing (..)
|
||||||
|
|
||||||
|
import Html exposing (Html)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
|
||||||
|
|
||||||
|
circle : List (Html.Attribute msg) -> Html msg
|
||||||
|
circle attrs =
|
||||||
|
Html.div
|
||||||
|
(style "animation" "lds-default 1.2s linear infinite"
|
||||||
|
:: style "background" "#000"
|
||||||
|
:: style "position" "absolute"
|
||||||
|
:: style "width" "6px"
|
||||||
|
:: style "height" "6px"
|
||||||
|
:: style "border-radius" "50%"
|
||||||
|
:: attrs
|
||||||
|
)
|
||||||
|
[]
|
||||||
|
|
||||||
|
|
||||||
|
view : Bool -> Bool -> Html msg
|
||||||
|
view isDebugMode display =
|
||||||
|
Html.div
|
||||||
|
[ id "__elm-pages-loading"
|
||||||
|
, class "lds-default"
|
||||||
|
, style "position" "fixed"
|
||||||
|
, style "bottom" "10px"
|
||||||
|
, style "right"
|
||||||
|
(if isDebugMode then
|
||||||
|
"110px"
|
||||||
|
|
||||||
|
else
|
||||||
|
"10px"
|
||||||
|
)
|
||||||
|
, style "width" "80px"
|
||||||
|
, style "height" "80px"
|
||||||
|
, style "background-color" "white"
|
||||||
|
, style "box-shadow" "0 8px 15px 0 rgba(0, 0, 0, 0.25), 0 2px 10px 0 rgba(0, 0, 0, 0.12)"
|
||||||
|
, style "display"
|
||||||
|
(case display of
|
||||||
|
True ->
|
||||||
|
"block"
|
||||||
|
|
||||||
|
False ->
|
||||||
|
"none"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
[ circle
|
||||||
|
[ style "animation-delay" "0s"
|
||||||
|
, style "top" "37px"
|
||||||
|
, style "left" "66px"
|
||||||
|
]
|
||||||
|
, circle
|
||||||
|
[ style "animation-delay" "-0.1s"
|
||||||
|
, style "top" "22px"
|
||||||
|
, style "left" "62px"
|
||||||
|
]
|
||||||
|
, circle
|
||||||
|
[ style "animation-delay" "-0.2s"
|
||||||
|
, style "top" "11px"
|
||||||
|
, style "left" "52px"
|
||||||
|
]
|
||||||
|
, circle
|
||||||
|
[ style "animation-delay" "-0.3s"
|
||||||
|
, style "top" "7px"
|
||||||
|
, style "left" "37px"
|
||||||
|
]
|
||||||
|
, circle
|
||||||
|
[ style "animation-delay" "-0.4s"
|
||||||
|
, style "top" "11px"
|
||||||
|
, style "left" "22px"
|
||||||
|
]
|
||||||
|
, circle
|
||||||
|
[ style "animation-delay" "-0.5s"
|
||||||
|
, style "top" "22px"
|
||||||
|
, style "left" "11px"
|
||||||
|
]
|
||||||
|
, circle
|
||||||
|
[ style "animation-delay" "-0.6s"
|
||||||
|
, style "top" "37px"
|
||||||
|
, style "left" "7px"
|
||||||
|
]
|
||||||
|
, circle
|
||||||
|
[ style "animation-delay" "-0.7s"
|
||||||
|
, style "top" "52px"
|
||||||
|
, style "left" "11px"
|
||||||
|
]
|
||||||
|
, circle
|
||||||
|
[ style "animation-delay" "-0.8s"
|
||||||
|
, style "top" "62px"
|
||||||
|
, style "left" "22px"
|
||||||
|
]
|
||||||
|
, circle
|
||||||
|
[ style "animation-delay" "-0.9s"
|
||||||
|
, style "top" "66px"
|
||||||
|
, style "left" "37px"
|
||||||
|
]
|
||||||
|
, circle
|
||||||
|
[ style "animation-delay" "-1s"
|
||||||
|
, style "top" "62px"
|
||||||
|
, style "left" "52px"
|
||||||
|
]
|
||||||
|
, circle
|
||||||
|
[ style "animation-delay" "-1.1s"
|
||||||
|
, style "top" "52px"
|
||||||
|
, style "left" "62px"
|
||||||
|
]
|
||||||
|
]
|
@ -1,4 +1,4 @@
|
|||||||
module Pages.Internal.Platform exposing (Content, Flags, Model, Msg, Page, Parser, Program, application, cliApplication)
|
module Pages.Internal.Platform exposing (Content, Flags, Model, Msg, Page, Program, application, cliApplication)
|
||||||
|
|
||||||
import Browser
|
import Browser
|
||||||
import Browser.Dom as Dom
|
import Browser.Dom as Dom
|
||||||
@ -6,14 +6,15 @@ import Browser.Navigation
|
|||||||
import Dict exposing (Dict)
|
import Dict exposing (Dict)
|
||||||
import Head
|
import Head
|
||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
import Html.Attributes
|
import Html.Attributes exposing (style)
|
||||||
|
import Html.Lazy
|
||||||
import Http
|
import Http
|
||||||
import Json.Decode as Decode
|
import Json.Decode as Decode
|
||||||
import Json.Encode
|
import Json.Encode
|
||||||
import List.Extra
|
|
||||||
import Mark
|
|
||||||
import Pages.ContentCache as ContentCache exposing (ContentCache)
|
import Pages.ContentCache as ContentCache exposing (ContentCache)
|
||||||
import Pages.Document
|
import Pages.Document
|
||||||
|
import Pages.Internal.ApplicationType as ApplicationType
|
||||||
|
import Pages.Internal.HotReloadLoadingIndicator as HotReloadLoadingIndicator
|
||||||
import Pages.Internal.Platform.Cli
|
import Pages.Internal.Platform.Cli
|
||||||
import Pages.Internal.String as String
|
import Pages.Internal.String as String
|
||||||
import Pages.Manifest as Manifest
|
import Pages.Manifest as Manifest
|
||||||
@ -122,7 +123,7 @@ pageViewOrError pathKey viewFn model cache =
|
|||||||
-- TODO handle error better
|
-- TODO handle error better
|
||||||
)
|
)
|
||||||
|> (\request ->
|
|> (\request ->
|
||||||
StaticHttpRequest.resolve request viewResult.staticData
|
StaticHttpRequest.resolve ApplicationType.Browser request viewResult.staticData
|
||||||
)
|
)
|
||||||
in
|
in
|
||||||
case viewResult.body of
|
case viewResult.body of
|
||||||
@ -146,6 +147,12 @@ pageViewOrError pathKey viewFn model cache =
|
|||||||
[ Html.text "I'm missing some StaticHttp data for this page:"
|
[ Html.text "I'm missing some StaticHttp data for this page:"
|
||||||
, Html.pre [] [ Html.text missingKey ]
|
, Html.pre [] [ Html.text missingKey ]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
StaticHttpRequest.UserCalledStaticHttpFail message ->
|
||||||
|
Html.div []
|
||||||
|
[ Html.text "I ran into a call to `Pages.StaticHttp.fail` with message:"
|
||||||
|
, Html.pre [] [ Html.text message ]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
Err error ->
|
Err error ->
|
||||||
@ -198,10 +205,28 @@ view pathKey content viewFn model =
|
|||||||
, body =
|
, body =
|
||||||
[ onViewChangeElement model.url
|
[ onViewChangeElement model.url
|
||||||
, body |> Html.map UserMsg |> Html.map AppMsg
|
, body |> Html.map UserMsg |> Html.map AppMsg
|
||||||
|
, Html.Lazy.lazy2 loadingView model.phase model.hmrStatus
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
loadingView : Phase -> HmrStatus -> Html msg
|
||||||
|
loadingView phase hmrStatus =
|
||||||
|
case phase of
|
||||||
|
DevClient isDebugMode ->
|
||||||
|
(case hmrStatus of
|
||||||
|
HmrLoading ->
|
||||||
|
True
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|> HotReloadLoadingIndicator.view isDebugMode
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Html.text ""
|
||||||
|
|
||||||
|
|
||||||
onViewChangeElement currentUrl =
|
onViewChangeElement currentUrl =
|
||||||
-- this is a hidden tag
|
-- this is a hidden tag
|
||||||
-- it is used from the JS-side to reliably
|
-- it is used from the JS-side to reliably
|
||||||
@ -224,6 +249,13 @@ type alias ContentJson =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
contentJsonDecoder : Decode.Decoder ContentJson
|
||||||
|
contentJsonDecoder =
|
||||||
|
Decode.map2 ContentJson
|
||||||
|
(Decode.field "body" Decode.string)
|
||||||
|
(Decode.field "staticData" (Decode.dict Decode.string))
|
||||||
|
|
||||||
|
|
||||||
init :
|
init :
|
||||||
pathKey
|
pathKey
|
||||||
-> String
|
-> String
|
||||||
@ -273,12 +305,6 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
|||||||
|> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder)
|
|> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder)
|
||||||
|> Result.toMaybe
|
|> Result.toMaybe
|
||||||
|
|
||||||
contentJsonDecoder : Decode.Decoder ContentJson
|
|
||||||
contentJsonDecoder =
|
|
||||||
Decode.map2 ContentJson
|
|
||||||
(Decode.field "body" Decode.string)
|
|
||||||
(Decode.field "staticData" (Decode.dict Decode.string))
|
|
||||||
|
|
||||||
baseUrl =
|
baseUrl =
|
||||||
flags
|
flags
|
||||||
|> Decode.decodeValue (Decode.field "baseUrl" Decode.string)
|
|> Decode.decodeValue (Decode.field "baseUrl" Decode.string)
|
||||||
@ -295,15 +321,26 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
|||||||
Ok okCache ->
|
Ok okCache ->
|
||||||
let
|
let
|
||||||
phase =
|
phase =
|
||||||
case Decode.decodeValue (Decode.field "isPrerendering" Decode.bool) flags of
|
case
|
||||||
Ok True ->
|
Decode.decodeValue
|
||||||
|
(Decode.map3 (\a b c -> ( a, b, c ))
|
||||||
|
(Decode.field "isPrerendering" Decode.bool)
|
||||||
|
(Decode.field "isDevServer" Decode.bool)
|
||||||
|
(Decode.field "isElmDebugMode" Decode.bool)
|
||||||
|
)
|
||||||
|
flags
|
||||||
|
of
|
||||||
|
Ok ( True, _, _ ) ->
|
||||||
Prerender
|
Prerender
|
||||||
|
|
||||||
Ok False ->
|
Ok ( False, True, isElmDebugMode ) ->
|
||||||
Client
|
DevClient isElmDebugMode
|
||||||
|
|
||||||
|
Ok ( False, False, _ ) ->
|
||||||
|
ProdClient
|
||||||
|
|
||||||
Err _ ->
|
Err _ ->
|
||||||
Client
|
DevClient False
|
||||||
|
|
||||||
( userModel, userCmd ) =
|
( userModel, userCmd ) =
|
||||||
maybePagePath
|
maybePagePath
|
||||||
@ -347,6 +384,7 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
|||||||
, userModel = userModel
|
, userModel = userModel
|
||||||
, contentCache = contentCache
|
, contentCache = contentCache
|
||||||
, phase = phase
|
, phase = phase
|
||||||
|
, hmrStatus = HmrLoaded
|
||||||
}
|
}
|
||||||
, cmd
|
, cmd
|
||||||
)
|
)
|
||||||
@ -361,7 +399,8 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
|||||||
, baseUrl = baseUrl
|
, baseUrl = baseUrl
|
||||||
, userModel = userModel
|
, userModel = userModel
|
||||||
, contentCache = contentCache
|
, contentCache = contentCache
|
||||||
, phase = Client
|
, phase = DevClient False
|
||||||
|
, hmrStatus = HmrLoaded
|
||||||
}
|
}
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ userCmd |> Cmd.map UserMsg
|
[ userCmd |> Cmd.map UserMsg
|
||||||
@ -389,7 +428,10 @@ type AppMsg userMsg metadata view
|
|||||||
| UserMsg userMsg
|
| UserMsg userMsg
|
||||||
| UpdateCache (Result Http.Error (ContentCache metadata view))
|
| UpdateCache (Result Http.Error (ContentCache metadata view))
|
||||||
| UpdateCacheAndUrl Url (Result Http.Error (ContentCache metadata view))
|
| UpdateCacheAndUrl Url (Result Http.Error (ContentCache metadata view))
|
||||||
|
| UpdateCacheForHotReload (Result Http.Error (ContentCache metadata view))
|
||||||
| PageScrollComplete
|
| PageScrollComplete
|
||||||
|
| HotReloadComplete ContentJson
|
||||||
|
| StartingHotReload
|
||||||
|
|
||||||
|
|
||||||
type Model userModel userMsg metadata view
|
type Model userModel userMsg metadata view
|
||||||
@ -404,16 +446,19 @@ type alias ModelDetails userModel metadata view =
|
|||||||
, contentCache : ContentCache metadata view
|
, contentCache : ContentCache metadata view
|
||||||
, userModel : userModel
|
, userModel : userModel
|
||||||
, phase : Phase
|
, phase : Phase
|
||||||
|
, hmrStatus : HmrStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Phase
|
type Phase
|
||||||
= Prerender
|
= Prerender
|
||||||
| Client
|
| DevClient Bool
|
||||||
|
| ProdClient
|
||||||
|
|
||||||
|
|
||||||
update :
|
update :
|
||||||
List String
|
Content
|
||||||
|
-> List String
|
||||||
-> String
|
-> String
|
||||||
->
|
->
|
||||||
(List ( PagePath pathKey, metadata )
|
(List ( PagePath pathKey, metadata )
|
||||||
@ -442,7 +487,7 @@ update :
|
|||||||
-> Msg userMsg metadata view
|
-> Msg userMsg metadata view
|
||||||
-> ModelDetails userModel metadata view
|
-> ModelDetails userModel metadata view
|
||||||
-> ( ModelDetails userModel metadata view, Cmd (AppMsg userMsg metadata view) )
|
-> ( ModelDetails userModel metadata view, Cmd (AppMsg userMsg metadata view) )
|
||||||
update allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJsPort document userUpdate msg model =
|
update content allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJsPort document userUpdate msg model =
|
||||||
case msg of
|
case msg of
|
||||||
AppMsg appMsg ->
|
AppMsg appMsg ->
|
||||||
case appMsg of
|
case appMsg of
|
||||||
@ -535,7 +580,7 @@ update allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJs
|
|||||||
)
|
)
|
||||||
{ path = pagePath, frontmatter = frontmatter }
|
{ path = pagePath, frontmatter = frontmatter }
|
||||||
|> (\request ->
|
|> (\request ->
|
||||||
StaticHttpRequest.resolve request staticDataThing
|
StaticHttpRequest.resolve ApplicationType.Browser request staticDataThing
|
||||||
)
|
)
|
||||||
in
|
in
|
||||||
( { model | contentCache = updatedCache }
|
( { model | contentCache = updatedCache }
|
||||||
@ -584,18 +629,42 @@ update allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJs
|
|||||||
-- TODO handle error
|
-- TODO handle error
|
||||||
( { model | url = url }, Cmd.none )
|
( { model | url = url }, Cmd.none )
|
||||||
|
|
||||||
|
UpdateCacheForHotReload cacheUpdateResult ->
|
||||||
|
case cacheUpdateResult of
|
||||||
|
Ok updatedCache ->
|
||||||
|
( { model | contentCache = updatedCache }, Cmd.none )
|
||||||
|
|
||||||
|
Err _ ->
|
||||||
|
-- TODO handle error
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
PageScrollComplete ->
|
PageScrollComplete ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
HotReloadComplete contentJson ->
|
||||||
|
( { model
|
||||||
|
| contentCache = ContentCache.init document content (Just { contentJson = contentJson, initialUrl = model.url })
|
||||||
|
, hmrStatus = HmrLoaded
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
-- ContentCache.init document content (Maybe.map (\cj -> { contentJson = contentJson, initialUrl = model.url }) Nothing)
|
||||||
|
--|> ContentCache.lazyLoad document
|
||||||
|
-- { currentUrl = model.url
|
||||||
|
-- , baseUrl = model.baseUrl
|
||||||
|
-- }
|
||||||
|
--|> Task.attempt UpdateCacheForHotReload
|
||||||
|
)
|
||||||
|
|
||||||
|
StartingHotReload ->
|
||||||
|
( { model | hmrStatus = HmrLoading }, Cmd.none )
|
||||||
|
|
||||||
CliMsg _ ->
|
CliMsg _ ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
type alias Parser metadata view =
|
type HmrStatus
|
||||||
Dict String String
|
= HmrLoading
|
||||||
-> List String
|
| HmrLoaded
|
||||||
-> List ( List String, metadata )
|
|
||||||
-> Mark.Document view
|
|
||||||
|
|
||||||
|
|
||||||
application :
|
application :
|
||||||
@ -622,6 +691,7 @@ application :
|
|||||||
, document : Pages.Document.Document metadata view
|
, document : Pages.Document.Document metadata view
|
||||||
, content : Content
|
, content : Content
|
||||||
, toJsPort : Json.Encode.Value -> Cmd Never
|
, toJsPort : Json.Encode.Value -> Cmd Never
|
||||||
|
, fromJsPort : Sub Decode.Value
|
||||||
, manifest : Manifest.Config pathKey
|
, manifest : Manifest.Config pathKey
|
||||||
, generateFiles :
|
, generateFiles :
|
||||||
List
|
List
|
||||||
@ -678,7 +748,7 @@ application config =
|
|||||||
Prerender ->
|
Prerender ->
|
||||||
noOpUpdate
|
noOpUpdate
|
||||||
|
|
||||||
Client ->
|
_ ->
|
||||||
config.update
|
config.update
|
||||||
|
|
||||||
noOpUpdate =
|
noOpUpdate =
|
||||||
@ -690,7 +760,7 @@ application config =
|
|||||||
|> List.map Tuple.first
|
|> List.map Tuple.first
|
||||||
|> List.map (String.join "/")
|
|> List.map (String.join "/")
|
||||||
in
|
in
|
||||||
update allRoutes config.canonicalSiteUrl config.view config.pathKey config.onPageChange config.toJsPort config.document userUpdate msg model
|
update config.content allRoutes config.canonicalSiteUrl config.view config.pathKey config.onPageChange config.toJsPort config.document userUpdate msg model
|
||||||
|> Tuple.mapFirst Model
|
|> Tuple.mapFirst Model
|
||||||
|> Tuple.mapSecond (Cmd.map AppMsg)
|
|> Tuple.mapSecond (Cmd.map AppMsg)
|
||||||
|
|
||||||
@ -700,9 +770,27 @@ application config =
|
|||||||
\outerModel ->
|
\outerModel ->
|
||||||
case outerModel of
|
case outerModel of
|
||||||
Model model ->
|
Model model ->
|
||||||
config.subscriptions model.userModel
|
Sub.batch
|
||||||
|> Sub.map UserMsg
|
[ config.subscriptions model.userModel
|
||||||
|> Sub.map AppMsg
|
|> Sub.map UserMsg
|
||||||
|
|> Sub.map AppMsg
|
||||||
|
, config.fromJsPort
|
||||||
|
|> Sub.map
|
||||||
|
(\decodeValue ->
|
||||||
|
case decodeValue |> Decode.decodeValue (Decode.field "thingy" Decode.string) of
|
||||||
|
Ok "hmr-check" ->
|
||||||
|
AppMsg StartingHotReload
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
case decodeValue |> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder) of
|
||||||
|
Ok contentJson ->
|
||||||
|
AppMsg (HotReloadComplete contentJson)
|
||||||
|
|
||||||
|
Err error ->
|
||||||
|
-- TODO should be no message here
|
||||||
|
AppMsg StartingHotReload
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
CliModel _ ->
|
CliModel _ ->
|
||||||
Sub.none
|
Sub.none
|
||||||
@ -735,6 +823,7 @@ cliApplication :
|
|||||||
, document : Pages.Document.Document metadata view
|
, document : Pages.Document.Document metadata view
|
||||||
, content : Content
|
, content : Content
|
||||||
, toJsPort : Json.Encode.Value -> Cmd Never
|
, toJsPort : Json.Encode.Value -> Cmd Never
|
||||||
|
, fromJsPort : Sub Decode.Value
|
||||||
, manifest : Manifest.Config pathKey
|
, manifest : Manifest.Config pathKey
|
||||||
, generateFiles :
|
, generateFiles :
|
||||||
List
|
List
|
||||||
|
@ -26,6 +26,7 @@ import Pages.ContentCache as ContentCache exposing (ContentCache)
|
|||||||
import Pages.Document
|
import Pages.Document
|
||||||
import Pages.Http
|
import Pages.Http
|
||||||
import Pages.ImagePath as ImagePath
|
import Pages.ImagePath as ImagePath
|
||||||
|
import Pages.Internal.ApplicationType as ApplicationType exposing (ApplicationType)
|
||||||
import Pages.Internal.StaticHttpBody as StaticHttpBody
|
import Pages.Internal.StaticHttpBody as StaticHttpBody
|
||||||
import Pages.Manifest as Manifest
|
import Pages.Manifest as Manifest
|
||||||
import Pages.PagePath as PagePath exposing (PagePath)
|
import Pages.PagePath as PagePath exposing (PagePath)
|
||||||
@ -47,6 +48,7 @@ type alias ToJsSuccessPayload pathKey =
|
|||||||
{ pages : Dict String (Dict String String)
|
{ pages : Dict String (Dict String String)
|
||||||
, manifest : Manifest.Config pathKey
|
, manifest : Manifest.Config pathKey
|
||||||
, filesToGenerate : List FileToGenerate
|
, filesToGenerate : List FileToGenerate
|
||||||
|
, staticHttpCache : Dict String String
|
||||||
, errors : List String
|
, errors : List String
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,8 +67,8 @@ toJsCodec =
|
|||||||
Errors errorList ->
|
Errors errorList ->
|
||||||
errorsTag errorList
|
errorsTag errorList
|
||||||
|
|
||||||
Success { pages, manifest, filesToGenerate, errors } ->
|
Success { pages, manifest, filesToGenerate, errors, staticHttpCache } ->
|
||||||
success (ToJsSuccessPayload pages manifest filesToGenerate errors)
|
success (ToJsSuccessPayload pages manifest filesToGenerate staticHttpCache errors)
|
||||||
)
|
)
|
||||||
|> Codec.variant1 "Errors" Errors Codec.string
|
|> Codec.variant1 "Errors" Errors Codec.string
|
||||||
|> Codec.variant1 "Success"
|
|> Codec.variant1 "Success"
|
||||||
@ -115,6 +117,9 @@ successCodec =
|
|||||||
)
|
)
|
||||||
(Decode.succeed [])
|
(Decode.succeed [])
|
||||||
)
|
)
|
||||||
|
|> Codec.field "staticHttpCache"
|
||||||
|
.staticHttpCache
|
||||||
|
(Codec.dict Codec.string)
|
||||||
|> Codec.field "errors" .errors (Codec.list Codec.string)
|
|> Codec.field "errors" .errors (Codec.list Codec.string)
|
||||||
|> Codec.buildObject
|
|> Codec.buildObject
|
||||||
|
|
||||||
@ -178,6 +183,7 @@ type alias Config pathKey userMsg userModel metadata view =
|
|||||||
, document : Pages.Document.Document metadata view
|
, document : Pages.Document.Document metadata view
|
||||||
, content : Content
|
, content : Content
|
||||||
, toJsPort : Json.Encode.Value -> Cmd Never
|
, toJsPort : Json.Encode.Value -> Cmd Never
|
||||||
|
, fromJsPort : Sub Decode.Value
|
||||||
, manifest : Manifest.Config pathKey
|
, manifest : Manifest.Config pathKey
|
||||||
, generateFiles :
|
, generateFiles :
|
||||||
List
|
List
|
||||||
@ -320,13 +326,20 @@ init :
|
|||||||
init toModel contentCache siteMetadata config flags =
|
init toModel contentCache siteMetadata config flags =
|
||||||
case
|
case
|
||||||
Decode.decodeValue
|
Decode.decodeValue
|
||||||
(Decode.map2 Tuple.pair
|
(Decode.map3 (\a b c -> ( a, b, c ))
|
||||||
(Decode.field "secrets" SecretsDict.decoder)
|
(Decode.field "secrets" SecretsDict.decoder)
|
||||||
(Decode.field "mode" modeDecoder)
|
(Decode.field "mode" modeDecoder)
|
||||||
|
(Decode.field "staticHttpCache"
|
||||||
|
(Decode.dict
|
||||||
|
(Decode.string
|
||||||
|
|> Decode.map Just
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
flags
|
flags
|
||||||
of
|
of
|
||||||
Ok ( secrets, mode ) ->
|
Ok ( secrets, mode, staticHttpCache ) ->
|
||||||
case contentCache of
|
case contentCache of
|
||||||
Ok _ ->
|
Ok _ ->
|
||||||
case ContentCache.pagesWithErrors contentCache of
|
case ContentCache.pagesWithErrors contentCache of
|
||||||
@ -343,14 +356,14 @@ init toModel contentCache siteMetadata config flags =
|
|||||||
staticResponses =
|
staticResponses =
|
||||||
case requests of
|
case requests of
|
||||||
Ok okRequests ->
|
Ok okRequests ->
|
||||||
staticResponsesInit siteMetadata config okRequests
|
staticResponsesInit staticHttpCache siteMetadata config okRequests
|
||||||
|
|
||||||
Err errors ->
|
Err errors ->
|
||||||
-- TODO need to handle errors better?
|
-- TODO need to handle errors better?
|
||||||
staticResponsesInit siteMetadata config []
|
staticResponsesInit staticHttpCache siteMetadata config []
|
||||||
|
|
||||||
( updatedRawResponses, effect ) =
|
( updatedRawResponses, effect ) =
|
||||||
sendStaticResponsesIfDone config siteMetadata mode secrets Dict.empty [] staticResponses
|
sendStaticResponsesIfDone config siteMetadata mode secrets staticHttpCache [] staticResponses
|
||||||
in
|
in
|
||||||
( Model staticResponses secrets [] updatedRawResponses mode |> toModel
|
( Model staticResponses secrets [] updatedRawResponses mode |> toModel
|
||||||
, effect
|
, effect
|
||||||
@ -369,11 +382,11 @@ init toModel contentCache siteMetadata config flags =
|
|||||||
staticResponses =
|
staticResponses =
|
||||||
case requests of
|
case requests of
|
||||||
Ok okRequests ->
|
Ok okRequests ->
|
||||||
staticResponsesInit siteMetadata config okRequests
|
staticResponsesInit staticHttpCache siteMetadata config okRequests
|
||||||
|
|
||||||
Err errors ->
|
Err errors ->
|
||||||
-- TODO need to handle errors better?
|
-- TODO need to handle errors better?
|
||||||
staticResponsesInit siteMetadata config []
|
staticResponsesInit staticHttpCache siteMetadata config []
|
||||||
in
|
in
|
||||||
updateAndSendPortIfDone
|
updateAndSendPortIfDone
|
||||||
config
|
config
|
||||||
@ -382,7 +395,7 @@ init toModel contentCache siteMetadata config flags =
|
|||||||
staticResponses
|
staticResponses
|
||||||
secrets
|
secrets
|
||||||
pageErrors
|
pageErrors
|
||||||
Dict.empty
|
staticHttpCache
|
||||||
mode
|
mode
|
||||||
)
|
)
|
||||||
toModel
|
toModel
|
||||||
@ -394,7 +407,7 @@ init toModel contentCache siteMetadata config flags =
|
|||||||
(Model Dict.empty
|
(Model Dict.empty
|
||||||
secrets
|
secrets
|
||||||
(metadataParserErrors |> List.map Tuple.second)
|
(metadataParserErrors |> List.map Tuple.second)
|
||||||
Dict.empty
|
staticHttpCache
|
||||||
mode
|
mode
|
||||||
)
|
)
|
||||||
toModel
|
toModel
|
||||||
@ -528,7 +541,7 @@ performStaticHttpRequests allRawResponses secrets staticRequests =
|
|||||||
(\( pagePath, request ) ->
|
(\( pagePath, request ) ->
|
||||||
allRawResponses
|
allRawResponses
|
||||||
|> dictCompact
|
|> dictCompact
|
||||||
|> StaticHttpRequest.resolveUrls request
|
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|
||||||
|> Tuple.second
|
|> Tuple.second
|
||||||
)
|
)
|
||||||
|> List.concat
|
|> List.concat
|
||||||
@ -580,18 +593,23 @@ cliDictKey =
|
|||||||
"////elm-pages-CLI////"
|
"////elm-pages-CLI////"
|
||||||
|
|
||||||
|
|
||||||
staticResponsesInit : Result (List BuildError) (List ( PagePath pathKey, metadata )) -> Config pathKey userMsg userModel metadata view -> List ( PagePath pathKey, StaticHttp.Request value ) -> StaticResponses
|
staticResponsesInit : Dict String (Maybe String) -> Result (List BuildError) (List ( PagePath pathKey, metadata )) -> Config pathKey userMsg userModel metadata view -> List ( PagePath pathKey, StaticHttp.Request value ) -> StaticResponses
|
||||||
staticResponsesInit siteMetadata config list =
|
staticResponsesInit staticHttpCache siteMetadataResult config list =
|
||||||
let
|
let
|
||||||
foo : StaticHttp.Request (List (Result String { path : List String, content : String }))
|
generateFilesRequest : StaticHttp.Request (List (Result String { path : List String, content : String }))
|
||||||
foo =
|
generateFilesRequest =
|
||||||
config.generateFiles thing2
|
config.generateFiles siteMetadataWithContent
|
||||||
|
|
||||||
generateFilesStaticRequest =
|
generateFilesStaticRequest =
|
||||||
( cliDictKey, NotFetched (foo |> StaticHttp.map (\_ -> ())) Dict.empty )
|
( -- we don't want to include the CLI-only StaticHttp responses in the production bundle
|
||||||
|
-- since that data is only needed to run these functions during the build step
|
||||||
|
-- in the future, this could be refactored to have a type to represent this more clearly
|
||||||
|
cliDictKey
|
||||||
|
, NotFetched (generateFilesRequest |> StaticHttp.map (\_ -> ())) Dict.empty
|
||||||
|
)
|
||||||
|
|
||||||
thing2 =
|
siteMetadataWithContent =
|
||||||
siteMetadata
|
siteMetadataResult
|
||||||
|> Result.withDefault []
|
|> Result.withDefault []
|
||||||
|> List.map
|
|> List.map
|
||||||
(\( pagePath, metadata ) ->
|
(\( pagePath, metadata ) ->
|
||||||
@ -625,8 +643,26 @@ staticResponsesInit siteMetadata config list =
|
|||||||
list
|
list
|
||||||
|> List.map
|
|> List.map
|
||||||
(\( path, staticRequest ) ->
|
(\( path, staticRequest ) ->
|
||||||
|
let
|
||||||
|
entry =
|
||||||
|
NotFetched (staticRequest |> StaticHttp.map (\_ -> ())) Dict.empty
|
||||||
|
|
||||||
|
updatedEntry =
|
||||||
|
staticHttpCache
|
||||||
|
|> dictCompact
|
||||||
|
|> Dict.toList
|
||||||
|
|> List.foldl
|
||||||
|
(\( hashedRequest, response ) entrySoFar ->
|
||||||
|
entrySoFar
|
||||||
|
|> addEntry
|
||||||
|
staticHttpCache
|
||||||
|
hashedRequest
|
||||||
|
(Ok response)
|
||||||
|
)
|
||||||
|
entry
|
||||||
|
in
|
||||||
( PagePath.toString path
|
( PagePath.toString path
|
||||||
, NotFetched (staticRequest |> StaticHttp.map (\_ -> ())) Dict.empty
|
, updatedEntry
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> List.append [ generateFilesStaticRequest ]
|
|> List.append [ generateFilesStaticRequest ]
|
||||||
@ -654,7 +690,7 @@ staticResponsesUpdate newEntry model =
|
|||||||
realUrls =
|
realUrls =
|
||||||
updatedAllResponses
|
updatedAllResponses
|
||||||
|> dictCompact
|
|> dictCompact
|
||||||
|> StaticHttpRequest.resolveUrls request
|
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|
||||||
|> Tuple.second
|
|> Tuple.second
|
||||||
|> List.map Secrets.maskedLookup
|
|> List.map Secrets.maskedLookup
|
||||||
|> List.map HashRequest.hash
|
|> List.map HashRequest.hash
|
||||||
@ -681,6 +717,36 @@ staticResponsesUpdate newEntry model =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
addEntry : Dict String (Maybe String) -> String -> Result () String -> StaticHttpResult -> StaticHttpResult
|
||||||
|
addEntry globalRawResponses hashedRequest rawResponse ((NotFetched request rawResponses) as entry) =
|
||||||
|
let
|
||||||
|
realUrls =
|
||||||
|
globalRawResponses
|
||||||
|
|> dictCompact
|
||||||
|
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|
||||||
|
|> Tuple.second
|
||||||
|
|> List.map Secrets.maskedLookup
|
||||||
|
|> List.map HashRequest.hash
|
||||||
|
|
||||||
|
includesUrl =
|
||||||
|
List.member
|
||||||
|
hashedRequest
|
||||||
|
realUrls
|
||||||
|
in
|
||||||
|
if includesUrl then
|
||||||
|
let
|
||||||
|
updatedRawResponses =
|
||||||
|
Dict.insert
|
||||||
|
hashedRequest
|
||||||
|
rawResponse
|
||||||
|
rawResponses
|
||||||
|
in
|
||||||
|
NotFetched request updatedRawResponses
|
||||||
|
|
||||||
|
else
|
||||||
|
entry
|
||||||
|
|
||||||
|
|
||||||
isJust : Maybe a -> Bool
|
isJust : Maybe a -> Bool
|
||||||
isJust maybeValue =
|
isJust maybeValue =
|
||||||
case maybeValue of
|
case maybeValue of
|
||||||
@ -721,7 +787,7 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
|||||||
|
|
||||||
hasPermanentError =
|
hasPermanentError =
|
||||||
usableRawResponses
|
usableRawResponses
|
||||||
|> StaticHttpRequest.permanentError request
|
|> StaticHttpRequest.permanentError ApplicationType.Cli request
|
||||||
|> isJust
|
|> isJust
|
||||||
|
|
||||||
hasPermanentHttpError =
|
hasPermanentHttpError =
|
||||||
@ -737,7 +803,9 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
|||||||
-- False
|
-- False
|
||||||
-- )
|
-- )
|
||||||
( allUrlsKnown, knownUrlsToFetch ) =
|
( allUrlsKnown, knownUrlsToFetch ) =
|
||||||
StaticHttpRequest.resolveUrls request
|
StaticHttpRequest.resolveUrls
|
||||||
|
ApplicationType.Cli
|
||||||
|
request
|
||||||
(rawResponses |> Dict.map (\key value -> value |> Result.withDefault ""))
|
(rawResponses |> Dict.map (\key value -> value |> Result.withDefault ""))
|
||||||
|
|
||||||
fetchedAllKnownUrls =
|
fetchedAllKnownUrls =
|
||||||
@ -773,7 +841,9 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
|||||||
)
|
)
|
||||||
|
|
||||||
maybePermanentError =
|
maybePermanentError =
|
||||||
StaticHttpRequest.permanentError request
|
StaticHttpRequest.permanentError
|
||||||
|
ApplicationType.Cli
|
||||||
|
request
|
||||||
usableRawResponses
|
usableRawResponses
|
||||||
|
|
||||||
decoderErrors =
|
decoderErrors =
|
||||||
@ -893,7 +963,8 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
|||||||
|
|
||||||
mythingy2 : Result StaticHttpRequest.Error (List (Result String { path : List String, content : String }))
|
mythingy2 : Result StaticHttpRequest.Error (List (Result String { path : List String, content : String }))
|
||||||
mythingy2 =
|
mythingy2 =
|
||||||
StaticHttpRequest.resolve (config.generateFiles metadataForGenerateFiles)
|
StaticHttpRequest.resolve ApplicationType.Cli
|
||||||
|
(config.generateFiles metadataForGenerateFiles)
|
||||||
(allRawResponses |> Dict.Extra.filterMap (\key value -> value))
|
(allRawResponses |> Dict.Extra.filterMap (\key value -> value))
|
||||||
|
|
||||||
generatedOkayFiles : List { path : List String, content : String }
|
generatedOkayFiles : List { path : List String, content : String }
|
||||||
@ -938,11 +1009,19 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
|||||||
(encodeStaticResponses mode staticResponses)
|
(encodeStaticResponses mode staticResponses)
|
||||||
config.manifest
|
config.manifest
|
||||||
generatedOkayFiles
|
generatedOkayFiles
|
||||||
|
allRawResponses
|
||||||
allErrors
|
allErrors
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
toJsPayload encodedStatic manifest generated allErrors =
|
toJsPayload :
|
||||||
|
Dict String (Dict String String)
|
||||||
|
-> Manifest.Config pathKey
|
||||||
|
-> List FileToGenerate
|
||||||
|
-> Dict String (Maybe String)
|
||||||
|
-> List { title : String, message : List Terminal.Text, fatal : Bool }
|
||||||
|
-> Effect pathKey
|
||||||
|
toJsPayload encodedStatic manifest generated allRawResponses allErrors =
|
||||||
SendJsData <|
|
SendJsData <|
|
||||||
if allErrors |> List.filter .fatal |> List.isEmpty then
|
if allErrors |> List.filter .fatal |> List.isEmpty then
|
||||||
Success
|
Success
|
||||||
@ -950,6 +1029,15 @@ toJsPayload encodedStatic manifest generated allErrors =
|
|||||||
encodedStatic
|
encodedStatic
|
||||||
manifest
|
manifest
|
||||||
generated
|
generated
|
||||||
|
(allRawResponses
|
||||||
|
|> Dict.toList
|
||||||
|
|> List.filterMap
|
||||||
|
(\( key, maybeValue ) ->
|
||||||
|
maybeValue
|
||||||
|
|> Maybe.map (\value -> ( key, value ))
|
||||||
|
)
|
||||||
|
|> Dict.fromList
|
||||||
|
)
|
||||||
(List.map BuildError.errorToString allErrors)
|
(List.map BuildError.errorToString allErrors)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -980,7 +1068,7 @@ encodeStaticResponses mode staticResponses =
|
|||||||
strippedResponses : Dict String String
|
strippedResponses : Dict String String
|
||||||
strippedResponses =
|
strippedResponses =
|
||||||
-- TODO should this return an Err and handle that here?
|
-- TODO should this return an Err and handle that here?
|
||||||
StaticHttpRequest.strippedResponses request relevantResponses
|
StaticHttpRequest.strippedResponses ApplicationType.Cli request relevantResponses
|
||||||
in
|
in
|
||||||
case mode of
|
case mode of
|
||||||
Dev ->
|
Dev ->
|
||||||
|
@ -322,6 +322,7 @@ application config =
|
|||||||
, content = config.internals.content
|
, content = config.internals.content
|
||||||
, generateFiles = config.generateFiles
|
, generateFiles = config.generateFiles
|
||||||
, toJsPort = config.internals.toJsPort
|
, toJsPort = config.internals.toJsPort
|
||||||
|
, fromJsPort = config.internals.fromJsPort
|
||||||
, manifest = config.manifest
|
, manifest = config.manifest
|
||||||
, canonicalSiteUrl = config.canonicalSiteUrl
|
, canonicalSiteUrl = config.canonicalSiteUrl
|
||||||
, onPageChange = config.onPageChange
|
, onPageChange = config.onPageChange
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
module Pages.StaticHttp exposing
|
module Pages.StaticHttp exposing
|
||||||
( Request, RequestDetails
|
( Request, RequestDetails
|
||||||
, get, request
|
, get, request
|
||||||
, map, succeed
|
, map, succeed, fail
|
||||||
, Body, emptyBody, stringBody, jsonBody
|
, Body, emptyBody, stringBody, jsonBody
|
||||||
, andThen, resolve, combine
|
, andThen, resolve, combine
|
||||||
, map2, map3, map4, map5, map6, map7, map8, map9
|
, map2, map3, map4, map5, map6, map7, map8, map9
|
||||||
@ -40,7 +40,7 @@ in [this article introducing StaticHttp requests and some concepts around it](ht
|
|||||||
|
|
||||||
@docs Request, RequestDetails
|
@docs Request, RequestDetails
|
||||||
@docs get, request
|
@docs get, request
|
||||||
@docs map, succeed
|
@docs map, succeed, fail
|
||||||
|
|
||||||
|
|
||||||
## Building a StaticHttp Request Body
|
## Building a StaticHttp Request Body
|
||||||
@ -76,9 +76,12 @@ your decoders. This can significantly reduce download sizes for your StaticHttp
|
|||||||
|
|
||||||
import Dict exposing (Dict)
|
import Dict exposing (Dict)
|
||||||
import Dict.Extra
|
import Dict.Extra
|
||||||
|
import Internal.OptimizedDecoder
|
||||||
import Json.Decode
|
import Json.Decode
|
||||||
import Json.Decode.Exploration as Decode exposing (Decoder)
|
import Json.Decode.Exploration
|
||||||
import Json.Encode as Encode
|
import Json.Encode as Encode
|
||||||
|
import OptimizedDecoder as Decode exposing (Decoder)
|
||||||
|
import Pages.Internal.ApplicationType as ApplicationType exposing (ApplicationType)
|
||||||
import Pages.Internal.StaticHttpBody as Body
|
import Pages.Internal.StaticHttpBody as Body
|
||||||
import Pages.Secrets
|
import Pages.Secrets
|
||||||
import Pages.StaticHttp.Request as HashRequest
|
import Pages.StaticHttp.Request as HashRequest
|
||||||
@ -155,8 +158,8 @@ map fn requestInfo =
|
|||||||
Request ( urls, lookupFn ) ->
|
Request ( urls, lookupFn ) ->
|
||||||
Request
|
Request
|
||||||
( urls
|
( urls
|
||||||
, \rawResponses ->
|
, \appType rawResponses ->
|
||||||
lookupFn rawResponses
|
lookupFn appType rawResponses
|
||||||
|> Result.map (\( partiallyStripped, nextRequest ) -> ( partiallyStripped, map fn nextRequest ))
|
|> Result.map (\( partiallyStripped, nextRequest ) -> ( partiallyStripped, map fn nextRequest ))
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -238,24 +241,24 @@ map2 fn request1 request2 =
|
|||||||
case ( request1, request2 ) of
|
case ( request1, request2 ) of
|
||||||
( Request ( urls1, lookupFn1 ), Request ( urls2, lookupFn2 ) ) ->
|
( Request ( urls1, lookupFn1 ), Request ( urls2, lookupFn2 ) ) ->
|
||||||
let
|
let
|
||||||
value : Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, Request c )
|
value : ApplicationType -> Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, Request c )
|
||||||
value rawResponses =
|
value appType rawResponses =
|
||||||
let
|
let
|
||||||
value1 =
|
value1 =
|
||||||
lookupFn1 rawResponses
|
lookupFn1 appType rawResponses
|
||||||
|> Result.map Tuple.second
|
|> Result.map Tuple.second
|
||||||
|
|
||||||
value2 =
|
value2 =
|
||||||
lookupFn2 rawResponses
|
lookupFn2 appType rawResponses
|
||||||
|> Result.map Tuple.second
|
|> Result.map Tuple.second
|
||||||
|
|
||||||
dict1 =
|
dict1 =
|
||||||
lookupFn1 rawResponses
|
lookupFn1 appType rawResponses
|
||||||
|> Result.map Tuple.first
|
|> Result.map Tuple.first
|
||||||
|> Result.withDefault Dict.empty
|
|> Result.withDefault Dict.empty
|
||||||
|
|
||||||
dict2 =
|
dict2 =
|
||||||
lookupFn2 rawResponses
|
lookupFn2 appType rawResponses
|
||||||
|> Result.map Tuple.first
|
|> Result.map Tuple.first
|
||||||
|> Result.withDefault Dict.empty
|
|> Result.withDefault Dict.empty
|
||||||
in
|
in
|
||||||
@ -274,14 +277,14 @@ map2 fn request1 request2 =
|
|||||||
( Request ( urls1, lookupFn1 ), Done value2 ) ->
|
( Request ( urls1, lookupFn1 ), Done value2 ) ->
|
||||||
Request
|
Request
|
||||||
( urls1
|
( urls1
|
||||||
, \rawResponses ->
|
, \appType rawResponses ->
|
||||||
let
|
let
|
||||||
value1 =
|
value1 =
|
||||||
lookupFn1 rawResponses
|
lookupFn1 appType rawResponses
|
||||||
|> Result.map Tuple.second
|
|> Result.map Tuple.second
|
||||||
|
|
||||||
dict1 =
|
dict1 =
|
||||||
lookupFn1 rawResponses
|
lookupFn1 appType rawResponses
|
||||||
|> Result.map Tuple.first
|
|> Result.map Tuple.first
|
||||||
|> Result.withDefault Dict.empty
|
|> Result.withDefault Dict.empty
|
||||||
in
|
in
|
||||||
@ -296,14 +299,14 @@ map2 fn request1 request2 =
|
|||||||
( Done value2, Request ( urls1, lookupFn1 ) ) ->
|
( Done value2, Request ( urls1, lookupFn1 ) ) ->
|
||||||
Request
|
Request
|
||||||
( urls1
|
( urls1
|
||||||
, \rawResponses ->
|
, \appType rawResponses ->
|
||||||
let
|
let
|
||||||
value1 =
|
value1 =
|
||||||
lookupFn1 rawResponses
|
lookupFn1 appType rawResponses
|
||||||
|> Result.map Tuple.second
|
|> Result.map Tuple.second
|
||||||
|
|
||||||
dict1 =
|
dict1 =
|
||||||
lookupFn1 rawResponses
|
lookupFn1 appType rawResponses
|
||||||
|> Result.map Tuple.first
|
|> Result.map Tuple.first
|
||||||
|> Result.withDefault Dict.empty
|
|> Result.withDefault Dict.empty
|
||||||
in
|
in
|
||||||
@ -336,14 +339,14 @@ combineReducedDicts dict1 dict2 =
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
lookup : Pages.StaticHttpRequest.Request value -> Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, value )
|
lookup : ApplicationType -> Pages.StaticHttpRequest.Request value -> Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, value )
|
||||||
lookup requestInfo rawResponses =
|
lookup appType requestInfo rawResponses =
|
||||||
case requestInfo of
|
case requestInfo of
|
||||||
Request ( urls, lookupFn ) ->
|
Request ( urls, lookupFn ) ->
|
||||||
lookupFn rawResponses
|
lookupFn appType rawResponses
|
||||||
|> Result.andThen
|
|> Result.andThen
|
||||||
(\( strippedResponses, nextRequest ) ->
|
(\( strippedResponses, nextRequest ) ->
|
||||||
lookup
|
lookup appType
|
||||||
(addUrls urls nextRequest)
|
(addUrls urls nextRequest)
|
||||||
strippedResponses
|
strippedResponses
|
||||||
)
|
)
|
||||||
@ -393,8 +396,8 @@ andThen : (a -> Request b) -> Request a -> Request b
|
|||||||
andThen fn requestInfo =
|
andThen fn requestInfo =
|
||||||
Request
|
Request
|
||||||
( lookupUrls requestInfo
|
( lookupUrls requestInfo
|
||||||
, \rawResponses ->
|
, \appType rawResponses ->
|
||||||
lookup
|
lookup appType
|
||||||
requestInfo
|
requestInfo
|
||||||
rawResponses
|
rawResponses
|
||||||
|> (\result ->
|
|> (\result ->
|
||||||
@ -436,11 +439,22 @@ succeed : a -> Request a
|
|||||||
succeed value =
|
succeed value =
|
||||||
Request
|
Request
|
||||||
( []
|
( []
|
||||||
, \rawResponses ->
|
, \appType rawResponses ->
|
||||||
Ok ( rawResponses, Done value )
|
Ok ( rawResponses, Done value )
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
{-| TODO
|
||||||
|
-}
|
||||||
|
fail : String -> Request a
|
||||||
|
fail errorMessage =
|
||||||
|
Request
|
||||||
|
( []
|
||||||
|
, \appType rawResponses ->
|
||||||
|
Err (Pages.StaticHttpRequest.UserCalledStaticHttpFail errorMessage)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
{-| A simplified helper around [`StaticHttp.request`](#request), which builds up a StaticHttp GET request.
|
{-| A simplified helper around [`StaticHttp.request`](#request), which builds up a StaticHttp GET request.
|
||||||
|
|
||||||
import Json.Decode as Decode exposing (Decoder)
|
import Json.Decode as Decode exposing (Decoder)
|
||||||
@ -575,70 +589,104 @@ unoptimizedRequest requestWithSecrets expect =
|
|||||||
ExpectJson decoder ->
|
ExpectJson decoder ->
|
||||||
Request
|
Request
|
||||||
( [ requestWithSecrets ]
|
( [ requestWithSecrets ]
|
||||||
, \rawResponseDict ->
|
, \appType rawResponseDict ->
|
||||||
rawResponseDict
|
case appType of
|
||||||
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
ApplicationType.Cli ->
|
||||||
|> (\maybeResponse ->
|
rawResponseDict
|
||||||
case maybeResponse of
|
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
||||||
Just rawResponse ->
|
|> (\maybeResponse ->
|
||||||
Ok
|
case maybeResponse of
|
||||||
( rawResponseDict
|
Just rawResponse ->
|
||||||
-- |> Dict.update url (\maybeValue -> Just """{"fake": 123}""")
|
Ok
|
||||||
, rawResponse
|
( rawResponseDict
|
||||||
)
|
, rawResponse
|
||||||
|
)
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
Secrets.maskedLookup requestWithSecrets
|
Secrets.maskedLookup requestWithSecrets
|
||||||
|> requestToString
|
|> requestToString
|
||||||
|> Pages.StaticHttpRequest.MissingHttpResponse
|
|> Pages.StaticHttpRequest.MissingHttpResponse
|
||||||
|> Err
|
|> Err
|
||||||
)
|
)
|
||||||
|> Result.andThen
|
|> Result.andThen
|
||||||
(\( strippedResponses, rawResponse ) ->
|
(\( strippedResponses, rawResponse ) ->
|
||||||
let
|
let
|
||||||
reduced =
|
reduced =
|
||||||
Decode.stripString decoder rawResponse
|
Json.Decode.Exploration.stripString (Internal.OptimizedDecoder.jde decoder) rawResponse
|
||||||
|> Result.withDefault "TODO"
|
|> Result.withDefault "TODO"
|
||||||
in
|
in
|
||||||
rawResponse
|
rawResponse
|
||||||
|> Decode.decodeString decoder
|
|> Json.Decode.Exploration.decodeString (decoder |> Internal.OptimizedDecoder.jde)
|
||||||
-- |> Result.mapError Json.Decode.Exploration.errorsToString
|
|> (\decodeResult ->
|
||||||
|> (\decodeResult ->
|
case decodeResult of
|
||||||
case decodeResult of
|
Json.Decode.Exploration.BadJson ->
|
||||||
Decode.BadJson ->
|
Pages.StaticHttpRequest.DecoderError "Payload sent back invalid JSON" |> Err
|
||||||
Pages.StaticHttpRequest.DecoderError "Payload sent back invalid JSON" |> Err
|
|
||||||
|
|
||||||
Decode.Errors errors ->
|
Json.Decode.Exploration.Errors errors ->
|
||||||
errors
|
errors
|
||||||
|> Decode.errorsToString
|
|> Json.Decode.Exploration.errorsToString
|
||||||
|> Pages.StaticHttpRequest.DecoderError
|
|> Pages.StaticHttpRequest.DecoderError
|
||||||
|> Err
|
|> Err
|
||||||
|
|
||||||
Decode.WithWarnings warnings a ->
|
Json.Decode.Exploration.WithWarnings warnings a ->
|
||||||
-- Pages.StaticHttpRequest.DecoderError "" |> Err
|
Ok a
|
||||||
Ok a
|
|
||||||
|
|
||||||
Decode.Success a ->
|
Json.Decode.Exploration.Success a ->
|
||||||
Ok a
|
Ok a
|
||||||
)
|
)
|
||||||
-- |> Result.mapError Pages.StaticHttpRequest.DecoderError
|
|> Result.map Done
|
||||||
|> Result.map Done
|
|> Result.map
|
||||||
|> Result.map
|
(\finalRequest ->
|
||||||
(\finalRequest ->
|
( strippedResponses
|
||||||
( strippedResponses
|
|> Dict.insert
|
||||||
|> Dict.insert
|
(Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
||||||
(Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
reduced
|
||||||
reduced
|
, finalRequest
|
||||||
, finalRequest
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
ApplicationType.Browser ->
|
||||||
|
rawResponseDict
|
||||||
|
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
||||||
|
|> (\maybeResponse ->
|
||||||
|
case maybeResponse of
|
||||||
|
Just rawResponse ->
|
||||||
|
Ok
|
||||||
|
( rawResponseDict
|
||||||
|
, rawResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Secrets.maskedLookup requestWithSecrets
|
||||||
|
|> requestToString
|
||||||
|
|> Pages.StaticHttpRequest.MissingHttpResponse
|
||||||
|
|> Err
|
||||||
|
)
|
||||||
|
|> Result.andThen
|
||||||
|
(\( strippedResponses, rawResponse ) ->
|
||||||
|
rawResponse
|
||||||
|
|> Json.Decode.decodeString (decoder |> Internal.OptimizedDecoder.jd)
|
||||||
|
|> (\decodeResult ->
|
||||||
|
case decodeResult of
|
||||||
|
Err _ ->
|
||||||
|
Pages.StaticHttpRequest.DecoderError "Payload sent back invalid JSON" |> Err
|
||||||
|
|
||||||
|
Ok a ->
|
||||||
|
Ok a
|
||||||
|
)
|
||||||
|
|> Result.map Done
|
||||||
|
|> Result.map
|
||||||
|
(\finalRequest ->
|
||||||
|
( strippedResponses, finalRequest )
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
ExpectUnoptimizedJson decoder ->
|
ExpectUnoptimizedJson decoder ->
|
||||||
Request
|
Request
|
||||||
( [ requestWithSecrets ]
|
( [ requestWithSecrets ]
|
||||||
, \rawResponseDict ->
|
, \appType rawResponseDict ->
|
||||||
rawResponseDict
|
rawResponseDict
|
||||||
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
||||||
|> (\maybeResponse ->
|
|> (\maybeResponse ->
|
||||||
@ -664,7 +712,10 @@ unoptimizedRequest requestWithSecrets expect =
|
|||||||
|> (\decodeResult ->
|
|> (\decodeResult ->
|
||||||
case decodeResult of
|
case decodeResult of
|
||||||
Err error ->
|
Err error ->
|
||||||
Pages.StaticHttpRequest.DecoderError "Payload sent back invalid JSON" |> Err
|
error
|
||||||
|
|> Decode.errorToString
|
||||||
|
|> Pages.StaticHttpRequest.DecoderError
|
||||||
|
|> Err
|
||||||
|
|
||||||
Ok a ->
|
Ok a ->
|
||||||
Ok a
|
Ok a
|
||||||
@ -685,7 +736,7 @@ unoptimizedRequest requestWithSecrets expect =
|
|||||||
ExpectString mapStringFn ->
|
ExpectString mapStringFn ->
|
||||||
Request
|
Request
|
||||||
( [ requestWithSecrets ]
|
( [ requestWithSecrets ]
|
||||||
, \rawResponseDict ->
|
, \appType rawResponseDict ->
|
||||||
rawResponseDict
|
rawResponseDict
|
||||||
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
||||||
|> (\maybeResponse ->
|
|> (\maybeResponse ->
|
||||||
|
@ -2,26 +2,27 @@ module Pages.StaticHttpRequest exposing (Error(..), Request(..), permanentError,
|
|||||||
|
|
||||||
import BuildError exposing (BuildError)
|
import BuildError exposing (BuildError)
|
||||||
import Dict exposing (Dict)
|
import Dict exposing (Dict)
|
||||||
|
import Pages.Internal.ApplicationType as ApplicationType exposing (ApplicationType)
|
||||||
import Pages.StaticHttp.Request
|
import Pages.StaticHttp.Request
|
||||||
import Secrets
|
import Secrets
|
||||||
import TerminalText as Terminal
|
import TerminalText as Terminal
|
||||||
|
|
||||||
|
|
||||||
type Request value
|
type Request value
|
||||||
= Request ( List (Secrets.Value Pages.StaticHttp.Request.Request), Dict String String -> Result Error ( Dict String String, Request value ) )
|
= Request ( List (Secrets.Value Pages.StaticHttp.Request.Request), ApplicationType -> Dict String String -> Result Error ( Dict String String, Request value ) )
|
||||||
| Done value
|
| Done value
|
||||||
|
|
||||||
|
|
||||||
strippedResponses : Request value -> Dict String String -> Dict String String
|
strippedResponses : ApplicationType -> Request value -> Dict String String -> Dict String String
|
||||||
strippedResponses request rawResponses =
|
strippedResponses appType request rawResponses =
|
||||||
case request of
|
case request of
|
||||||
Request ( list, lookupFn ) ->
|
Request ( list, lookupFn ) ->
|
||||||
case lookupFn rawResponses of
|
case lookupFn appType rawResponses of
|
||||||
Err error ->
|
Err error ->
|
||||||
rawResponses
|
rawResponses
|
||||||
|
|
||||||
Ok ( partiallyStrippedResponses, followupRequest ) ->
|
Ok ( partiallyStrippedResponses, followupRequest ) ->
|
||||||
strippedResponses followupRequest partiallyStrippedResponses
|
strippedResponses appType followupRequest partiallyStrippedResponses
|
||||||
|
|
||||||
Done value ->
|
Done value ->
|
||||||
rawResponses
|
rawResponses
|
||||||
@ -30,6 +31,7 @@ strippedResponses request rawResponses =
|
|||||||
type Error
|
type Error
|
||||||
= MissingHttpResponse String
|
= MissingHttpResponse String
|
||||||
| DecoderError String
|
| DecoderError String
|
||||||
|
| UserCalledStaticHttpFail String
|
||||||
|
|
||||||
|
|
||||||
urls : Request value -> List (Secrets.Value Pages.StaticHttp.Request.Request)
|
urls : Request value -> List (Secrets.Value Pages.StaticHttp.Request.Request)
|
||||||
@ -65,14 +67,24 @@ toBuildError path error =
|
|||||||
, fatal = True
|
, fatal = True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UserCalledStaticHttpFail decodeErrorMessage ->
|
||||||
|
{ title = "Called Static Http Fail"
|
||||||
|
, message =
|
||||||
|
[ Terminal.text path
|
||||||
|
, Terminal.text "\n\n"
|
||||||
|
, Terminal.text <| "I ran into a call to `Pages.StaticHttp.fail` with message: " ++ decodeErrorMessage
|
||||||
|
]
|
||||||
|
, fatal = True
|
||||||
|
}
|
||||||
|
|
||||||
permanentError : Request value -> Dict String String -> Maybe Error
|
|
||||||
permanentError request rawResponses =
|
permanentError : ApplicationType -> Request value -> Dict String String -> Maybe Error
|
||||||
|
permanentError appType request rawResponses =
|
||||||
case request of
|
case request of
|
||||||
Request ( urlList, lookupFn ) ->
|
Request ( urlList, lookupFn ) ->
|
||||||
case lookupFn rawResponses of
|
case lookupFn appType rawResponses of
|
||||||
Ok ( partiallyStrippedResponses, nextRequest ) ->
|
Ok ( partiallyStrippedResponses, nextRequest ) ->
|
||||||
permanentError nextRequest rawResponses
|
permanentError appType nextRequest rawResponses
|
||||||
|
|
||||||
Err error ->
|
Err error ->
|
||||||
case error of
|
case error of
|
||||||
@ -82,17 +94,20 @@ permanentError request rawResponses =
|
|||||||
DecoderError _ ->
|
DecoderError _ ->
|
||||||
Just error
|
Just error
|
||||||
|
|
||||||
|
UserCalledStaticHttpFail string ->
|
||||||
|
Just error
|
||||||
|
|
||||||
Done value ->
|
Done value ->
|
||||||
Nothing
|
Nothing
|
||||||
|
|
||||||
|
|
||||||
resolve : Request value -> Dict String String -> Result Error value
|
resolve : ApplicationType -> Request value -> Dict String String -> Result Error value
|
||||||
resolve request rawResponses =
|
resolve appType request rawResponses =
|
||||||
case request of
|
case request of
|
||||||
Request ( urlList, lookupFn ) ->
|
Request ( urlList, lookupFn ) ->
|
||||||
case lookupFn rawResponses of
|
case lookupFn appType rawResponses of
|
||||||
Ok ( partiallyStrippedResponses, nextRequest ) ->
|
Ok ( partiallyStrippedResponses, nextRequest ) ->
|
||||||
resolve nextRequest rawResponses
|
resolve appType nextRequest rawResponses
|
||||||
|
|
||||||
Err error ->
|
Err error ->
|
||||||
Err error
|
Err error
|
||||||
@ -101,13 +116,13 @@ resolve request rawResponses =
|
|||||||
Ok value
|
Ok value
|
||||||
|
|
||||||
|
|
||||||
resolveUrls : Request value -> Dict String String -> ( Bool, List (Secrets.Value Pages.StaticHttp.Request.Request) )
|
resolveUrls : ApplicationType -> Request value -> Dict String String -> ( Bool, List (Secrets.Value Pages.StaticHttp.Request.Request) )
|
||||||
resolveUrls request rawResponses =
|
resolveUrls appType request rawResponses =
|
||||||
case request of
|
case request of
|
||||||
Request ( urlList, lookupFn ) ->
|
Request ( urlList, lookupFn ) ->
|
||||||
case lookupFn rawResponses of
|
case lookupFn appType rawResponses of
|
||||||
Ok ( partiallyStrippedResponses, nextRequest ) ->
|
Ok ( partiallyStrippedResponses, nextRequest ) ->
|
||||||
resolveUrls nextRequest rawResponses
|
resolveUrls appType nextRequest rawResponses
|
||||||
|> Tuple.mapSecond ((++) urlList)
|
|> Tuple.mapSecond ((++) urlList)
|
||||||
|
|
||||||
Err error ->
|
Err error ->
|
||||||
|
234
src/StructuredData.elm
Normal file
234
src/StructuredData.elm
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
module StructuredData exposing (..)
|
||||||
|
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
|
||||||
|
{-| <https://schema.org/SoftwareSourceCode>
|
||||||
|
-}
|
||||||
|
softwareSourceCode :
|
||||||
|
{ codeRepositoryUrl : String
|
||||||
|
, description : String
|
||||||
|
, author : String
|
||||||
|
, programmingLanguage : Encode.Value
|
||||||
|
}
|
||||||
|
-> Encode.Value
|
||||||
|
softwareSourceCode info =
|
||||||
|
Encode.object
|
||||||
|
[ ( "@type", Encode.string "SoftwareSourceCode" )
|
||||||
|
, ( "codeRepository", Encode.string info.codeRepositoryUrl )
|
||||||
|
, ( "description", Encode.string info.description )
|
||||||
|
, ( "author", Encode.string info.author )
|
||||||
|
, ( "programmingLanguage", info.programmingLanguage )
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| <https://schema.org/ComputerLanguage>
|
||||||
|
-}
|
||||||
|
computerLanguage : { url : String, name : String, imageUrl : String, identifier : String } -> Encode.Value
|
||||||
|
computerLanguage info =
|
||||||
|
Encode.object
|
||||||
|
[ ( "@type", Encode.string "ComputerLanguage" )
|
||||||
|
, ( "url", Encode.string info.url )
|
||||||
|
, ( "name", Encode.string info.name )
|
||||||
|
, ( "image", Encode.string info.imageUrl )
|
||||||
|
, ( "identifier", Encode.string info.identifier )
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
elmLang : Encode.Value
|
||||||
|
elmLang =
|
||||||
|
computerLanguage
|
||||||
|
{ url = "http://elm-lang.org/"
|
||||||
|
, name = "Elm"
|
||||||
|
, imageUrl = "http://elm-lang.org/"
|
||||||
|
, identifier = "http://elm-lang.org/"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| <https://schema.org/Article>
|
||||||
|
-}
|
||||||
|
article :
|
||||||
|
{ title : String
|
||||||
|
, description : String
|
||||||
|
, author : StructuredData { authorMemberOf | personOrOrganization : () } authorPossibleFields
|
||||||
|
, publisher : StructuredData { publisherMemberOf | personOrOrganization : () } publisherPossibleFields
|
||||||
|
, url : String
|
||||||
|
, imageUrl : String
|
||||||
|
, datePublished : String
|
||||||
|
, mainEntityOfPage : Encode.Value
|
||||||
|
}
|
||||||
|
-> Encode.Value
|
||||||
|
article info =
|
||||||
|
Encode.object
|
||||||
|
[ ( "@context", Encode.string "http://schema.org/" )
|
||||||
|
, ( "@type", Encode.string "Article" )
|
||||||
|
, ( "headline", Encode.string info.title )
|
||||||
|
, ( "description", Encode.string info.description )
|
||||||
|
, ( "image", Encode.string info.imageUrl )
|
||||||
|
, ( "author", encode info.author )
|
||||||
|
, ( "publisher", encode info.publisher )
|
||||||
|
, ( "url", Encode.string info.url )
|
||||||
|
, ( "datePublished", Encode.string info.datePublished )
|
||||||
|
, ( "mainEntityOfPage", info.mainEntityOfPage )
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
type StructuredData memberOf possibleFields
|
||||||
|
= StructuredData String (List ( String, Encode.Value ))
|
||||||
|
|
||||||
|
|
||||||
|
{-| <https://schema.org/Person>
|
||||||
|
-}
|
||||||
|
person :
|
||||||
|
{ name : String
|
||||||
|
}
|
||||||
|
->
|
||||||
|
StructuredData { personOrOrganization : () }
|
||||||
|
{ additionalName : ()
|
||||||
|
, address : ()
|
||||||
|
, affiliation : ()
|
||||||
|
}
|
||||||
|
person info =
|
||||||
|
StructuredData "Person" [ ( "name", Encode.string info.name ) ]
|
||||||
|
|
||||||
|
|
||||||
|
additionalName : String -> StructuredData memberOf { possibleFields | additionalName : () } -> StructuredData memberOf possibleFields
|
||||||
|
additionalName value (StructuredData typeName fields) =
|
||||||
|
StructuredData typeName (( "additionalName", Encode.string value ) :: fields)
|
||||||
|
|
||||||
|
|
||||||
|
{-| <https://schema.org/Article>
|
||||||
|
-}
|
||||||
|
article_ :
|
||||||
|
{ title : String
|
||||||
|
, description : String
|
||||||
|
, author : String
|
||||||
|
, publisher : StructuredData { personOrOrganization : () } possibleFieldsPublisher
|
||||||
|
, url : String
|
||||||
|
, imageUrl : String
|
||||||
|
, datePublished : String
|
||||||
|
, mainEntityOfPage : Encode.Value
|
||||||
|
}
|
||||||
|
-> Encode.Value
|
||||||
|
article_ info =
|
||||||
|
Encode.object
|
||||||
|
[ ( "@context", Encode.string "http://schema.org/" )
|
||||||
|
, ( "@type", Encode.string "Article" )
|
||||||
|
, ( "headline", Encode.string info.title )
|
||||||
|
, ( "description", Encode.string info.description )
|
||||||
|
, ( "image", Encode.string info.imageUrl )
|
||||||
|
, ( "author", Encode.string info.author )
|
||||||
|
, ( "publisher", encode info.publisher )
|
||||||
|
, ( "url", Encode.string info.url )
|
||||||
|
, ( "datePublished", Encode.string info.datePublished )
|
||||||
|
, ( "mainEntityOfPage", info.mainEntityOfPage )
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
encode : StructuredData memberOf possibleFieldsPublisher -> Encode.Value
|
||||||
|
encode (StructuredData typeName fields) =
|
||||||
|
Encode.object
|
||||||
|
(( "@type", Encode.string typeName ) :: fields)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--example : StructuredData { personOrOrganization : () } { address : (), affiliation : () }
|
||||||
|
|
||||||
|
|
||||||
|
example =
|
||||||
|
person { name = "Dillon Kearns" }
|
||||||
|
|> additionalName "Cornelius"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--organization :
|
||||||
|
-- {}
|
||||||
|
-- -> StructuredData { personOrOrganization : () }
|
||||||
|
--organization info =
|
||||||
|
-- StructuredData "Organization" []
|
||||||
|
--needsPersonOrOrg : StructuredData {}
|
||||||
|
--needsPersonOrOrg =
|
||||||
|
-- StructuredData "" []
|
||||||
|
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": "http://schema.org/",
|
||||||
|
"@type": "PodcastSeries",
|
||||||
|
"image": "https://www.relay.fm/inquisitive_artwork.png",
|
||||||
|
"url": "http://www.relay.fm/inquisitive",
|
||||||
|
"name": "Inquisitive",
|
||||||
|
"description": "Inquisitive is a show for the naturally curious. Each week, Myke Hurley takes a look at what makes creative people successful and what steps they have taken to get there.",
|
||||||
|
"webFeed": "http://www.relay.fm//inquisitive/feed",
|
||||||
|
"author": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "Myke Hurley"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
-}
|
||||||
|
series : Encode.Value
|
||||||
|
series =
|
||||||
|
Encode.object
|
||||||
|
[ ( "@context", Encode.string "http://schema.org/" )
|
||||||
|
, ( "@type", Encode.string "PodcastSeries" )
|
||||||
|
, ( "image", Encode.string "TODO" )
|
||||||
|
, ( "url", Encode.string "http://elm-radio.com/episode/getting-started-with-elm-pages" )
|
||||||
|
, ( "name", Encode.string "Elm Radio" )
|
||||||
|
, ( "description", Encode.string "TODO" )
|
||||||
|
, ( "webFeed", Encode.string "https://elm-radio.com/feed.xml" )
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": "http://schema.org/",
|
||||||
|
"@type": "PodcastEpisode",
|
||||||
|
"url": "http://elm-radio.com/episode/getting-started-with-elm-pages",
|
||||||
|
"name": "001: Getting Started with elm-pages",
|
||||||
|
"datePublished": "2015-02-18",
|
||||||
|
"timeRequired": "PT37M",
|
||||||
|
"description": "In the first episode of “Behind the App”, a special series of Inquisitive, we take a look at the beginnings of iOS app development, by focusing on the introduction of the iPhone and the App Store.",
|
||||||
|
"associatedMedia": {
|
||||||
|
"@type": "MediaObject",
|
||||||
|
"contentUrl": "https://cdn.simplecast.com/audio/6a206b/6a206baa-9c8e-4c25-9037-2b674204ba84/ca009f6e-1710-4518-b869-ca34cb0b7d17/001-getting-started-elm-pages_tc.mp3 "
|
||||||
|
},
|
||||||
|
"partOfSeries": {
|
||||||
|
"@type": "PodcastSeries",
|
||||||
|
"name": "Elm Radio",
|
||||||
|
"url": "https://elm-radio.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
-}
|
||||||
|
episode : Encode.Value
|
||||||
|
episode =
|
||||||
|
Encode.object
|
||||||
|
[ ( "@context", Encode.string "http://schema.org/" )
|
||||||
|
, ( "@type", Encode.string "PodcastEpisode" )
|
||||||
|
, ( "url", Encode.string "http://elm-radio.com/episode/getting-started-with-elm-pages" )
|
||||||
|
, ( "name", Encode.string "Getting Started with elm-pages" )
|
||||||
|
, ( "datePublished", Encode.string "2015-02-18" )
|
||||||
|
, ( "timeRequired", Encode.string "PT37M" )
|
||||||
|
, ( "description", Encode.string "TODO" )
|
||||||
|
, ( "associatedMedia"
|
||||||
|
, Encode.object
|
||||||
|
[ ( "@type", Encode.string "MediaObject" )
|
||||||
|
, ( "contentUrl", Encode.string "https://cdn.simplecast.com/audio/6a206b/6a206baa-9c8e-4c25-9037-2b674204ba84/ca009f6e-1710-4518-b869-ca34cb0b7d17/001-getting-started-elm-pages_tc.mp3" )
|
||||||
|
]
|
||||||
|
)
|
||||||
|
, ( "partOfSeries"
|
||||||
|
, Encode.object
|
||||||
|
[ ( "@type", Encode.string "PodcastSeries" )
|
||||||
|
, ( "name", Encode.string "Elm Radio" )
|
||||||
|
, ( "url", Encode.string "https://elm-radio.com" )
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
@ -68,10 +68,10 @@ colorToString color =
|
|||||||
"[34m"
|
"[34m"
|
||||||
|
|
||||||
Green ->
|
Green ->
|
||||||
"[32;1m"
|
"[32m"
|
||||||
|
|
||||||
Yellow ->
|
Yellow ->
|
||||||
"[33;1m"
|
"[33m"
|
||||||
|
|
||||||
Cyan ->
|
Cyan ->
|
||||||
"[36m"
|
"[36m"
|
||||||
|
@ -5,7 +5,9 @@ import Dict exposing (Dict)
|
|||||||
import Expect
|
import Expect
|
||||||
import Html
|
import Html
|
||||||
import Json.Decode as JD
|
import Json.Decode as JD
|
||||||
import Json.Decode.Exploration as Decode exposing (Decoder)
|
import Json.Decode.Exploration
|
||||||
|
import Json.Encode as Encode
|
||||||
|
import OptimizedDecoder as Decode exposing (Decoder)
|
||||||
import Pages.ContentCache as ContentCache
|
import Pages.ContentCache as ContentCache
|
||||||
import Pages.Document as Document
|
import Pages.Document as Document
|
||||||
import Pages.Http
|
import Pages.Http
|
||||||
@ -604,11 +606,68 @@ Body: """)
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
, describe "staticHttpCache"
|
||||||
|
[ test "it doesn't perform http requests that are provided in the http cache flag" <|
|
||||||
|
\() ->
|
||||||
|
startWithHttpCache
|
||||||
|
[ ( { url = "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||||
|
, method = "GET"
|
||||||
|
, headers = []
|
||||||
|
, body = StaticHttpBody.EmptyBody
|
||||||
|
}
|
||||||
|
, """{"stargazer_count":86}"""
|
||||||
|
)
|
||||||
|
]
|
||||||
|
[ ( []
|
||||||
|
, StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") starDecoder
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|> expectSuccess
|
||||||
|
[ ( ""
|
||||||
|
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||||
|
, """{"stargazer_count":86}"""
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
, test "it ignores unused cache" <|
|
||||||
|
\() ->
|
||||||
|
startWithHttpCache
|
||||||
|
[ ( { url = "https://this-is-never-used.example.com/"
|
||||||
|
, method = "GET"
|
||||||
|
, headers = []
|
||||||
|
, body = StaticHttpBody.EmptyBody
|
||||||
|
}
|
||||||
|
, """{"stargazer_count":86}"""
|
||||||
|
)
|
||||||
|
]
|
||||||
|
[ ( []
|
||||||
|
, StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") starDecoder
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|> ProgramTest.simulateHttpOk
|
||||||
|
"GET"
|
||||||
|
"https://api.github.com/repos/dillonkearns/elm-pages"
|
||||||
|
"""{ "stargazer_count": 86 }"""
|
||||||
|
|> expectSuccess
|
||||||
|
[ ( ""
|
||||||
|
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||||
|
, """{"stargazer_count":86}"""
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
start : List ( List String, StaticHttp.Request a ) -> ProgramTest Main.Model Main.Msg (Main.Effect PathKey)
|
start : List ( List String, StaticHttp.Request a ) -> ProgramTest Main.Model Main.Msg (Main.Effect PathKey)
|
||||||
start pages =
|
start pages =
|
||||||
|
startWithHttpCache [] pages
|
||||||
|
|
||||||
|
|
||||||
|
startWithHttpCache : List ( Request.Request, String ) -> List ( List String, StaticHttp.Request a ) -> ProgramTest Main.Model Main.Msg (Main.Effect PathKey)
|
||||||
|
startWithHttpCache staticHttpCache pages =
|
||||||
let
|
let
|
||||||
document =
|
document =
|
||||||
Document.fromList
|
Document.fromList
|
||||||
@ -637,6 +696,7 @@ start pages =
|
|||||||
|
|
||||||
config =
|
config =
|
||||||
{ toJsPort = toJsPort
|
{ toJsPort = toJsPort
|
||||||
|
, fromJsPort = fromJsPort
|
||||||
, manifest = manifest
|
, manifest = manifest
|
||||||
, generateFiles = \_ -> StaticHttp.succeed []
|
, generateFiles = \_ -> StaticHttp.succeed []
|
||||||
, init = \_ -> ( (), Cmd.none )
|
, init = \_ -> ( (), Cmd.none )
|
||||||
@ -669,6 +729,30 @@ start pages =
|
|||||||
, pathKey = PathKey
|
, pathKey = PathKey
|
||||||
, onPageChange = \_ -> ()
|
, onPageChange = \_ -> ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
encodedFlags =
|
||||||
|
--{"secrets":
|
||||||
|
-- {"API_KEY": "ABCD1234","BEARER": "XYZ789"}, "mode": "prod", "staticHttpCache": {}
|
||||||
|
-- }
|
||||||
|
Encode.object
|
||||||
|
[ ( "secrets"
|
||||||
|
, [ ( "API_KEY", "ABCD1234" )
|
||||||
|
, ( "BEARER", "XYZ789" )
|
||||||
|
]
|
||||||
|
|> Dict.fromList
|
||||||
|
|> Encode.dict identity Encode.string
|
||||||
|
)
|
||||||
|
, ( "mode", Encode.string "prod" )
|
||||||
|
, ( "staticHttpCache", encodedStaticHttpCache )
|
||||||
|
]
|
||||||
|
|
||||||
|
encodedStaticHttpCache =
|
||||||
|
staticHttpCache
|
||||||
|
|> List.map
|
||||||
|
(\( request, httpResponseString ) ->
|
||||||
|
( Request.hash request, Encode.string httpResponseString )
|
||||||
|
)
|
||||||
|
|> Encode.object
|
||||||
in
|
in
|
||||||
{-
|
{-
|
||||||
(Model -> model)
|
(Model -> model)
|
||||||
@ -684,9 +768,7 @@ start pages =
|
|||||||
, view = \_ -> { title = "", body = [] }
|
, view = \_ -> { title = "", body = [] }
|
||||||
}
|
}
|
||||||
|> ProgramTest.withSimulatedEffects simulateEffects
|
|> ProgramTest.withSimulatedEffects simulateEffects
|
||||||
|> ProgramTest.start (flags """{"secrets":
|
|> ProgramTest.start (flags (Encode.encode 0 encodedFlags))
|
||||||
{"API_KEY": "ABCD1234","BEARER": "XYZ789"}, "mode": "prod"
|
|
||||||
}""")
|
|
||||||
|
|
||||||
|
|
||||||
flags : String -> JD.Value
|
flags : String -> JD.Value
|
||||||
@ -780,6 +862,10 @@ toJsPort foo =
|
|||||||
Cmd.none
|
Cmd.none
|
||||||
|
|
||||||
|
|
||||||
|
fromJsPort =
|
||||||
|
Sub.none
|
||||||
|
|
||||||
|
|
||||||
type PathKey
|
type PathKey
|
||||||
= PathKey
|
= PathKey
|
||||||
|
|
||||||
@ -831,27 +917,31 @@ expectSuccess expectedRequests previous =
|
|||||||
|> ProgramTest.expectOutgoingPortValues
|
|> ProgramTest.expectOutgoingPortValues
|
||||||
"toJsPort"
|
"toJsPort"
|
||||||
(Codec.decoder Main.toJsCodec)
|
(Codec.decoder Main.toJsCodec)
|
||||||
(Expect.equal
|
(\value ->
|
||||||
[ Main.Success
|
case value of
|
||||||
{ pages =
|
[ Main.Success portPayload ] ->
|
||||||
expectedRequests
|
portPayload.pages
|
||||||
|> List.map
|
|> Expect.equal
|
||||||
(\( url, requests ) ->
|
(expectedRequests
|
||||||
( url
|
|> List.map
|
||||||
, requests
|
(\( url, requests ) ->
|
||||||
|> List.map
|
( url
|
||||||
(\( request, response ) ->
|
, requests
|
||||||
( Request.hash request, response )
|
|> List.map
|
||||||
|
(\( request, response ) ->
|
||||||
|
( Request.hash request, response )
|
||||||
|
)
|
||||||
|
|> Dict.fromList
|
||||||
)
|
)
|
||||||
|> Dict.fromList
|
)
|
||||||
)
|
|> Dict.fromList
|
||||||
)
|
)
|
||||||
|> Dict.fromList
|
|
||||||
, manifest = manifest
|
[ _ ] ->
|
||||||
, filesToGenerate = []
|
Expect.fail "Expected success port."
|
||||||
, errors = []
|
|
||||||
}
|
_ ->
|
||||||
]
|
Expect.fail ("Expected ports to be called once, but instead there were " ++ String.fromInt (List.length value) ++ " calls.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,9 @@ module StaticHttpUnitTests exposing (all)
|
|||||||
|
|
||||||
import Dict exposing (Dict)
|
import Dict exposing (Dict)
|
||||||
import Expect
|
import Expect
|
||||||
import Json.Decode.Exploration as Decode
|
import Json.Decode.Exploration
|
||||||
|
import OptimizedDecoder as Decode
|
||||||
|
import Pages.Internal.ApplicationType as ApplicationType
|
||||||
import Pages.StaticHttp as StaticHttp
|
import Pages.StaticHttp as StaticHttp
|
||||||
import Pages.StaticHttp.Request as Request
|
import Pages.StaticHttp.Request as Request
|
||||||
import Pages.StaticHttpRequest as StaticHttpRequest
|
import Pages.StaticHttpRequest as StaticHttpRequest
|
||||||
@ -42,11 +44,11 @@ all =
|
|||||||
StaticHttp.get (Secrets.succeed "first") (Decode.succeed "NEXT")
|
StaticHttp.get (Secrets.succeed "first") (Decode.succeed "NEXT")
|
||||||
|> StaticHttp.andThen
|
|> StaticHttp.andThen
|
||||||
(\continueUrl ->
|
(\continueUrl ->
|
||||||
-- StaticHttp.get continueUrl (Decode.succeed ())
|
|
||||||
getWithoutSecrets "NEXT" (Decode.succeed ())
|
getWithoutSecrets "NEXT" (Decode.succeed ())
|
||||||
)
|
)
|
||||||
|> (\request ->
|
|> (\request ->
|
||||||
StaticHttpRequest.resolveUrls request
|
StaticHttpRequest.resolveUrls ApplicationType.Cli
|
||||||
|
request
|
||||||
(requestsDict
|
(requestsDict
|
||||||
[ ( get "first", "null" )
|
[ ( get "first", "null" )
|
||||||
, ( get "NEXT", "null" )
|
, ( get "NEXT", "null" )
|
||||||
@ -63,7 +65,8 @@ all =
|
|||||||
getWithoutSecrets "NEXT" (Decode.succeed ())
|
getWithoutSecrets "NEXT" (Decode.succeed ())
|
||||||
)
|
)
|
||||||
|> (\request ->
|
|> (\request ->
|
||||||
StaticHttpRequest.resolveUrls request
|
StaticHttpRequest.resolveUrls ApplicationType.Cli
|
||||||
|
request
|
||||||
(requestsDict
|
(requestsDict
|
||||||
[ ( get "NEXT", "null" )
|
[ ( get "NEXT", "null" )
|
||||||
]
|
]
|
||||||
@ -81,7 +84,8 @@ all =
|
|||||||
)
|
)
|
||||||
|> StaticHttp.map (\_ -> ())
|
|> StaticHttp.map (\_ -> ())
|
||||||
|> (\request ->
|
|> (\request ->
|
||||||
StaticHttpRequest.resolveUrls request
|
StaticHttpRequest.resolveUrls ApplicationType.Cli
|
||||||
|
request
|
||||||
(requestsDict
|
(requestsDict
|
||||||
[ ( get "first", "null" )
|
[ ( get "first", "null" )
|
||||||
, ( get "NEXT", "null" )
|
, ( get "NEXT", "null" )
|
||||||
@ -98,7 +102,8 @@ all =
|
|||||||
getWithoutSecrets "NEXT" (Decode.succeed ())
|
getWithoutSecrets "NEXT" (Decode.succeed ())
|
||||||
)
|
)
|
||||||
|> (\request ->
|
|> (\request ->
|
||||||
StaticHttpRequest.resolveUrls request
|
StaticHttpRequest.resolveUrls ApplicationType.Cli
|
||||||
|
request
|
||||||
(requestsDict
|
(requestsDict
|
||||||
[ ( get "first", "null" )
|
[ ( get "first", "null" )
|
||||||
]
|
]
|
||||||
@ -119,7 +124,8 @@ all =
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> (\request ->
|
|> (\request ->
|
||||||
StaticHttpRequest.resolveUrls request
|
StaticHttpRequest.resolveUrls ApplicationType.Cli
|
||||||
|
request
|
||||||
(requestsDict
|
(requestsDict
|
||||||
[ ( get "first", "1" )
|
[ ( get "first", "1" )
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user