mirror of
https://github.com/dillonkearns/elm-pages-v3-beta.git
synced 2024-12-23 11:55:41 +03:00
Merge branch 'master' into pr/90
This commit is contained in:
commit
29719cc428
@ -40,6 +40,15 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lukewestby",
|
||||
"name": "Luke Westby",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/1508245?v=4",
|
||||
"profile": "https://sunrisemovement.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
@ -47,5 +56,6 @@
|
||||
"projectOwner": "dillonkearns",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true
|
||||
"skipCi": true,
|
||||
"commitConvention": "none"
|
||||
}
|
||||
|
@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [4.0.1] - 2020-03-28
|
||||
|
||||
### 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.
|
||||
This works using [HTML `<base>` tags](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base). The paths you get from `PagePath.toString` and `ImagePath.toString`
|
||||
will use relative paths (e.g. `blog/my-article`) instead of absolute URLs (e.g. `/blog/my-article`), so you can take advantage of this functionality by just making sure you
|
||||
use the path helpers and don't hardcode any absolute URL strings. See https://github.com/dillonkearns/elm-pages/pull/73.
|
||||
|
||||
## [4.0.0] - 2020-03-04
|
||||
|
||||
### Changed
|
||||
|
@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.3.0] - 2020-03-28
|
||||
|
||||
### 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.
|
||||
This works using [HTML `<base>` tags](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base). The paths you get from `PagePath.toString` and `ImagePath.toString`
|
||||
will use relative paths (e.g. `blog/my-article`) instead of absolute URLs (e.g. `/blog/my-article`), so you can take advantage of this functionality by just making sure you
|
||||
use the path helpers and don't hardcode any absolute URL strings. See https://github.com/dillonkearns/elm-pages/pull/73.
|
||||
|
||||
## [1.2.11] - 2020-03-18
|
||||
|
||||
### Fixed
|
||||
|
@ -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/)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
|
||||
[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-)
|
||||
|
||||
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-)
|
||||
<!-- 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)
|
||||
@ -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://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://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>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-enable -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
22
docs/FAQ.md
Normal file
22
docs/FAQ.md
Normal file
@ -0,0 +1,22 @@
|
||||
## Can you pass flags in to your `elm-pages` app?
|
||||
There's no way to pass flags in right now. I'm collecting use cases and trying to figure out what the most intuitive thing would be conceptual, given that the value of flags will be different during Pre-Rendering and Client-Side Rendering.
|
||||
So for example, if you get the window dimensions from the flags and do responsive design based on that, then you'll see a flash after the client-side code takes over since it will run with a different value for flags. So that semantics of the flags are not quite intuitive there. You can achieve the same thing with a port, but the semantics are a little more obvious there because you now have to explicitly say how to handle the case where you don't have access to flags.
|
||||
|
||||
|
||||
## How do you handle responsive layouts when you don't the browser dimensions at build time?
|
||||
|
||||
A lot of users are building their `elm-pages` views with `elm-ui`, so this is a common question because
|
||||
`elm-ui` is designed to do responsive layouts by storing the browser dimensions in the Model and
|
||||
doing conditionals based on that state.
|
||||
|
||||
With `elm-pages`, and static sites in general, we are building pre-rendered HTML so we can serve it up
|
||||
really quickly through a CDN, rather than serving it up with a traditional server framework. That means
|
||||
that to have responsive pages that don't have a page flash, we need to use media queries to make our pages responsive.
|
||||
That way, the view is the same no matter what the dimensions are, so it will pre-render and look right on whatever
|
||||
device the user is on because the media queries will take care of making it responsive.
|
||||
|
||||
Since `elm-ui` isn't currently built with media queries in mind, it isn't a first-class experience to use them with
|
||||
`elm-ui`. One workaround you can use is to define some responsive classes that simply show or hide an element based on
|
||||
a media query, and apply those classes. For example, you could show the mobile or desktop version of the navbar
|
||||
by having a `mobile-responsive` and `desktop-responsive` class and rendering one element with each respsective class.
|
||||
But the media query will only show one at a time based on the dimensions.
|
7
elm.json
7
elm.json
@ -3,10 +3,12 @@
|
||||
"name": "dillonkearns/elm-pages",
|
||||
"summary": "A statically typed site generator.",
|
||||
"license": "BSD-3-Clause",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.1",
|
||||
"exposed-modules": [
|
||||
"Head",
|
||||
"Head.Seo",
|
||||
"OptimizedDecoder",
|
||||
"OptimizedDecoder.Pipeline",
|
||||
"Pages.Document",
|
||||
"Pages.ImagePath",
|
||||
"Pages.PagePath",
|
||||
@ -32,7 +34,6 @@
|
||||
"elm-community/list-extra": "8.2.2 <= v < 9.0.0",
|
||||
"elm-community/result-extra": "2.2.1 <= v < 3.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",
|
||||
"miniBill/elm-codec": "1.2.0 <= v < 2.0.0",
|
||||
"noahzgordon/elm-color-extra": "1.0.2 <= v < 2.0.0",
|
||||
@ -44,4 +45,4 @@
|
||||
"elm-explorations/test": "1.2.2 <= v < 2.0.0",
|
||||
"jgrenat/elm-html-test-runner": "1.0.3 <= v < 2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
"author": "Dillon Kearns",
|
||||
"title": "Extensible Markdown Parsing in Pure Elm",
|
||||
"description": "Introducing a new parser that extends your palette with no additional syntax",
|
||||
"image": "/images/article-covers/extensible-markdown-parsing.jpg",
|
||||
"image": "images/article-covers/extensible-markdown-parsing.jpg",
|
||||
"published": "2019-10-08",
|
||||
}
|
||||
---
|
||||
|
@ -5,7 +5,7 @@
|
||||
"author": "Dillon Kearns",
|
||||
"title": "Generating Files with Pure Elm",
|
||||
"description": "Introducing a new parser that extends your palette with no additional syntax",
|
||||
"image": "/images/article-covers/generating-files.jpg",
|
||||
"image": "images/article-covers/generating-files.jpg",
|
||||
"published": "2020-01-28",
|
||||
}
|
||||
---
|
||||
|
@ -4,7 +4,7 @@
|
||||
"author": "Dillon Kearns",
|
||||
"title": "Introducing elm-pages 🚀 - a type-centric static site generator",
|
||||
"description": "Elm is the perfect fit for a static site generator. Learn about some of the features and philosophy behind elm-pages.",
|
||||
"image": "/images/article-covers/introducing-elm-pages.jpg",
|
||||
"image": "images/article-covers/introducing-elm-pages.jpg",
|
||||
"published": "2019-09-24",
|
||||
}
|
||||
---
|
||||
|
@ -3,8 +3,8 @@
|
||||
"type": "blog",
|
||||
"author": "Dillon Kearns",
|
||||
"title": "A is for API - Introducing Static HTTP Requests",
|
||||
"description": "",
|
||||
"image": "/images/article-covers/static-http.jpg",
|
||||
"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",
|
||||
"published": "2019-12-10",
|
||||
}
|
||||
---
|
||||
@ -85,7 +85,7 @@ head : Int -> List (Head.Tag Pages.PathKey)
|
||||
head starCount =
|
||||
Seo.summaryLarge
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages - "
|
||||
, siteName = "elm-pages - "
|
||||
++ String.fromInt starCount
|
||||
++ " GitHub Stars"
|
||||
, image =
|
||||
@ -197,4 +197,4 @@ You can [take a look at this an end-to-end example app that uses the new `Static
|
||||
|
||||
Or just use the [`elm-pages-starter` repo](https://github.com/dillonkearns/elm-pages-starter) and start building something cool! Let me know your thoughts on Slack, I'd love to hear from you! Or continue the conversation on Twitter!
|
||||
|
||||
<Oembed url="https://twitter.com/dillontkearns/status/1214238507163471872" />
|
||||
<Oembed url="https://twitter.com/dillontkearns/status/1214238507163471872" />
|
||||
|
@ -4,7 +4,7 @@
|
||||
"author": "Dillon Kearns",
|
||||
"title": "Types Over Conventions",
|
||||
"description": "How elm-pages approaches configuration, using type-safe Elm.",
|
||||
"image": "/images/article-covers/introducing-elm-pages.jpg",
|
||||
"image": "images/article-covers/introducing-elm-pages.jpg",
|
||||
"draft": true,
|
||||
"published": "2019-09-21",
|
||||
}
|
||||
@ -164,6 +164,6 @@ Now, in our `elm-pages` app, our `view` function will get the markdown that we r
|
||||
## Takeaways
|
||||
So which is better, configuration through types or configuration by convention?
|
||||
|
||||
They both have their benefits. If you're like me, then you enjoy being able to figure out what your Elm code is doing by just following the types. And I hope you'll agree that `elm-pages` gives you that experience for wiring up your content and your parsers.
|
||||
They both have their benefits. If you're like me, then you enjoy being able to figure out what your Elm code is doing by just following the types. And I hope you'll agree that `elm-pages` gives you that experience for wiring up your content and your parsers.
|
||||
|
||||
And when you need to do something more advanced, you've got all the typed data right there and you're empowered to solve the problem using Elm!
|
||||
And when you need to do something more advanced, you've got all the typed data right there and you're empowered to solve the problem using Elm!
|
||||
|
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",
|
||||
"justinmimbs/date": "3.2.0",
|
||||
"lukewestby/elm-string-interpolate": "1.0.4",
|
||||
"mdgriffith/elm-markup": "3.0.1",
|
||||
"mdgriffith/elm-ui": "1.1.5",
|
||||
"miniBill/elm-codec": "1.2.0",
|
||||
"noahzgordon/elm-color-extra": "1.0.2",
|
||||
@ -59,4 +58,4 @@
|
||||
},
|
||||
"indirect": {}
|
||||
}
|
||||
}
|
||||
}
|
5
examples/docs/package-lock.json
generated
5
examples/docs/package-lock.json
generated
@ -260,6 +260,7 @@
|
||||
"chokidar": "^2.1.5",
|
||||
"closure-webpack-plugin": "^2.0.1",
|
||||
"copy-webpack-plugin": "^5.0.4",
|
||||
"cross-spawn": "6.0.5",
|
||||
"css-loader": "^3.2.0",
|
||||
"elm": "^0.19.1-3",
|
||||
"elm-hot-webpack-loader": "^1.1.2",
|
||||
@ -267,18 +268,20 @@
|
||||
"express": "^4.17.1",
|
||||
"favicons-webpack-plugin": "^3.0.0",
|
||||
"file-loader": "^4.2.0",
|
||||
"find-elm-dependencies": "2.0.2",
|
||||
"globby": "^10.0.1",
|
||||
"google-closure-compiler": "^20190909.0.0",
|
||||
"gray-matter": "^4.0.2",
|
||||
"html-webpack-plugin": "^4.0.0-beta.11",
|
||||
"imagemin-mozjpeg": "^8.0.0",
|
||||
"imagemin-webpack-plugin": "^2.4.2",
|
||||
"node-elm-compiler": "^5.0.4",
|
||||
"lodash": "4.17.15",
|
||||
"node-sass": "^4.12.0",
|
||||
"prerender-spa-plugin": "^3.4.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"script-ext-html-webpack-plugin": "^2.1.4",
|
||||
"style-loader": "^1.0.0",
|
||||
"temp": "^0.9.0",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-dev-middleware": "^3.7.0",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
|
@ -19,10 +19,11 @@ import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
import Index
|
||||
import Json.Decode as Decode exposing (Decoder)
|
||||
import Json.Decode.Exploration as D
|
||||
import Json.Encode
|
||||
import MarkdownRenderer
|
||||
import Metadata exposing (Metadata)
|
||||
import MySitemap
|
||||
import OptimizedDecoder as D
|
||||
import Pages exposing (images, pages)
|
||||
import Pages.Directory as Directory exposing (Directory)
|
||||
import Pages.Document
|
||||
@ -35,6 +36,7 @@ import Pages.StaticHttp as StaticHttp
|
||||
import Palette
|
||||
import Secrets
|
||||
import Showcase
|
||||
import StructuredData
|
||||
|
||||
|
||||
manifest : Manifest.Config Pages.PathKey
|
||||
@ -80,16 +82,19 @@ generateFiles :
|
||||
, body : String
|
||||
}
|
||||
->
|
||||
List
|
||||
(Result String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
StaticHttp.Request
|
||||
(List
|
||||
(Result String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
)
|
||||
)
|
||||
generateFiles siteMetadata =
|
||||
[ Feed.fileToGenerate { siteTagline = siteTagline, siteUrl = canonicalSiteUrl } siteMetadata |> Ok
|
||||
, MySitemap.build { siteUrl = canonicalSiteUrl } siteMetadata |> Ok
|
||||
]
|
||||
StaticHttp.succeed
|
||||
[ Feed.fileToGenerate { siteTagline = siteTagline, siteUrl = canonicalSiteUrl } siteMetadata |> Ok
|
||||
, MySitemap.build { siteUrl = canonicalSiteUrl } siteMetadata |> Ok
|
||||
]
|
||||
|
||||
|
||||
markdownDocument : ( String, Pages.Document.DocumentHandler Metadata ( MarkdownRenderer.TableOfContents, List (Element Msg) ) )
|
||||
@ -166,7 +171,7 @@ view siteMetadata page =
|
||||
]
|
||||
}
|
||||
|> 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")
|
||||
@ -183,49 +188,11 @@ view siteMetadata page =
|
||||
\model viewForPage ->
|
||||
pageView stars model siteMetadata page viewForPage
|
||||
|> 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 :
|
||||
Int
|
||||
-> Model
|
||||
@ -506,8 +473,8 @@ commonHeadTags =
|
||||
<https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names>
|
||||
<https://ogp.me/>
|
||||
-}
|
||||
head : Metadata -> List (Head.Tag Pages.PathKey)
|
||||
head metadata =
|
||||
head : PagePath Pages.PathKey -> Metadata -> List (Head.Tag Pages.PathKey)
|
||||
head currentPath metadata =
|
||||
commonHeadTags
|
||||
++ (case metadata of
|
||||
Metadata.Page meta ->
|
||||
@ -543,26 +510,45 @@ head metadata =
|
||||
|> Seo.website
|
||||
|
||||
Metadata.Article meta ->
|
||||
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
|
||||
Head.structuredData
|
||||
(StructuredData.article
|
||||
{ title = meta.title
|
||||
, description = meta.description
|
||||
, author = StructuredData.person { name = meta.author.name }
|
||||
, publisher = StructuredData.person { name = "Dillon Kearns" }
|
||||
, url = canonicalSiteUrl ++ "/" ++ PagePath.toString currentPath
|
||||
, imageUrl = canonicalSiteUrl ++ "/" ++ ImagePath.toString meta.image
|
||||
, datePublished = Date.toIsoString meta.published
|
||||
, mainEntityOfPage =
|
||||
StructuredData.softwareSourceCode
|
||||
{ codeRepositoryUrl = "https://github.com/dillonkearns/elm-pages"
|
||||
, description = "A statically typed site generator for Elm."
|
||||
, author = "Dillon Kearns"
|
||||
, programmingLanguage = StructuredData.elmLang
|
||||
}
|
||||
}
|
||||
)
|
||||
:: (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 ->
|
||||
let
|
||||
|
@ -91,7 +91,7 @@ decoder =
|
||||
|> Decode.map Article
|
||||
|
||||
_ ->
|
||||
Decode.fail <| "Unexpected page type " ++ pageType
|
||||
Decode.fail <| "Unexpected page \"type\" " ++ pageType
|
||||
)
|
||||
|
||||
|
||||
@ -111,9 +111,6 @@ imageDecoder =
|
||||
|
||||
findMatchingImage : String -> Maybe (ImagePath Pages.PathKey)
|
||||
findMatchingImage imageAssetPath =
|
||||
Pages.allImages
|
||||
|> List.Extra.find
|
||||
(\image ->
|
||||
ImagePath.toString image
|
||||
== imageAssetPath
|
||||
)
|
||||
List.Extra.find
|
||||
(\image -> ImagePath.toString image == imageAssetPath)
|
||||
Pages.allImages
|
||||
|
@ -4,7 +4,7 @@ import Element
|
||||
import Element.Border
|
||||
import Element.Font
|
||||
import FontAwesome
|
||||
import Json.Decode.Exploration as Decode
|
||||
import OptimizedDecoder as Decode
|
||||
import Pages.Secrets as Secrets
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Palette
|
||||
|
3
examples/external-data/content/other-page.md
Normal file
3
examples/external-data/content/other-page.md
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: "Hello!"
|
||||
---
|
@ -27,7 +27,6 @@
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"justinmimbs/date": "3.1.2",
|
||||
"lukewestby/elm-string-interpolate": "1.0.4",
|
||||
"mdgriffith/elm-markup": "3.0.1",
|
||||
"mdgriffith/elm-ui": "1.1.5",
|
||||
"mgold/elm-nonempty-list": "4.0.2",
|
||||
"miniBill/elm-codec": "1.2.0",
|
||||
@ -51,4 +50,4 @@
|
||||
},
|
||||
"indirect": {}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import "elm-oembed";
|
||||
import "./style.css";
|
||||
// @ts-ignore
|
||||
const { Elm } = require("./src/Main.elm");
|
||||
const pagesInit = require("elm-pages");
|
||||
const pagesInit = require("../../index.js");
|
||||
|
||||
pagesInit({
|
||||
mainElmModule: Elm.Main
|
||||
|
477
examples/external-data/package-lock.json
generated
477
examples/external-data/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -190,7 +190,7 @@ view siteMetadata page =
|
||||
\model viewForPage ->
|
||||
{ title = "Landing Page"
|
||||
, body =
|
||||
[ header starCount
|
||||
[ header page starCount
|
||||
, Element.text "Built at: "
|
||||
, Element.text <| String.fromInt <| Time.posixToMillis Pages.builtAt
|
||||
, pokemon
|
||||
@ -239,8 +239,8 @@ articleImageView articleImage =
|
||||
}
|
||||
|
||||
|
||||
header : Int -> Element msg
|
||||
header starCount =
|
||||
header : { path : PagePath Pages.PathKey, frontmatter : Metadata } -> Int -> Element msg
|
||||
header currentPage starCount =
|
||||
Element.column [ Element.width Element.fill ]
|
||||
[ Element.el
|
||||
[ Element.height (Element.px 4)
|
||||
@ -263,7 +263,12 @@ header starCount =
|
||||
, Element.Border.color (Element.rgba255 40 80 40 0.4)
|
||||
]
|
||||
[ Element.link []
|
||||
{ url = "/"
|
||||
{ url =
|
||||
if currentPage.path == pages.index then
|
||||
PagePath.toString pages.otherPage
|
||||
|
||||
else
|
||||
PagePath.toString pages.index
|
||||
, label =
|
||||
Element.row
|
||||
[ Font.size 30
|
||||
|
@ -36,11 +36,10 @@ prerenderRcFormattedPath : String -> String
|
||||
prerenderRcFormattedPath path =
|
||||
path
|
||||
|> dropExtension
|
||||
|> chopForwardSlashes
|
||||
|> String.split "/"
|
||||
|> dropIndexFromLast
|
||||
|> List.drop 1
|
||||
|> String.join "/"
|
||||
|> (\pathSoFar -> "/" ++ pathSoFar)
|
||||
|
||||
|
||||
dropIndexFromLast : List String -> List String
|
||||
@ -50,7 +49,7 @@ dropIndexFromLast path =
|
||||
|> (\reversePath ->
|
||||
case List.head reversePath of
|
||||
Just "index" ->
|
||||
reversePath |> List.drop 1
|
||||
List.drop 1 reversePath
|
||||
|
||||
_ ->
|
||||
reversePath
|
||||
@ -58,12 +57,17 @@ dropIndexFromLast path =
|
||||
|> List.reverse
|
||||
|
||||
|
||||
chopForwardSlashes : String -> String
|
||||
chopForwardSlashes =
|
||||
String.split "/" >> List.filter ((/=) "") >> String.join "/"
|
||||
|
||||
|
||||
pathFor : { entry | path : String } -> String
|
||||
pathFor page =
|
||||
page.path
|
||||
|> dropExtension
|
||||
|> chopForwardSlashes
|
||||
|> String.split "/"
|
||||
|> List.drop 1
|
||||
|> dropIndexFromLast
|
||||
|> List.map (\pathPart -> String.concat [ "\"", pathPart, "\"" ])
|
||||
|> String.join ", "
|
||||
@ -72,11 +76,11 @@ pathFor page =
|
||||
|
||||
dropExtension : String -> String
|
||||
dropExtension path =
|
||||
if path |> String.endsWith ".emu" then
|
||||
path |> String.dropRight 4
|
||||
if String.endsWith ".emu" path then
|
||||
String.dropRight 4 path
|
||||
|
||||
else if path |> String.endsWith ".md" then
|
||||
path |> String.dropRight 3
|
||||
else if String.endsWith ".md" path then
|
||||
String.dropRight 3 path
|
||||
|
||||
else
|
||||
path
|
||||
@ -91,20 +95,13 @@ import Dict exposing (Dict)
|
||||
|
||||
content : { markdown : List ( List String, { frontMatter : String, body : Maybe String } ), markup : List ( List String, String ) }
|
||||
content =
|
||||
{ markdown = markdown, markup = markup }
|
||||
{ markdown = markdown }
|
||||
|
||||
|
||||
markdown : List ( List String, { frontMatter : String, body : Maybe String } )
|
||||
markdown =
|
||||
[ {1}
|
||||
]
|
||||
|
||||
|
||||
markup : List ( List String, String )
|
||||
markup =
|
||||
[
|
||||
{0}
|
||||
]
|
||||
"""
|
||||
[ List.map generatePage content |> String.join "\n ,"
|
||||
, List.map generateMarkdownPage markdownContent |> String.join "\n ,"
|
||||
@ -219,14 +216,13 @@ init flags cliOptions =
|
||||
|
||||
|
||||
generateFileContents : List MarkdownContent -> List ( String, String )
|
||||
generateFileContents markdownFiles =
|
||||
markdownFiles
|
||||
|> List.map
|
||||
(\file ->
|
||||
( prerenderRcFormattedPath file.path |> String.dropLeft 1
|
||||
, file.body
|
||||
)
|
||||
generateFileContents =
|
||||
List.map
|
||||
(\file ->
|
||||
( prerenderRcFormattedPath file.path
|
||||
, file.body
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
main : Program.StatelessProgram Never Extras
|
||||
|
@ -2,6 +2,7 @@ const path = require("path");
|
||||
const fs = require("fs");
|
||||
const globby = require("globby");
|
||||
const parseFrontmatter = require("./frontmatter.js");
|
||||
const webpack = require('webpack')
|
||||
|
||||
function unpackFile(filePath) {
|
||||
const { content, data } = parseFrontmatter(
|
||||
@ -15,55 +16,74 @@ function unpackFile(filePath) {
|
||||
|
||||
return {
|
||||
baseRoute,
|
||||
content
|
||||
content,
|
||||
filePath
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = class AddFilesPlugin {
|
||||
constructor(data, filesToGenerate) {
|
||||
this.pagesWithRequests = data;
|
||||
this.filesToGenerate = filesToGenerate;
|
||||
}
|
||||
apply(compiler) {
|
||||
compiler.hooks.emit.tap("AddFilesPlugin", compilation => {
|
||||
const files = globby
|
||||
.sync(["content/**/*.*", "!content/**/*.emu"], {})
|
||||
.map(unpackFile);
|
||||
apply(/** @type {webpack.Compiler} */ compiler) {
|
||||
|
||||
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
|
||||
(global.mode === "dev" ? compiler.hooks.emit : compiler.hooks.make).tapAsync("AddFilesPlugin", (compilation, callback) => {
|
||||
|
||||
let route = `/${file.baseRoute}`.replace(/\/$/, '');
|
||||
if (route === '') {
|
||||
route = '/';
|
||||
|
||||
const files = globby.sync("content").map(unpackFile);
|
||||
|
||||
|
||||
let staticRequestData = {}
|
||||
global.pagesWithRequests.then(payload => {
|
||||
|
||||
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]))
|
||||
}
|
||||
const staticRequests = this.pagesWithRequests[route];
|
||||
else {
|
||||
staticRequestData = payload.pages
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
const filename = path.join(file.baseRoute, "content.json");
|
||||
compilation.fileDependencies.add(filename);
|
||||
const rawContents = JSON.stringify({
|
||||
body: file.content,
|
||||
staticData: staticRequests || {}
|
||||
});
|
||||
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
|
||||
|
||||
compilation.assets[filename] = {
|
||||
source: () => rawContents,
|
||||
size: () => rawContents.length
|
||||
};
|
||||
});
|
||||
let route = file.baseRoute.replace(/\/$/, '');
|
||||
const staticRequests = staticRequestData[route];
|
||||
|
||||
(this.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
|
||||
};
|
||||
});
|
||||
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");
|
||||
|
||||
module.exports = runElm;
|
||||
function runElm(/** @type string */ mode, /** @type any */ callback) {
|
||||
const elmBaseDirectory = "./elm-stuff/elm-pages";
|
||||
const mainElmFile = "../../src/Main.elm";
|
||||
const startingDir = process.cwd();
|
||||
process.chdir(elmBaseDirectory);
|
||||
compileToString([mainElmFile], {}).then(function(data) {
|
||||
(function() {
|
||||
function runElm(/** @type string */ mode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const elmBaseDirectory = "./elm-stuff/elm-pages";
|
||||
const mainElmFile = "../../src/Main.elm";
|
||||
const startingDir = process.cwd();
|
||||
process.chdir(elmBaseDirectory);
|
||||
const data = compileToStringSync([mainElmFile], {});
|
||||
process.chdir(startingDir);
|
||||
(function () {
|
||||
const warnOriginal = console.warn;
|
||||
console.warn = function() {};
|
||||
console.warn = function () { };
|
||||
eval(data.toString());
|
||||
const app = Elm.Main.init({
|
||||
flags: { secrets: process.env, mode }
|
||||
flags: { secrets: process.env, mode, staticHttpCache: global.staticHttpCache }
|
||||
});
|
||||
|
||||
app.ports.toJsPort.subscribe(payload => {
|
||||
process.chdir(startingDir);
|
||||
|
||||
if (payload.tag === "Success") {
|
||||
callback(payload.args[0]);
|
||||
global.staticHttpCache = payload.args[0].staticHttpCache;
|
||||
resolve(payload.args[0])
|
||||
} else {
|
||||
console.log(payload.args[0]);
|
||||
process.exit(1);
|
||||
reject(payload.args[0])
|
||||
}
|
||||
delete Elm;
|
||||
console.warn = warnOriginal;
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
const webpack = require("webpack");
|
||||
const middleware = require("webpack-dev-middleware");
|
||||
const path = require("path");
|
||||
const HTMLWebpackPlugin = require("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 TerserPlugin = require('terser-webpack-plugin');
|
||||
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 };
|
||||
function start({ routes, debug, customPort, manifestConfig, routesWithRequests, filesToGenerate }) {
|
||||
function start({ routes, debug, customPort, manifestConfig }) {
|
||||
const config = webpackOptions(false, routes, {
|
||||
debug,
|
||||
manifestConfig,
|
||||
routesWithRequests,
|
||||
filesToGenerate
|
||||
manifestConfig
|
||||
});
|
||||
|
||||
const compiler = webpack(config);
|
||||
@ -31,8 +43,7 @@ function start({ routes, debug, customPort, manifestConfig, routesWithRequests,
|
||||
hot: true,
|
||||
inline: true,
|
||||
host: "localhost",
|
||||
stats: "errors-only",
|
||||
publicPath: "/"
|
||||
stats: "errors-only"
|
||||
};
|
||||
|
||||
const app = express();
|
||||
@ -40,21 +51,33 @@ function start({ routes, debug, customPort, manifestConfig, routesWithRequests,
|
||||
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, {
|
||||
log: console.log, path: '/__webpack_hmr'
|
||||
}))
|
||||
|
||||
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
|
||||
// see: https://github.com/jantimon/html-webpack-plugin/issues/145#issuecomment-170554832
|
||||
const filename = path.join(compiler.outputPath, "index.html");
|
||||
const route = req.originalUrl.replace(/(\w)\/$/, "$1").replace(/^\//, "");
|
||||
const isPage = routes.includes(route);
|
||||
|
||||
compiler.outputFileSystem.readFile(filename, function (err, result) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const contents = isPage
|
||||
? replaceBaseAndLinks(result.toString(), route)
|
||||
: result
|
||||
|
||||
res.set("content-type", "text/html");
|
||||
res.send(result);
|
||||
res.send(contents);
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
@ -67,20 +90,18 @@ function start({ routes, debug, customPort, manifestConfig, routesWithRequests,
|
||||
// app.use(express.static(__dirname + "/path-to-static-folder"));
|
||||
}
|
||||
|
||||
function run({ routes, manifestConfig, routesWithRequests, filesToGenerate }, callback) {
|
||||
function run({ routes, manifestConfig }) {
|
||||
webpack(
|
||||
webpackOptions(true, routes, {
|
||||
debug: false,
|
||||
manifestConfig,
|
||||
routesWithRequests,
|
||||
filesToGenerate
|
||||
})
|
||||
).run((err, stats) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
} else {
|
||||
callback();
|
||||
// done
|
||||
}
|
||||
|
||||
console.log(
|
||||
@ -121,12 +142,13 @@ function printProgress(progress, message) {
|
||||
function webpackOptions(
|
||||
production,
|
||||
routes,
|
||||
{ debug, manifestConfig, routesWithRequests, filesToGenerate }
|
||||
{ debug, manifestConfig }
|
||||
) {
|
||||
const common = {
|
||||
mode: production ? "production" : "development",
|
||||
plugins: [
|
||||
new AddFilesPlugin(routesWithRequests, filesToGenerate),
|
||||
new PluginGenerateElmPagesBuild(),
|
||||
new AddFilesPlugin(),
|
||||
new CopyPlugin([
|
||||
{
|
||||
from: "static/**/*",
|
||||
@ -160,16 +182,48 @@ function webpackOptions(
|
||||
|
||||
new HTMLWebpackPlugin({
|
||||
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({
|
||||
preload: /\.js$/,
|
||||
defaultAttribute: 'defer'
|
||||
}),
|
||||
new FaviconsWebpackPlugin({
|
||||
logo: path.resolve(process.cwd(), `./${manifestConfig.sourceIcon}`),
|
||||
logo: `./${manifestConfig.sourceIcon}`,
|
||||
prefix: "assets/",
|
||||
|
||||
publicPath: "",
|
||||
outputPath: "",
|
||||
|
||||
favicons: {
|
||||
path: "/", // Path for overriding default icons path. `string`
|
||||
manifestRelativePaths: true,
|
||||
path: "", // Path for overriding default icons path. `string`
|
||||
appName: manifestConfig.name, // Your application's name. `string`
|
||||
appShortName: manifestConfig.short_name, // Your application's short_name. `string`. Optional. If not set, appName will be used
|
||||
appDescription: manifestConfig.description, // Your application's description. `string`
|
||||
@ -221,14 +275,12 @@ function webpackOptions(
|
||||
/assets\//
|
||||
],
|
||||
swDest: "service-worker.js"
|
||||
})
|
||||
}),
|
||||
// comment this out to do performance profiling
|
||||
// (drag-and-drop `events.json` file into Chrome performance tab)
|
||||
// , new webpack.debug.ProfilingPlugin()
|
||||
// new webpack.debug.ProfilingPlugin()
|
||||
],
|
||||
output: {
|
||||
publicPath: "/"
|
||||
},
|
||||
output: {},
|
||||
resolve: {
|
||||
modules: [
|
||||
path.resolve(process.cwd(), `./node_modules`),
|
||||
@ -315,10 +367,22 @@ function webpackOptions(
|
||||
}),
|
||||
new PrerenderSPAPlugin({
|
||||
staticDir: path.join(process.cwd(), "dist"),
|
||||
routes: routes,
|
||||
routes: routes.map(r => `/${r}`),
|
||||
|
||||
renderer: new PrerenderSPAPlugin.PuppeteerRenderer({
|
||||
renderAfterDocumentEvent: "prerender-trigger",
|
||||
})
|
||||
headless: true,
|
||||
devtools: false,
|
||||
}),
|
||||
|
||||
postProcess: renderedRoute => {
|
||||
renderedRoute.html = replaceBaseAndLinks(
|
||||
renderedRoute.html,
|
||||
renderedRoute.route
|
||||
)
|
||||
|
||||
return renderedRoute
|
||||
}
|
||||
})
|
||||
],
|
||||
module: {
|
||||
@ -339,14 +403,14 @@ function webpackOptions(
|
||||
} else {
|
||||
return merge(common, {
|
||||
entry: [
|
||||
require.resolve("webpack-hot-middleware/client"),
|
||||
hmrClientPath(),
|
||||
"./index.js",
|
||||
],
|
||||
plugins: [
|
||||
new webpack.NamedModulesPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
// Prevents compilation errors causing the hot loader to lose state
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin()
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
@ -369,3 +433,63 @@ 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) {
|
||||
return route.replace(/(^\/|\/$)/, "")
|
||||
}
|
||||
|
||||
|
||||
function pathToRoot(cleanedRoute) {
|
||||
return cleanedRoute === ""
|
||||
? cleanedRoute
|
||||
: cleanedRoute
|
||||
.split("/")
|
||||
.map(_ => "..")
|
||||
.join("/")
|
||||
.replace(/\.$/, "./")
|
||||
}
|
||||
|
||||
|
||||
function replaceBaseAndLinks(html, route) {
|
||||
const cleanedRoute = cleanRoute(route)
|
||||
|
||||
const href = cleanedRoute === '' ? './' : pathToRoot(cleanedRoute)
|
||||
return (html || "").replace(`<base href="/"`, `<base href="${href}"`)
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
generateRawContent = require("./generate-raw-content.js");
|
||||
const exposingList =
|
||||
"(PathKey, allPages, allImages, internals, images, isValidRoute, pages, builtAt)";
|
||||
"(PathKey, allPages, allImages, internals, images, isValidRoute, pages, builtAt)";
|
||||
|
||||
function staticRouteStuff(staticRoutes) {
|
||||
return `
|
||||
return `
|
||||
|
||||
|
||||
${staticRoutes.allRoutes}
|
||||
@ -12,6 +12,7 @@ ${staticRoutes.routeRecord}
|
||||
|
||||
${staticRoutes.imageAssetsRecord}
|
||||
|
||||
|
||||
allImages : List (ImagePath PathKey)
|
||||
allImages =
|
||||
[${staticRoutes.allImages.join("\n , ")}
|
||||
@ -40,8 +41,8 @@ isValidRoute route =
|
||||
`;
|
||||
}
|
||||
|
||||
function elmPagesUiFile(staticRoutes, markdownContent, markupContent) {
|
||||
return `port module Pages exposing ${exposingList}
|
||||
function elmPagesUiFile(staticRoutes, markdownContent) {
|
||||
return `port module Pages exposing ${exposingList}
|
||||
|
||||
import Color exposing (Color)
|
||||
import Pages.Internal
|
||||
@ -49,7 +50,6 @@ import Head
|
||||
import Html exposing (Html)
|
||||
import Json.Decode
|
||||
import Json.Encode
|
||||
import Mark
|
||||
import Pages.Platform
|
||||
import Pages.Manifest exposing (DisplayMode, Orientation)
|
||||
import Pages.Manifest.Category as Category exposing (Category)
|
||||
@ -65,6 +65,7 @@ builtAt : Time.Posix
|
||||
builtAt =
|
||||
Time.millisToPosix ${Math.round((global.builtAt).getTime())}
|
||||
|
||||
|
||||
type PathKey
|
||||
= PathKey
|
||||
|
||||
@ -74,7 +75,6 @@ buildImage path =
|
||||
ImagePath.build PathKey ("images" :: path)
|
||||
|
||||
|
||||
|
||||
buildPage : List String -> PagePath PathKey
|
||||
buildPage path =
|
||||
PagePath.build PathKey path
|
||||
@ -92,23 +92,26 @@ directoryWithoutIndex path =
|
||||
|
||||
port toJsPort : Json.Encode.Value -> Cmd msg
|
||||
|
||||
port fromJsPort : (Json.Decode.Value -> msg) -> Sub msg
|
||||
|
||||
|
||||
internals : Pages.Internal.Internal PathKey
|
||||
internals =
|
||||
{ applicationType = Pages.Internal.Browser
|
||||
, toJsPort = toJsPort
|
||||
, fromJsPort = fromJsPort identity
|
||||
, content = content
|
||||
, pathKey = PathKey
|
||||
}
|
||||
|
||||
|
||||
${staticRouteStuff(staticRoutes)}
|
||||
|
||||
${generateRawContent(markdownContent, markupContent, false)}
|
||||
${generateRawContent(markdownContent, false)}
|
||||
`;
|
||||
}
|
||||
|
||||
function elmPagesCliFile(staticRoutes, markdownContent, markupContent) {
|
||||
return `port module Pages exposing ${exposingList}
|
||||
function elmPagesCliFile(staticRoutes, markdownContent) {
|
||||
return `port module Pages exposing ${exposingList}
|
||||
|
||||
import Color exposing (Color)
|
||||
import Pages.Internal
|
||||
@ -116,7 +119,6 @@ import Head
|
||||
import Html exposing (Html)
|
||||
import Json.Decode
|
||||
import Json.Encode
|
||||
import Mark
|
||||
import Pages.Platform
|
||||
import Pages.Manifest exposing (DisplayMode, Orientation)
|
||||
import Pages.Manifest.Category as Category exposing (Category)
|
||||
@ -132,6 +134,7 @@ builtAt : Time.Posix
|
||||
builtAt =
|
||||
Time.millisToPosix ${Math.round((global.builtAt).getTime())}
|
||||
|
||||
|
||||
type PathKey
|
||||
= PathKey
|
||||
|
||||
@ -159,10 +162,14 @@ directoryWithoutIndex path =
|
||||
port toJsPort : Json.Encode.Value -> Cmd msg
|
||||
|
||||
|
||||
port fromJsPort : (Json.Decode.Value -> msg) -> Sub msg
|
||||
|
||||
|
||||
internals : Pages.Internal.Internal PathKey
|
||||
internals =
|
||||
{ applicationType = Pages.Internal.Cli
|
||||
, toJsPort = toJsPort
|
||||
, fromJsPort = fromJsPort identity
|
||||
, content = content
|
||||
, pathKey = PathKey
|
||||
}
|
||||
@ -170,7 +177,7 @@ internals =
|
||||
|
||||
${staticRouteStuff(staticRoutes)}
|
||||
|
||||
${generateRawContent(markdownContent, markupContent, true)}
|
||||
${generateRawContent(markdownContent, true)}
|
||||
`;
|
||||
}
|
||||
module.exports = { elmPagesUiFile, elmPagesCliFile };
|
||||
|
@ -5,66 +5,38 @@ const { version } = require("../../package.json");
|
||||
const fs = require("fs");
|
||||
const globby = require("globby");
|
||||
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 path = require("path");
|
||||
const { ensureDirSync, deleteIfExists } = require('./file-helpers.js')
|
||||
const generateRecords = require("./generate-records.js");
|
||||
const doCliStuff = require("./generate-elm-stuff.js");
|
||||
global.builtAt = new Date();
|
||||
|
||||
const contentGlobPath = "content/**/*.emu";
|
||||
|
||||
let watcher = null;
|
||||
let devServerRunning = false;
|
||||
global.staticHttpCache = {};
|
||||
|
||||
function unpackFile(path) {
|
||||
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) {
|
||||
const { content, data } = parseFrontmatter(path, fileContents);
|
||||
return {
|
||||
path,
|
||||
metadata: JSON.stringify(data),
|
||||
body: content,
|
||||
extension: "md"
|
||||
body: content
|
||||
};
|
||||
}
|
||||
|
||||
function run() {
|
||||
console.log("Running elm-pages...");
|
||||
const content = globby.sync([contentGlobPath], {}).map(unpackMarkup);
|
||||
const staticRoutes = generateRecords();
|
||||
|
||||
const markdownContent = globby
|
||||
.sync(["content/**/*.*", "!content/**/*.emu"], {})
|
||||
.sync(["content/**/*.*"], {})
|
||||
.map(unpackFile)
|
||||
.map(({ path, contents }) => {
|
||||
return parseMarkdown(path, contents);
|
||||
});
|
||||
const images = globby
|
||||
.sync("images/**/*", {})
|
||||
.filter(imagePath => !fs.lstatSync(imagePath).isDirectory());
|
||||
|
||||
let app = Elm.Main.init({
|
||||
flags: {
|
||||
argv: process.argv,
|
||||
versionMessage: version,
|
||||
content,
|
||||
markdownContent,
|
||||
images
|
||||
}
|
||||
});
|
||||
|
||||
@ -78,89 +50,51 @@ function run() {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
app.ports.writeFile.subscribe(contents => {
|
||||
const routes = toRoutes(markdownContent.concat(content));
|
||||
app.ports.writeFile.subscribe(cliOptions => {
|
||||
|
||||
|
||||
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(
|
||||
contents.watch ? "dev" : "prod",
|
||||
global.mode,
|
||||
staticRoutes,
|
||||
markdownContent,
|
||||
content,
|
||||
function(payload) {
|
||||
if (contents.watch) {
|
||||
startWatchIfNeeded();
|
||||
if (!devServerRunning) {
|
||||
devServerRunning = true;
|
||||
develop.start({
|
||||
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);
|
||||
}
|
||||
markdownContent
|
||||
).then((payload) => {
|
||||
if (cliOptions.watch) {
|
||||
develop.start({
|
||||
routes,
|
||||
debug: cliOptions.debug,
|
||||
customPort: cliOptions.customPort,
|
||||
manifestConfig: payload.manifest,
|
||||
|
||||
develop.run(
|
||||
{
|
||||
routes,
|
||||
manifestConfig: payload.manifest,
|
||||
routesWithRequests: payload.pages,
|
||||
filesToGenerate: payload.filesToGenerate
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
develop.run({
|
||||
routes,
|
||||
debug: cliOptions.debug,
|
||||
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();
|
||||
|
||||
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) {
|
||||
return entries.map(toRoute);
|
||||
}
|
||||
@ -169,7 +103,8 @@ function toRoute(entry) {
|
||||
let fullPath = entry.path
|
||||
.replace(/(index)?\.[^/.]+$/, "")
|
||||
.split("/")
|
||||
.filter(item => item !== "");
|
||||
fullPath.splice(0, 1);
|
||||
return `/${fullPath.join("/")}`;
|
||||
.filter(item => item !== "")
|
||||
.slice(1);
|
||||
|
||||
return fullPath.join("/");
|
||||
}
|
||||
|
@ -1,23 +1,5 @@
|
||||
const path = require("path");
|
||||
const matter = require("gray-matter");
|
||||
|
||||
module.exports = function parseFrontmatter(filePath, fileContents) {
|
||||
return path.extname(filePath) === ".emu"
|
||||
? matter(fileContents, markupFrontmatterOptions)
|
||||
: matter(fileContents);
|
||||
};
|
||||
|
||||
const markupFrontmatterOptions = {
|
||||
language: "markup",
|
||||
engines: {
|
||||
markup: {
|
||||
parse: function(string) {
|
||||
return string;
|
||||
},
|
||||
|
||||
stringify: function(string) {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
}
|
||||
return matter(fileContents);
|
||||
};
|
||||
|
@ -1,35 +1,57 @@
|
||||
const fs = require("fs");
|
||||
const runElm = require("./compile-elm.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 { ensureDirSync, deleteIfExists } = require('./file-helpers.js')
|
||||
let wasEqualBefore = false
|
||||
|
||||
|
||||
module.exports = function run(
|
||||
mode,
|
||||
staticRoutes,
|
||||
markdownContent,
|
||||
markupContent,
|
||||
callback
|
||||
markdownContent
|
||||
) {
|
||||
ensureDirSync("./elm-stuff");
|
||||
ensureDirSync("./gen");
|
||||
ensureDirSync("./elm-stuff/elm-pages");
|
||||
|
||||
// prevent compilation errors if migrating from previous elm-pages version
|
||||
deleteIfExists("./elm-stuff/elm-pages/Pages/ContentCache.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
|
||||
fs.writeFileSync(
|
||||
"./elm-stuff/elm-pages/Pages.elm",
|
||||
elmPagesCliFile(staticRoutes, markdownContent, markupContent)
|
||||
elmPagesCliFile(staticRoutes, markdownContent)
|
||||
);
|
||||
|
||||
// write modified elm.json to elm-stuff/elm-pages/
|
||||
copyModifiedElmJson();
|
||||
|
||||
// run Main.elm from elm-stuff/elm-pages with `runElm`
|
||||
runElm(mode, callback);
|
||||
return runElm(mode);
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
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 } )
|
||||
content =
|
||||
[ ${markdown.concat(markup).map(entry => toEntry(entry, includeBody))}
|
||||
[ ${markdown.map(entry => toEntry(entry, includeBody))}
|
||||
]`;
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
const path = require("path");
|
||||
const matter = require("gray-matter");
|
||||
const dir = "content/";
|
||||
const glob = require("glob");
|
||||
const fs = require("fs");
|
||||
@ -138,14 +137,14 @@ function allImageAssetNames() {
|
||||
});
|
||||
}
|
||||
function toPascalCase(str) {
|
||||
var pascal = str.replace(/(\-\w)/g, function(m) {
|
||||
var pascal = str.replace(/(\-\w)/g, function (m) {
|
||||
return m[1].toUpperCase();
|
||||
});
|
||||
return pascal.charAt(0).toUpperCase() + pascal.slice(1);
|
||||
}
|
||||
|
||||
function toCamelCase(str) {
|
||||
var pascal = str.replace(/(\-\w)/g, function(m) {
|
||||
var pascal = str.replace(/(\-\w)/g, function (m) {
|
||||
return m[1].toUpperCase();
|
||||
});
|
||||
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
||||
@ -180,14 +179,14 @@ function formatRecord(directoryPath, rec, asType, level) {
|
||||
} else {
|
||||
keyVals.push(
|
||||
key +
|
||||
" =\n" +
|
||||
formatRecord(directoryPath.concat(key), val, asType, level + 1)
|
||||
" =\n" +
|
||||
formatRecord(directoryPath.concat(key), val, asType, level + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
keyVals.push(
|
||||
`directory = ${
|
||||
keys.includes("index") ? "directoryWithIndex" : "directoryWithoutIndex"
|
||||
keys.includes("index") ? "directoryWithIndex" : "directoryWithoutIndex"
|
||||
} [${directoryPath.map(pathFragment => `"${pathFragment}"`).join(", ")}]`
|
||||
);
|
||||
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,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="preload" href="./content.json" as="fetch" crossorigin />
|
||||
<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>
|
166
index.js
166
index.js
@ -11,14 +11,17 @@ module.exports = function pagesInit(
|
||||
prefetchedPages = [window.location.pathname];
|
||||
initialLocationHash = document.location.hash.replace(/^#/, "");
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
document.addEventListener("DOMContentLoaded", _ => {
|
||||
new MutationObserver(function() {
|
||||
elmViewRendered = true;
|
||||
if (headTagsAdded) {
|
||||
document.dispatchEvent(new Event("prerender-trigger"));
|
||||
}
|
||||
}).observe(document.body, { attributes: true, childList: true, subtree: true});
|
||||
new MutationObserver(function () {
|
||||
elmViewRendered = true;
|
||||
if (headTagsAdded) {
|
||||
document.dispatchEvent(new Event("prerender-trigger"));
|
||||
}
|
||||
}).observe(
|
||||
document.body,
|
||||
{ attributes: true, childList: true, subtree: true }
|
||||
);
|
||||
|
||||
loadContentAndInitializeApp(mainElmModule).then(resolve, reject);
|
||||
});
|
||||
@ -26,45 +29,101 @@ module.exports = function pagesInit(
|
||||
};
|
||||
|
||||
function loadContentAndInitializeApp(/** @type { init: any } */ mainElmModule) {
|
||||
return httpGet(`${window.location.origin}${window.location.pathname}/content.json`).then(function(/** @type JSON */ contentJson) {
|
||||
const isPrerendering = navigator.userAgent.indexOf("Headless") >= 0
|
||||
const path = window.location.pathname.replace(/(\w)$/, "$1/")
|
||||
|
||||
const app = mainElmModule.init({
|
||||
flags: {
|
||||
secrets: null,
|
||||
isPrerendering: navigator.userAgent.indexOf("Headless") >= 0,
|
||||
contentJson
|
||||
}
|
||||
});
|
||||
return Promise.all([
|
||||
getConfig(),
|
||||
httpGet(`${window.location.origin}${path}content.json`)]).then(function (/** @type {[DevServerConfig?, JSON]} */[devServerConfig, contentJson]) {
|
||||
console.log('devServerConfig', devServerConfig);
|
||||
|
||||
app.ports.toJsPort.subscribe((
|
||||
/** @type { { head: HeadTag[], allRoutes: string[] } } */ fromElm
|
||||
) => {
|
||||
appendTag({
|
||||
name: "meta",
|
||||
attributes: [
|
||||
["name", "generator"],
|
||||
["content", `elm-pages v${elmPagesVersion}`]
|
||||
]
|
||||
const app = mainElmModule.init({
|
||||
flags: {
|
||||
secrets: null,
|
||||
baseUrl: isPrerendering
|
||||
? window.location.origin
|
||||
: document.baseURI,
|
||||
isPrerendering: isPrerendering,
|
||||
isDevServer: !!module.hot,
|
||||
isElmDebugMode: devServerConfig ? devServerConfig.elmDebugger : false,
|
||||
contentJson,
|
||||
}
|
||||
});
|
||||
window.allRoutes = fromElm.allRoutes;
|
||||
|
||||
|
||||
if (navigator.userAgent.indexOf("Headless") >= 0) {
|
||||
fromElm.head.forEach(headTag => {
|
||||
appendTag(headTag);
|
||||
app.ports.toJsPort.subscribe((
|
||||
/** @type { { head: SeoTag[], allRoutes: string[] } } */ fromElm
|
||||
) => {
|
||||
appendTag({
|
||||
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;
|
||||
if (elmViewRendered) {
|
||||
document.dispatchEvent(new Event("prerender-trigger"));
|
||||
}
|
||||
} else {
|
||||
setupLinkPrefetching();
|
||||
} else {
|
||||
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() {
|
||||
@ -123,7 +182,7 @@ function setupLinkPrefetchingHelp(
|
||||
const links = document.querySelectorAll("a");
|
||||
links.forEach(link => {
|
||||
// console.log(link.pathname);
|
||||
link.addEventListener("mouseenter", function(event) {
|
||||
link.addEventListener("mouseenter", function (event) {
|
||||
if (
|
||||
event &&
|
||||
event.target &&
|
||||
@ -141,8 +200,8 @@ function prefetchIfNeeded(/** @type {HTMLAnchorElement} */ target) {
|
||||
if (target.host === window.location.host) {
|
||||
if (prefetchedPages.includes(target.pathname)) {
|
||||
// console.log("Already preloaded", target.href);
|
||||
} else if (!allRoutes.includes(target.pathname)) {
|
||||
// console.log("Not a known route, skipping preload", target.pathname);
|
||||
} else if (!allRoutes.includes(new URL(target.pathname, document.baseURI).href)) {
|
||||
}
|
||||
else {
|
||||
prefetchedPages.push(target.pathname);
|
||||
@ -157,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) {
|
||||
const meta = document.createElement(tagDetails.name);
|
||||
tagDetails.attributes.forEach(([name, value]) => {
|
||||
@ -166,15 +227,36 @@ function appendTag(/** @type {HeadTag} */ tagDetails) {
|
||||
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) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const xmlHttp = new XMLHttpRequest();
|
||||
xmlHttp.onreadystatechange = function() {
|
||||
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
|
||||
resolve(JSON.parse(xmlHttp.responseText));
|
||||
xmlHttp.onreadystatechange = function () {
|
||||
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
|
||||
resolve(JSON.parse(xmlHttp.responseText));
|
||||
}
|
||||
xmlHttp.onerror = reject;
|
||||
xmlHttp.open("GET", theUrl, true); // true for asynchronous
|
||||
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 */
|
||||
|
5140
package-lock.json
generated
5140
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "elm-pages",
|
||||
"version": "1.2.11",
|
||||
"version": "1.3.0",
|
||||
"homepage": "http://elm-pages.com",
|
||||
"description": "Type-safe static sites, written in pure elm with your own custom elm-markup syntax.",
|
||||
"main": "index.js",
|
||||
@ -22,7 +22,6 @@
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"babel-loader": "^8.0.6",
|
||||
"chokidar": "^2.1.5",
|
||||
"copy-webpack-plugin": "^5.0.4",
|
||||
"cross-spawn": "6.0.5",
|
||||
"css-loader": "^3.2.0",
|
||||
@ -35,18 +34,19 @@
|
||||
"find-elm-dependencies": "2.0.2",
|
||||
"globby": "^10.0.1",
|
||||
"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-webpack-plugin": "^2.4.2",
|
||||
"lodash": "4.17.15",
|
||||
"node-sass": "^4.12.0",
|
||||
"prerender-spa-plugin": "^3.4.0",
|
||||
"raw-loader": "^4.0.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"script-ext-html-webpack-plugin": "^2.1.4",
|
||||
"style-loader": "^1.0.0",
|
||||
"temp": "^0.9.0",
|
||||
"terser-webpack-plugin": "^2.3.5",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack": "4.42.1",
|
||||
"webpack-dev-middleware": "^3.7.0",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-merge": "^4.2.1",
|
||||
@ -54,7 +54,6 @@
|
||||
"xhr2": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chokidar": "^2.1.3",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/node": "^12.7.7",
|
||||
"@types/webpack": "^4.32.1",
|
||||
|
127
src/Head.elm
127
src/Head.elm
@ -1,6 +1,7 @@
|
||||
module Head exposing
|
||||
( Tag, metaName, metaProperty
|
||||
, rssLink, sitemapLink
|
||||
, structuredData
|
||||
, AttributeValue
|
||||
, currentPageFullUrl, fullImageUrl, fullPageUrl, raw
|
||||
, toJson, canonicalLink
|
||||
@ -19,6 +20,11 @@ writing a plugin package to extend `elm-pages`.
|
||||
@docs rssLink, sitemapLink
|
||||
|
||||
|
||||
## Structured Data
|
||||
|
||||
@docs structuredData
|
||||
|
||||
|
||||
## `AttributeValue`s
|
||||
|
||||
@docs AttributeValue
|
||||
@ -33,6 +39,7 @@ writing a plugin package to extend `elm-pages`.
|
||||
|
||||
import Json.Encode
|
||||
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
||||
import Pages.Internal.String as String
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
|
||||
|
||||
@ -41,6 +48,7 @@ through the `head` function.
|
||||
-}
|
||||
type Tag pathKey
|
||||
= Tag (Details pathKey)
|
||||
| StructuredData Json.Encode.Value
|
||||
|
||||
|
||||
type alias Details pathKey =
|
||||
@ -49,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).
|
||||
-}
|
||||
raw : String -> AttributeValue pathKey
|
||||
@ -195,11 +297,20 @@ node name attributes =
|
||||
code will run this for you to generate your `manifest.json` file automatically!
|
||||
-}
|
||||
toJson : String -> String -> Tag pathKey -> Json.Encode.Value
|
||||
toJson canonicalSiteUrl currentPagePath (Tag tag) =
|
||||
Json.Encode.object
|
||||
[ ( "name", Json.Encode.string tag.name )
|
||||
, ( "attributes", Json.Encode.list (encodeProperty canonicalSiteUrl currentPagePath) tag.attributes )
|
||||
]
|
||||
toJson canonicalSiteUrl currentPagePath tag =
|
||||
case tag of
|
||||
Tag headTag ->
|
||||
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
|
||||
@ -217,8 +328,4 @@ encodeProperty canonicalSiteUrl currentPagePath ( name, value ) =
|
||||
|
||||
joinPaths : String -> String -> String
|
||||
joinPaths base path =
|
||||
if (base |> String.endsWith "/") && (path |> String.startsWith "/") then
|
||||
base ++ String.dropLeft 1 path
|
||||
|
||||
else
|
||||
base ++ path
|
||||
String.chopEnd "/" base ++ "/" ++ String.chopStart "/" path
|
||||
|
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,15 +21,12 @@ import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
import Http
|
||||
import Json.Decode as Decode
|
||||
import Mark
|
||||
import Mark.Error
|
||||
import Pages.Document as Document exposing (Document)
|
||||
import Pages.Internal.String as String
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Result.Extra
|
||||
import Task exposing (Task)
|
||||
import TerminalText as Terminal
|
||||
import Url exposing (Url)
|
||||
import Url.Builder
|
||||
|
||||
|
||||
type alias Content =
|
||||
@ -88,22 +85,21 @@ pagesWithErrors cache =
|
||||
cache
|
||||
|> Result.map
|
||||
(\okCache ->
|
||||
okCache
|
||||
|> Dict.toList
|
||||
|> List.filterMap
|
||||
(\( path, value ) ->
|
||||
case value of
|
||||
Parsed metadata { body } ->
|
||||
case body of
|
||||
Err parseError ->
|
||||
createBuildError path parseError |> Just
|
||||
List.filterMap
|
||||
(\( path, value ) ->
|
||||
case value of
|
||||
Parsed metadata { body } ->
|
||||
case body of
|
||||
Err parseError ->
|
||||
createBuildError path parseError |> Just
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
)
|
||||
_ ->
|
||||
Nothing
|
||||
)
|
||||
(Dict.toList okCache)
|
||||
)
|
||||
|> Result.withDefault []
|
||||
|
||||
@ -114,22 +110,17 @@ init :
|
||||
-> Maybe { contentJson : ContentJson String, initialUrl : Url }
|
||||
-> ContentCache metadata view
|
||||
init document content maybeInitialPageContent =
|
||||
parseMetadata maybeInitialPageContent document content
|
||||
content
|
||||
|> parseMetadata maybeInitialPageContent document
|
||||
|> List.map
|
||||
(\tuple ->
|
||||
Tuple.mapSecond
|
||||
(\result ->
|
||||
result
|
||||
|> Result.mapError
|
||||
(\error ->
|
||||
-- ( Tuple.first tuple, error )
|
||||
createErrors (Tuple.first tuple) error
|
||||
)
|
||||
)
|
||||
tuple
|
||||
tuple
|
||||
|> Tuple.first
|
||||
|> createErrors
|
||||
|> Result.mapError
|
||||
|> (\f -> Tuple.mapSecond f tuple)
|
||||
)
|
||||
|> combineTupleResults
|
||||
-- |> Result.mapError Dict.fromList
|
||||
|> Result.map Dict.fromList
|
||||
|
||||
|
||||
@ -142,7 +133,7 @@ createBuildError path decodeError =
|
||||
{ title = "Metadata Decode Error"
|
||||
, message =
|
||||
[ Terminal.text "I ran into a problem when parsing the metadata for the page with this path: "
|
||||
, Terminal.text ("/" ++ (path |> String.join "/"))
|
||||
, Terminal.text (String.join "/" path)
|
||||
, Terminal.text "\n\n"
|
||||
, Terminal.text decodeError
|
||||
]
|
||||
@ -156,44 +147,43 @@ parseMetadata :
|
||||
-> List ( List String, { extension : String, frontMatter : String, body : Maybe String } )
|
||||
-> List ( List String, Result String (Entry metadata view) )
|
||||
parseMetadata maybeInitialPageContent document content =
|
||||
content
|
||||
|> List.map
|
||||
(\( path, { frontMatter, extension, body } ) ->
|
||||
let
|
||||
maybeDocumentEntry =
|
||||
Document.get extension document
|
||||
in
|
||||
case maybeDocumentEntry of
|
||||
Just documentEntry ->
|
||||
frontMatter
|
||||
|> documentEntry.frontmatterParser
|
||||
|> Result.map
|
||||
(\metadata ->
|
||||
let
|
||||
renderer =
|
||||
\value ->
|
||||
parseContent extension value document
|
||||
in
|
||||
case maybeInitialPageContent of
|
||||
Just { contentJson, initialUrl } ->
|
||||
if normalizePath initialUrl.path == (String.join "/" path |> normalizePath) then
|
||||
Parsed metadata
|
||||
{ body = renderer contentJson.body
|
||||
, staticData = contentJson.staticData
|
||||
}
|
||||
List.map
|
||||
(\( path, { frontMatter, extension, body } ) ->
|
||||
let
|
||||
maybeDocumentEntry =
|
||||
Document.get extension document
|
||||
in
|
||||
case maybeDocumentEntry of
|
||||
Just documentEntry ->
|
||||
frontMatter
|
||||
|> documentEntry.frontmatterParser
|
||||
|> Result.map
|
||||
(\metadata ->
|
||||
let
|
||||
renderer value =
|
||||
parseContent extension value document
|
||||
in
|
||||
case maybeInitialPageContent of
|
||||
Just { contentJson, initialUrl } ->
|
||||
if normalizePath initialUrl.path == (String.join "/" path |> normalizePath) then
|
||||
Parsed metadata
|
||||
{ body = renderer contentJson.body
|
||||
, staticData = contentJson.staticData
|
||||
}
|
||||
|
||||
else
|
||||
NeedContent extension metadata
|
||||
|
||||
Nothing ->
|
||||
else
|
||||
NeedContent extension metadata
|
||||
)
|
||||
|> Tuple.pair path
|
||||
|
||||
Nothing ->
|
||||
Err ("Could not find extension '" ++ extension ++ "'")
|
||||
|> Tuple.pair path
|
||||
)
|
||||
Nothing ->
|
||||
NeedContent extension metadata
|
||||
)
|
||||
|> Tuple.pair path
|
||||
|
||||
Nothing ->
|
||||
Err ("Could not find extension '" ++ extension ++ "'")
|
||||
|> Tuple.pair path
|
||||
)
|
||||
content
|
||||
|
||||
|
||||
normalizePath : String -> String
|
||||
@ -206,16 +196,15 @@ normalizePath pathString =
|
||||
String.endsWith "/" pathString
|
||||
in
|
||||
if pathString == "" then
|
||||
"/"
|
||||
pathString
|
||||
|
||||
else
|
||||
String.concat
|
||||
[ if hasPrefix then
|
||||
""
|
||||
String.dropLeft 1 pathString
|
||||
|
||||
else
|
||||
"/"
|
||||
, pathString
|
||||
pathString
|
||||
, if hasSuffix then
|
||||
""
|
||||
|
||||
@ -257,7 +246,7 @@ createHtmlError : List String -> String -> Html msg
|
||||
createHtmlError path error =
|
||||
Html.div []
|
||||
[ Html.h2 []
|
||||
[ Html.text ("/" ++ (path |> String.join "/"))
|
||||
[ Html.text (String.join "/" path)
|
||||
]
|
||||
, Html.p [] [ Html.text "I couldn't parse the frontmatter in this page. I ran into this error with your JSON decoder:" ]
|
||||
, Html.pre [] [ Html.text error ]
|
||||
@ -269,7 +258,6 @@ routes record =
|
||||
record
|
||||
|> List.map Tuple.first
|
||||
|> List.map (String.join "/")
|
||||
|> List.map (\route -> "/" ++ route)
|
||||
|
||||
|
||||
routesForCache : ContentCache metadata view -> List String
|
||||
@ -291,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 (path |> String.join "/")
|
||||
, errors
|
||||
|> List.map (Mark.Error.toHtml Mark.Error.Light)
|
||||
|> Html.div []
|
||||
]
|
||||
|
||||
|
||||
combineTupleResults :
|
||||
List ( List String, Result error success )
|
||||
-> Result (List error) (List ( List String, success ))
|
||||
@ -349,37 +327,40 @@ parse it before returning it and store the parsed version in the Cache
|
||||
-}
|
||||
lazyLoad :
|
||||
Document metadata view
|
||||
-> Url
|
||||
-> { currentUrl : Url, baseUrl : Url }
|
||||
-> ContentCache metadata view
|
||||
-> Task Http.Error (ContentCache metadata view)
|
||||
lazyLoad document url cacheResult =
|
||||
lazyLoad document urls cacheResult =
|
||||
case cacheResult of
|
||||
Err _ ->
|
||||
Task.succeed cacheResult
|
||||
|
||||
Ok cache ->
|
||||
case Dict.get (pathForUrl url) cache of
|
||||
case Dict.get (pathForUrl urls) cache of
|
||||
Just entry ->
|
||||
case entry of
|
||||
NeedContent extension _ ->
|
||||
httpTask url
|
||||
urls.currentUrl
|
||||
|> httpTask
|
||||
|> Task.map
|
||||
(\downloadedContent ->
|
||||
update cacheResult
|
||||
update
|
||||
cacheResult
|
||||
(\value ->
|
||||
parseContent extension value document
|
||||
)
|
||||
url
|
||||
urls
|
||||
downloadedContent
|
||||
)
|
||||
|
||||
Unparsed extension metadata content ->
|
||||
update cacheResult
|
||||
(\thing ->
|
||||
parseContent extension thing document
|
||||
)
|
||||
url
|
||||
content
|
||||
content
|
||||
|> update
|
||||
cacheResult
|
||||
(\thing ->
|
||||
parseContent extension thing document
|
||||
)
|
||||
urls
|
||||
|> Task.succeed
|
||||
|
||||
Parsed _ _ ->
|
||||
@ -395,12 +376,13 @@ httpTask url =
|
||||
{ method = "GET"
|
||||
, headers = []
|
||||
, url =
|
||||
Url.Builder.absolute
|
||||
((url.path |> String.split "/" |> List.filter (not << String.isEmpty))
|
||||
++ [ "content.json"
|
||||
]
|
||||
)
|
||||
[]
|
||||
url.path
|
||||
|> String.chopForwardSlashes
|
||||
|> String.split "/"
|
||||
|> List.filter ((/=) "")
|
||||
|> (\l -> l ++ [ "content.json" ])
|
||||
|> String.join "/"
|
||||
|> String.append "/"
|
||||
, body = Http.emptyBody
|
||||
, resolver =
|
||||
Http.stringResolver
|
||||
@ -443,13 +425,14 @@ contentJsonDecoder =
|
||||
update :
|
||||
ContentCache metadata view
|
||||
-> (String -> Result ParseError view)
|
||||
-> Url
|
||||
-> { currentUrl : Url, baseUrl : Url }
|
||||
-> ContentJson String
|
||||
-> ContentCache metadata view
|
||||
update cacheResult renderer url rawContent =
|
||||
update cacheResult renderer urls rawContent =
|
||||
case cacheResult of
|
||||
Ok cache ->
|
||||
Dict.update (pathForUrl url)
|
||||
Dict.update
|
||||
(pathForUrl urls)
|
||||
(\entry ->
|
||||
case entry of
|
||||
Just (Parsed metadata view) ->
|
||||
@ -482,27 +465,29 @@ update cacheResult renderer url rawContent =
|
||||
Err error
|
||||
|
||||
|
||||
pathForUrl : Url -> Path
|
||||
pathForUrl url =
|
||||
url.path
|
||||
|> dropTrailingSlash
|
||||
pathForUrl : { currentUrl : Url, baseUrl : Url } -> Path
|
||||
pathForUrl { currentUrl, baseUrl } =
|
||||
currentUrl.path
|
||||
|> String.dropLeft (String.length baseUrl.path)
|
||||
|> String.chopForwardSlashes
|
||||
|> String.split "/"
|
||||
|> List.drop 1
|
||||
|> List.filter ((/=) "")
|
||||
|
||||
|
||||
lookup :
|
||||
pathKey
|
||||
-> ContentCache metadata view
|
||||
-> Url
|
||||
-> { currentUrl : Url, baseUrl : Url }
|
||||
-> Maybe ( PagePath pathKey, Entry metadata view )
|
||||
lookup pathKey content url =
|
||||
lookup pathKey content urls =
|
||||
case content of
|
||||
Ok dict ->
|
||||
let
|
||||
path =
|
||||
pathForUrl url
|
||||
pathForUrl urls
|
||||
in
|
||||
Dict.get path dict
|
||||
dict
|
||||
|> Dict.get path
|
||||
|> Maybe.map
|
||||
(\entry ->
|
||||
( PagePath.build pathKey path, entry )
|
||||
@ -515,10 +500,11 @@ lookup pathKey content url =
|
||||
lookupMetadata :
|
||||
pathKey
|
||||
-> ContentCache metadata view
|
||||
-> Url
|
||||
-> { currentUrl : Url, baseUrl : Url }
|
||||
-> Maybe ( PagePath pathKey, metadata )
|
||||
lookupMetadata pathKey content url =
|
||||
lookup pathKey content url
|
||||
lookupMetadata pathKey content urls =
|
||||
urls
|
||||
|> lookup pathKey content
|
||||
|> Maybe.map
|
||||
(\( pagePath, entry ) ->
|
||||
case entry of
|
||||
@ -531,11 +517,3 @@ lookupMetadata pathKey content url =
|
||||
Parsed metadata _ ->
|
||||
( pagePath, metadata )
|
||||
)
|
||||
|
||||
|
||||
dropTrailingSlash path =
|
||||
if path |> String.endsWith "/" then
|
||||
String.dropRight 1 path
|
||||
|
||||
else
|
||||
path
|
||||
|
@ -110,7 +110,7 @@ includes (Directory key allPagePaths directoryPath) pagePath =
|
||||
Pages.pages.blog.directory
|
||||
|
||||
-- blogDirectory |> Directory.indexPath |> PagePath.toString
|
||||
-- => "/blog"
|
||||
-- => "blog"
|
||||
|
||||
See `Directory.includes` for an example of this in action.
|
||||
|
||||
@ -121,9 +121,8 @@ indexPath (Directory key allPagePaths directoryPath) =
|
||||
|
||||
|
||||
toString : List String -> String
|
||||
toString rawPath =
|
||||
"/"
|
||||
++ (rawPath |> String.join "/")
|
||||
toString =
|
||||
String.join "/"
|
||||
|
||||
|
||||
{-| Used by the generated `Pages.elm` module. There's no need to use this
|
||||
|
@ -1,6 +1,6 @@
|
||||
module Pages.Document exposing
|
||||
( Document, DocumentHandler
|
||||
, parser, markupParser
|
||||
, parser
|
||||
, fromList, get
|
||||
)
|
||||
|
||||
@ -80,7 +80,7 @@ Hello!!!
|
||||
)
|
||||
|
||||
@docs Document, DocumentHandler
|
||||
@docs parser, markupParser
|
||||
@docs parser
|
||||
|
||||
|
||||
## Functions for use by generated code
|
||||
@ -92,8 +92,6 @@ Hello!!!
|
||||
import Dict exposing (Dict)
|
||||
import Html exposing (Html)
|
||||
import Json.Decode
|
||||
import Mark
|
||||
import Mark.Error
|
||||
|
||||
|
||||
{-| 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
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
{-| 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"
|
||||
)
|
||||
|
@ -25,7 +25,7 @@ This gives you a record, based on all the files in your local
|
||||
Pages.pages.index
|
||||
|
||||
-- ImagePath.toString homePath
|
||||
-- => "/"
|
||||
-- => ""
|
||||
|
||||
or
|
||||
|
||||
@ -37,7 +37,7 @@ or
|
||||
Pages.images.profilePhotos.dillon
|
||||
|
||||
-- ImagePath.toString helloWorldPostPath
|
||||
-- => "/images/profile-photos/dillon.jpg"
|
||||
-- => "images/profile-photos/dillon.jpg"
|
||||
|
||||
@docs ImagePath, toString, external
|
||||
|
||||
@ -65,7 +65,7 @@ type ImagePath key
|
||||
| External String
|
||||
|
||||
|
||||
{-| Gives you the image's absolute URL as a String. This is useful for constructing `<img>` tags:
|
||||
{-| Gives you the image's relative URL as a String. This is useful for constructing `<img>` tags:
|
||||
|
||||
import Html exposing (Html, img)
|
||||
import Html.Attributes exposing (src)
|
||||
@ -87,8 +87,7 @@ toString : ImagePath key -> String
|
||||
toString path =
|
||||
case path of
|
||||
Internal rawPath ->
|
||||
"/"
|
||||
++ (rawPath |> String.join "/")
|
||||
String.join "/" rawPath
|
||||
|
||||
External url ->
|
||||
url
|
||||
|
@ -13,11 +13,12 @@ that is in the generated `Pages` module (see <Pages.Platform>).
|
||||
|
||||
-}
|
||||
|
||||
import Json.Decode
|
||||
import Json.Encode
|
||||
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
|
||||
= Browser
|
||||
@ -31,4 +32,5 @@ type alias Internal pathKey =
|
||||
, content : Pages.Internal.Platform.Content
|
||||
, pathKey : pathKey
|
||||
, 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.Dom as Dom
|
||||
@ -6,15 +6,17 @@ import Browser.Navigation
|
||||
import Dict exposing (Dict)
|
||||
import Head
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes
|
||||
import Html.Attributes exposing (style)
|
||||
import Html.Lazy
|
||||
import Http
|
||||
import Json.Decode as Decode
|
||||
import Json.Encode
|
||||
import List.Extra
|
||||
import Mark
|
||||
import Pages.ContentCache as ContentCache exposing (ContentCache)
|
||||
import Pages.Document
|
||||
import Pages.Internal.ApplicationType as ApplicationType
|
||||
import Pages.Internal.HotReloadLoadingIndicator as HotReloadLoadingIndicator
|
||||
import Pages.Internal.Platform.Cli
|
||||
import Pages.Internal.String as String
|
||||
import Pages.Manifest as Manifest
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
@ -24,14 +26,6 @@ import Task exposing (Task)
|
||||
import Url exposing (Url)
|
||||
|
||||
|
||||
dropTrailingSlash path =
|
||||
if path |> String.endsWith "/" then
|
||||
String.dropRight 1 path
|
||||
|
||||
else
|
||||
path
|
||||
|
||||
|
||||
type alias Page metadata view pathKey =
|
||||
{ metadata : metadata
|
||||
, path : PagePath pathKey
|
||||
@ -81,12 +75,13 @@ mainView pathKey pageView model =
|
||||
}
|
||||
|
||||
|
||||
urlToPagePath : pathKey -> Url -> PagePath pathKey
|
||||
urlToPagePath pathKey url =
|
||||
urlToPagePath : pathKey -> Url -> Url -> PagePath pathKey
|
||||
urlToPagePath pathKey url baseUrl =
|
||||
url.path
|
||||
|> dropTrailingSlash
|
||||
|> String.dropLeft (String.length baseUrl.path)
|
||||
|> String.chopForwardSlashes
|
||||
|> String.split "/"
|
||||
|> List.drop 1
|
||||
|> List.filter ((/=) "")
|
||||
|> PagePath.build pathKey
|
||||
|
||||
|
||||
@ -108,21 +103,27 @@ pageViewOrError :
|
||||
-> ContentCache metadata view
|
||||
-> { title : String, body : Html userMsg }
|
||||
pageViewOrError pathKey viewFn model cache =
|
||||
case ContentCache.lookup pathKey cache model.url of
|
||||
let
|
||||
urls =
|
||||
{ currentUrl = model.url
|
||||
, baseUrl = model.baseUrl
|
||||
}
|
||||
in
|
||||
case ContentCache.lookup pathKey cache urls of
|
||||
Just ( pagePath, entry ) ->
|
||||
case entry of
|
||||
ContentCache.Parsed metadata viewResult ->
|
||||
let
|
||||
viewFnResult =
|
||||
viewFn
|
||||
(cache
|
||||
|> Result.map (ContentCache.extractMetadata pathKey)
|
||||
|> Result.withDefault []
|
||||
-- TODO handle error better
|
||||
)
|
||||
{ path = pagePath, frontmatter = metadata }
|
||||
{ path = pagePath, frontmatter = metadata }
|
||||
|> viewFn
|
||||
(cache
|
||||
|> Result.map (ContentCache.extractMetadata pathKey)
|
||||
|> Result.withDefault []
|
||||
-- TODO handle error better
|
||||
)
|
||||
|> (\request ->
|
||||
StaticHttpRequest.resolve request viewResult.staticData
|
||||
StaticHttpRequest.resolve ApplicationType.Browser request viewResult.staticData
|
||||
)
|
||||
in
|
||||
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.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 ->
|
||||
@ -198,10 +205,28 @@ view pathKey content viewFn model =
|
||||
, body =
|
||||
[ onViewChangeElement model.url
|
||||
, 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 =
|
||||
-- this is a hidden tag
|
||||
-- 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 :
|
||||
pathKey
|
||||
-> String
|
||||
@ -263,51 +295,72 @@ init :
|
||||
init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel flags url key =
|
||||
let
|
||||
contentCache =
|
||||
ContentCache.init document content (Maybe.map (\cj -> { contentJson = cj, initialUrl = url }) contentJson)
|
||||
ContentCache.init
|
||||
document
|
||||
content
|
||||
(Maybe.map (\cj -> { contentJson = cj, initialUrl = url }) contentJson)
|
||||
|
||||
contentJson =
|
||||
flags
|
||||
|> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder)
|
||||
|> Result.toMaybe
|
||||
|
||||
contentJsonDecoder : Decode.Decoder ContentJson
|
||||
contentJsonDecoder =
|
||||
Decode.map2 ContentJson
|
||||
(Decode.field "body" Decode.string)
|
||||
(Decode.field "staticData" (Decode.dict Decode.string))
|
||||
baseUrl =
|
||||
flags
|
||||
|> Decode.decodeValue (Decode.field "baseUrl" Decode.string)
|
||||
|> Result.toMaybe
|
||||
|> Maybe.andThen Url.fromString
|
||||
|> Maybe.withDefault url
|
||||
|
||||
urls =
|
||||
{ currentUrl = url
|
||||
, baseUrl = baseUrl
|
||||
}
|
||||
in
|
||||
case contentCache of
|
||||
Ok okCache ->
|
||||
let
|
||||
phase =
|
||||
case Decode.decodeValue (Decode.field "isPrerendering" Decode.bool) flags of
|
||||
Ok True ->
|
||||
case
|
||||
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
|
||||
|
||||
Ok False ->
|
||||
Client
|
||||
Ok ( False, True, isElmDebugMode ) ->
|
||||
DevClient isElmDebugMode
|
||||
|
||||
Ok ( False, False, _ ) ->
|
||||
ProdClient
|
||||
|
||||
Err _ ->
|
||||
Client
|
||||
DevClient False
|
||||
|
||||
( userModel, userCmd ) =
|
||||
initUserModel
|
||||
(maybePagePath
|
||||
|> Maybe.map
|
||||
(\pagePath ->
|
||||
{ path = pagePath
|
||||
, query = url.query
|
||||
, fragment = url.fragment
|
||||
}
|
||||
)
|
||||
)
|
||||
maybePagePath
|
||||
|> Maybe.map
|
||||
(\pagePath ->
|
||||
{ path = pagePath
|
||||
, query = url.query
|
||||
, fragment = url.fragment
|
||||
}
|
||||
)
|
||||
|> initUserModel
|
||||
|
||||
cmd =
|
||||
case ( maybePagePath, maybeMetadata ) of
|
||||
( Just pagePath, Just frontmatter ) ->
|
||||
[ userCmd |> Cmd.map UserMsg |> Just
|
||||
[ userCmd
|
||||
|> Cmd.map UserMsg
|
||||
|> Just
|
||||
, contentCache
|
||||
|> ContentCache.lazyLoad document url
|
||||
|> ContentCache.lazyLoad document urls
|
||||
|> Task.attempt UpdateCache
|
||||
|> Just
|
||||
]
|
||||
@ -318,7 +371,7 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
||||
Cmd.none
|
||||
|
||||
( maybePagePath, maybeMetadata ) =
|
||||
case ContentCache.lookupMetadata pathKey (Ok okCache) url of
|
||||
case ContentCache.lookupMetadata pathKey (Ok okCache) urls of
|
||||
Just ( pagePath, metadata ) ->
|
||||
( Just pagePath, Just metadata )
|
||||
|
||||
@ -327,9 +380,11 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
||||
in
|
||||
( { key = key
|
||||
, url = url
|
||||
, baseUrl = baseUrl
|
||||
, userModel = userModel
|
||||
, contentCache = contentCache
|
||||
, phase = phase
|
||||
, hmrStatus = HmrLoaded
|
||||
}
|
||||
, cmd
|
||||
)
|
||||
@ -341,9 +396,11 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
||||
in
|
||||
( { key = key
|
||||
, url = url
|
||||
, baseUrl = baseUrl
|
||||
, userModel = userModel
|
||||
, contentCache = contentCache
|
||||
, phase = Client
|
||||
, phase = DevClient False
|
||||
, hmrStatus = HmrLoaded
|
||||
}
|
||||
, Cmd.batch
|
||||
[ userCmd |> Cmd.map UserMsg
|
||||
@ -371,7 +428,10 @@ type AppMsg userMsg metadata view
|
||||
| UserMsg userMsg
|
||||
| UpdateCache (Result Http.Error (ContentCache metadata view))
|
||||
| UpdateCacheAndUrl Url (Result Http.Error (ContentCache metadata view))
|
||||
| UpdateCacheForHotReload (Result Http.Error (ContentCache metadata view))
|
||||
| PageScrollComplete
|
||||
| HotReloadComplete ContentJson
|
||||
| StartingHotReload
|
||||
|
||||
|
||||
type Model userModel userMsg metadata view
|
||||
@ -381,20 +441,24 @@ type Model userModel userMsg metadata view
|
||||
|
||||
type alias ModelDetails userModel metadata view =
|
||||
{ key : Browser.Navigation.Key
|
||||
, url : Url.Url
|
||||
, url : Url
|
||||
, baseUrl : Url
|
||||
, contentCache : ContentCache metadata view
|
||||
, userModel : userModel
|
||||
, phase : Phase
|
||||
, hmrStatus : HmrStatus
|
||||
}
|
||||
|
||||
|
||||
type Phase
|
||||
= Prerender
|
||||
| Client
|
||||
| DevClient Bool
|
||||
| ProdClient
|
||||
|
||||
|
||||
update :
|
||||
List String
|
||||
Content
|
||||
-> List String
|
||||
-> String
|
||||
->
|
||||
(List ( PagePath pathKey, metadata )
|
||||
@ -422,7 +486,7 @@ update :
|
||||
-> Msg userMsg metadata view
|
||||
-> ModelDetails userModel metadata view
|
||||
-> ( ModelDetails userModel metadata view, Cmd (AppMsg userMsg metadata view) )
|
||||
update allRoutes canonicalSiteUrl viewFunction pathKey onPageChangeMsg toJsPort document userUpdate msg model =
|
||||
update content allRoutes canonicalSiteUrl viewFunction pathKey onPageChangeMsg toJsPort document userUpdate msg model =
|
||||
case msg of
|
||||
AppMsg appMsg ->
|
||||
case appMsg of
|
||||
@ -431,10 +495,7 @@ update allRoutes canonicalSiteUrl viewFunction pathKey onPageChangeMsg toJsPort
|
||||
Browser.Internal url ->
|
||||
let
|
||||
navigatingToSamePage =
|
||||
url.path
|
||||
== model.url.path
|
||||
&& url
|
||||
/= model.url
|
||||
(url.path == model.url.path) && (url /= model.url)
|
||||
in
|
||||
if navigatingToSamePage then
|
||||
-- this is a workaround for an issue with anchor fragment navigation
|
||||
@ -450,10 +511,12 @@ update allRoutes canonicalSiteUrl viewFunction pathKey onPageChangeMsg toJsPort
|
||||
UrlChanged url ->
|
||||
let
|
||||
navigatingToSamePage =
|
||||
url.path
|
||||
== model.url.path
|
||||
&& url
|
||||
/= model.url
|
||||
(url.path == model.url.path) && (url /= model.url)
|
||||
|
||||
urls =
|
||||
{ currentUrl = url
|
||||
, baseUrl = model.baseUrl
|
||||
}
|
||||
in
|
||||
( model
|
||||
, if navigatingToSamePage then
|
||||
@ -466,7 +529,7 @@ update allRoutes canonicalSiteUrl viewFunction pathKey onPageChangeMsg toJsPort
|
||||
|
||||
else
|
||||
model.contentCache
|
||||
|> ContentCache.lazyLoad document url
|
||||
|> ContentCache.lazyLoad document urls
|
||||
|> Task.attempt (UpdateCacheAndUrl url)
|
||||
)
|
||||
|
||||
@ -483,8 +546,13 @@ update allRoutes canonicalSiteUrl viewFunction pathKey onPageChangeMsg toJsPort
|
||||
-- to keep track of the last url change
|
||||
Ok updatedCache ->
|
||||
let
|
||||
urls =
|
||||
{ currentUrl = model.url
|
||||
, baseUrl = model.baseUrl
|
||||
}
|
||||
|
||||
maybeCmd =
|
||||
case ContentCache.lookup pathKey updatedCache model.url of
|
||||
case ContentCache.lookup pathKey updatedCache urls of
|
||||
Just ( pagePath, entry ) ->
|
||||
case entry of
|
||||
ContentCache.Parsed frontmatter viewResult ->
|
||||
@ -511,7 +579,7 @@ update allRoutes canonicalSiteUrl viewFunction pathKey onPageChangeMsg toJsPort
|
||||
)
|
||||
{ path = pagePath, frontmatter = frontmatter }
|
||||
|> (\request ->
|
||||
StaticHttpRequest.resolve request staticDataThing
|
||||
StaticHttpRequest.resolve ApplicationType.Browser request staticDataThing
|
||||
)
|
||||
in
|
||||
( { model | contentCache = updatedCache }
|
||||
@ -533,7 +601,7 @@ update allRoutes canonicalSiteUrl viewFunction pathKey onPageChangeMsg toJsPort
|
||||
( userModel, userCmd ) =
|
||||
userUpdate
|
||||
(onPageChangeMsg
|
||||
{ path = url |> urlToPagePath pathKey
|
||||
{ path = urlToPagePath pathKey url model.baseUrl
|
||||
, query = url.query
|
||||
, fragment = url.fragment
|
||||
}
|
||||
@ -555,18 +623,42 @@ update allRoutes canonicalSiteUrl viewFunction pathKey onPageChangeMsg toJsPort
|
||||
-- TODO handle error
|
||||
( { 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 ->
|
||||
( 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 _ ->
|
||||
( model, Cmd.none )
|
||||
|
||||
|
||||
type alias Parser metadata view =
|
||||
Dict String String
|
||||
-> List String
|
||||
-> List ( List String, metadata )
|
||||
-> Mark.Document view
|
||||
type HmrStatus
|
||||
= HmrLoading
|
||||
| HmrLoaded
|
||||
|
||||
|
||||
application :
|
||||
@ -593,6 +685,7 @@ application :
|
||||
, document : Pages.Document.Document metadata view
|
||||
, content : Content
|
||||
, toJsPort : Json.Encode.Value -> Cmd Never
|
||||
, fromJsPort : Sub Decode.Value
|
||||
, manifest : Manifest.Config pathKey
|
||||
, generateFiles :
|
||||
List
|
||||
@ -601,11 +694,13 @@ application :
|
||||
, body : String
|
||||
}
|
||||
->
|
||||
List
|
||||
(Result String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
StaticHttp.Request
|
||||
(List
|
||||
(Result String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
)
|
||||
)
|
||||
, canonicalSiteUrl : String
|
||||
, pathKey : pathKey
|
||||
@ -645,7 +740,7 @@ application config =
|
||||
Prerender ->
|
||||
noOpUpdate
|
||||
|
||||
Client ->
|
||||
_ ->
|
||||
config.update
|
||||
|
||||
noOpUpdate =
|
||||
@ -656,9 +751,8 @@ application config =
|
||||
config.content
|
||||
|> List.map Tuple.first
|
||||
|> List.map (String.join "/")
|
||||
|> List.map (\route -> "/" ++ route)
|
||||
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.mapSecond (Cmd.map AppMsg)
|
||||
|
||||
@ -668,9 +762,27 @@ application config =
|
||||
\outerModel ->
|
||||
case outerModel of
|
||||
Model model ->
|
||||
config.subscriptions model.userModel
|
||||
|> Sub.map UserMsg
|
||||
|> Sub.map AppMsg
|
||||
Sub.batch
|
||||
[ config.subscriptions model.userModel
|
||||
|> 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 _ ->
|
||||
Sub.none
|
||||
@ -703,6 +815,7 @@ cliApplication :
|
||||
, document : Pages.Document.Document metadata view
|
||||
, content : Content
|
||||
, toJsPort : Json.Encode.Value -> Cmd Never
|
||||
, fromJsPort : Sub Decode.Value
|
||||
, manifest : Manifest.Config pathKey
|
||||
, generateFiles :
|
||||
List
|
||||
@ -711,11 +824,13 @@ cliApplication :
|
||||
, body : String
|
||||
}
|
||||
->
|
||||
List
|
||||
(Result String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
StaticHttp.Request
|
||||
(List
|
||||
(Result String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
)
|
||||
)
|
||||
, canonicalSiteUrl : String
|
||||
, pathKey : pathKey
|
||||
|
@ -26,6 +26,7 @@ import Pages.ContentCache as ContentCache exposing (ContentCache)
|
||||
import Pages.Document
|
||||
import Pages.Http
|
||||
import Pages.ImagePath as ImagePath
|
||||
import Pages.Internal.ApplicationType as ApplicationType exposing (ApplicationType)
|
||||
import Pages.Internal.StaticHttpBody as StaticHttpBody
|
||||
import Pages.Manifest as Manifest
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
@ -47,6 +48,7 @@ type alias ToJsSuccessPayload pathKey =
|
||||
{ pages : Dict String (Dict String String)
|
||||
, manifest : Manifest.Config pathKey
|
||||
, filesToGenerate : List FileToGenerate
|
||||
, staticHttpCache : Dict String String
|
||||
, errors : List String
|
||||
}
|
||||
|
||||
@ -65,8 +67,8 @@ toJsCodec =
|
||||
Errors errorList ->
|
||||
errorsTag errorList
|
||||
|
||||
Success { pages, manifest, filesToGenerate, errors } ->
|
||||
success (ToJsSuccessPayload pages manifest filesToGenerate errors)
|
||||
Success { pages, manifest, filesToGenerate, errors, staticHttpCache } ->
|
||||
success (ToJsSuccessPayload pages manifest filesToGenerate staticHttpCache errors)
|
||||
)
|
||||
|> Codec.variant1 "Errors" Errors Codec.string
|
||||
|> Codec.variant1 "Success"
|
||||
@ -115,6 +117,9 @@ successCodec =
|
||||
)
|
||||
(Decode.succeed [])
|
||||
)
|
||||
|> Codec.field "staticHttpCache"
|
||||
.staticHttpCache
|
||||
(Codec.dict Codec.string)
|
||||
|> Codec.field "errors" .errors (Codec.list Codec.string)
|
||||
|> Codec.buildObject
|
||||
|
||||
@ -178,6 +183,7 @@ type alias Config pathKey userMsg userModel metadata view =
|
||||
, document : Pages.Document.Document metadata view
|
||||
, content : Content
|
||||
, toJsPort : Json.Encode.Value -> Cmd Never
|
||||
, fromJsPort : Sub Decode.Value
|
||||
, manifest : Manifest.Config pathKey
|
||||
, generateFiles :
|
||||
List
|
||||
@ -186,11 +192,13 @@ type alias Config pathKey userMsg userModel metadata view =
|
||||
, body : String
|
||||
}
|
||||
->
|
||||
List
|
||||
(Result String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
StaticHttp.Request
|
||||
(List
|
||||
(Result String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
)
|
||||
)
|
||||
, canonicalSiteUrl : String
|
||||
, pathKey : pathKey
|
||||
@ -275,10 +283,10 @@ perform cliMsgConstructor toJsPort effect =
|
||||
|> Cmd.batch
|
||||
|
||||
FetchHttp ({ unmasked, masked } as requests) ->
|
||||
--let
|
||||
-- _ =
|
||||
-- Debug.log "Fetching" masked.url
|
||||
--in
|
||||
-- let
|
||||
-- _ =
|
||||
-- Debug.log "Fetching" masked.url
|
||||
-- in
|
||||
Http.request
|
||||
{ method = unmasked.method
|
||||
, url = unmasked.url
|
||||
@ -316,37 +324,44 @@ init :
|
||||
init toModel contentCache siteMetadata config flags =
|
||||
case
|
||||
Decode.decodeValue
|
||||
(Decode.map2 Tuple.pair
|
||||
(Decode.map3 (\a b c -> ( a, b, c ))
|
||||
(Decode.field "secrets" SecretsDict.decoder)
|
||||
(Decode.field "mode" modeDecoder)
|
||||
(Decode.field "staticHttpCache"
|
||||
(Decode.dict
|
||||
(Decode.string
|
||||
|> Decode.map Just
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
flags
|
||||
of
|
||||
Ok ( secrets, mode ) ->
|
||||
Ok ( secrets, mode, staticHttpCache ) ->
|
||||
case contentCache of
|
||||
Ok _ ->
|
||||
case contentCache |> ContentCache.pagesWithErrors of
|
||||
case ContentCache.pagesWithErrors contentCache of
|
||||
[] ->
|
||||
let
|
||||
requests =
|
||||
siteMetadata
|
||||
|> Result.andThen
|
||||
(\metadata ->
|
||||
staticResponseForPage metadata config.view
|
||||
)
|
||||
Result.andThen
|
||||
(\metadata ->
|
||||
staticResponseForPage metadata config.view
|
||||
)
|
||||
siteMetadata
|
||||
|
||||
staticResponses : StaticResponses
|
||||
staticResponses =
|
||||
case requests of
|
||||
Ok okRequests ->
|
||||
staticResponsesInit okRequests
|
||||
staticResponsesInit staticHttpCache siteMetadata config okRequests
|
||||
|
||||
Err errors ->
|
||||
-- TODO need to handle errors better?
|
||||
staticResponsesInit []
|
||||
staticResponsesInit staticHttpCache siteMetadata config []
|
||||
|
||||
( updatedRawResponses, effect ) =
|
||||
sendStaticResponsesIfDone config siteMetadata mode secrets Dict.empty [] staticResponses
|
||||
sendStaticResponsesIfDone config siteMetadata mode secrets staticHttpCache [] staticResponses
|
||||
in
|
||||
( Model staticResponses secrets [] updatedRawResponses mode |> toModel
|
||||
, effect
|
||||
@ -355,21 +370,21 @@ init toModel contentCache siteMetadata config flags =
|
||||
pageErrors ->
|
||||
let
|
||||
requests =
|
||||
siteMetadata
|
||||
|> Result.andThen
|
||||
(\metadata ->
|
||||
staticResponseForPage metadata config.view
|
||||
)
|
||||
Result.andThen
|
||||
(\metadata ->
|
||||
staticResponseForPage metadata config.view
|
||||
)
|
||||
siteMetadata
|
||||
|
||||
staticResponses : StaticResponses
|
||||
staticResponses =
|
||||
case requests of
|
||||
Ok okRequests ->
|
||||
staticResponsesInit okRequests
|
||||
staticResponsesInit staticHttpCache siteMetadata config okRequests
|
||||
|
||||
Err errors ->
|
||||
-- TODO need to handle errors better?
|
||||
staticResponsesInit []
|
||||
staticResponsesInit staticHttpCache siteMetadata config []
|
||||
in
|
||||
updateAndSendPortIfDone
|
||||
config
|
||||
@ -378,7 +393,7 @@ init toModel contentCache siteMetadata config flags =
|
||||
staticResponses
|
||||
secrets
|
||||
pageErrors
|
||||
Dict.empty
|
||||
staticHttpCache
|
||||
mode
|
||||
)
|
||||
toModel
|
||||
@ -390,7 +405,7 @@ init toModel contentCache siteMetadata config flags =
|
||||
(Model Dict.empty
|
||||
secrets
|
||||
(metadataParserErrors |> List.map Tuple.second)
|
||||
Dict.empty
|
||||
staticHttpCache
|
||||
mode
|
||||
)
|
||||
toModel
|
||||
@ -449,25 +464,25 @@ update siteMetadata config msg model =
|
||||
case msg of
|
||||
GotStaticHttpResponse { request, response } ->
|
||||
let
|
||||
--_ =
|
||||
-- Debug.log "Got response" request.masked.url
|
||||
-- _ =
|
||||
-- Debug.log "Got response" request.masked.url
|
||||
--
|
||||
updatedModel =
|
||||
(case response of
|
||||
Ok okResponse ->
|
||||
staticResponsesUpdate
|
||||
{ request = request
|
||||
, response =
|
||||
response |> Result.mapError (\_ -> ())
|
||||
, response = Result.mapError (\_ -> ()) response
|
||||
}
|
||||
model
|
||||
|
||||
Err error ->
|
||||
{ model
|
||||
| errors =
|
||||
model.errors
|
||||
++ [ { title = "Static HTTP Error"
|
||||
, message =
|
||||
List.append
|
||||
model.errors
|
||||
[ { title = "Static HTTP Error"
|
||||
, message =
|
||||
[ Terminal.text "I got an error making an HTTP request to this URL: "
|
||||
|
||||
-- TODO include HTTP method, headers, and body
|
||||
@ -492,16 +507,15 @@ update siteMetadata config msg model =
|
||||
Pages.Http.NetworkError ->
|
||||
Terminal.text "Network error"
|
||||
]
|
||||
, fatal = True
|
||||
}
|
||||
]
|
||||
, fatal = True
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|> staticResponsesUpdate
|
||||
-- TODO for hash pass in RequestDetails here
|
||||
{ request = request
|
||||
, response =
|
||||
response |> Result.mapError (\_ -> ())
|
||||
, response = Result.mapError (\_ -> ()) response
|
||||
}
|
||||
|
||||
( updatedAllRawResponses, effect ) =
|
||||
@ -523,10 +537,9 @@ performStaticHttpRequests allRawResponses secrets staticRequests =
|
||||
staticRequests
|
||||
|> List.map
|
||||
(\( pagePath, request ) ->
|
||||
StaticHttpRequest.resolveUrls request
|
||||
(allRawResponses
|
||||
|> dictCompact
|
||||
)
|
||||
allRawResponses
|
||||
|> dictCompact
|
||||
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|
||||
|> Tuple.second
|
||||
)
|
||||
|> List.concat
|
||||
@ -535,10 +548,13 @@ performStaticHttpRequests allRawResponses secrets staticRequests =
|
||||
-- |> Set.toList
|
||||
|> List.map
|
||||
(\urlBuilder ->
|
||||
Secrets.lookup secrets urlBuilder
|
||||
urlBuilder
|
||||
|> Secrets.lookup secrets
|
||||
|> Result.map
|
||||
(\unmasked ->
|
||||
{ unmasked = unmasked, masked = Secrets.maskedLookup urlBuilder }
|
||||
{ unmasked = unmasked
|
||||
, masked = Secrets.maskedLookup urlBuilder
|
||||
}
|
||||
)
|
||||
)
|
||||
|> combineMultipleErrors
|
||||
@ -570,15 +586,84 @@ combineMultipleErrors results =
|
||||
results
|
||||
|
||||
|
||||
staticResponsesInit : List ( PagePath pathKey, StaticHttp.Request value ) -> StaticResponses
|
||||
staticResponsesInit list =
|
||||
cliDictKey : String
|
||||
cliDictKey =
|
||||
"////elm-pages-CLI////"
|
||||
|
||||
|
||||
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 staticHttpCache siteMetadataResult config list =
|
||||
let
|
||||
generateFilesRequest : StaticHttp.Request (List (Result String { path : List String, content : String }))
|
||||
generateFilesRequest =
|
||||
config.generateFiles siteMetadataWithContent
|
||||
|
||||
generateFilesStaticRequest =
|
||||
( -- 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
|
||||
)
|
||||
|
||||
siteMetadataWithContent =
|
||||
siteMetadataResult
|
||||
|> Result.withDefault []
|
||||
|> List.map
|
||||
(\( pagePath, metadata ) ->
|
||||
let
|
||||
contentForPage =
|
||||
config.content
|
||||
|> List.filterMap
|
||||
(\( path, { body } ) ->
|
||||
let
|
||||
pagePathToGenerate =
|
||||
PagePath.toString pagePath
|
||||
|
||||
currentContentPath =
|
||||
"/" ++ (path |> String.join "/")
|
||||
in
|
||||
if pagePathToGenerate == currentContentPath then
|
||||
Just body
|
||||
|
||||
else
|
||||
Nothing
|
||||
)
|
||||
|> List.head
|
||||
|> Maybe.andThen identity
|
||||
in
|
||||
{ path = pagePath
|
||||
, frontmatter = metadata
|
||||
, body = contentForPage |> Maybe.withDefault ""
|
||||
}
|
||||
)
|
||||
in
|
||||
list
|
||||
|> List.map
|
||||
(\( 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
|
||||
, NotFetched (staticRequest |> StaticHttp.map (\_ -> ())) Dict.empty
|
||||
, updatedEntry
|
||||
)
|
||||
)
|
||||
|> List.append [ generateFilesStaticRequest ]
|
||||
|> Dict.fromList
|
||||
|
||||
|
||||
@ -586,44 +671,80 @@ staticResponsesUpdate : { request : { masked : RequestDetails, unmasked : Reques
|
||||
staticResponsesUpdate newEntry model =
|
||||
let
|
||||
updatedAllResponses =
|
||||
model.allRawResponses
|
||||
-- @@@@@@@@@ TODO handle errors here, change Dict to have `Result` instead of `Maybe`
|
||||
|> Dict.insert (HashRequest.hash newEntry.request.masked) (Just (newEntry.response |> Result.withDefault "TODO"))
|
||||
-- @@@@@@@@@ TODO handle errors here, change Dict to have `Result` instead of `Maybe`
|
||||
Dict.insert
|
||||
(HashRequest.hash newEntry.request.masked)
|
||||
(Just <| Result.withDefault "TODO" newEntry.response)
|
||||
model.allRawResponses
|
||||
in
|
||||
{ model
|
||||
| allRawResponses = updatedAllResponses
|
||||
, staticResponses =
|
||||
model.staticResponses
|
||||
|> Dict.map
|
||||
(\pageUrl entry ->
|
||||
case entry of
|
||||
NotFetched request rawResponses ->
|
||||
Dict.map
|
||||
(\pageUrl entry ->
|
||||
case entry of
|
||||
NotFetched request rawResponses ->
|
||||
let
|
||||
realUrls =
|
||||
updatedAllResponses
|
||||
|> dictCompact
|
||||
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|
||||
|> Tuple.second
|
||||
|> List.map Secrets.maskedLookup
|
||||
|> List.map HashRequest.hash
|
||||
|
||||
includesUrl =
|
||||
List.member
|
||||
(HashRequest.hash newEntry.request.masked)
|
||||
realUrls
|
||||
in
|
||||
if includesUrl then
|
||||
let
|
||||
realUrls =
|
||||
StaticHttpRequest.resolveUrls request
|
||||
(updatedAllResponses |> dictCompact)
|
||||
|> Tuple.second
|
||||
|> List.map Secrets.maskedLookup
|
||||
|> List.map HashRequest.hash
|
||||
|
||||
includesUrl =
|
||||
List.member (HashRequest.hash newEntry.request.masked)
|
||||
realUrls
|
||||
in
|
||||
if includesUrl then
|
||||
let
|
||||
updatedRawResponses =
|
||||
updatedRawResponses =
|
||||
Dict.insert
|
||||
(HashRequest.hash newEntry.request.masked)
|
||||
newEntry.response
|
||||
rawResponses
|
||||
|> Dict.insert (HashRequest.hash newEntry.request.masked) newEntry.response
|
||||
in
|
||||
NotFetched request updatedRawResponses
|
||||
in
|
||||
NotFetched request updatedRawResponses
|
||||
|
||||
else
|
||||
entry
|
||||
)
|
||||
else
|
||||
entry
|
||||
)
|
||||
model.staticResponses
|
||||
}
|
||||
|
||||
|
||||
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 maybeValue =
|
||||
case maybeValue of
|
||||
@ -654,20 +775,21 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
||||
let
|
||||
usableRawResponses : Dict String String
|
||||
usableRawResponses =
|
||||
rawResponses
|
||||
|> Dict.Extra.filterMap
|
||||
(\key value ->
|
||||
value
|
||||
|> Result.map Just
|
||||
|> Result.withDefault Nothing
|
||||
)
|
||||
Dict.Extra.filterMap
|
||||
(\key value ->
|
||||
value
|
||||
|> Result.map Just
|
||||
|> Result.withDefault Nothing
|
||||
)
|
||||
rawResponses
|
||||
|
||||
hasPermanentError =
|
||||
StaticHttpRequest.permanentError request usableRawResponses
|
||||
usableRawResponses
|
||||
|> StaticHttpRequest.permanentError ApplicationType.Cli request
|
||||
|> isJust
|
||||
|
||||
hasPermanentHttpError =
|
||||
not <| List.isEmpty errors
|
||||
not (List.isEmpty errors)
|
||||
|
||||
--|> List.any
|
||||
-- (\error ->
|
||||
@ -679,7 +801,9 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
||||
-- False
|
||||
-- )
|
||||
( allUrlsKnown, knownUrlsToFetch ) =
|
||||
StaticHttpRequest.resolveUrls request
|
||||
StaticHttpRequest.resolveUrls
|
||||
ApplicationType.Cli
|
||||
request
|
||||
(rawResponses |> Dict.map (\key value -> value |> Result.withDefault ""))
|
||||
|
||||
fetchedAllKnownUrls =
|
||||
@ -715,7 +839,9 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
||||
)
|
||||
|
||||
maybePermanentError =
|
||||
StaticHttpRequest.permanentError request
|
||||
StaticHttpRequest.permanentError
|
||||
ApplicationType.Cli
|
||||
request
|
||||
usableRawResponses
|
||||
|
||||
decoderErrors =
|
||||
@ -795,7 +921,7 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
||||
updatedAllRawResponses =
|
||||
Dict.empty
|
||||
|
||||
generatedFiles =
|
||||
metadataForGenerateFiles =
|
||||
siteMetadata
|
||||
|> Result.withDefault []
|
||||
|> List.map
|
||||
@ -810,7 +936,7 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
||||
PagePath.toString pagePath
|
||||
|
||||
currentContentPath =
|
||||
"/" ++ (path |> String.join "/")
|
||||
String.join "/" path
|
||||
in
|
||||
if pagePathToGenerate == currentContentPath then
|
||||
Just body
|
||||
@ -826,8 +952,20 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
||||
, body = contentForPage |> Maybe.withDefault ""
|
||||
}
|
||||
)
|
||||
|> config.generateFiles
|
||||
|
||||
--generatedFiles : StaticHttp.Request (List (Result String { path : List String, content : String }))
|
||||
--generatedFiles : List (Result String { path : List String, content : String })
|
||||
generatedFiles =
|
||||
mythingy2
|
||||
|> Result.withDefault []
|
||||
|
||||
mythingy2 : Result StaticHttpRequest.Error (List (Result String { path : List String, content : String }))
|
||||
mythingy2 =
|
||||
StaticHttpRequest.resolve ApplicationType.Cli
|
||||
(config.generateFiles metadataForGenerateFiles)
|
||||
(allRawResponses |> Dict.Extra.filterMap (\key value -> value))
|
||||
|
||||
generatedOkayFiles : List { path : List String, content : String }
|
||||
generatedOkayFiles =
|
||||
generatedFiles
|
||||
|> List.filterMap
|
||||
@ -840,6 +978,7 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
||||
Nothing
|
||||
)
|
||||
|
||||
generatedFileErrors : List { title : String, message : List Terminal.Text, fatal : Bool }
|
||||
generatedFileErrors =
|
||||
generatedFiles
|
||||
|> List.filterMap
|
||||
@ -868,11 +1007,19 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
||||
(encodeStaticResponses mode staticResponses)
|
||||
config.manifest
|
||||
generatedOkayFiles
|
||||
allRawResponses
|
||||
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 <|
|
||||
if allErrors |> List.filter .fatal |> List.isEmpty then
|
||||
Success
|
||||
@ -880,6 +1027,15 @@ toJsPayload encodedStatic manifest generated allErrors =
|
||||
encodedStatic
|
||||
manifest
|
||||
generated
|
||||
(allRawResponses
|
||||
|> Dict.toList
|
||||
|> List.filterMap
|
||||
(\( key, maybeValue ) ->
|
||||
maybeValue
|
||||
|> Maybe.map (\value -> ( key, value ))
|
||||
)
|
||||
|> Dict.fromList
|
||||
)
|
||||
(List.map BuildError.errorToString allErrors)
|
||||
)
|
||||
|
||||
@ -890,24 +1046,27 @@ toJsPayload encodedStatic manifest generated allErrors =
|
||||
encodeStaticResponses : Mode -> StaticResponses -> Dict String (Dict String String)
|
||||
encodeStaticResponses mode staticResponses =
|
||||
staticResponses
|
||||
|> Dict.filter
|
||||
(\key value ->
|
||||
key /= cliDictKey
|
||||
)
|
||||
|> Dict.map
|
||||
(\path result ->
|
||||
case result of
|
||||
NotFetched request rawResponsesDict ->
|
||||
let
|
||||
relevantResponses =
|
||||
rawResponsesDict
|
||||
|> Dict.map
|
||||
(\key value ->
|
||||
value
|
||||
-- TODO avoid running this code at all if there are errors here
|
||||
|> Result.withDefault ""
|
||||
)
|
||||
Dict.map
|
||||
(\_ ->
|
||||
-- TODO avoid running this code at all if there are errors here
|
||||
Result.withDefault ""
|
||||
)
|
||||
rawResponsesDict
|
||||
|
||||
strippedResponses : Dict String String
|
||||
strippedResponses =
|
||||
-- TODO should this return an Err and handle that here?
|
||||
StaticHttpRequest.strippedResponses request relevantResponses
|
||||
StaticHttpRequest.strippedResponses ApplicationType.Cli request relevantResponses
|
||||
in
|
||||
case mode of
|
||||
Dev ->
|
||||
|
43
src/Pages/Internal/String.elm
Normal file
43
src/Pages/Internal/String.elm
Normal file
@ -0,0 +1,43 @@
|
||||
module Pages.Internal.String exposing (..)
|
||||
|
||||
{-| Remove a piece from the beginning of a string until it's not there anymore.
|
||||
|
||||
>>> chopStart "{" "{{{<-"
|
||||
"<-"
|
||||
|
||||
-}
|
||||
|
||||
|
||||
chopStart : String -> String -> String
|
||||
chopStart needle string =
|
||||
if String.startsWith needle string then
|
||||
string
|
||||
|> String.dropLeft (String.length needle)
|
||||
|> chopStart needle
|
||||
|
||||
else
|
||||
string
|
||||
|
||||
|
||||
{-| Remove a piece from the end of a string until it's not there anymore.
|
||||
|
||||
>>> chopEnd "}" "->}}}"
|
||||
"->"
|
||||
|
||||
-}
|
||||
chopEnd : String -> String -> String
|
||||
chopEnd needle string =
|
||||
if String.endsWith needle string then
|
||||
string
|
||||
|> String.dropRight (String.length needle)
|
||||
|> chopEnd needle
|
||||
|
||||
else
|
||||
string
|
||||
|
||||
|
||||
{-| Removes `/` characters from both ends of a string.
|
||||
-}
|
||||
chopForwardSlashes : String -> String
|
||||
chopForwardSlashes =
|
||||
chopStart "/" >> chopEnd "/"
|
@ -259,7 +259,7 @@ toJson config =
|
||||
)
|
||||
, ( "serviceworker"
|
||||
, Encode.object
|
||||
[ ( "src", Encode.string "/service-worker.js" )
|
||||
[ ( "src", Encode.string "../service-worker.js" )
|
||||
, ( "scope", Encode.string "/" )
|
||||
, ( "type", Encode.string "" )
|
||||
, ( "update_via_cache", Encode.string "none" )
|
||||
|
@ -43,7 +43,7 @@ This gives you a record, based on your local `content` directory, that lets you
|
||||
Pages.pages.index
|
||||
|
||||
-- PagePath.toString homePath
|
||||
-- => "/"
|
||||
-- => ""
|
||||
|
||||
or
|
||||
|
||||
@ -55,7 +55,7 @@ or
|
||||
Pages.pages.blog.helloWorld
|
||||
|
||||
-- PagePath.toString helloWorldPostPath
|
||||
-- => "/blog/hello-world"
|
||||
-- => "blog/hello-world"
|
||||
|
||||
Note that in the `hello-world` example it changes from the kebab casing of the actual
|
||||
URL to camelCasing for the record key.
|
||||
@ -92,7 +92,7 @@ type PagePath key
|
||||
| External String
|
||||
|
||||
|
||||
{-| Gives you the page's absolute URL as a String. This is useful for constructing links:
|
||||
{-| Gives you the page's relative URL as a String. This is useful for constructing links:
|
||||
|
||||
import Html exposing (Html, a)
|
||||
import Html.Attributes exposing (href)
|
||||
@ -114,8 +114,7 @@ toString : PagePath key -> String
|
||||
toString path =
|
||||
case path of
|
||||
Internal rawPath ->
|
||||
"/"
|
||||
++ (rawPath |> String.join "/")
|
||||
String.join "/" rawPath
|
||||
|
||||
External url ->
|
||||
url
|
||||
|
@ -90,11 +90,13 @@ application :
|
||||
, body : String
|
||||
}
|
||||
->
|
||||
List
|
||||
(Result String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
StaticHttp.Request
|
||||
(List
|
||||
(Result String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
)
|
||||
)
|
||||
, onPageChange :
|
||||
{ path : PagePath pathKey
|
||||
@ -123,6 +125,7 @@ application config =
|
||||
, content = config.internals.content
|
||||
, generateFiles = config.generateFiles
|
||||
, toJsPort = config.internals.toJsPort
|
||||
, fromJsPort = config.internals.fromJsPort
|
||||
, manifest = config.manifest
|
||||
, canonicalSiteUrl = config.canonicalSiteUrl
|
||||
, onPageChange = config.onPageChange
|
||||
|
@ -1,7 +1,7 @@
|
||||
module Pages.StaticHttp exposing
|
||||
( Request, RequestDetails
|
||||
, get, request
|
||||
, map, succeed
|
||||
, map, succeed, fail
|
||||
, Body, emptyBody, stringBody, jsonBody
|
||||
, andThen, resolve, combine
|
||||
, map2, map3, map4, map5, map6, map7, map8, map9
|
||||
@ -15,7 +15,7 @@ The key differences are:
|
||||
|
||||
- `StaticHttp.Request`s are performed once at build time (`Http.Request`s are performed at runtime, at whenever point you perform them)
|
||||
- `StaticHttp.Request`s strip out unused JSON data from the data your decoder doesn't touch to minimize the JSON payload
|
||||
- `StaticHttp.Request`s can use [`Pages.Secrets`](Pages.Secrets) to securely use credentials from your environemnt variables which are completely masked in the production assets.
|
||||
- `StaticHttp.Request`s can use [`Pages.Secrets`](Pages.Secrets) to securely use credentials from your environment variables which are completely masked in the production assets.
|
||||
- `StaticHttp.Request`s have a built-in `StaticHttp.andThen` that allows you to perform follow-up requests without using tasks
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ in [this article introducing StaticHttp requests and some concepts around it](ht
|
||||
|
||||
@docs Request, RequestDetails
|
||||
@docs get, request
|
||||
@docs map, succeed
|
||||
@docs map, succeed, fail
|
||||
|
||||
|
||||
## 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.Extra
|
||||
import Internal.OptimizedDecoder
|
||||
import Json.Decode
|
||||
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.Internal.ApplicationType as ApplicationType exposing (ApplicationType)
|
||||
import Pages.Internal.StaticHttpBody as Body
|
||||
import Pages.Secrets
|
||||
import Pages.StaticHttp.Request as HashRequest
|
||||
@ -155,8 +158,8 @@ map fn requestInfo =
|
||||
Request ( urls, lookupFn ) ->
|
||||
Request
|
||||
( urls
|
||||
, \rawResponses ->
|
||||
lookupFn rawResponses
|
||||
, \appType rawResponses ->
|
||||
lookupFn appType rawResponses
|
||||
|> Result.map (\( partiallyStripped, nextRequest ) -> ( partiallyStripped, map fn nextRequest ))
|
||||
)
|
||||
|
||||
@ -167,10 +170,8 @@ map fn requestInfo =
|
||||
{-| Helper to remove an inner layer of Request wrapping.
|
||||
-}
|
||||
resolve : Request (List (Request value)) -> Request (List value)
|
||||
resolve topRequest =
|
||||
topRequest
|
||||
|> andThen
|
||||
(\continuationRequests -> combine continuationRequests)
|
||||
resolve =
|
||||
andThen combine
|
||||
|
||||
|
||||
{-| Turn a list of `StaticHttp.Request`s into a single one.
|
||||
@ -208,9 +209,8 @@ resolve topRequest =
|
||||
|
||||
-}
|
||||
combine : List (Request value) -> Request (List value)
|
||||
combine requests =
|
||||
requests
|
||||
|> List.foldl (map2 (::)) (succeed [])
|
||||
combine =
|
||||
List.foldl (map2 (::)) (succeed [])
|
||||
|
||||
|
||||
{-| Like map, but it takes in two `Request`s.
|
||||
@ -241,24 +241,24 @@ map2 fn request1 request2 =
|
||||
case ( request1, request2 ) of
|
||||
( Request ( urls1, lookupFn1 ), Request ( urls2, lookupFn2 ) ) ->
|
||||
let
|
||||
value : Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, Request c )
|
||||
value rawResponses =
|
||||
value : ApplicationType -> Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, Request c )
|
||||
value appType rawResponses =
|
||||
let
|
||||
value1 =
|
||||
lookupFn1 rawResponses
|
||||
lookupFn1 appType rawResponses
|
||||
|> Result.map Tuple.second
|
||||
|
||||
value2 =
|
||||
lookupFn2 rawResponses
|
||||
lookupFn2 appType rawResponses
|
||||
|> Result.map Tuple.second
|
||||
|
||||
dict1 =
|
||||
lookupFn1 rawResponses
|
||||
lookupFn1 appType rawResponses
|
||||
|> Result.map Tuple.first
|
||||
|> Result.withDefault Dict.empty
|
||||
|
||||
dict2 =
|
||||
lookupFn2 rawResponses
|
||||
lookupFn2 appType rawResponses
|
||||
|> Result.map Tuple.first
|
||||
|> Result.withDefault Dict.empty
|
||||
in
|
||||
@ -277,14 +277,14 @@ map2 fn request1 request2 =
|
||||
( Request ( urls1, lookupFn1 ), Done value2 ) ->
|
||||
Request
|
||||
( urls1
|
||||
, \rawResponses ->
|
||||
, \appType rawResponses ->
|
||||
let
|
||||
value1 =
|
||||
lookupFn1 rawResponses
|
||||
lookupFn1 appType rawResponses
|
||||
|> Result.map Tuple.second
|
||||
|
||||
dict1 =
|
||||
lookupFn1 rawResponses
|
||||
lookupFn1 appType rawResponses
|
||||
|> Result.map Tuple.first
|
||||
|> Result.withDefault Dict.empty
|
||||
in
|
||||
@ -299,14 +299,14 @@ map2 fn request1 request2 =
|
||||
( Done value2, Request ( urls1, lookupFn1 ) ) ->
|
||||
Request
|
||||
( urls1
|
||||
, \rawResponses ->
|
||||
, \appType rawResponses ->
|
||||
let
|
||||
value1 =
|
||||
lookupFn1 rawResponses
|
||||
lookupFn1 appType rawResponses
|
||||
|> Result.map Tuple.second
|
||||
|
||||
dict1 =
|
||||
lookupFn1 rawResponses
|
||||
lookupFn1 appType rawResponses
|
||||
|> Result.map Tuple.first
|
||||
|> Result.withDefault Dict.empty
|
||||
in
|
||||
@ -339,14 +339,14 @@ combineReducedDicts dict1 dict2 =
|
||||
)
|
||||
|
||||
|
||||
lookup : Pages.StaticHttpRequest.Request value -> Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, value )
|
||||
lookup requestInfo rawResponses =
|
||||
lookup : ApplicationType -> Pages.StaticHttpRequest.Request value -> Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, value )
|
||||
lookup appType requestInfo rawResponses =
|
||||
case requestInfo of
|
||||
Request ( urls, lookupFn ) ->
|
||||
lookupFn rawResponses
|
||||
lookupFn appType rawResponses
|
||||
|> Result.andThen
|
||||
(\( strippedResponses, nextRequest ) ->
|
||||
lookup
|
||||
lookup appType
|
||||
(addUrls urls nextRequest)
|
||||
strippedResponses
|
||||
)
|
||||
@ -396,8 +396,8 @@ andThen : (a -> Request b) -> Request a -> Request b
|
||||
andThen fn requestInfo =
|
||||
Request
|
||||
( lookupUrls requestInfo
|
||||
, \rawResponses ->
|
||||
lookup
|
||||
, \appType rawResponses ->
|
||||
lookup appType
|
||||
requestInfo
|
||||
rawResponses
|
||||
|> (\result ->
|
||||
@ -439,11 +439,22 @@ succeed : a -> Request a
|
||||
succeed value =
|
||||
Request
|
||||
( []
|
||||
, \rawResponses ->
|
||||
, \appType rawResponses ->
|
||||
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.
|
||||
|
||||
import Json.Decode as Decode exposing (Decoder)
|
||||
@ -462,15 +473,15 @@ get :
|
||||
-> Request a
|
||||
get url decoder =
|
||||
request
|
||||
(url
|
||||
|> Secrets.map
|
||||
(\okUrl ->
|
||||
{ url = okUrl
|
||||
, method = "GET"
|
||||
, headers = []
|
||||
, body = emptyBody
|
||||
}
|
||||
)
|
||||
(Secrets.map
|
||||
(\okUrl ->
|
||||
{ url = okUrl
|
||||
, method = "GET"
|
||||
, headers = []
|
||||
, body = emptyBody
|
||||
}
|
||||
)
|
||||
url
|
||||
)
|
||||
decoder
|
||||
|
||||
@ -578,70 +589,104 @@ unoptimizedRequest requestWithSecrets expect =
|
||||
ExpectJson decoder ->
|
||||
Request
|
||||
( [ requestWithSecrets ]
|
||||
, \rawResponseDict ->
|
||||
rawResponseDict
|
||||
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
||||
|> (\maybeResponse ->
|
||||
case maybeResponse of
|
||||
Just rawResponse ->
|
||||
Ok
|
||||
( rawResponseDict
|
||||
-- |> Dict.update url (\maybeValue -> Just """{"fake": 123}""")
|
||||
, rawResponse
|
||||
)
|
||||
, \appType rawResponseDict ->
|
||||
case appType of
|
||||
ApplicationType.Cli ->
|
||||
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 ) ->
|
||||
let
|
||||
reduced =
|
||||
Decode.stripString decoder rawResponse
|
||||
|> Result.withDefault "TODO"
|
||||
in
|
||||
rawResponse
|
||||
|> Decode.decodeString decoder
|
||||
-- |> Result.mapError Json.Decode.Exploration.errorsToString
|
||||
|> (\decodeResult ->
|
||||
case decodeResult of
|
||||
Decode.BadJson ->
|
||||
Pages.StaticHttpRequest.DecoderError "Payload sent back invalid JSON" |> Err
|
||||
Nothing ->
|
||||
Secrets.maskedLookup requestWithSecrets
|
||||
|> requestToString
|
||||
|> Pages.StaticHttpRequest.MissingHttpResponse
|
||||
|> Err
|
||||
)
|
||||
|> Result.andThen
|
||||
(\( strippedResponses, rawResponse ) ->
|
||||
let
|
||||
reduced =
|
||||
Json.Decode.Exploration.stripString (Internal.OptimizedDecoder.jde decoder) rawResponse
|
||||
|> Result.withDefault "TODO"
|
||||
in
|
||||
rawResponse
|
||||
|> Json.Decode.Exploration.decodeString (decoder |> Internal.OptimizedDecoder.jde)
|
||||
|> (\decodeResult ->
|
||||
case decodeResult of
|
||||
Json.Decode.Exploration.BadJson ->
|
||||
Pages.StaticHttpRequest.DecoderError "Payload sent back invalid JSON" |> Err
|
||||
|
||||
Decode.Errors errors ->
|
||||
errors
|
||||
|> Decode.errorsToString
|
||||
|> Pages.StaticHttpRequest.DecoderError
|
||||
|> Err
|
||||
Json.Decode.Exploration.Errors errors ->
|
||||
errors
|
||||
|> Json.Decode.Exploration.errorsToString
|
||||
|> Pages.StaticHttpRequest.DecoderError
|
||||
|> Err
|
||||
|
||||
Decode.WithWarnings warnings a ->
|
||||
-- Pages.StaticHttpRequest.DecoderError "" |> Err
|
||||
Ok a
|
||||
Json.Decode.Exploration.WithWarnings warnings a ->
|
||||
Ok a
|
||||
|
||||
Decode.Success a ->
|
||||
Ok a
|
||||
)
|
||||
-- |> Result.mapError Pages.StaticHttpRequest.DecoderError
|
||||
|> Result.map Done
|
||||
|> Result.map
|
||||
(\finalRequest ->
|
||||
( strippedResponses
|
||||
|> Dict.insert
|
||||
(Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
||||
reduced
|
||||
, finalRequest
|
||||
)
|
||||
)
|
||||
)
|
||||
Json.Decode.Exploration.Success a ->
|
||||
Ok a
|
||||
)
|
||||
|> Result.map Done
|
||||
|> Result.map
|
||||
(\finalRequest ->
|
||||
( strippedResponses
|
||||
|> Dict.insert
|
||||
(Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
||||
reduced
|
||||
, 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 ->
|
||||
Request
|
||||
( [ requestWithSecrets ]
|
||||
, \rawResponseDict ->
|
||||
, \appType rawResponseDict ->
|
||||
rawResponseDict
|
||||
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
||||
|> (\maybeResponse ->
|
||||
@ -667,7 +712,10 @@ unoptimizedRequest requestWithSecrets expect =
|
||||
|> (\decodeResult ->
|
||||
case decodeResult of
|
||||
Err error ->
|
||||
Pages.StaticHttpRequest.DecoderError "Payload sent back invalid JSON" |> Err
|
||||
error
|
||||
|> Decode.errorToString
|
||||
|> Pages.StaticHttpRequest.DecoderError
|
||||
|> Err
|
||||
|
||||
Ok a ->
|
||||
Ok a
|
||||
@ -688,7 +736,7 @@ unoptimizedRequest requestWithSecrets expect =
|
||||
ExpectString mapStringFn ->
|
||||
Request
|
||||
( [ requestWithSecrets ]
|
||||
, \rawResponseDict ->
|
||||
, \appType rawResponseDict ->
|
||||
rawResponseDict
|
||||
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|
||||
|> (\maybeResponse ->
|
||||
|
@ -2,26 +2,27 @@ module Pages.StaticHttpRequest exposing (Error(..), Request(..), permanentError,
|
||||
|
||||
import BuildError exposing (BuildError)
|
||||
import Dict exposing (Dict)
|
||||
import Pages.Internal.ApplicationType as ApplicationType exposing (ApplicationType)
|
||||
import Pages.StaticHttp.Request
|
||||
import Secrets
|
||||
import TerminalText as Terminal
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
strippedResponses : Request value -> Dict String String -> Dict String String
|
||||
strippedResponses request rawResponses =
|
||||
strippedResponses : ApplicationType -> Request value -> Dict String String -> Dict String String
|
||||
strippedResponses appType request rawResponses =
|
||||
case request of
|
||||
Request ( list, lookupFn ) ->
|
||||
case lookupFn rawResponses of
|
||||
case lookupFn appType rawResponses of
|
||||
Err error ->
|
||||
rawResponses
|
||||
|
||||
Ok ( partiallyStrippedResponses, followupRequest ) ->
|
||||
strippedResponses followupRequest partiallyStrippedResponses
|
||||
strippedResponses appType followupRequest partiallyStrippedResponses
|
||||
|
||||
Done value ->
|
||||
rawResponses
|
||||
@ -30,6 +31,7 @@ strippedResponses request rawResponses =
|
||||
type Error
|
||||
= MissingHttpResponse String
|
||||
| DecoderError String
|
||||
| UserCalledStaticHttpFail String
|
||||
|
||||
|
||||
urls : Request value -> List (Secrets.Value Pages.StaticHttp.Request.Request)
|
||||
@ -65,14 +67,24 @@ toBuildError path error =
|
||||
, 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
|
||||
Request ( urlList, lookupFn ) ->
|
||||
case lookupFn rawResponses of
|
||||
case lookupFn appType rawResponses of
|
||||
Ok ( partiallyStrippedResponses, nextRequest ) ->
|
||||
permanentError nextRequest rawResponses
|
||||
permanentError appType nextRequest rawResponses
|
||||
|
||||
Err error ->
|
||||
case error of
|
||||
@ -82,17 +94,20 @@ permanentError request rawResponses =
|
||||
DecoderError _ ->
|
||||
Just error
|
||||
|
||||
UserCalledStaticHttpFail string ->
|
||||
Just error
|
||||
|
||||
Done value ->
|
||||
Nothing
|
||||
|
||||
|
||||
resolve : Request value -> Dict String String -> Result Error value
|
||||
resolve request rawResponses =
|
||||
resolve : ApplicationType -> Request value -> Dict String String -> Result Error value
|
||||
resolve appType request rawResponses =
|
||||
case request of
|
||||
Request ( urlList, lookupFn ) ->
|
||||
case lookupFn rawResponses of
|
||||
case lookupFn appType rawResponses of
|
||||
Ok ( partiallyStrippedResponses, nextRequest ) ->
|
||||
resolve nextRequest rawResponses
|
||||
resolve appType nextRequest rawResponses
|
||||
|
||||
Err error ->
|
||||
Err error
|
||||
@ -101,13 +116,13 @@ resolve request rawResponses =
|
||||
Ok value
|
||||
|
||||
|
||||
resolveUrls : Request value -> Dict String String -> ( Bool, List (Secrets.Value Pages.StaticHttp.Request.Request) )
|
||||
resolveUrls request rawResponses =
|
||||
resolveUrls : ApplicationType -> Request value -> Dict String String -> ( Bool, List (Secrets.Value Pages.StaticHttp.Request.Request) )
|
||||
resolveUrls appType request rawResponses =
|
||||
case request of
|
||||
Request ( urlList, lookupFn ) ->
|
||||
case lookupFn rawResponses of
|
||||
case lookupFn appType rawResponses of
|
||||
Ok ( partiallyStrippedResponses, nextRequest ) ->
|
||||
resolveUrls nextRequest rawResponses
|
||||
resolveUrls appType nextRequest rawResponses
|
||||
|> Tuple.mapSecond ((++) urlList)
|
||||
|
||||
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"
|
||||
|
||||
Green ->
|
||||
"[32;1m"
|
||||
"[32m"
|
||||
|
||||
Yellow ->
|
||||
"[33;1m"
|
||||
"[33m"
|
||||
|
||||
Cyan ->
|
||||
"[36m"
|
||||
|
@ -5,7 +5,9 @@ import Dict exposing (Dict)
|
||||
import Expect
|
||||
import Html
|
||||
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.Document as Document
|
||||
import Pages.Http
|
||||
@ -42,7 +44,7 @@ all =
|
||||
"https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
"""{ "stargazer_count": 86 }"""
|
||||
|> expectSuccess
|
||||
[ ( "/"
|
||||
[ ( ""
|
||||
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
, """{"stargazer_count":86}"""
|
||||
)
|
||||
@ -69,7 +71,7 @@ all =
|
||||
"NEXT-REQUEST"
|
||||
"""null"""
|
||||
|> expectSuccess
|
||||
[ ( "/elm-pages"
|
||||
[ ( "elm-pages"
|
||||
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
, """null"""
|
||||
)
|
||||
@ -162,7 +164,7 @@ all =
|
||||
"url10"
|
||||
"""{"image": "image10.jpg"}"""
|
||||
|> expectSuccess
|
||||
[ ( "/elm-pages"
|
||||
[ ( "elm-pages"
|
||||
, [ ( get "https://pokeapi.co/api/v2/pokemon/"
|
||||
, """[{"url":"url1"},{"url":"url2"},{"url":"url3"},{"url":"url4"},{"url":"url5"},{"url":"url6"},{"url":"url7"},{"url":"url8"},{"url":"url9"},{"url":"url10"}]"""
|
||||
)
|
||||
@ -218,13 +220,13 @@ all =
|
||||
"https://api.github.com/repos/dillonkearns/elm-pages-starter"
|
||||
"""{ "stargazer_count": 22 }"""
|
||||
|> expectSuccess
|
||||
[ ( "/elm-pages"
|
||||
[ ( "elm-pages"
|
||||
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
, """{"stargazer_count":86}"""
|
||||
)
|
||||
]
|
||||
)
|
||||
, ( "/elm-pages-starter"
|
||||
, ( "elm-pages-starter"
|
||||
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages-starter"
|
||||
, """{"stargazer_count":22}"""
|
||||
)
|
||||
@ -243,7 +245,7 @@ all =
|
||||
"https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
"""{ "stargazer_count": 86, "unused_field": 123 }"""
|
||||
|> expectSuccess
|
||||
[ ( "/"
|
||||
[ ( ""
|
||||
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
, """{"stargazer_count":86}"""
|
||||
)
|
||||
@ -272,7 +274,7 @@ all =
|
||||
"https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
"""{ "stargazer_count": 86, "unused_field": 123 }"""
|
||||
|> expectSuccess
|
||||
[ ( "/"
|
||||
[ ( ""
|
||||
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
, """{ "stargazer_count": 86, "unused_field": 123 }"""
|
||||
)
|
||||
@ -299,7 +301,7 @@ all =
|
||||
"https://example.com/file.txt"
|
||||
"This is a raw text file."
|
||||
|> expectSuccess
|
||||
[ ( "/"
|
||||
[ ( ""
|
||||
, [ ( get "https://example.com/file.txt"
|
||||
, "This is a raw text file."
|
||||
)
|
||||
@ -339,7 +341,7 @@ all =
|
||||
(expectErrorsPort
|
||||
"""-- STATIC HTTP DECODING ERROR ----------------------------------------------------- elm-pages
|
||||
|
||||
/
|
||||
|
||||
|
||||
String was not uppercased"""
|
||||
)
|
||||
@ -363,7 +365,7 @@ String was not uppercased"""
|
||||
"https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
"""{ "stargazer_count": 86, "unused_field": 123 }"""
|
||||
|> expectSuccess
|
||||
[ ( "/"
|
||||
[ ( ""
|
||||
, [ ( { method = "POST"
|
||||
, url = "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
, headers = []
|
||||
@ -394,7 +396,7 @@ String was not uppercased"""
|
||||
"https://api.github.com/repos/dillonkearns/elm-pages-starter"
|
||||
"""{ "stargazer_count": 50, "unused_field": 456 }"""
|
||||
|> expectSuccess
|
||||
[ ( "/"
|
||||
[ ( ""
|
||||
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
, """{"stargazer_count":100}"""
|
||||
)
|
||||
@ -422,7 +424,7 @@ String was not uppercased"""
|
||||
"https://api.github.com/repos/dillonkearns/elm-pages-starter"
|
||||
"""{ "stargazer_count": 50, "unused_field": 456 }"""
|
||||
|> expectSuccess
|
||||
[ ( "/"
|
||||
[ ( ""
|
||||
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
, """{"stargazer_count":100}"""
|
||||
)
|
||||
@ -439,7 +441,7 @@ String was not uppercased"""
|
||||
, StaticHttp.succeed ()
|
||||
)
|
||||
]
|
||||
|> expectSuccess [ ( "/", [] ) ]
|
||||
|> expectSuccess [ ( "", [] ) ]
|
||||
, test "the port sends out when there are duplicate http requests for the same page" <|
|
||||
\() ->
|
||||
start
|
||||
@ -454,7 +456,7 @@ String was not uppercased"""
|
||||
"http://example.com"
|
||||
"""null"""
|
||||
|> expectSuccess
|
||||
[ ( "/"
|
||||
[ ( ""
|
||||
, [ ( get "http://example.com"
|
||||
, """null"""
|
||||
)
|
||||
@ -478,7 +480,7 @@ String was not uppercased"""
|
||||
(expectErrorsPort
|
||||
"""-- STATIC HTTP DECODING ERROR ----------------------------------------------------- elm-pages
|
||||
|
||||
/elm-pages
|
||||
elm-pages
|
||||
|
||||
I encountered some errors while decoding this JSON:
|
||||
|
||||
@ -591,7 +593,7 @@ Body: """)
|
||||
}
|
||||
)
|
||||
|> expectSuccess
|
||||
[ ( "/"
|
||||
[ ( ""
|
||||
, [ ( { method = "GET"
|
||||
, url = "https://api.github.com/repos/dillonkearns/elm-pages?apiKey=<API_KEY>"
|
||||
, headers =
|
||||
@ -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 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
|
||||
document =
|
||||
Document.fromList
|
||||
@ -637,8 +696,9 @@ start pages =
|
||||
|
||||
config =
|
||||
{ toJsPort = toJsPort
|
||||
, fromJsPort = fromJsPort
|
||||
, manifest = manifest
|
||||
, generateFiles = \_ -> []
|
||||
, generateFiles = \_ -> StaticHttp.succeed []
|
||||
, init = \_ -> ( (), Cmd.none )
|
||||
, update = \_ _ -> ( (), Cmd.none )
|
||||
, view =
|
||||
@ -650,7 +710,6 @@ start pages =
|
||||
|> Dict.get
|
||||
(page.path
|
||||
|> PagePath.toString
|
||||
|> String.dropLeft 1
|
||||
|> String.split "/"
|
||||
|> List.filter (\pathPart -> pathPart /= "")
|
||||
)
|
||||
@ -670,6 +729,30 @@ start pages =
|
||||
, pathKey = PathKey
|
||||
, 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
|
||||
{-
|
||||
(Model -> model)
|
||||
@ -685,9 +768,7 @@ start pages =
|
||||
, view = \_ -> { title = "", body = [] }
|
||||
}
|
||||
|> ProgramTest.withSimulatedEffects simulateEffects
|
||||
|> ProgramTest.start (flags """{"secrets":
|
||||
{"API_KEY": "ABCD1234","BEARER": "XYZ789"}, "mode": "prod"
|
||||
}""")
|
||||
|> ProgramTest.start (flags (Encode.encode 0 encodedFlags))
|
||||
|
||||
|
||||
flags : String -> JD.Value
|
||||
@ -781,6 +862,10 @@ toJsPort foo =
|
||||
Cmd.none
|
||||
|
||||
|
||||
fromJsPort =
|
||||
Sub.none
|
||||
|
||||
|
||||
type PathKey
|
||||
= PathKey
|
||||
|
||||
@ -806,7 +891,7 @@ starDecoder =
|
||||
|
||||
|
||||
thingy =
|
||||
[ ( "/"
|
||||
[ ( ""
|
||||
, [ ( { method = "GET"
|
||||
, url = "https://api.github.com/repos/dillonkearns/elm-pages"
|
||||
, headers = []
|
||||
@ -832,27 +917,31 @@ expectSuccess expectedRequests previous =
|
||||
|> ProgramTest.expectOutgoingPortValues
|
||||
"toJsPort"
|
||||
(Codec.decoder Main.toJsCodec)
|
||||
(Expect.equal
|
||||
[ Main.Success
|
||||
{ pages =
|
||||
expectedRequests
|
||||
|> List.map
|
||||
(\( url, requests ) ->
|
||||
( url
|
||||
, requests
|
||||
|> List.map
|
||||
(\( request, response ) ->
|
||||
( Request.hash request, response )
|
||||
(\value ->
|
||||
case value of
|
||||
[ Main.Success portPayload ] ->
|
||||
portPayload.pages
|
||||
|> Expect.equal
|
||||
(expectedRequests
|
||||
|> List.map
|
||||
(\( url, requests ) ->
|
||||
( url
|
||||
, requests
|
||||
|> List.map
|
||||
(\( request, response ) ->
|
||||
( Request.hash request, response )
|
||||
)
|
||||
|> Dict.fromList
|
||||
)
|
||||
|> Dict.fromList
|
||||
)
|
||||
)
|
||||
|> Dict.fromList
|
||||
)
|
||||
|> Dict.fromList
|
||||
, manifest = manifest
|
||||
, filesToGenerate = []
|
||||
, errors = []
|
||||
}
|
||||
]
|
||||
|
||||
[ _ ] ->
|
||||
Expect.fail "Expected success port."
|
||||
|
||||
_ ->
|
||||
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 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.Request as Request
|
||||
import Pages.StaticHttpRequest as StaticHttpRequest
|
||||
@ -42,11 +44,11 @@ all =
|
||||
StaticHttp.get (Secrets.succeed "first") (Decode.succeed "NEXT")
|
||||
|> StaticHttp.andThen
|
||||
(\continueUrl ->
|
||||
-- StaticHttp.get continueUrl (Decode.succeed ())
|
||||
getWithoutSecrets "NEXT" (Decode.succeed ())
|
||||
)
|
||||
|> (\request ->
|
||||
StaticHttpRequest.resolveUrls request
|
||||
StaticHttpRequest.resolveUrls ApplicationType.Cli
|
||||
request
|
||||
(requestsDict
|
||||
[ ( get "first", "null" )
|
||||
, ( get "NEXT", "null" )
|
||||
@ -63,7 +65,8 @@ all =
|
||||
getWithoutSecrets "NEXT" (Decode.succeed ())
|
||||
)
|
||||
|> (\request ->
|
||||
StaticHttpRequest.resolveUrls request
|
||||
StaticHttpRequest.resolveUrls ApplicationType.Cli
|
||||
request
|
||||
(requestsDict
|
||||
[ ( get "NEXT", "null" )
|
||||
]
|
||||
@ -81,7 +84,8 @@ all =
|
||||
)
|
||||
|> StaticHttp.map (\_ -> ())
|
||||
|> (\request ->
|
||||
StaticHttpRequest.resolveUrls request
|
||||
StaticHttpRequest.resolveUrls ApplicationType.Cli
|
||||
request
|
||||
(requestsDict
|
||||
[ ( get "first", "null" )
|
||||
, ( get "NEXT", "null" )
|
||||
@ -98,7 +102,8 @@ all =
|
||||
getWithoutSecrets "NEXT" (Decode.succeed ())
|
||||
)
|
||||
|> (\request ->
|
||||
StaticHttpRequest.resolveUrls request
|
||||
StaticHttpRequest.resolveUrls ApplicationType.Cli
|
||||
request
|
||||
(requestsDict
|
||||
[ ( get "first", "null" )
|
||||
]
|
||||
@ -119,7 +124,8 @@ all =
|
||||
)
|
||||
)
|
||||
|> (\request ->
|
||||
StaticHttpRequest.resolveUrls request
|
||||
StaticHttpRequest.resolveUrls ApplicationType.Cli
|
||||
request
|
||||
(requestsDict
|
||||
[ ( get "first", "1" )
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user