mirror of
https://github.com/dillonkearns/elm-pages-v3-beta.git
synced 2024-11-24 06:54:03 +03:00
Merge branch 'master' into phantom-builder
This commit is contained in:
commit
3a61933d81
@ -40,6 +40,15 @@
|
||||
"contributions": [
|
||||
"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,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.2.12] - 2020-03-28
|
||||
## [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.
|
||||
|
@ -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!
|
||||
|
5
elm.json
5
elm.json
@ -7,6 +7,8 @@
|
||||
"exposed-modules": [
|
||||
"Head",
|
||||
"Head.Seo",
|
||||
"OptimizedDecoder",
|
||||
"OptimizedDecoder.Pipeline",
|
||||
"Pages.ImagePath",
|
||||
"Pages.PagePath",
|
||||
"Pages.StaticHttp",
|
||||
@ -31,7 +33,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",
|
||||
@ -43,4 +44,4 @@
|
||||
"elm-explorations/test": "1.2.2 <= v < 2.0.0",
|
||||
"jgrenat/elm-html-test-runner": "1.0.3 <= v < 2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
"type": "blog",
|
||||
"author": "Dillon Kearns",
|
||||
"title": "A is for API - Introducing Static HTTP Requests",
|
||||
"description": "",
|
||||
"description": "The new StaticHttp API lets you fetch data when your site is built. That lets you remove loading spinners, and even access environment variables.",
|
||||
"image": "images/article-covers/static-http.jpg",
|
||||
"published": "2019-12-10",
|
||||
}
|
||||
|
7
examples/docs/content/docs/core-concepts.md
Normal file
7
examples/docs/content/docs/core-concepts.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Core Concepts
|
||||
type: doc
|
||||
---
|
||||
## StaticHttp
|
||||
|
||||
Gives you a way to pull in data during the build step. This data changes every time you run a build. You won't see a loading spinner or error with this data in your built production site. You might get a build error that you can fix.
|
@ -31,7 +31,6 @@
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
}
|
@ -17,10 +17,12 @@ import Head.Seo as Seo
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
import Index
|
||||
import Json.Decode.Exploration as D
|
||||
import Json.Decode as Decode exposing (Decoder)
|
||||
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.ImagePath as ImagePath exposing (ImagePath)
|
||||
@ -34,6 +36,7 @@ import Rss
|
||||
import RssPlugin
|
||||
import Secrets
|
||||
import Showcase
|
||||
import StructuredData
|
||||
|
||||
|
||||
manifest : Manifest.Config Pages.PathKey
|
||||
@ -202,7 +205,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")
|
||||
@ -219,49 +222,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
|
||||
@ -535,8 +500,8 @@ highlightableLink currentPath linkDirectory displayName =
|
||||
<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 =
|
||||
case metadata of
|
||||
Metadata.Page meta ->
|
||||
Seo.summary
|
||||
@ -571,26 +536,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
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
@ -95,20 +95,13 @@ import Dict exposing (Dict)
|
||||
|
||||
content : { markdown : List ( List String, { frontMatter : String, body : Maybe String } ), markup : List ( List String, String ) }
|
||||
content =
|
||||
{ markdown = 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 ,"
|
||||
|
@ -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,52 +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(/\/$/, '');
|
||||
const staticRequests = this.pagesWithRequests[route];
|
||||
|
||||
const filename = path.join(file.baseRoute, "content.json");
|
||||
compilation.fileDependencies.add(filename);
|
||||
const rawContents = JSON.stringify({
|
||||
body: file.content,
|
||||
staticData: staticRequests || {}
|
||||
});
|
||||
const files = globby.sync("content").map(unpackFile);
|
||||
|
||||
compilation.assets[filename] = {
|
||||
source: () => rawContents,
|
||||
size: () => rawContents.length
|
||||
};
|
||||
});
|
||||
|
||||
(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
|
||||
};
|
||||
});
|
||||
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]))
|
||||
}
|
||||
else {
|
||||
staticRequestData = payload.pages
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
files.forEach(file => {
|
||||
// Couldn't find this documented in the webpack docs,
|
||||
// but I found the example code for it here:
|
||||
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
|
||||
|
||||
let route = file.baseRoute.replace(/\/$/, '');
|
||||
const staticRequests = staticRequestData[route];
|
||||
|
||||
const filename = path.join(file.baseRoute, "content.json");
|
||||
if (compilation.contextDependencies) {
|
||||
compilation.contextDependencies.add('content')
|
||||
}
|
||||
// compilation.fileDependencies.add(filename);
|
||||
if (compilation.fileDependencies) {
|
||||
compilation.fileDependencies.add(path.resolve(file.filePath));
|
||||
}
|
||||
const rawContents = JSON.stringify({
|
||||
body: file.content,
|
||||
staticData: staticRequests || {}
|
||||
});
|
||||
|
||||
compilation.assets[filename] = {
|
||||
source: () => rawContents,
|
||||
size: () => rawContents.length
|
||||
};
|
||||
});
|
||||
|
||||
(global.filesToGenerate || []).forEach(file => {
|
||||
// Couldn't find this documented in the webpack docs,
|
||||
// but I found the example code for it here:
|
||||
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
|
||||
compilation.assets[file.path] = {
|
||||
source: () => file.content,
|
||||
size: () => file.content.length
|
||||
};
|
||||
});
|
||||
|
||||
callback()
|
||||
|
||||
})
|
||||
|
||||
});
|
||||
}
|
||||
|
@ -1,33 +1,36 @@
|
||||
const { compileToString } = require("../node-elm-compiler/index.js");
|
||||
const { compileToStringSync } = require("../node-elm-compiler/index.js");
|
||||
XMLHttpRequest = require("xhr2");
|
||||
|
||||
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 ClosurePlugin = require("closure-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);
|
||||
@ -39,27 +51,31 @@ 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.use("*", function(req, res, next) {
|
||||
app.get('/elm-pages-dev-server-options', function (req, res) {
|
||||
res.json({ elmDebugger: debug });
|
||||
});
|
||||
|
||||
app.use("*", function (req, res, next) {
|
||||
// don't know why this works, but it does
|
||||
// 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) {
|
||||
const contents = isPage
|
||||
? replaceBaseAndLinks(result.toString(), route)
|
||||
: result
|
||||
|
||||
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(contents);
|
||||
res.end();
|
||||
@ -74,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(
|
||||
@ -128,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/**/*",
|
||||
@ -167,7 +182,33 @@ 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$/,
|
||||
@ -234,10 +275,10 @@ 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: {},
|
||||
resolve: {
|
||||
@ -249,7 +290,7 @@ function webpackOptions(
|
||||
// process.cwd prefixed node_modules above).
|
||||
path.resolve(path.dirname(require.resolve('webpack')), '../../'),
|
||||
|
||||
],
|
||||
],
|
||||
extensions: [".js", ".elm", ".scss", ".png", ".html"]
|
||||
},
|
||||
module: {
|
||||
@ -323,7 +364,7 @@ function webpackOptions(
|
||||
renderer: new PrerenderSPAPlugin.PuppeteerRenderer({
|
||||
renderAfterDocumentEvent: "prerender-trigger",
|
||||
headless: true,
|
||||
devtools: false
|
||||
devtools: false,
|
||||
}),
|
||||
|
||||
postProcess: renderedRoute => {
|
||||
@ -354,14 +395,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: [
|
||||
@ -385,6 +426,42 @@ function webpackOptions(
|
||||
}
|
||||
}
|
||||
|
||||
function hmrClientPath() {
|
||||
var ansiColors = {
|
||||
reset: ['ffffff', 'transparent'], // [FOREGROUD_COLOR, BACKGROUND_COLOR]
|
||||
black: '000',
|
||||
red: 'c91b00',
|
||||
green: '00c200',
|
||||
yellow: 'c7c400',
|
||||
blue: '0225c7',
|
||||
magenta: 'c930c7',
|
||||
cyan: '00c5c7',
|
||||
lightgrey: 'f0f0f0',
|
||||
darkgrey: '888'
|
||||
};
|
||||
var overlayStyles = {
|
||||
// options from https://github.com/webpack-contrib/webpack-hot-middleware/blob/master/client-overlay.js
|
||||
|
||||
background: 'rgba(0,0,0,0.90)',
|
||||
color: '#e8e8e8',
|
||||
lineHeight: '1.6',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'Menlo, Consolas, monospace',
|
||||
fontSize: '16px',
|
||||
// position: 'fixed',
|
||||
// zIndex: 9999,
|
||||
// padding: '10px',
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// top: 0,
|
||||
// bottom: 0,
|
||||
// overflow: 'auto',
|
||||
// dir: 'ltr',
|
||||
// textAlign: 'left',
|
||||
};
|
||||
return `${require.resolve("webpack-hot-middleware/client")}?ansiColors=${encodeURIComponent(JSON.stringify(ansiColors))}&overlayStyles=${encodeURIComponent(JSON.stringify(overlayStyles))}`;
|
||||
}
|
||||
|
||||
|
||||
function cleanRoute(route) {
|
||||
return route.replace(/(^\/|\/$)/, "")
|
||||
@ -395,10 +472,10 @@ function pathToRoot(cleanedRoute) {
|
||||
return cleanedRoute === ""
|
||||
? cleanedRoute
|
||||
: cleanedRoute
|
||||
.split("/")
|
||||
.map(_ => "..")
|
||||
.join("/")
|
||||
.replace(/\.$/, "./")
|
||||
.split("/")
|
||||
.map(_ => "..")
|
||||
.join("/")
|
||||
.replace(/\.$/, "./")
|
||||
}
|
||||
|
||||
|
||||
|
@ -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}
|
||||
@ -41,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
|
||||
@ -50,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)
|
||||
@ -92,23 +91,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 +118,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)
|
||||
@ -159,10 +160,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 +175,7 @@ internals =
|
||||
|
||||
${staticRouteStuff(staticRoutes)}
|
||||
|
||||
${generateRawContent(markdownContent, markupContent, true)}
|
||||
${generateRawContent(markdownContent, true)}
|
||||
`;
|
||||
}
|
||||
module.exports = { elmPagesUiFile, elmPagesCliFile };
|
||||
|
@ -5,67 +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
|
||||
}
|
||||
});
|
||||
|
||||
@ -79,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);
|
||||
}
|
||||
|
@ -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,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="preload" href="content.json" as="fetch" crossorigin />
|
||||
|
||||
<base href="/" />
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<script>
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker.register("service-worker.js");
|
||||
});
|
||||
} else {
|
||||
console.log("No service worker registered.");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
</html>
|
153
index.js
153
index.js
@ -11,9 +11,9 @@ module.exports = function pagesInit(
|
||||
prefetchedPages = [window.location.pathname];
|
||||
initialLocationHash = document.location.hash.replace(/^#/, "");
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
document.addEventListener("DOMContentLoaded", _ => {
|
||||
new MutationObserver(function() {
|
||||
new MutationObserver(function () {
|
||||
elmViewRendered = true;
|
||||
if (headTagsAdded) {
|
||||
document.dispatchEvent(new Event("prerender-trigger"));
|
||||
@ -32,48 +32,98 @@ function loadContentAndInitializeApp(/** @type { init: any } */ mainElmModule)
|
||||
const isPrerendering = navigator.userAgent.indexOf("Headless") >= 0
|
||||
const path = window.location.pathname.replace(/(\w)$/, "$1/")
|
||||
|
||||
return httpGet(`${window.location.origin}${path}content.json`).then(function(/** @type JSON */ contentJson) {
|
||||
return Promise.all([
|
||||
getConfig(),
|
||||
httpGet(`${window.location.origin}${path}content.json`)]).then(function (/** @type {[DevServerConfig?, JSON]} */[devServerConfig, contentJson]) {
|
||||
console.log('devServerConfig', devServerConfig);
|
||||
|
||||
const app = mainElmModule.init({
|
||||
flags: {
|
||||
secrets: null,
|
||||
baseUrl: isPrerendering
|
||||
? window.location.origin
|
||||
: document.baseURI,
|
||||
isPrerendering: isPrerendering,
|
||||
contentJson
|
||||
}
|
||||
});
|
||||
|
||||
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.map(route => new URL(route, document.baseURI).href);
|
||||
|
||||
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() {
|
||||
@ -132,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 &&
|
||||
@ -166,7 +216,9 @@ function prefetchIfNeeded(/** @type {HTMLAnchorElement} */ target) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {{ name: string; attributes: string[][]; }} HeadTag */
|
||||
/** @typedef {HeadTag | JsonLdTag} SeoTag */
|
||||
|
||||
/** @typedef {{ name: string; attributes: string[][]; type: 'head' }} HeadTag */
|
||||
function appendTag(/** @type {HeadTag} */ tagDetails) {
|
||||
const meta = document.createElement(tagDetails.name);
|
||||
tagDetails.attributes.forEach(([name, value]) => {
|
||||
@ -175,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 */
|
||||
|
5026
package-lock.json
generated
5026
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -22,9 +22,9 @@
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"babel-loader": "^8.0.6",
|
||||
"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",
|
||||
@ -32,30 +32,29 @@
|
||||
"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",
|
||||
"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",
|
||||
"webpack": "^4.41.5",
|
||||
"temp": "^0.9.0",
|
||||
"webpack": "4.42.1",
|
||||
"webpack-dev-middleware": "^3.7.0",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-merge": "^4.2.1",
|
||||
"workbox-webpack-plugin": "^4.3.1",
|
||||
"xhr2": "^0.2.0",
|
||||
"cross-spawn": "6.0.5",
|
||||
"find-elm-dependencies": "2.0.2",
|
||||
"lodash": "4.17.15",
|
||||
"temp": "^0.9.0"
|
||||
"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",
|
||||
|
120
src/Head.elm
120
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
|
||||
@ -42,6 +48,7 @@ through the `head` function.
|
||||
-}
|
||||
type Tag pathKey
|
||||
= Tag (Details pathKey)
|
||||
| StructuredData Json.Encode.Value
|
||||
|
||||
|
||||
type alias Details pathKey =
|
||||
@ -50,6 +57,100 @@ type alias Details pathKey =
|
||||
}
|
||||
|
||||
|
||||
{-| You can learn more about structured data in [Google's intro to structured data](https://developers.google.com/search/docs/guides/intro-structured-data).
|
||||
|
||||
When you add a `structuredData` item to one of your pages in `elm-pages`, it will add `json-ld` data to your document that looks like this:
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context":"http://schema.org/",
|
||||
"@type":"Article",
|
||||
"headline":"Extensible Markdown Parsing in Pure Elm",
|
||||
"description":"Introducing a new parser that extends your palette with no additional syntax",
|
||||
"image":"https://elm-pages.com/images/article-covers/extensible-markdown-parsing.jpg",
|
||||
"author":{
|
||||
"@type":"Person",
|
||||
"name":"Dillon Kearns"
|
||||
},
|
||||
"publisher":{
|
||||
"@type":"Person",
|
||||
"name":"Dillon Kearns"
|
||||
},
|
||||
"url":"https://elm-pages.com/blog/extensible-markdown-parsing-in-elm",
|
||||
"datePublished":"2019-10-08",
|
||||
"mainEntityOfPage":{
|
||||
"@type":"SoftwareSourceCode",
|
||||
"codeRepository":"https://github.com/dillonkearns/elm-pages",
|
||||
"description":"A statically typed site generator for Elm.",
|
||||
"author":"Dillon Kearns",
|
||||
"programmingLanguage":{
|
||||
"@type":"ComputerLanguage",
|
||||
"url":"http://elm-lang.org/",
|
||||
"name":"Elm",
|
||||
"image":"http://elm-lang.org/",
|
||||
"identifier":"http://elm-lang.org/"
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
To get that data, you would write this in your `elm-pages` head tags:
|
||||
|
||||
import Json.Encode as Encode
|
||||
|
||||
{-| <https://schema.org/Article>
|
||||
-}
|
||||
encodeArticle :
|
||||
{ title : String
|
||||
, description : String
|
||||
, author : StructuredData { authorMemberOf | personOrOrganization : () } authorPossibleFields
|
||||
, publisher : StructuredData { publisherMemberOf | personOrOrganization : () } publisherPossibleFields
|
||||
, url : String
|
||||
, imageUrl : String
|
||||
, datePublished : String
|
||||
, mainEntityOfPage : Encode.Value
|
||||
}
|
||||
-> Head.Tag pathKey
|
||||
encodeArticle info =
|
||||
Encode.object
|
||||
[ ( "@context", Encode.string "http://schema.org/" )
|
||||
, ( "@type", Encode.string "Article" )
|
||||
, ( "headline", Encode.string info.title )
|
||||
, ( "description", Encode.string info.description )
|
||||
, ( "image", Encode.string info.imageUrl )
|
||||
, ( "author", encode info.author )
|
||||
, ( "publisher", encode info.publisher )
|
||||
, ( "url", Encode.string info.url )
|
||||
, ( "datePublished", Encode.string info.datePublished )
|
||||
, ( "mainEntityOfPage", info.mainEntityOfPage )
|
||||
]
|
||||
|> Head.structuredData
|
||||
|
||||
Take a look at this [Google Search Gallery](https://developers.google.com/search/docs/guides/search-gallery)
|
||||
to see some examples of how structured data can be used by search engines to give rich search results. It can help boost
|
||||
your rankings, get better engagement for your content, and also make your content more accessible. For example,
|
||||
voice assistant devices can make use of structured data. If you're hosting a conference and want to make the event
|
||||
date and location easy for attendees to find, this can make that information more accessible.
|
||||
|
||||
For the current version of API, you'll need to make sure that the format is correct and contains the required and recommended
|
||||
structure.
|
||||
|
||||
Check out <https://schema.org> for a comprehensive listing of possible data types and fields. And take a look at
|
||||
Google's [Structured Data Testing Tool](https://search.google.com/structured-data/testing-tool)
|
||||
too make sure that your structured data is valid and includes the recommended values.
|
||||
|
||||
In the future, `elm-pages` will likely support a typed API, but schema.org is a massive spec, and changes frequently.
|
||||
And there are multiple sources of information on the possible and recommended structure. So it will take some time
|
||||
for the right API design to evolve. In the meantime, this allows you to make use of this for SEO purposes.
|
||||
|
||||
-}
|
||||
structuredData : Json.Encode.Value -> Tag pathKey
|
||||
structuredData value =
|
||||
StructuredData value
|
||||
|
||||
|
||||
{-| Create a raw `AttributeValue` (as opposed to some kind of absolute URL).
|
||||
-}
|
||||
raw : String -> AttributeValue pathKey
|
||||
@ -196,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
|
||||
|
18
src/Internal/OptimizedDecoder.elm
Normal file
18
src/Internal/OptimizedDecoder.elm
Normal file
@ -0,0 +1,18 @@
|
||||
module Internal.OptimizedDecoder exposing (OptimizedDecoder(..), jd, jde)
|
||||
|
||||
import Json.Decode
|
||||
import Json.Decode.Exploration
|
||||
|
||||
|
||||
type OptimizedDecoder a
|
||||
= OptimizedDecoder (Json.Decode.Decoder a) (Json.Decode.Exploration.Decoder a)
|
||||
|
||||
|
||||
jd : OptimizedDecoder a -> Json.Decode.Decoder a
|
||||
jd (OptimizedDecoder jd_ jde_) =
|
||||
jd_
|
||||
|
||||
|
||||
jde : OptimizedDecoder a -> Json.Decode.Exploration.Decoder a
|
||||
jde (OptimizedDecoder jd_ jde_) =
|
||||
jde_
|
769
src/OptimizedDecoder.elm
Normal file
769
src/OptimizedDecoder.elm
Normal file
@ -0,0 +1,769 @@
|
||||
module OptimizedDecoder exposing
|
||||
( Error, errorToString
|
||||
, Decoder, string, bool, int, float, Value
|
||||
, nullable, list, array, dict, keyValuePairs
|
||||
, field, at, index
|
||||
, maybe, oneOf
|
||||
, lazy, value, null, succeed, fail, andThen
|
||||
, map, map2, map3, map4, map5, map6, map7, map8, andMap
|
||||
, decodeString, decodeValue, decoder
|
||||
)
|
||||
|
||||
{-| This module allows you to build decoders that `elm-pages` can optimize for you in your `StaticHttp` requests.
|
||||
It does this by stripping of unused fields during the CLI build step. When it runs in production, it will
|
||||
just run a plain `elm/json` decoder, so you're fetching and decoding the stripped-down data, but without any
|
||||
performance penalty.
|
||||
|
||||
For convenience, this library also includes a `Json.Decode.Exploration.Pipeline`
|
||||
module which is largely a copy of [`NoRedInk/elm-decode-pipeline`][edp].
|
||||
|
||||
[edp]: http://package.elm-lang.org/packages/NoRedInk/elm-decode-pipeline/latest
|
||||
|
||||
|
||||
## Dealing with warnings and errors
|
||||
|
||||
@docs Error, errorToString
|
||||
|
||||
|
||||
# Primitives
|
||||
|
||||
@docs Decoder, string, bool, int, float, Value
|
||||
|
||||
|
||||
# Data Structures
|
||||
|
||||
@docs nullable, list, array, dict, keyValuePairs
|
||||
|
||||
|
||||
# Object Primitives
|
||||
|
||||
@docs field, at, index
|
||||
|
||||
|
||||
# Inconsistent Structure
|
||||
|
||||
@docs maybe, oneOf
|
||||
|
||||
|
||||
# Fancy Decoding
|
||||
|
||||
@docs lazy, value, null, succeed, fail, andThen
|
||||
|
||||
|
||||
# Mapping
|
||||
|
||||
**Note:** If you run out of map functions, take a look at [the pipeline module][pipe]
|
||||
which makes it easier to handle large objects.
|
||||
|
||||
[pipe]: http://package.elm-lang.org/packages/zwilias/json-decode-exploration/latest/Json-Decode-Exploration-Pipeline
|
||||
|
||||
@docs map, map2, map3, map4, map5, map6, map7, map8, andMap
|
||||
|
||||
|
||||
# Directly Running Decoders
|
||||
|
||||
Usually you'll be passing your decoders to
|
||||
|
||||
@docs decodeString, decodeValue, decoder
|
||||
|
||||
-}
|
||||
|
||||
import Array exposing (Array)
|
||||
import Dict exposing (Dict)
|
||||
import Internal.OptimizedDecoder exposing (OptimizedDecoder(..))
|
||||
import Json.Decode as JD
|
||||
import Json.Decode.Exploration as JDE
|
||||
|
||||
|
||||
{-| A decoder that will be optimized in your production bundle.
|
||||
-}
|
||||
type alias Decoder a =
|
||||
OptimizedDecoder a
|
||||
|
||||
|
||||
{-| A simple type alias for `Json.Decode.Value`.
|
||||
-}
|
||||
type alias Value =
|
||||
JD.Value
|
||||
|
||||
|
||||
{-| A simple type alias for `Json.Decode.Error`.
|
||||
-}
|
||||
type alias Error =
|
||||
JD.Error
|
||||
|
||||
|
||||
{-| A simple wrapper for `Json.Decode.errorToString`.
|
||||
-}
|
||||
errorToString : JD.Error -> String
|
||||
errorToString =
|
||||
JD.errorToString
|
||||
|
||||
|
||||
{-| Usually you'll want to directly pass your `OptimizedDecoder` to `StaticHttp` or other `elm-pages` APIs.
|
||||
But if you want to re-use your decoder somewhere else, it may be useful to turn it into a plain `elm/json` decoder.
|
||||
-}
|
||||
decoder : Decoder a -> JD.Decoder a
|
||||
decoder (OptimizedDecoder jd jde) =
|
||||
jd
|
||||
|
||||
|
||||
{-| A simple wrapper for `Json.Decode.errorToString`.
|
||||
|
||||
This will directly call the raw `elm/json` decoder that is stored under the hood.
|
||||
|
||||
-}
|
||||
decodeString : Decoder a -> String -> Result Error a
|
||||
decodeString (OptimizedDecoder jd jde) =
|
||||
JD.decodeString jd
|
||||
|
||||
|
||||
{-| A simple wrapper for `Json.Decode.errorToString`.
|
||||
|
||||
This will directly call the raw `elm/json` decoder that is stored under the hood.
|
||||
|
||||
-}
|
||||
decodeValue : Decoder a -> Value -> Result Error a
|
||||
decodeValue (OptimizedDecoder jd jde) =
|
||||
JD.decodeValue jd
|
||||
|
||||
|
||||
{-| A decoder that will ignore the actual JSON and succeed with the provided
|
||||
value. Note that this may still fail when dealing with an invalid JSON string.
|
||||
|
||||
If a value in the JSON ends up being ignored because of this, this will cause a
|
||||
warning.
|
||||
|
||||
import List.Nonempty exposing (Nonempty(..))
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
|
||||
|
||||
""" null """
|
||||
|> decodeString (value |> andThen (\_ -> succeed "hello world"))
|
||||
--> Success "hello world"
|
||||
|
||||
|
||||
""" null """
|
||||
|> decodeString (succeed "hello world")
|
||||
--> WithWarnings
|
||||
--> (Nonempty (Here <| UnusedValue Encode.null) [])
|
||||
--> "hello world"
|
||||
|
||||
|
||||
""" foo """
|
||||
|> decodeString (succeed "hello world")
|
||||
--> BadJson
|
||||
|
||||
-}
|
||||
succeed : a -> Decoder a
|
||||
succeed a =
|
||||
OptimizedDecoder (JD.succeed a) (JDE.succeed a)
|
||||
|
||||
|
||||
{-| Ignore the json and fail with a provided message.
|
||||
|
||||
import List.Nonempty exposing (Nonempty(..))
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
|
||||
""" "hello" """
|
||||
|> decodeString (fail "failure")
|
||||
--> Errors (Nonempty (Here <| Failure "failure" (Just <| Encode.string "hello")) [])
|
||||
|
||||
-}
|
||||
fail : String -> Decoder a
|
||||
fail message =
|
||||
OptimizedDecoder (JD.fail message) (JDE.fail message)
|
||||
|
||||
|
||||
{-| Decode a string.
|
||||
|
||||
import List.Nonempty exposing (Nonempty(..))
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
|
||||
|
||||
""" "hello world" """
|
||||
|> decodeString string
|
||||
--> Success "hello world"
|
||||
|
||||
|
||||
""" 123 """
|
||||
|> decodeString string
|
||||
--> Errors (Nonempty (Here <| Expected TString (Encode.int 123)) [])
|
||||
|
||||
-}
|
||||
string : Decoder String
|
||||
string =
|
||||
OptimizedDecoder JD.string JDE.string
|
||||
|
||||
|
||||
{-| Extract a piece without actually decoding it.
|
||||
|
||||
If a structure is decoded as a `value`, everything _in_ the structure will be
|
||||
considered as having been used and will not appear in `UnusedValue` warnings.
|
||||
|
||||
import Json.Encode as Encode
|
||||
|
||||
|
||||
""" [ 123, "world" ] """
|
||||
|> decodeString value
|
||||
--> Success (Encode.list identity [ Encode.int 123, Encode.string "world" ])
|
||||
|
||||
-}
|
||||
value : Decoder Value
|
||||
value =
|
||||
OptimizedDecoder JD.value JDE.value
|
||||
|
||||
|
||||
{-| Decode a number into a `Float`.
|
||||
|
||||
import List.Nonempty exposing (Nonempty(..))
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
|
||||
|
||||
""" 12.34 """
|
||||
|> decodeString float
|
||||
--> Success 12.34
|
||||
|
||||
|
||||
""" 12 """
|
||||
|> decodeString float
|
||||
--> Success 12
|
||||
|
||||
|
||||
""" null """
|
||||
|> decodeString float
|
||||
--> Errors (Nonempty (Here <| Expected TNumber Encode.null) [])
|
||||
|
||||
-}
|
||||
float : Decoder Float
|
||||
float =
|
||||
OptimizedDecoder JD.float JDE.float
|
||||
|
||||
|
||||
{-| Decode a number into an `Int`.
|
||||
|
||||
import List.Nonempty exposing (Nonempty(..))
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
|
||||
|
||||
""" 123 """
|
||||
|> decodeString int
|
||||
--> Success 123
|
||||
|
||||
|
||||
""" 0.1 """
|
||||
|> decodeString int
|
||||
--> Errors <|
|
||||
--> Nonempty
|
||||
--> (Here <| Expected TInt (Encode.float 0.1))
|
||||
--> []
|
||||
|
||||
-}
|
||||
int : Decoder Int
|
||||
int =
|
||||
OptimizedDecoder JD.int JDE.int
|
||||
|
||||
|
||||
{-| Decode a boolean value.
|
||||
|
||||
""" [ true, false ] """
|
||||
|> decodeString (list bool)
|
||||
--> Success [ True, False ]
|
||||
|
||||
-}
|
||||
bool : Decoder Bool
|
||||
bool =
|
||||
OptimizedDecoder JD.bool JDE.bool
|
||||
|
||||
|
||||
{-| Decode a `null` and succeed with some value.
|
||||
|
||||
""" null """
|
||||
|> decodeString (null "it was null")
|
||||
--> Success "it was null"
|
||||
|
||||
Note that `undefined` and `null` are not the same thing. This cannot be used to
|
||||
verify that a field is _missing_, only that it is explicitly set to `null`.
|
||||
|
||||
""" { "foo": null } """
|
||||
|> decodeString (field "foo" (null ()))
|
||||
--> Success ()
|
||||
|
||||
|
||||
import List.Nonempty exposing (Nonempty(..))
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
|
||||
|
||||
""" { } """
|
||||
|> decodeString (field "foo" (null ()))
|
||||
--> Errors <|
|
||||
--> Nonempty
|
||||
--> (Here <| Expected (TObjectField "foo") (Encode.object []))
|
||||
--> []
|
||||
|
||||
-}
|
||||
null : a -> Decoder a
|
||||
null val =
|
||||
OptimizedDecoder (JD.null val) (JDE.null val)
|
||||
|
||||
|
||||
{-| Decode a list of values, decoding each entry with the provided decoder.
|
||||
|
||||
import List.Nonempty exposing (Nonempty(..))
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
|
||||
|
||||
""" [ "foo", "bar" ] """
|
||||
|> decodeString (list string)
|
||||
--> Success [ "foo", "bar" ]
|
||||
|
||||
|
||||
""" [ "foo", null ] """
|
||||
|> decodeString (list string)
|
||||
--> Errors <|
|
||||
--> Nonempty
|
||||
--> (AtIndex 1 <|
|
||||
--> Nonempty (Here <| Expected TString Encode.null) []
|
||||
--> )
|
||||
--> []
|
||||
|
||||
-}
|
||||
list : Decoder a -> Decoder (List a)
|
||||
list (OptimizedDecoder jd jde) =
|
||||
OptimizedDecoder (JD.list jd) (JDE.list jde)
|
||||
|
||||
|
||||
{-| _Convenience function._ Decode a JSON array into an Elm `Array`.
|
||||
|
||||
import Array
|
||||
|
||||
""" [ 1, 2, 3 ] """
|
||||
|> decodeString (array int)
|
||||
--> Success <| Array.fromList [ 1, 2, 3 ]
|
||||
|
||||
-}
|
||||
array : Decoder a -> Decoder (Array a)
|
||||
array (OptimizedDecoder jd jde) =
|
||||
OptimizedDecoder (JD.array jd) (JDE.array jde)
|
||||
|
||||
|
||||
{-| _Convenience function._ Decode a JSON object into an Elm `Dict String`.
|
||||
|
||||
import Dict
|
||||
|
||||
|
||||
""" { "foo": "bar", "bar": "hi there" } """
|
||||
|> decodeString (dict string)
|
||||
--> Success <| Dict.fromList
|
||||
--> [ ( "bar", "hi there" )
|
||||
--> , ( "foo", "bar" )
|
||||
--> ]
|
||||
|
||||
-}
|
||||
dict : Decoder v -> Decoder (Dict String v)
|
||||
dict (OptimizedDecoder jd jde) =
|
||||
OptimizedDecoder (JD.dict jd) (JDE.dict jde)
|
||||
|
||||
|
||||
{-| Decode a specific index using a specified `Decoder`.
|
||||
|
||||
import List.Nonempty exposing (Nonempty(..))
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
|
||||
|
||||
""" [ "hello", 123 ] """
|
||||
|> decodeString (map2 Tuple.pair (index 0 string) (index 1 int))
|
||||
--> Success ( "hello", 123 )
|
||||
|
||||
|
||||
""" [ "hello", "there" ] """
|
||||
|> decodeString (index 1 string)
|
||||
--> WithWarnings (Nonempty (AtIndex 0 (Nonempty (Here (UnusedValue (Encode.string "hello"))) [])) [])
|
||||
--> "there"
|
||||
|
||||
-}
|
||||
index : Int -> Decoder a -> Decoder a
|
||||
index idx (OptimizedDecoder jd jde) =
|
||||
OptimizedDecoder (JD.index idx jd) (JDE.index idx jde)
|
||||
|
||||
|
||||
{-| Decode a JSON object into a list of key-value pairs. The decoder you provide
|
||||
will be used to decode the values.
|
||||
|
||||
""" { "foo": "bar", "hello": "world" } """
|
||||
|> decodeString (keyValuePairs string)
|
||||
--> Success [ ( "foo", "bar" ), ( "hello", "world" ) ]
|
||||
|
||||
-}
|
||||
keyValuePairs : Decoder a -> Decoder (List ( String, a ))
|
||||
keyValuePairs (OptimizedDecoder jd jde) =
|
||||
OptimizedDecoder (JD.keyValuePairs jd) (JDE.keyValuePairs jde)
|
||||
|
||||
|
||||
{-| Decode the content of a field using a provided decoder.
|
||||
|
||||
import List.Nonempty as Nonempty
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
|
||||
""" { "foo": "bar" } """
|
||||
|> decodeString (field "foo" string)
|
||||
--> Success "bar"
|
||||
|
||||
|
||||
""" [ { "foo": "bar" }, { "foo": "baz", "hello": "world" } ] """
|
||||
|> decodeString (list (field "foo" string))
|
||||
--> WithWarnings expectedWarnings [ "bar", "baz" ]
|
||||
|
||||
|
||||
expectedWarnings : Warnings
|
||||
expectedWarnings =
|
||||
UnusedField "hello"
|
||||
|> Here
|
||||
|> Nonempty.fromElement
|
||||
|> AtIndex 1
|
||||
|> Nonempty.fromElement
|
||||
|
||||
-}
|
||||
field : String -> Decoder a -> Decoder a
|
||||
field fieldName (OptimizedDecoder jd jde) =
|
||||
OptimizedDecoder (JD.field fieldName jd) (JDE.field fieldName jde)
|
||||
|
||||
|
||||
{-| Decodes a value at a certain path, using a provided decoder. Essentially,
|
||||
writing `at [ "a", "b", "c" ] string` is sugar over writing
|
||||
`field "a" (field "b" (field "c" string))`}.
|
||||
|
||||
""" { "a": { "b": { "c": "hi there" } } } """
|
||||
|> decodeString (at [ "a", "b", "c" ] string)
|
||||
--> Success "hi there"
|
||||
|
||||
-}
|
||||
at : List String -> Decoder a -> Decoder a
|
||||
at fields (OptimizedDecoder jd jde) =
|
||||
OptimizedDecoder (JD.at fields jd) (JDE.at fields jde)
|
||||
|
||||
|
||||
|
||||
-- Choosing
|
||||
|
||||
|
||||
{-| Tries a bunch of decoders. The first one to not fail will be the one used.
|
||||
|
||||
If all fail, the errors are collected into a `BadOneOf`.
|
||||
|
||||
import List.Nonempty as Nonempty
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
|
||||
""" [ 12, "whatever" ] """
|
||||
|> decodeString (list <| oneOf [ map String.fromInt int, string ])
|
||||
--> Success [ "12", "whatever" ]
|
||||
|
||||
|
||||
""" null """
|
||||
|> decodeString (oneOf [ string, map String.fromInt int ])
|
||||
--> Errors <| Nonempty.fromElement <| Here <| BadOneOf
|
||||
--> [ Nonempty.fromElement <| Here <| Expected TString Encode.null
|
||||
--> , Nonempty.fromElement <| Here <| Expected TInt Encode.null
|
||||
--> ]
|
||||
|
||||
-}
|
||||
oneOf : List (Decoder a) -> Decoder a
|
||||
oneOf decoders =
|
||||
let
|
||||
jds =
|
||||
List.map
|
||||
(\(OptimizedDecoder jd jde) ->
|
||||
jd
|
||||
)
|
||||
decoders
|
||||
|
||||
jdes =
|
||||
List.map
|
||||
(\(OptimizedDecoder jd jde) ->
|
||||
jde
|
||||
)
|
||||
decoders
|
||||
in
|
||||
OptimizedDecoder (JD.oneOf jds) (JDE.oneOf jdes)
|
||||
|
||||
|
||||
{-| Decodes successfully and wraps with a `Just`, handling failure by succeeding
|
||||
with `Nothing`.
|
||||
|
||||
import List.Nonempty as Nonempty
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
|
||||
|
||||
""" [ "foo", 12 ] """
|
||||
|> decodeString (list <| maybe string)
|
||||
--> WithWarnings expectedWarnings [ Just "foo", Nothing ]
|
||||
|
||||
|
||||
expectedWarnings : Warnings
|
||||
expectedWarnings =
|
||||
UnusedValue (Encode.int 12)
|
||||
|> Here
|
||||
|> Nonempty.fromElement
|
||||
|> AtIndex 1
|
||||
|> Nonempty.fromElement
|
||||
|
||||
-}
|
||||
maybe : Decoder a -> Decoder (Maybe a)
|
||||
maybe (OptimizedDecoder jd jde) =
|
||||
OptimizedDecoder (JD.maybe jd) (JDE.maybe jde)
|
||||
|
||||
|
||||
{-| Decodes successfully and wraps with a `Just`. If the values is `null`
|
||||
succeeds with `Nothing`.
|
||||
|
||||
""" [ { "foo": "bar" }, { "foo": null } ] """
|
||||
|> decodeString (list <| field "foo" <| nullable string)
|
||||
--> Success [ Just "bar", Nothing ]
|
||||
|
||||
-}
|
||||
nullable : Decoder a -> Decoder (Maybe a)
|
||||
nullable (OptimizedDecoder jd jde) =
|
||||
OptimizedDecoder (JD.nullable jd) (JDE.nullable jde)
|
||||
|
||||
|
||||
|
||||
--
|
||||
|
||||
|
||||
{-| Required when using (mutually) recursive decoders.
|
||||
-}
|
||||
lazy : (() -> Decoder a) -> Decoder a
|
||||
lazy toDecoder =
|
||||
let
|
||||
jd : JD.Decoder a
|
||||
jd =
|
||||
(\() ->
|
||||
case toDecoder () of
|
||||
OptimizedDecoder jd_ jde_ ->
|
||||
jd_
|
||||
)
|
||||
|> JD.lazy
|
||||
|
||||
jde : JDE.Decoder a
|
||||
jde =
|
||||
(\() ->
|
||||
case toDecoder () of
|
||||
OptimizedDecoder jd_ jde_ ->
|
||||
jde_
|
||||
)
|
||||
|> JDE.lazy
|
||||
in
|
||||
OptimizedDecoder
|
||||
jd
|
||||
jde
|
||||
|
||||
|
||||
{-| Useful for checking a value in the JSON matches the value you expect it to
|
||||
have. If it does, succeeds with the second decoder. If it doesn't it fails.
|
||||
|
||||
This can be used to decode union types:
|
||||
|
||||
type Pet = Cat | Dog | Rabbit
|
||||
|
||||
petDecoder : Decoder Pet
|
||||
petDecoder =
|
||||
oneOf
|
||||
[ check string "cat" <| succeed Cat
|
||||
, check string "dog" <| succeed Dog
|
||||
, check string "rabbit" <| succeed Rabbit
|
||||
]
|
||||
|
||||
""" [ "dog", "rabbit", "cat" ] """
|
||||
|> decodeString (list petDecoder)
|
||||
--> Success [ Dog, Rabbit, Cat ]
|
||||
|
||||
-}
|
||||
check : Decoder a -> a -> Decoder b -> Decoder b
|
||||
check checkDecoder expectedVal actualDecoder =
|
||||
checkDecoder
|
||||
|> andThen
|
||||
(\actual ->
|
||||
if actual == expectedVal then
|
||||
actualDecoder
|
||||
|
||||
else
|
||||
fail "Verification failed"
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- Mapping and chaining
|
||||
|
||||
|
||||
{-| Useful for transforming decoders.
|
||||
|
||||
""" "foo" """
|
||||
|> decodeString (map String.toUpper string)
|
||||
--> Success "FOO"
|
||||
|
||||
-}
|
||||
map : (a -> b) -> Decoder a -> Decoder b
|
||||
map f (OptimizedDecoder jd jde) =
|
||||
OptimizedDecoder (JD.map f jd) (JDE.map f jde)
|
||||
|
||||
|
||||
{-| Chain decoders where one decoder depends on the value of another decoder.
|
||||
-}
|
||||
andThen : (a -> Decoder b) -> Decoder a -> Decoder b
|
||||
andThen toDecoderB (OptimizedDecoder jd jde) =
|
||||
OptimizedDecoder
|
||||
(JD.andThen (toDecoderB >> Internal.OptimizedDecoder.jd) jd)
|
||||
(JDE.andThen (toDecoderB >> Internal.OptimizedDecoder.jde) jde)
|
||||
|
||||
|
||||
{-| Combine 2 decoders.
|
||||
-}
|
||||
map2 : (a -> b -> c) -> Decoder a -> Decoder b -> Decoder c
|
||||
map2 f (OptimizedDecoder jdA jdeA) (OptimizedDecoder jdB jdeB) =
|
||||
OptimizedDecoder
|
||||
(JD.map2 f jdA jdB)
|
||||
(JDE.map2 f jdeA jdeB)
|
||||
|
||||
|
||||
{-| Decode an argument and provide it to a function in a decoder.
|
||||
|
||||
decoder : Decoder String
|
||||
decoder =
|
||||
succeed (String.repeat)
|
||||
|> andMap (field "count" int)
|
||||
|> andMap (field "val" string)
|
||||
|
||||
|
||||
""" { "val": "hi", "count": 3 } """
|
||||
|> decodeString decoder
|
||||
--> Success "hihihi"
|
||||
|
||||
-}
|
||||
andMap : Decoder a -> Decoder (a -> b) -> Decoder b
|
||||
andMap =
|
||||
map2 (|>)
|
||||
|
||||
|
||||
{-| Combine 3 decoders.
|
||||
-}
|
||||
map3 :
|
||||
(a -> b -> c -> d)
|
||||
-> Decoder a
|
||||
-> Decoder b
|
||||
-> Decoder c
|
||||
-> Decoder d
|
||||
map3 f decoderA decoderB decoderC =
|
||||
map f decoderA
|
||||
|> andMap decoderB
|
||||
|> andMap decoderC
|
||||
|
||||
|
||||
{-| Combine 4 decoders.
|
||||
-}
|
||||
map4 :
|
||||
(a -> b -> c -> d -> e)
|
||||
-> Decoder a
|
||||
-> Decoder b
|
||||
-> Decoder c
|
||||
-> Decoder d
|
||||
-> Decoder e
|
||||
map4 f decoderA decoderB decoderC decoderD =
|
||||
map f decoderA
|
||||
|> andMap decoderB
|
||||
|> andMap decoderC
|
||||
|> andMap decoderD
|
||||
|
||||
|
||||
{-| Combine 5 decoders.
|
||||
-}
|
||||
map5 :
|
||||
(a -> b -> c -> d -> e -> f)
|
||||
-> Decoder a
|
||||
-> Decoder b
|
||||
-> Decoder c
|
||||
-> Decoder d
|
||||
-> Decoder e
|
||||
-> Decoder f
|
||||
map5 f decoderA decoderB decoderC decoderD decoderE =
|
||||
map f decoderA
|
||||
|> andMap decoderB
|
||||
|> andMap decoderC
|
||||
|> andMap decoderD
|
||||
|> andMap decoderE
|
||||
|
||||
|
||||
{-| Combine 6 decoders.
|
||||
-}
|
||||
map6 :
|
||||
(a -> b -> c -> d -> e -> f -> g)
|
||||
-> Decoder a
|
||||
-> Decoder b
|
||||
-> Decoder c
|
||||
-> Decoder d
|
||||
-> Decoder e
|
||||
-> Decoder f
|
||||
-> Decoder g
|
||||
map6 f decoderA decoderB decoderC decoderD decoderE decoderF =
|
||||
map f decoderA
|
||||
|> andMap decoderB
|
||||
|> andMap decoderC
|
||||
|> andMap decoderD
|
||||
|> andMap decoderE
|
||||
|> andMap decoderF
|
||||
|
||||
|
||||
{-| Combine 7 decoders.
|
||||
-}
|
||||
map7 :
|
||||
(a -> b -> c -> d -> e -> f -> g -> h)
|
||||
-> Decoder a
|
||||
-> Decoder b
|
||||
-> Decoder c
|
||||
-> Decoder d
|
||||
-> Decoder e
|
||||
-> Decoder f
|
||||
-> Decoder g
|
||||
-> Decoder h
|
||||
map7 f decoderA decoderB decoderC decoderD decoderE decoderF decoderG =
|
||||
map f decoderA
|
||||
|> andMap decoderB
|
||||
|> andMap decoderC
|
||||
|> andMap decoderD
|
||||
|> andMap decoderE
|
||||
|> andMap decoderF
|
||||
|> andMap decoderG
|
||||
|
||||
|
||||
{-| Combine 8 decoders.
|
||||
-}
|
||||
map8 :
|
||||
(a -> b -> c -> d -> e -> f -> g -> h -> i)
|
||||
-> Decoder a
|
||||
-> Decoder b
|
||||
-> Decoder c
|
||||
-> Decoder d
|
||||
-> Decoder e
|
||||
-> Decoder f
|
||||
-> Decoder g
|
||||
-> Decoder h
|
||||
-> Decoder i
|
||||
map8 f decoderA decoderB decoderC decoderD decoderE decoderF decoderG decoderH =
|
||||
map f decoderA
|
||||
|> andMap decoderB
|
||||
|> andMap decoderC
|
||||
|> andMap decoderD
|
||||
|> andMap decoderE
|
||||
|> andMap decoderF
|
||||
|> andMap decoderG
|
||||
|> andMap decoderH
|
333
src/OptimizedDecoder/Pipeline.elm
Normal file
333
src/OptimizedDecoder/Pipeline.elm
Normal file
@ -0,0 +1,333 @@
|
||||
module OptimizedDecoder.Pipeline exposing
|
||||
( required, requiredAt, optional, optionalAt, hardcoded, custom
|
||||
, decode, resolve
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Json.Decode.Pipeline
|
||||
|
||||
Use the `(|>)` operator to build JSON decoders.
|
||||
|
||||
|
||||
## Decoding fields
|
||||
|
||||
@docs required, requiredAt, optional, optionalAt, hardcoded, custom
|
||||
|
||||
|
||||
## Beginning and ending pipelines
|
||||
|
||||
@docs decode, resolve
|
||||
|
||||
|
||||
### Verified docs
|
||||
|
||||
The examples all expect imports set up like this:
|
||||
|
||||
import Json.Decode.Exploration exposing (..)
|
||||
import Json.Decode.Exploration.Pipeline exposing (..)
|
||||
import Json.Decode.Exploration.Located exposing (Located(..))
|
||||
import Json.Encode as Encode
|
||||
import List.Nonempty as Nonempty
|
||||
|
||||
For automated verification of these examples, this import is also required.
|
||||
Please ignore it.
|
||||
|
||||
import DocVerificationHelpers exposing (User)
|
||||
|
||||
-}
|
||||
|
||||
import OptimizedDecoder as Decode exposing (Decoder)
|
||||
|
||||
|
||||
{-| Decode a required field.
|
||||
|
||||
import Json.Decode.Exploration exposing (..)
|
||||
|
||||
type alias User =
|
||||
{ id : Int
|
||||
, name : String
|
||||
, email : String
|
||||
}
|
||||
|
||||
userDecoder : Decoder User
|
||||
userDecoder =
|
||||
decode User
|
||||
|> required "id" int
|
||||
|> required "name" string
|
||||
|> required "email" string
|
||||
|
||||
""" {"id": 123, "email": "sam@example.com", "name": "Sam"} """
|
||||
|> decodeString userDecoder
|
||||
--> Success { id = 123, name = "Sam", email = "sam@example.com" }
|
||||
|
||||
-}
|
||||
required : String -> Decoder a -> Decoder (a -> b) -> Decoder b
|
||||
required key valDecoder decoder =
|
||||
decoder |> Decode.andMap (Decode.field key valDecoder)
|
||||
|
||||
|
||||
{-| Decode a required nested field.
|
||||
|
||||
import Json.Decode.Exploration exposing (..)
|
||||
|
||||
type alias User =
|
||||
{ id : Int
|
||||
, name : String
|
||||
, email : String
|
||||
}
|
||||
|
||||
userDecoder : Decoder User
|
||||
userDecoder =
|
||||
decode User
|
||||
|> required "id" int
|
||||
|> requiredAt [ "profile", "name" ] string
|
||||
|> required "email" string
|
||||
|
||||
"""
|
||||
{
|
||||
"id": 123,
|
||||
"email": "sam@example.com",
|
||||
"profile": { "name": "Sam" }
|
||||
}
|
||||
"""
|
||||
|> decodeString userDecoder
|
||||
--> Success { id = 123, name = "Sam", email = "sam@example.com" }
|
||||
|
||||
-}
|
||||
requiredAt : List String -> Decoder a -> Decoder (a -> b) -> Decoder b
|
||||
requiredAt path valDecoder decoder =
|
||||
decoder |> Decode.andMap (Decode.at path valDecoder)
|
||||
|
||||
|
||||
{-| Decode a field that may be missing or have a null value. If the field is
|
||||
missing, then it decodes as the `fallback` value. If the field is present,
|
||||
then `valDecoder` is used to decode its value. If `valDecoder` fails on a
|
||||
`null` value, then the `fallback` is used as if the field were missing
|
||||
entirely.
|
||||
|
||||
import Json.Decode.Exploration exposing (..)
|
||||
|
||||
type alias User =
|
||||
{ id : Int
|
||||
, name : String
|
||||
, email : String
|
||||
}
|
||||
|
||||
userDecoder : Decoder User
|
||||
userDecoder =
|
||||
decode User
|
||||
|> required "id" int
|
||||
|> optional "name" string "blah"
|
||||
|> required "email" string
|
||||
|
||||
""" { "id": 123, "email": "sam@example.com" } """
|
||||
|> decodeString userDecoder
|
||||
--> Success { id = 123, name = "blah", email = "sam@example.com" }
|
||||
|
||||
Because `valDecoder` is given an opportunity to decode `null` values before
|
||||
resorting to the `fallback`, you can distinguish between missing and `null`
|
||||
values if you need to:
|
||||
|
||||
userDecoder2 =
|
||||
decode User
|
||||
|> required "id" int
|
||||
|> optional "name" (oneOf [ string, null "NULL" ]) "MISSING"
|
||||
|> required "email" string
|
||||
|
||||
Note also that this behaves _slightly_ different than the stock pipeline
|
||||
package.
|
||||
|
||||
In the stock pipeline package, running the following decoder with an array as
|
||||
the input would _succeed_.
|
||||
|
||||
fooDecoder =
|
||||
decode identity
|
||||
|> optional "foo" (maybe string) Nothing
|
||||
|
||||
In this package, such a decoder will error out instead, saying that it expected
|
||||
the input to be an object. The _key_ `"foo"` is optional, but it really does
|
||||
have to be an object before we even consider trying your decoder or returning
|
||||
the fallback.
|
||||
|
||||
-}
|
||||
optional : String -> Decoder a -> a -> Decoder (a -> b) -> Decoder b
|
||||
optional key valDecoder fallback decoder =
|
||||
-- source: https://github.com/NoRedInk/elm-json-decode-pipeline/blob/d9c10a2b388176569fe3e88ef0e2b6fc19d9beeb/src/Json/Decode/Pipeline.elm#L113
|
||||
custom (optionalDecoder (Decode.field key Decode.value) valDecoder fallback) decoder
|
||||
|
||||
|
||||
{-| Decode an optional nested field.
|
||||
-}
|
||||
optionalAt : List String -> Decoder a -> a -> Decoder (a -> b) -> Decoder b
|
||||
optionalAt path valDecoder fallback decoder =
|
||||
custom (optionalDecoder (Decode.at path Decode.value) valDecoder fallback) decoder
|
||||
|
||||
|
||||
|
||||
-- source: https://github.com/NoRedInk/elm-json-decode-pipeline/blob/d9c10a2b388176569fe3e88ef0e2b6fc19d9beeb/src/Json/Decode/Pipeline.elm#L116-L148
|
||||
|
||||
|
||||
optionalDecoder : Decode.Decoder Decode.Value -> Decoder a -> a -> Decoder a
|
||||
optionalDecoder pathDecoder valDecoder fallback =
|
||||
let
|
||||
nullOr decoder =
|
||||
Decode.oneOf [ decoder, Decode.null fallback ]
|
||||
|
||||
handleResult input =
|
||||
case Decode.decodeValue pathDecoder input of
|
||||
Ok rawValue ->
|
||||
-- The field was present, so now let's try to decode that value.
|
||||
-- (If it was present but fails to decode, this should and will fail!)
|
||||
case Decode.decodeValue (nullOr valDecoder) rawValue of
|
||||
Ok finalResult ->
|
||||
Decode.succeed finalResult
|
||||
|
||||
Err finalErr ->
|
||||
-- TODO is there some way to preserve the structure
|
||||
-- of the original error instead of using toString here?
|
||||
Decode.fail (Decode.errorToString finalErr)
|
||||
|
||||
Err _ ->
|
||||
-- The field was not present, so use the fallback.
|
||||
Decode.succeed fallback
|
||||
in
|
||||
Decode.value
|
||||
|> Decode.andThen handleResult
|
||||
|
||||
|
||||
{-| Rather than decoding anything, use a fixed value for the next step in the
|
||||
pipeline. `harcoded` does not look at the JSON at all.
|
||||
|
||||
import Json.Decode.Exploration exposing (..)
|
||||
|
||||
|
||||
type alias User =
|
||||
{ id : Int
|
||||
, name : String
|
||||
, email : String
|
||||
}
|
||||
|
||||
userDecoder : Decoder User
|
||||
userDecoder =
|
||||
decode User
|
||||
|> required "id" int
|
||||
|> hardcoded "Alex"
|
||||
|> required "email" string
|
||||
|
||||
""" { "id": 123, "email": "sam@example.com" } """
|
||||
|> decodeString userDecoder
|
||||
--> Success { id = 123, name = "Alex", email = "sam@example.com" }
|
||||
|
||||
-}
|
||||
hardcoded : a -> Decoder (a -> b) -> Decoder b
|
||||
hardcoded =
|
||||
Decode.andMap << Decode.succeed
|
||||
|
||||
|
||||
{-| Run the given decoder and feed its result into the pipeline at this point.
|
||||
|
||||
Consider this example.
|
||||
|
||||
import Json.Decode.Exploration exposing (..)
|
||||
|
||||
|
||||
type alias User =
|
||||
{ id : Int
|
||||
, name : String
|
||||
, email : String
|
||||
}
|
||||
|
||||
userDecoder : Decoder User
|
||||
userDecoder =
|
||||
decode User
|
||||
|> required "id" int
|
||||
|> custom (at [ "profile", "name" ] string)
|
||||
|> required "email" string
|
||||
|
||||
"""
|
||||
{
|
||||
"id": 123,
|
||||
"email": "sam@example.com",
|
||||
"profile": {"name": "Sam"}
|
||||
}
|
||||
"""
|
||||
|> decodeString userDecoder
|
||||
--> Success { id = 123, name = "Sam", email = "sam@example.com" }
|
||||
|
||||
-}
|
||||
custom : Decoder a -> Decoder (a -> b) -> Decoder b
|
||||
custom =
|
||||
Decode.andMap
|
||||
|
||||
|
||||
{-| Convert a `Decoder (Result x a)` into a `Decoder a`. Useful when you want
|
||||
to perform some custom processing just before completing the decoding operation.
|
||||
|
||||
import Json.Decode.Exploration exposing (..)
|
||||
|
||||
type alias User =
|
||||
{ id : Int
|
||||
, name : String
|
||||
, email : String
|
||||
}
|
||||
|
||||
userDecoder : Decoder User
|
||||
userDecoder =
|
||||
let
|
||||
-- toDecoder gets run *after* all the
|
||||
-- (|> required ...) steps are done.
|
||||
toDecoder : Int -> String -> String -> Int -> Decoder User
|
||||
toDecoder id name email version =
|
||||
if version >= 2 then
|
||||
succeed (User id name email)
|
||||
else
|
||||
fail "This JSON is from a deprecated source. Please upgrade!"
|
||||
in
|
||||
decode toDecoder
|
||||
|> required "id" int
|
||||
|> required "name" string
|
||||
|> required "email" string
|
||||
|> required "version" int
|
||||
-- version is part of toDecoder,
|
||||
-- but it is not a part of User
|
||||
|> resolve
|
||||
|
||||
"""
|
||||
{
|
||||
"id": 123,
|
||||
"name": "Sam",
|
||||
"email": "sam@example.com",
|
||||
"version": 3
|
||||
}
|
||||
"""
|
||||
|> decodeString userDecoder
|
||||
--> Success { id = 123, name = "Sam", email = "sam@example.com" }
|
||||
|
||||
-}
|
||||
resolve : Decoder (Decoder a) -> Decoder a
|
||||
resolve =
|
||||
Decode.andThen identity
|
||||
|
||||
|
||||
{-| Begin a decoding pipeline. This is a synonym for [Json.Decode.succeed](http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#succeed),
|
||||
intended to make things read more clearly.
|
||||
|
||||
type alias User =
|
||||
{ id : Int
|
||||
, email : String
|
||||
, name : String
|
||||
}
|
||||
|
||||
userDecoder : Decoder User
|
||||
userDecoder =
|
||||
decode User
|
||||
|> required "id" int
|
||||
|> required "email" string
|
||||
|> optional "name" string ""
|
||||
|
||||
-}
|
||||
decode : a -> Decoder a
|
||||
decode =
|
||||
Decode.succeed
|
@ -21,16 +21,12 @@ import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
import 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 =
|
||||
@ -283,16 +279,6 @@ type alias Page metadata view pathKey =
|
||||
}
|
||||
|
||||
|
||||
renderErrors : ( List String, List Mark.Error.Error ) -> Html msg
|
||||
renderErrors ( path, errors ) =
|
||||
Html.div []
|
||||
[ Html.text (String.join "/" path)
|
||||
, errors
|
||||
|> List.map (Mark.Error.toHtml Mark.Error.Light)
|
||||
|> Html.div []
|
||||
]
|
||||
|
||||
|
||||
combineTupleResults :
|
||||
List ( List String, Result error success )
|
||||
-> Result (List error) (List ( List String, success ))
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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,14 +6,15 @@ 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
|
||||
@ -122,7 +123,7 @@ pageViewOrError pathKey viewFn model cache =
|
||||
-- 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
|
||||
@ -273,12 +305,6 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
||||
|> 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)
|
||||
@ -295,15 +321,26 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
||||
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 ) =
|
||||
maybePagePath
|
||||
@ -347,6 +384,7 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
||||
, userModel = userModel
|
||||
, contentCache = contentCache
|
||||
, phase = phase
|
||||
, hmrStatus = HmrLoaded
|
||||
}
|
||||
, cmd
|
||||
)
|
||||
@ -361,7 +399,8 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
||||
, baseUrl = baseUrl
|
||||
, userModel = userModel
|
||||
, contentCache = contentCache
|
||||
, phase = Client
|
||||
, phase = DevClient False
|
||||
, hmrStatus = HmrLoaded
|
||||
}
|
||||
, Cmd.batch
|
||||
[ userCmd |> Cmd.map UserMsg
|
||||
@ -389,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
|
||||
@ -404,16 +446,19 @@ type alias ModelDetails userModel metadata view =
|
||||
, 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 )
|
||||
@ -442,7 +487,7 @@ update :
|
||||
-> Msg userMsg metadata view
|
||||
-> ModelDetails userModel metadata view
|
||||
-> ( ModelDetails userModel metadata view, Cmd (AppMsg userMsg metadata view) )
|
||||
update allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJsPort document userUpdate msg model =
|
||||
update content allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJsPort document userUpdate msg model =
|
||||
case msg of
|
||||
AppMsg appMsg ->
|
||||
case appMsg of
|
||||
@ -535,7 +580,7 @@ update allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJs
|
||||
)
|
||||
{ path = pagePath, frontmatter = frontmatter }
|
||||
|> (\request ->
|
||||
StaticHttpRequest.resolve request staticDataThing
|
||||
StaticHttpRequest.resolve ApplicationType.Browser request staticDataThing
|
||||
)
|
||||
in
|
||||
( { model | contentCache = updatedCache }
|
||||
@ -584,18 +629,42 @@ update allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJs
|
||||
-- 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 :
|
||||
@ -622,6 +691,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
|
||||
@ -678,7 +748,7 @@ application config =
|
||||
Prerender ->
|
||||
noOpUpdate
|
||||
|
||||
Client ->
|
||||
_ ->
|
||||
config.update
|
||||
|
||||
noOpUpdate =
|
||||
@ -690,7 +760,7 @@ application config =
|
||||
|> List.map Tuple.first
|
||||
|> List.map (String.join "/")
|
||||
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)
|
||||
|
||||
@ -700,9 +770,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
|
||||
@ -735,6 +823,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
|
||||
|
@ -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
|
||||
@ -320,13 +326,20 @@ 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.pagesWithErrors contentCache of
|
||||
@ -343,14 +356,14 @@ init toModel contentCache siteMetadata config flags =
|
||||
staticResponses =
|
||||
case requests of
|
||||
Ok okRequests ->
|
||||
staticResponsesInit siteMetadata config okRequests
|
||||
staticResponsesInit staticHttpCache siteMetadata config okRequests
|
||||
|
||||
Err errors ->
|
||||
-- TODO need to handle errors better?
|
||||
staticResponsesInit siteMetadata config []
|
||||
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
|
||||
@ -369,11 +382,11 @@ init toModel contentCache siteMetadata config flags =
|
||||
staticResponses =
|
||||
case requests of
|
||||
Ok okRequests ->
|
||||
staticResponsesInit siteMetadata config okRequests
|
||||
staticResponsesInit staticHttpCache siteMetadata config okRequests
|
||||
|
||||
Err errors ->
|
||||
-- TODO need to handle errors better?
|
||||
staticResponsesInit siteMetadata config []
|
||||
staticResponsesInit staticHttpCache siteMetadata config []
|
||||
in
|
||||
updateAndSendPortIfDone
|
||||
config
|
||||
@ -382,7 +395,7 @@ init toModel contentCache siteMetadata config flags =
|
||||
staticResponses
|
||||
secrets
|
||||
pageErrors
|
||||
Dict.empty
|
||||
staticHttpCache
|
||||
mode
|
||||
)
|
||||
toModel
|
||||
@ -394,7 +407,7 @@ init toModel contentCache siteMetadata config flags =
|
||||
(Model Dict.empty
|
||||
secrets
|
||||
(metadataParserErrors |> List.map Tuple.second)
|
||||
Dict.empty
|
||||
staticHttpCache
|
||||
mode
|
||||
)
|
||||
toModel
|
||||
@ -528,7 +541,7 @@ performStaticHttpRequests allRawResponses secrets staticRequests =
|
||||
(\( pagePath, request ) ->
|
||||
allRawResponses
|
||||
|> dictCompact
|
||||
|> StaticHttpRequest.resolveUrls request
|
||||
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|
||||
|> Tuple.second
|
||||
)
|
||||
|> List.concat
|
||||
@ -580,18 +593,23 @@ cliDictKey =
|
||||
"////elm-pages-CLI////"
|
||||
|
||||
|
||||
staticResponsesInit : Result (List BuildError) (List ( PagePath pathKey, metadata )) -> Config pathKey userMsg userModel metadata view -> List ( PagePath pathKey, StaticHttp.Request value ) -> StaticResponses
|
||||
staticResponsesInit siteMetadata config list =
|
||||
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
|
||||
foo : StaticHttp.Request (List (Result String { path : List String, content : String }))
|
||||
foo =
|
||||
config.generateFiles thing2
|
||||
generateFilesRequest : StaticHttp.Request (List (Result String { path : List String, content : String }))
|
||||
generateFilesRequest =
|
||||
config.generateFiles siteMetadataWithContent
|
||||
|
||||
generateFilesStaticRequest =
|
||||
( cliDictKey, NotFetched (foo |> StaticHttp.map (\_ -> ())) Dict.empty )
|
||||
( -- we don't want to include the CLI-only StaticHttp responses in the production bundle
|
||||
-- since that data is only needed to run these functions during the build step
|
||||
-- in the future, this could be refactored to have a type to represent this more clearly
|
||||
cliDictKey
|
||||
, NotFetched (generateFilesRequest |> StaticHttp.map (\_ -> ())) Dict.empty
|
||||
)
|
||||
|
||||
thing2 =
|
||||
siteMetadata
|
||||
siteMetadataWithContent =
|
||||
siteMetadataResult
|
||||
|> Result.withDefault []
|
||||
|> List.map
|
||||
(\( pagePath, metadata ) ->
|
||||
@ -625,8 +643,26 @@ staticResponsesInit siteMetadata config list =
|
||||
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 ]
|
||||
@ -654,7 +690,7 @@ staticResponsesUpdate newEntry model =
|
||||
realUrls =
|
||||
updatedAllResponses
|
||||
|> dictCompact
|
||||
|> StaticHttpRequest.resolveUrls request
|
||||
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|
||||
|> Tuple.second
|
||||
|> List.map Secrets.maskedLookup
|
||||
|> List.map HashRequest.hash
|
||||
@ -681,6 +717,36 @@ staticResponsesUpdate newEntry model =
|
||||
}
|
||||
|
||||
|
||||
addEntry : Dict String (Maybe String) -> String -> Result () String -> StaticHttpResult -> StaticHttpResult
|
||||
addEntry globalRawResponses hashedRequest rawResponse ((NotFetched request rawResponses) as entry) =
|
||||
let
|
||||
realUrls =
|
||||
globalRawResponses
|
||||
|> dictCompact
|
||||
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|
||||
|> Tuple.second
|
||||
|> List.map Secrets.maskedLookup
|
||||
|> List.map HashRequest.hash
|
||||
|
||||
includesUrl =
|
||||
List.member
|
||||
hashedRequest
|
||||
realUrls
|
||||
in
|
||||
if includesUrl then
|
||||
let
|
||||
updatedRawResponses =
|
||||
Dict.insert
|
||||
hashedRequest
|
||||
rawResponse
|
||||
rawResponses
|
||||
in
|
||||
NotFetched request updatedRawResponses
|
||||
|
||||
else
|
||||
entry
|
||||
|
||||
|
||||
isJust : Maybe a -> Bool
|
||||
isJust maybeValue =
|
||||
case maybeValue of
|
||||
@ -721,7 +787,7 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
||||
|
||||
hasPermanentError =
|
||||
usableRawResponses
|
||||
|> StaticHttpRequest.permanentError request
|
||||
|> StaticHttpRequest.permanentError ApplicationType.Cli request
|
||||
|> isJust
|
||||
|
||||
hasPermanentHttpError =
|
||||
@ -737,7 +803,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 =
|
||||
@ -773,7 +841,9 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
||||
)
|
||||
|
||||
maybePermanentError =
|
||||
StaticHttpRequest.permanentError request
|
||||
StaticHttpRequest.permanentError
|
||||
ApplicationType.Cli
|
||||
request
|
||||
usableRawResponses
|
||||
|
||||
decoderErrors =
|
||||
@ -893,7 +963,8 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
|
||||
|
||||
mythingy2 : Result StaticHttpRequest.Error (List (Result String { path : List String, content : String }))
|
||||
mythingy2 =
|
||||
StaticHttpRequest.resolve (config.generateFiles metadataForGenerateFiles)
|
||||
StaticHttpRequest.resolve ApplicationType.Cli
|
||||
(config.generateFiles metadataForGenerateFiles)
|
||||
(allRawResponses |> Dict.Extra.filterMap (\key value -> value))
|
||||
|
||||
generatedOkayFiles : List { path : List String, content : String }
|
||||
@ -938,11 +1009,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
|
||||
@ -950,6 +1029,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)
|
||||
)
|
||||
|
||||
@ -980,7 +1068,7 @@ encodeStaticResponses mode staticResponses =
|
||||
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 ->
|
||||
|
@ -322,6 +322,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
|
||||
@ -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 ))
|
||||
)
|
||||
|
||||
@ -238,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
|
||||
@ -274,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
|
||||
@ -296,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
|
||||
@ -336,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
|
||||
)
|
||||
@ -393,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 ->
|
||||
@ -436,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)
|
||||
@ -575,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 ->
|
||||
@ -664,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
|
||||
@ -685,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
|
||||
@ -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,6 +696,7 @@ start pages =
|
||||
|
||||
config =
|
||||
{ toJsPort = toJsPort
|
||||
, fromJsPort = fromJsPort
|
||||
, manifest = manifest
|
||||
, generateFiles = \_ -> StaticHttp.succeed []
|
||||
, init = \_ -> ( (), Cmd.none )
|
||||
@ -669,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)
|
||||
@ -684,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
|
||||
@ -780,6 +862,10 @@ toJsPort foo =
|
||||
Cmd.none
|
||||
|
||||
|
||||
fromJsPort =
|
||||
Sub.none
|
||||
|
||||
|
||||
type PathKey
|
||||
= PathKey
|
||||
|
||||
@ -831,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