Merge branch 'master' into phantom-builder

This commit is contained in:
Dillon Kearns 2020-05-11 10:40:58 -07:00
commit 3a61933d81
44 changed files with 5404 additions and 2919 deletions

View File

@ -40,6 +40,15 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "lukewestby",
"name": "Luke Westby",
"avatar_url": "https://avatars1.githubusercontent.com/u/1508245?v=4",
"profile": "https://sunrisemovement.com",
"contributions": [
"code"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,
@ -47,5 +56,6 @@
"projectOwner": "dillonkearns", "projectOwner": "dillonkearns",
"repoType": "github", "repoType": "github",
"repoHost": "https://github.com", "repoHost": "https://github.com",
"skipCi": true "skipCi": true,
"commitConvention": "none"
} }

View File

@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased] ## [Unreleased]
## [1.2.12] - 2020-03-28 ## [1.3.0] - 2020-03-28
### Added ### Added
- You can now host your `elm-pages` site in a sub-directory. For example, you could host it at mysite.com/blog, where the top-level mysite.com/ is hosting a different app. - You can now host your `elm-pages` site in a sub-directory. For example, you could host it at mysite.com/blog, where the top-level mysite.com/ is hosting a different app.

View File

@ -1,9 +1,7 @@
# `elm-pages` [![Netlify Status](https://api.netlify.com/api/v1/badges/8ee4a674-4f37-4f16-b99e-607c0a02ee75/deploy-status)](https://app.netlify.com/sites/elm-pages/deploys) [![Build Status](https://github.com/dillonkearns/elm-pages/workflows/Elm%20CI/badge.svg)](https://github.com/dillonkearns/elm-pages/actions?query=branch%3Amaster) [![npm](https://img.shields.io/npm/v/elm-pages.svg)](https://npmjs.com/package/elm-pages) [![Elm package](https://img.shields.io/elm-package/v/dillonkearns/elm-pages.svg)](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/) # `elm-pages` [![Netlify Status](https://api.netlify.com/api/v1/badges/8ee4a674-4f37-4f16-b99e-607c0a02ee75/deploy-status)](https://app.netlify.com/sites/elm-pages/deploys) [![Build Status](https://github.com/dillonkearns/elm-pages/workflows/Elm%20CI/badge.svg)](https://github.com/dillonkearns/elm-pages/actions?query=branch%3Amaster) [![npm](https://img.shields.io/npm/v/elm-pages.svg)](https://npmjs.com/package/elm-pages) [![Elm package](https://img.shields.io/elm-package/v/dillonkearns/elm-pages.svg)](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END --> <!-- ALL-CONTRIBUTORS-BADGE:END -->
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/dillonkearns/elm-pages-starter) [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/dillonkearns/elm-pages-starter)
@ -165,12 +163,12 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://citric.id"><img src="https://avatars1.githubusercontent.com/u/296665?v=4" width="100px;" alt=""/><br /><sub><b>Steven Vandevelde</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=icidasset" title="Code">💻</a></td> <td align="center"><a href="https://citric.id"><img src="https://avatars1.githubusercontent.com/u/296665?v=4" width="100px;" alt=""/><br /><sub><b>Steven Vandevelde</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=icidasset" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Y0hy0h"><img src="https://avatars0.githubusercontent.com/u/11377826?v=4" width="100px;" alt=""/><br /><sub><b>Johannes Maas</b></sub></a><br /><a href="#userTesting-Y0hy0h" title="User Testing">📓</a></td> <td align="center"><a href="https://github.com/Y0hy0h"><img src="https://avatars0.githubusercontent.com/u/11377826?v=4" width="100px;" alt=""/><br /><sub><b>Johannes Maas</b></sub></a><br /><a href="#userTesting-Y0hy0h" title="User Testing">📓</a></td>
<td align="center"><a href="https://github.com/vViktorPL"><img src="https://avatars1.githubusercontent.com/u/2961541?v=4" width="100px;" alt=""/><br /><sub><b>Wiktor Toporek</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=vViktorPL" title="Code">💻</a></td> <td align="center"><a href="https://github.com/vViktorPL"><img src="https://avatars1.githubusercontent.com/u/2961541?v=4" width="100px;" alt=""/><br /><sub><b>Wiktor Toporek</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=vViktorPL" title="Code">💻</a></td>
<td align="center"><a href="https://sunrisemovement.com"><img src="https://avatars1.githubusercontent.com/u/1508245?v=4" width="100px;" alt=""/><br /><sub><b>Luke Westby</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=lukewestby" title="Code">💻</a></td>
</tr> </tr>
</table> </table>
<!-- markdownlint-enable --> <!-- markdownlint-enable -->
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END --> <!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@ -7,6 +7,8 @@
"exposed-modules": [ "exposed-modules": [
"Head", "Head",
"Head.Seo", "Head.Seo",
"OptimizedDecoder",
"OptimizedDecoder.Pipeline",
"Pages.ImagePath", "Pages.ImagePath",
"Pages.PagePath", "Pages.PagePath",
"Pages.StaticHttp", "Pages.StaticHttp",
@ -31,7 +33,6 @@
"elm-community/list-extra": "8.2.2 <= v < 9.0.0", "elm-community/list-extra": "8.2.2 <= v < 9.0.0",
"elm-community/result-extra": "2.2.1 <= v < 3.0.0", "elm-community/result-extra": "2.2.1 <= v < 3.0.0",
"lukewestby/elm-string-interpolate": "1.0.4 <= v < 2.0.0", "lukewestby/elm-string-interpolate": "1.0.4 <= v < 2.0.0",
"mdgriffith/elm-markup": "3.0.1 <= v < 4.0.0",
"mgold/elm-nonempty-list": "4.0.2 <= v < 5.0.0", "mgold/elm-nonempty-list": "4.0.2 <= v < 5.0.0",
"miniBill/elm-codec": "1.2.0 <= v < 2.0.0", "miniBill/elm-codec": "1.2.0 <= v < 2.0.0",
"noahzgordon/elm-color-extra": "1.0.2 <= v < 2.0.0", "noahzgordon/elm-color-extra": "1.0.2 <= v < 2.0.0",
@ -43,4 +44,4 @@
"elm-explorations/test": "1.2.2 <= v < 2.0.0", "elm-explorations/test": "1.2.2 <= v < 2.0.0",
"jgrenat/elm-html-test-runner": "1.0.3 <= v < 2.0.0" "jgrenat/elm-html-test-runner": "1.0.3 <= v < 2.0.0"
} }
} }

View File

@ -3,7 +3,7 @@
"type": "blog", "type": "blog",
"author": "Dillon Kearns", "author": "Dillon Kearns",
"title": "A is for API - Introducing Static HTTP Requests", "title": "A is for API - Introducing Static HTTP Requests",
"description": "", "description": "The new StaticHttp API lets you fetch data when your site is built. That lets you remove loading spinners, and even access environment variables.",
"image": "images/article-covers/static-http.jpg", "image": "images/article-covers/static-http.jpg",
"published": "2019-12-10", "published": "2019-12-10",
} }

View 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.

View File

@ -31,7 +31,6 @@
"elm-explorations/markdown": "1.0.0", "elm-explorations/markdown": "1.0.0",
"justinmimbs/date": "3.2.0", "justinmimbs/date": "3.2.0",
"lukewestby/elm-string-interpolate": "1.0.4", "lukewestby/elm-string-interpolate": "1.0.4",
"mdgriffith/elm-markup": "3.0.1",
"mdgriffith/elm-ui": "1.1.5", "mdgriffith/elm-ui": "1.1.5",
"miniBill/elm-codec": "1.2.0", "miniBill/elm-codec": "1.2.0",
"noahzgordon/elm-color-extra": "1.0.2", "noahzgordon/elm-color-extra": "1.0.2",
@ -59,4 +58,4 @@
}, },
"indirect": {} "indirect": {}
} }
} }

View File

@ -17,10 +17,12 @@ import Head.Seo as Seo
import Html exposing (Html) import Html exposing (Html)
import Html.Attributes as Attr import Html.Attributes as Attr
import Index import Index
import Json.Decode.Exploration as D import Json.Decode as Decode exposing (Decoder)
import Json.Encode
import MarkdownRenderer import MarkdownRenderer
import Metadata exposing (Metadata) import Metadata exposing (Metadata)
import MySitemap import MySitemap
import OptimizedDecoder as D
import Pages exposing (images, pages) import Pages exposing (images, pages)
import Pages.Directory as Directory exposing (Directory) import Pages.Directory as Directory exposing (Directory)
import Pages.ImagePath as ImagePath exposing (ImagePath) import Pages.ImagePath as ImagePath exposing (ImagePath)
@ -34,6 +36,7 @@ import Rss
import RssPlugin import RssPlugin
import Secrets import Secrets
import Showcase import Showcase
import StructuredData
manifest : Manifest.Config Pages.PathKey manifest : Manifest.Config Pages.PathKey
@ -202,7 +205,7 @@ view siteMetadata page =
] ]
} }
|> wrapBody stars page model |> wrapBody stars page model
, head = head page.frontmatter , head = head page.path page.frontmatter
} }
) )
(StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") (StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
@ -219,49 +222,11 @@ view siteMetadata page =
\model viewForPage -> \model viewForPage ->
pageView stars model siteMetadata page viewForPage pageView stars model siteMetadata page viewForPage
|> wrapBody stars page model |> wrapBody stars page model
, head = head page.frontmatter , head = head page.path page.frontmatter
} }
) )
--let
-- viewFn =
-- case page.frontmatter of
-- Metadata.Page metadata ->
-- StaticHttp.map3
-- (\elmPagesStars elmPagesStarterStars netlifyStars ->
-- { view =
-- \model viewForPage ->
-- { title = metadata.title
-- , body =
-- "elm-pages ⭐️'s: "
-- ++ String.fromInt elmPagesStars
-- ++ "\n\nelm-pages-starter ⭐️'s: "
-- ++ String.fromInt elmPagesStarterStars
-- ++ "\n\nelm-markdown ⭐️'s: "
-- ++ String.fromInt netlifyStars
-- |> Element.text
-- |> wrapBody
-- }
-- , head = head page.frontmatter
-- }
-- )
-- (StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
-- (D.field "stargazers_count" D.int)
-- )
-- (StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages-starter")
-- (D.field "stargazers_count" D.int)
-- )
-- (StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-markdown")
-- (D.field "stargazers_count" D.int)
-- )
--
-- _ ->
-- StaticHttp.withData "https://api.github.com/repos/dillonkearns/elm-pages"
-- (Decode.field "stargazers_count" Decode.int)
pageView : pageView :
Int Int
-> Model -> Model
@ -535,8 +500,8 @@ highlightableLink currentPath linkDirectory displayName =
<https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names> <https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names>
<https://ogp.me/> <https://ogp.me/>
-} -}
head : Metadata -> List (Head.Tag Pages.PathKey) head : PagePath Pages.PathKey -> Metadata -> List (Head.Tag Pages.PathKey)
head metadata = head currentPath metadata =
case metadata of case metadata of
Metadata.Page meta -> Metadata.Page meta ->
Seo.summary Seo.summary
@ -571,26 +536,45 @@ head metadata =
|> Seo.website |> Seo.website
Metadata.Article meta -> Metadata.Article meta ->
Seo.summaryLarge Head.structuredData
{ canonicalUrlOverride = Nothing (StructuredData.article
, siteName = "elm-pages" { title = meta.title
, image = , description = meta.description
{ url = meta.image , author = StructuredData.person { name = meta.author.name }
, alt = meta.description , publisher = StructuredData.person { name = "Dillon Kearns" }
, dimensions = Nothing , url = canonicalSiteUrl ++ "/" ++ PagePath.toString currentPath
, mimeType = Nothing , imageUrl = canonicalSiteUrl ++ "/" ++ ImagePath.toString meta.image
} , datePublished = Date.toIsoString meta.published
, description = meta.description , mainEntityOfPage =
, locale = Nothing StructuredData.softwareSourceCode
, title = meta.title { codeRepositoryUrl = "https://github.com/dillonkearns/elm-pages"
} , description = "A statically typed site generator for Elm."
|> Seo.article , author = "Dillon Kearns"
{ tags = [] , programmingLanguage = StructuredData.elmLang
, section = Nothing }
, publishedTime = Just (Date.toIsoString meta.published)
, modifiedTime = Nothing
, expirationTime = Nothing
} }
)
:: (Seo.summaryLarge
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = meta.image
, alt = meta.description
, dimensions = Nothing
, mimeType = Nothing
}
, description = meta.description
, locale = Nothing
, title = meta.title
}
|> Seo.article
{ tags = []
, section = Nothing
, publishedTime = Just (Date.toIsoString meta.published)
, modifiedTime = Nothing
, expirationTime = Nothing
}
)
Metadata.Author meta -> Metadata.Author meta ->
let let

View File

@ -91,7 +91,7 @@ decoder =
|> Decode.map Article |> Decode.map Article
_ -> _ ->
Decode.fail <| "Unexpected page type " ++ pageType Decode.fail <| "Unexpected page \"type\" " ++ pageType
) )

View File

@ -4,7 +4,7 @@ import Element
import Element.Border import Element.Border
import Element.Font import Element.Font
import FontAwesome import FontAwesome
import Json.Decode.Exploration as Decode import OptimizedDecoder as Decode
import Pages.Secrets as Secrets import Pages.Secrets as Secrets
import Pages.StaticHttp as StaticHttp import Pages.StaticHttp as StaticHttp
import Palette import Palette

View File

@ -27,7 +27,6 @@
"elm-explorations/markdown": "1.0.0", "elm-explorations/markdown": "1.0.0",
"justinmimbs/date": "3.1.2", "justinmimbs/date": "3.1.2",
"lukewestby/elm-string-interpolate": "1.0.4", "lukewestby/elm-string-interpolate": "1.0.4",
"mdgriffith/elm-markup": "3.0.1",
"mdgriffith/elm-ui": "1.1.5", "mdgriffith/elm-ui": "1.1.5",
"mgold/elm-nonempty-list": "4.0.2", "mgold/elm-nonempty-list": "4.0.2",
"miniBill/elm-codec": "1.2.0", "miniBill/elm-codec": "1.2.0",
@ -51,4 +50,4 @@
}, },
"indirect": {} "indirect": {}
} }
} }

View File

@ -95,20 +95,13 @@ import Dict exposing (Dict)
content : { markdown : List ( List String, { frontMatter : String, body : Maybe String } ), markup : List ( List String, String ) } content : { markdown : List ( List String, { frontMatter : String, body : Maybe String } ), markup : List ( List String, String ) }
content = content =
{ markdown = markdown, markup = markup } { markdown = markdown }
markdown : List ( List String, { frontMatter : String, body : Maybe String } ) markdown : List ( List String, { frontMatter : String, body : Maybe String } )
markdown = markdown =
[ {1} [ {1}
] ]
markup : List ( List String, String )
markup =
[
{0}
]
""" """
[ List.map generatePage content |> String.join "\n ," [ List.map generatePage content |> String.join "\n ,"
, List.map generateMarkdownPage markdownContent |> String.join "\n ," , List.map generateMarkdownPage markdownContent |> String.join "\n ,"

View File

@ -2,6 +2,7 @@ const path = require("path");
const fs = require("fs"); const fs = require("fs");
const globby = require("globby"); const globby = require("globby");
const parseFrontmatter = require("./frontmatter.js"); const parseFrontmatter = require("./frontmatter.js");
const webpack = require('webpack')
function unpackFile(filePath) { function unpackFile(filePath) {
const { content, data } = parseFrontmatter( const { content, data } = parseFrontmatter(
@ -15,52 +16,74 @@ function unpackFile(filePath) {
return { return {
baseRoute, baseRoute,
content content,
filePath
}; };
} }
module.exports = class AddFilesPlugin { module.exports = class AddFilesPlugin {
constructor(data, filesToGenerate) { apply(/** @type {webpack.Compiler} */ compiler) {
this.pagesWithRequests = data;
this.filesToGenerate = filesToGenerate;
}
apply(compiler) {
compiler.hooks.emit.tap("AddFilesPlugin", compilation => {
const files = globby
.sync(["content/**/*.*", "!content/**/*.emu"], {})
.map(unpackFile);
files.forEach(file => { (global.mode === "dev" ? compiler.hooks.emit : compiler.hooks.make).tapAsync("AddFilesPlugin", (compilation, callback) => {
// Couldn't find this documented in the webpack docs,
// but I found the example code for it here:
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
let route = file.baseRoute.replace(/\/$/, '');
const staticRequests = this.pagesWithRequests[route];
const filename = path.join(file.baseRoute, "content.json"); const files = globby.sync("content").map(unpackFile);
compilation.fileDependencies.add(filename);
const rawContents = JSON.stringify({
body: file.content,
staticData: staticRequests || {}
});
compilation.assets[filename] = {
source: () => rawContents,
size: () => rawContents.length
};
});
(this.filesToGenerate || []).forEach(file => { let staticRequestData = {}
// Couldn't find this documented in the webpack docs, global.pagesWithRequests.then(payload => {
// but I found the example code for it here:
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
compilation.assets[file.path] = {
source: () => file.content,
size: () => file.content.length
};
});
if (payload.type === 'error') {
compilation.errors.push(new Error(payload.message))
} else if (payload.errors && payload.errors.length > 0) {
compilation.errors.push(new Error(payload.errors[0]))
}
else {
staticRequestData = payload.pages
}
})
.finally(() => {
files.forEach(file => {
// Couldn't find this documented in the webpack docs,
// but I found the example code for it here:
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
let route = file.baseRoute.replace(/\/$/, '');
const staticRequests = staticRequestData[route];
const filename = path.join(file.baseRoute, "content.json");
if (compilation.contextDependencies) {
compilation.contextDependencies.add('content')
}
// compilation.fileDependencies.add(filename);
if (compilation.fileDependencies) {
compilation.fileDependencies.add(path.resolve(file.filePath));
}
const rawContents = JSON.stringify({
body: file.content,
staticData: staticRequests || {}
});
compilation.assets[filename] = {
source: () => rawContents,
size: () => rawContents.length
};
});
(global.filesToGenerate || []).forEach(file => {
// Couldn't find this documented in the webpack docs,
// but I found the example code for it here:
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
compilation.assets[file.path] = {
source: () => file.content,
size: () => file.content.length
};
});
callback()
})
}); });
} }

View File

@ -1,33 +1,36 @@
const { compileToString } = require("../node-elm-compiler/index.js"); const { compileToStringSync } = require("../node-elm-compiler/index.js");
XMLHttpRequest = require("xhr2"); XMLHttpRequest = require("xhr2");
module.exports = runElm; module.exports = runElm;
function runElm(/** @type string */ mode, /** @type any */ callback) { function runElm(/** @type string */ mode) {
const elmBaseDirectory = "./elm-stuff/elm-pages"; return new Promise((resolve, reject) => {
const mainElmFile = "../../src/Main.elm"; const elmBaseDirectory = "./elm-stuff/elm-pages";
const startingDir = process.cwd(); const mainElmFile = "../../src/Main.elm";
process.chdir(elmBaseDirectory); const startingDir = process.cwd();
compileToString([mainElmFile], {}).then(function(data) { process.chdir(elmBaseDirectory);
(function() { const data = compileToStringSync([mainElmFile], {});
process.chdir(startingDir);
(function () {
const warnOriginal = console.warn; const warnOriginal = console.warn;
console.warn = function() {}; console.warn = function () { };
eval(data.toString()); eval(data.toString());
const app = Elm.Main.init({ const app = Elm.Main.init({
flags: { secrets: process.env, mode } flags: { secrets: process.env, mode, staticHttpCache: global.staticHttpCache }
}); });
app.ports.toJsPort.subscribe(payload => { app.ports.toJsPort.subscribe(payload => {
process.chdir(startingDir);
if (payload.tag === "Success") { if (payload.tag === "Success") {
callback(payload.args[0]); global.staticHttpCache = payload.args[0].staticHttpCache;
resolve(payload.args[0])
} else { } else {
console.log(payload.args[0]); reject(payload.args[0])
process.exit(1);
} }
delete Elm; delete Elm;
console.warn = warnOriginal; console.warn = warnOriginal;
}); });
})(); })();
}); });
} }

View File

@ -1,5 +1,4 @@
const webpack = require("webpack"); const webpack = require("webpack");
const middleware = require("webpack-dev-middleware");
const path = require("path"); const path = require("path");
const HTMLWebpackPlugin = require("html-webpack-plugin"); const HTMLWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin'); const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
@ -14,14 +13,27 @@ const imageminMozjpeg = require("imagemin-mozjpeg");
const express = require("express"); const express = require("express");
const ClosurePlugin = require("closure-webpack-plugin"); const ClosurePlugin = require("closure-webpack-plugin");
const readline = require("readline"); const readline = require("readline");
const webpackDevMiddleware = require("webpack-dev-middleware");
const PluginGenerateElmPagesBuild = require('./plugin-generate-elm-pages-build')
const hotReloadIndicatorStyle = `
<style>
@keyframes lds-default {
0%, 20%, 80%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.5);
}
}
</style>
`
module.exports = { start, run }; module.exports = { start, run };
function start({ routes, debug, customPort, manifestConfig, routesWithRequests, filesToGenerate }) { function start({ routes, debug, customPort, manifestConfig }) {
const config = webpackOptions(false, routes, { const config = webpackOptions(false, routes, {
debug, debug,
manifestConfig, manifestConfig
routesWithRequests,
filesToGenerate
}); });
const compiler = webpack(config); const compiler = webpack(config);
@ -39,27 +51,31 @@ function start({ routes, debug, customPort, manifestConfig, routesWithRequests,
app.use('/images', express.static(path.resolve(process.cwd(), "./images"))); app.use('/images', express.static(path.resolve(process.cwd(), "./images")));
app.use(require("webpack-dev-middleware")(compiler, options)); app.use(webpackDevMiddleware(compiler, options));
app.use(require("webpack-hot-middleware")(compiler, { app.use(require("webpack-hot-middleware")(compiler, {
log: console.log, path: '/__webpack_hmr' log: console.log, path: '/__webpack_hmr'
})) }))
app.use("*", function(req, res, next) { app.get('/elm-pages-dev-server-options', function (req, res) {
res.json({ elmDebugger: debug });
});
app.use("*", function (req, res, next) {
// don't know why this works, but it does // don't know why this works, but it does
// see: https://github.com/jantimon/html-webpack-plugin/issues/145#issuecomment-170554832 // see: https://github.com/jantimon/html-webpack-plugin/issues/145#issuecomment-170554832
const filename = path.join(compiler.outputPath, "index.html"); const filename = path.join(compiler.outputPath, "index.html");
const route = req.originalUrl.replace(/(\w)\/$/, "$1").replace(/^\//, ""); const route = req.originalUrl.replace(/(\w)\/$/, "$1").replace(/^\//, "");
const isPage = routes.includes(route); const isPage = routes.includes(route);
compiler.outputFileSystem.readFile(filename, function(err, result) { compiler.outputFileSystem.readFile(filename, function (err, result) {
const contents = isPage
? replaceBaseAndLinks(result.toString(), route)
: result
if (err) { if (err) {
return next(err); return next(err);
} }
const contents = isPage
? replaceBaseAndLinks(result.toString(), route)
: result
res.set("content-type", "text/html"); res.set("content-type", "text/html");
res.send(contents); res.send(contents);
res.end(); res.end();
@ -74,20 +90,18 @@ function start({ routes, debug, customPort, manifestConfig, routesWithRequests,
// app.use(express.static(__dirname + "/path-to-static-folder")); // app.use(express.static(__dirname + "/path-to-static-folder"));
} }
function run({ routes, manifestConfig, routesWithRequests, filesToGenerate }, callback) { function run({ routes, manifestConfig }) {
webpack( webpack(
webpackOptions(true, routes, { webpackOptions(true, routes, {
debug: false, debug: false,
manifestConfig, manifestConfig,
routesWithRequests,
filesToGenerate
}) })
).run((err, stats) => { ).run((err, stats) => {
if (err) { if (err) {
console.error(err); console.error(err);
process.exit(1); process.exit(1);
} else { } else {
callback(); // done
} }
console.log( console.log(
@ -128,12 +142,13 @@ function printProgress(progress, message) {
function webpackOptions( function webpackOptions(
production, production,
routes, routes,
{ debug, manifestConfig, routesWithRequests, filesToGenerate } { debug, manifestConfig }
) { ) {
const common = { const common = {
mode: production ? "production" : "development", mode: production ? "production" : "development",
plugins: [ plugins: [
new AddFilesPlugin(routesWithRequests, filesToGenerate), new PluginGenerateElmPagesBuild(),
new AddFilesPlugin(),
new CopyPlugin([ new CopyPlugin([
{ {
from: "static/**/*", from: "static/**/*",
@ -167,7 +182,33 @@ function webpackOptions(
new HTMLWebpackPlugin({ new HTMLWebpackPlugin({
inject: "head", inject: "head",
template: path.resolve(__dirname, "template.html") templateContent: `<!DOCTYPE html>
<html lang="en">
<head>
<link rel="preload" href="content.json" as="fetch" crossorigin />
<base href="/" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("service-worker.js");
});
} else {
console.log("No service worker registered.");
}
</script>
${production ? '' : hotReloadIndicatorStyle}
</head>
<body></body>
</html>`
}), }),
new ScriptExtHtmlWebpackPlugin({ new ScriptExtHtmlWebpackPlugin({
preload: /\.js$/, preload: /\.js$/,
@ -234,10 +275,10 @@ function webpackOptions(
/assets\// /assets\//
], ],
swDest: "service-worker.js" swDest: "service-worker.js"
}) }),
// comment this out to do performance profiling // comment this out to do performance profiling
// (drag-and-drop `events.json` file into Chrome performance tab) // (drag-and-drop `events.json` file into Chrome performance tab)
// , new webpack.debug.ProfilingPlugin() // new webpack.debug.ProfilingPlugin()
], ],
output: {}, output: {},
resolve: { resolve: {
@ -249,7 +290,7 @@ function webpackOptions(
// process.cwd prefixed node_modules above). // process.cwd prefixed node_modules above).
path.resolve(path.dirname(require.resolve('webpack')), '../../'), path.resolve(path.dirname(require.resolve('webpack')), '../../'),
], ],
extensions: [".js", ".elm", ".scss", ".png", ".html"] extensions: [".js", ".elm", ".scss", ".png", ".html"]
}, },
module: { module: {
@ -323,7 +364,7 @@ function webpackOptions(
renderer: new PrerenderSPAPlugin.PuppeteerRenderer({ renderer: new PrerenderSPAPlugin.PuppeteerRenderer({
renderAfterDocumentEvent: "prerender-trigger", renderAfterDocumentEvent: "prerender-trigger",
headless: true, headless: true,
devtools: false devtools: false,
}), }),
postProcess: renderedRoute => { postProcess: renderedRoute => {
@ -354,14 +395,14 @@ function webpackOptions(
} else { } else {
return merge(common, { return merge(common, {
entry: [ entry: [
require.resolve("webpack-hot-middleware/client"), hmrClientPath(),
"./index.js", "./index.js",
], ],
plugins: [ plugins: [
new webpack.NamedModulesPlugin(), new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
// Prevents compilation errors causing the hot loader to lose state // Prevents compilation errors causing the hot loader to lose state
new webpack.NoEmitOnErrorsPlugin(), new webpack.NoEmitOnErrorsPlugin(),
new webpack.HotModuleReplacementPlugin()
], ],
module: { module: {
rules: [ rules: [
@ -385,6 +426,42 @@ function webpackOptions(
} }
} }
function hmrClientPath() {
var ansiColors = {
reset: ['ffffff', 'transparent'], // [FOREGROUD_COLOR, BACKGROUND_COLOR]
black: '000',
red: 'c91b00',
green: '00c200',
yellow: 'c7c400',
blue: '0225c7',
magenta: 'c930c7',
cyan: '00c5c7',
lightgrey: 'f0f0f0',
darkgrey: '888'
};
var overlayStyles = {
// options from https://github.com/webpack-contrib/webpack-hot-middleware/blob/master/client-overlay.js
background: 'rgba(0,0,0,0.90)',
color: '#e8e8e8',
lineHeight: '1.6',
whiteSpace: 'pre-wrap',
fontFamily: 'Menlo, Consolas, monospace',
fontSize: '16px',
// position: 'fixed',
// zIndex: 9999,
// padding: '10px',
// left: 0,
// right: 0,
// top: 0,
// bottom: 0,
// overflow: 'auto',
// dir: 'ltr',
// textAlign: 'left',
};
return `${require.resolve("webpack-hot-middleware/client")}?ansiColors=${encodeURIComponent(JSON.stringify(ansiColors))}&overlayStyles=${encodeURIComponent(JSON.stringify(overlayStyles))}`;
}
function cleanRoute(route) { function cleanRoute(route) {
return route.replace(/(^\/|\/$)/, "") return route.replace(/(^\/|\/$)/, "")
@ -395,10 +472,10 @@ function pathToRoot(cleanedRoute) {
return cleanedRoute === "" return cleanedRoute === ""
? cleanedRoute ? cleanedRoute
: cleanedRoute : cleanedRoute
.split("/") .split("/")
.map(_ => "..") .map(_ => "..")
.join("/") .join("/")
.replace(/\.$/, "./") .replace(/\.$/, "./")
} }

View File

@ -1,9 +1,9 @@
generateRawContent = require("./generate-raw-content.js"); generateRawContent = require("./generate-raw-content.js");
const exposingList = const exposingList =
"(PathKey, allPages, allImages, internals, images, isValidRoute, pages, builtAt)"; "(PathKey, allPages, allImages, internals, images, isValidRoute, pages, builtAt)";
function staticRouteStuff(staticRoutes) { function staticRouteStuff(staticRoutes) {
return ` return `
${staticRoutes.allRoutes} ${staticRoutes.allRoutes}
@ -41,8 +41,8 @@ isValidRoute route =
`; `;
} }
function elmPagesUiFile(staticRoutes, markdownContent, markupContent) { function elmPagesUiFile(staticRoutes, markdownContent) {
return `port module Pages exposing ${exposingList} return `port module Pages exposing ${exposingList}
import Color exposing (Color) import Color exposing (Color)
import Pages.Internal import Pages.Internal
@ -50,7 +50,6 @@ import Head
import Html exposing (Html) import Html exposing (Html)
import Json.Decode import Json.Decode
import Json.Encode import Json.Encode
import Mark
import Pages.Platform import Pages.Platform
import Pages.Manifest exposing (DisplayMode, Orientation) import Pages.Manifest exposing (DisplayMode, Orientation)
import Pages.Manifest.Category as Category exposing (Category) import Pages.Manifest.Category as Category exposing (Category)
@ -92,23 +91,26 @@ directoryWithoutIndex path =
port toJsPort : Json.Encode.Value -> Cmd msg port toJsPort : Json.Encode.Value -> Cmd msg
port fromJsPort : (Json.Decode.Value -> msg) -> Sub msg
internals : Pages.Internal.Internal PathKey internals : Pages.Internal.Internal PathKey
internals = internals =
{ applicationType = Pages.Internal.Browser { applicationType = Pages.Internal.Browser
, toJsPort = toJsPort , toJsPort = toJsPort
, fromJsPort = fromJsPort identity
, content = content , content = content
, pathKey = PathKey , pathKey = PathKey
} }
${staticRouteStuff(staticRoutes)} ${staticRouteStuff(staticRoutes)}
${generateRawContent(markdownContent, markupContent, false)} ${generateRawContent(markdownContent, false)}
`; `;
} }
function elmPagesCliFile(staticRoutes, markdownContent, markupContent) { function elmPagesCliFile(staticRoutes, markdownContent) {
return `port module Pages exposing ${exposingList} return `port module Pages exposing ${exposingList}
import Color exposing (Color) import Color exposing (Color)
import Pages.Internal import Pages.Internal
@ -116,7 +118,6 @@ import Head
import Html exposing (Html) import Html exposing (Html)
import Json.Decode import Json.Decode
import Json.Encode import Json.Encode
import Mark
import Pages.Platform import Pages.Platform
import Pages.Manifest exposing (DisplayMode, Orientation) import Pages.Manifest exposing (DisplayMode, Orientation)
import Pages.Manifest.Category as Category exposing (Category) import Pages.Manifest.Category as Category exposing (Category)
@ -159,10 +160,14 @@ directoryWithoutIndex path =
port toJsPort : Json.Encode.Value -> Cmd msg port toJsPort : Json.Encode.Value -> Cmd msg
port fromJsPort : (Json.Decode.Value -> msg) -> Sub msg
internals : Pages.Internal.Internal PathKey internals : Pages.Internal.Internal PathKey
internals = internals =
{ applicationType = Pages.Internal.Cli { applicationType = Pages.Internal.Cli
, toJsPort = toJsPort , toJsPort = toJsPort
, fromJsPort = fromJsPort identity
, content = content , content = content
, pathKey = PathKey , pathKey = PathKey
} }
@ -170,7 +175,7 @@ internals =
${staticRouteStuff(staticRoutes)} ${staticRouteStuff(staticRoutes)}
${generateRawContent(markdownContent, markupContent, true)} ${generateRawContent(markdownContent, true)}
`; `;
} }
module.exports = { elmPagesUiFile, elmPagesCliFile }; module.exports = { elmPagesUiFile, elmPagesCliFile };

View File

@ -5,67 +5,38 @@ const { version } = require("../../package.json");
const fs = require("fs"); const fs = require("fs");
const globby = require("globby"); const globby = require("globby");
const develop = require("./develop.js"); const develop = require("./develop.js");
const chokidar = require("chokidar");
const doCliStuff = require("./generate-elm-stuff.js");
const { elmPagesUiFile } = require("./elm-file-constants.js");
const generateRecords = require("./generate-records.js");
const parseFrontmatter = require("./frontmatter.js"); const parseFrontmatter = require("./frontmatter.js");
const path = require("path"); const generateRecords = require("./generate-records.js");
const { ensureDirSync, deleteIfExists } = require('./file-helpers.js') const doCliStuff = require("./generate-elm-stuff.js");
global.builtAt = new Date(); global.builtAt = new Date();
global.staticHttpCache = {};
const contentGlobPath = "content/**/*.emu";
let watcher = null;
let devServerRunning = false;
function unpackFile(path) { function unpackFile(path) {
return { path, contents: fs.readFileSync(path).toString() }; return { path, contents: fs.readFileSync(path).toString() };
} }
function unpackMarkup(path) {
const separated = parseFrontmatter(path, fs.readFileSync(path).toString());
return {
path,
metadata: separated.matter,
body: separated.content,
extension: "emu"
};
}
function parseMarkdown(path, fileContents) { function parseMarkdown(path, fileContents) {
const { content, data } = parseFrontmatter(path, fileContents); const { content, data } = parseFrontmatter(path, fileContents);
return { return {
path, path,
metadata: JSON.stringify(data), metadata: JSON.stringify(data),
body: content, body: content
extension: "md"
}; };
} }
function run() { function run() {
console.log("Running elm-pages...");
const content = globby.sync([contentGlobPath], {}).map(unpackMarkup);
const staticRoutes = generateRecords();
const markdownContent = globby const markdownContent = globby
.sync(["content/**/*.*", "!content/**/*.emu"], {}) .sync(["content/**/*.*"], {})
.map(unpackFile) .map(unpackFile)
.map(({ path, contents }) => { .map(({ path, contents }) => {
return parseMarkdown(path, contents); return parseMarkdown(path, contents);
}); });
const images = globby
.sync("images/**/*", {})
.filter(imagePath => !fs.lstatSync(imagePath).isDirectory());
let app = Elm.Main.init({ let app = Elm.Main.init({
flags: { flags: {
argv: process.argv, argv: process.argv,
versionMessage: version, versionMessage: version,
content,
markdownContent,
images
} }
}); });
@ -79,89 +50,51 @@ function run() {
process.exit(1); process.exit(1);
}); });
app.ports.writeFile.subscribe(contents => { app.ports.writeFile.subscribe(cliOptions => {
const routes = toRoutes(markdownContent.concat(content));
const markdownContent = globby
.sync(["content/**/*.*"], {})
.map(unpackFile)
.map(({ path, contents }) => {
return parseMarkdown(path, contents);
});
const routes = toRoutes(markdownContent);
global.mode = cliOptions.watch ? "dev" : "prod"
const staticRoutes = generateRecords();
doCliStuff( doCliStuff(
contents.watch ? "dev" : "prod", global.mode,
staticRoutes, staticRoutes,
markdownContent, markdownContent
content, ).then((payload) => {
function(payload) { if (cliOptions.watch) {
if (contents.watch) { develop.start({
startWatchIfNeeded(); routes,
if (!devServerRunning) { debug: cliOptions.debug,
devServerRunning = true; customPort: cliOptions.customPort,
develop.start({ manifestConfig: payload.manifest,
routes,
debug: contents.debug,
manifestConfig: payload.manifest,
routesWithRequests: payload.pages,
filesToGenerate: payload.filesToGenerate,
customPort: contents.customPort
});
}
} else {
if (payload.errors && payload.errors.length > 0) {
printErrorsAndExit(payload.errors);
}
develop.run( });
{ } else {
routes, develop.run({
manifestConfig: payload.manifest, routes,
routesWithRequests: payload.pages, debug: cliOptions.debug,
filesToGenerate: payload.filesToGenerate customPort: cliOptions.customPort,
}, manifestConfig: payload.manifest,
() => {} });
); }
}
ensureDirSync("./gen"); })
// prevent compilation errors if migrating from previous elm-pages version
deleteIfExists("./gen/Pages/ContentCache.elm");
deleteIfExists("./gen/Pages/Platform.elm");
fs.writeFileSync(
"./gen/Pages.elm",
elmPagesUiFile(staticRoutes, markdownContent, content)
);
console.log("elm-pages DONE");
}
);
}); });
} }
run(); run();
function printErrorsAndExit(errors) {
console.error(
"Found errors. Exiting. Fix your content or parsers and re-run, or run in dev mode with `elm-pages develop`."
);
console.error(errors.join("\n\n"));
process.exit(1);
}
function startWatchIfNeeded() {
if (!watcher) {
console.log("Watching...");
watcher = chokidar
.watch(["content/**/*.*"], {
awaitWriteFinish: {
stabilityThreshold: 500
},
ignoreInitial: true
})
.on("all", function(event, filePath) {
console.log(`Rerunning for ${filePath}...`);
run();
console.log("Done!");
});
}
}
function toRoutes(entries) { function toRoutes(entries) {
return entries.map(toRoute); return entries.map(toRoute);
} }

View File

@ -1,23 +1,5 @@
const path = require("path");
const matter = require("gray-matter"); const matter = require("gray-matter");
module.exports = function parseFrontmatter(filePath, fileContents) { module.exports = function parseFrontmatter(filePath, fileContents) {
return path.extname(filePath) === ".emu" return matter(fileContents);
? matter(fileContents, markupFrontmatterOptions)
: matter(fileContents);
};
const markupFrontmatterOptions = {
language: "markup",
engines: {
markup: {
parse: function(string) {
return string;
},
stringify: function(string) {
return string;
}
}
}
}; };

View File

@ -1,35 +1,57 @@
const fs = require("fs"); const fs = require("fs");
const runElm = require("./compile-elm.js"); const runElm = require("./compile-elm.js");
const copyModifiedElmJson = require("./rewrite-elm-json.js"); const copyModifiedElmJson = require("./rewrite-elm-json.js");
const { elmPagesCliFile } = require("./elm-file-constants.js"); const { elmPagesCliFile, elmPagesUiFile } = require("./elm-file-constants.js");
const path = require("path"); const path = require("path");
const { ensureDirSync, deleteIfExists } = require('./file-helpers.js') const { ensureDirSync, deleteIfExists } = require('./file-helpers.js')
let wasEqualBefore = false
module.exports = function run( module.exports = function run(
mode, mode,
staticRoutes, staticRoutes,
markdownContent, markdownContent
markupContent,
callback
) { ) {
ensureDirSync("./elm-stuff"); ensureDirSync("./elm-stuff");
ensureDirSync("./gen");
ensureDirSync("./elm-stuff/elm-pages"); ensureDirSync("./elm-stuff/elm-pages");
// prevent compilation errors if migrating from previous elm-pages version // prevent compilation errors if migrating from previous elm-pages version
deleteIfExists("./elm-stuff/elm-pages/Pages/ContentCache.elm"); deleteIfExists("./elm-stuff/elm-pages/Pages/ContentCache.elm");
deleteIfExists("./elm-stuff/elm-pages/Pages/Platform.elm"); deleteIfExists("./elm-stuff/elm-pages/Pages/Platform.elm");
const uiFileContent = elmPagesUiFile(staticRoutes, markdownContent)
// TODO should just write it once, but webpack doesn't seem to pick up the changes
// so this wasEqualBefore code causes it to get written twice to make sure the changes come through for HMR
if (wasEqualBefore) {
fs.writeFileSync(
"./gen/Pages.elm",
uiFileContent
);
}
if (global.previousUiFileContent === uiFileContent) {
wasEqualBefore = false
} else {
wasEqualBefore = true
fs.writeFileSync(
"./gen/Pages.elm",
uiFileContent
);
}
global.previousUiFileContent = uiFileContent
// write `Pages.elm` with cli interface // write `Pages.elm` with cli interface
fs.writeFileSync( fs.writeFileSync(
"./elm-stuff/elm-pages/Pages.elm", "./elm-stuff/elm-pages/Pages.elm",
elmPagesCliFile(staticRoutes, markdownContent, markupContent) elmPagesCliFile(staticRoutes, markdownContent)
); );
// write modified elm.json to elm-stuff/elm-pages/ // write modified elm.json to elm-stuff/elm-pages/
copyModifiedElmJson(); copyModifiedElmJson();
// run Main.elm from elm-stuff/elm-pages with `runElm` // run Main.elm from elm-stuff/elm-pages with `runElm`
runElm(mode, callback); return runElm(mode);
}; };

View File

@ -1,9 +1,9 @@
const path = require("path"); const path = require("path");
module.exports = function(markdown, markup, includeBody) { module.exports = function (markdown, includeBody) {
return `content : List ( List String, { extension: String, frontMatter : String, body : Maybe String } ) return `content : List ( List String, { extension: String, frontMatter : String, body : Maybe String } )
content = content =
[ ${markdown.concat(markup).map(entry => toEntry(entry, includeBody))} [ ${markdown.map(entry => toEntry(entry, includeBody))}
]`; ]`;
}; };

View File

@ -1,5 +1,4 @@
const path = require("path"); const path = require("path");
const matter = require("gray-matter");
const dir = "content/"; const dir = "content/";
const glob = require("glob"); const glob = require("glob");
const fs = require("fs"); const fs = require("fs");
@ -138,14 +137,14 @@ function allImageAssetNames() {
}); });
} }
function toPascalCase(str) { function toPascalCase(str) {
var pascal = str.replace(/(\-\w)/g, function(m) { var pascal = str.replace(/(\-\w)/g, function (m) {
return m[1].toUpperCase(); return m[1].toUpperCase();
}); });
return pascal.charAt(0).toUpperCase() + pascal.slice(1); return pascal.charAt(0).toUpperCase() + pascal.slice(1);
} }
function toCamelCase(str) { function toCamelCase(str) {
var pascal = str.replace(/(\-\w)/g, function(m) { var pascal = str.replace(/(\-\w)/g, function (m) {
return m[1].toUpperCase(); return m[1].toUpperCase();
}); });
return pascal.charAt(0).toLowerCase() + pascal.slice(1); return pascal.charAt(0).toLowerCase() + pascal.slice(1);
@ -180,14 +179,14 @@ function formatRecord(directoryPath, rec, asType, level) {
} else { } else {
keyVals.push( keyVals.push(
key + key +
" =\n" + " =\n" +
formatRecord(directoryPath.concat(key), val, asType, level + 1) formatRecord(directoryPath.concat(key), val, asType, level + 1)
); );
} }
} }
keyVals.push( keyVals.push(
`directory = ${ `directory = ${
keys.includes("index") ? "directoryWithIndex" : "directoryWithoutIndex" keys.includes("index") ? "directoryWithIndex" : "directoryWithoutIndex"
} [${directoryPath.map(pathFragment => `"${pathFragment}"`).join(", ")}]` } [${directoryPath.map(pathFragment => `"${pathFragment}"`).join(", ")}]`
); );
const indentationDelimiter = `\n${indentation}, `; const indentationDelimiter = `\n${indentation}, `;

View 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,
};
}

View File

@ -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
View File

@ -11,9 +11,9 @@ module.exports = function pagesInit(
prefetchedPages = [window.location.pathname]; prefetchedPages = [window.location.pathname];
initialLocationHash = document.location.hash.replace(/^#/, ""); initialLocationHash = document.location.hash.replace(/^#/, "");
return new Promise(function(resolve, reject) { return new Promise(function (resolve, reject) {
document.addEventListener("DOMContentLoaded", _ => { document.addEventListener("DOMContentLoaded", _ => {
new MutationObserver(function() { new MutationObserver(function () {
elmViewRendered = true; elmViewRendered = true;
if (headTagsAdded) { if (headTagsAdded) {
document.dispatchEvent(new Event("prerender-trigger")); document.dispatchEvent(new Event("prerender-trigger"));
@ -32,48 +32,98 @@ function loadContentAndInitializeApp(/** @type { init: any } */ mainElmModule)
const isPrerendering = navigator.userAgent.indexOf("Headless") >= 0 const isPrerendering = navigator.userAgent.indexOf("Headless") >= 0
const path = window.location.pathname.replace(/(\w)$/, "$1/") const path = window.location.pathname.replace(/(\w)$/, "$1/")
return httpGet(`${window.location.origin}${path}content.json`).then(function(/** @type JSON */ contentJson) { return Promise.all([
getConfig(),
httpGet(`${window.location.origin}${path}content.json`)]).then(function (/** @type {[DevServerConfig?, JSON]} */[devServerConfig, contentJson]) {
console.log('devServerConfig', devServerConfig);
const app = mainElmModule.init({ const app = mainElmModule.init({
flags: { flags: {
secrets: null, secrets: null,
baseUrl: isPrerendering baseUrl: isPrerendering
? window.location.origin ? window.location.origin
: document.baseURI, : document.baseURI,
isPrerendering: isPrerendering, isPrerendering: isPrerendering,
contentJson isDevServer: !!module.hot,
} isElmDebugMode: devServerConfig ? devServerConfig.elmDebugger : false,
}); contentJson,
}
app.ports.toJsPort.subscribe((
/** @type { { head: HeadTag[], allRoutes: string[] } } */ fromElm
) => {
appendTag({
name: "meta",
attributes: [
["name", "generator"],
["content", `elm-pages v${elmPagesVersion}`]
]
}); });
window.allRoutes = fromElm.allRoutes.map(route => new URL(route, document.baseURI).href); app.ports.toJsPort.subscribe((
/** @type { { head: SeoTag[], allRoutes: string[] } } */ fromElm
if (navigator.userAgent.indexOf("Headless") >= 0) { ) => {
fromElm.head.forEach(headTag => { appendTag({
appendTag(headTag); type: 'head',
name: "meta",
attributes: [
["name", "generator"],
["content", `elm-pages v${elmPagesVersion}`]
]
}); });
window.allRoutes = fromElm.allRoutes.map(route => new URL(route, document.baseURI).href);
if (navigator.userAgent.indexOf("Headless") >= 0) {
fromElm.head.forEach(headTag => {
if (headTag.type === 'head') {
appendTag(headTag);
} else if (headTag.type === 'json-ld') {
appendJsonLdTag(headTag);
} else {
throw new Error(`Unknown tag type #{headTag}`)
}
});
headTagsAdded = true; headTagsAdded = true;
if (elmViewRendered) { if (elmViewRendered) {
document.dispatchEvent(new Event("prerender-trigger")); document.dispatchEvent(new Event("prerender-trigger"));
} }
} else { } else {
setupLinkPrefetching(); setupLinkPrefetching();
}
});
if (module.hot) {
// found this trick in the next.js source code
// https://github.com/zeit/next.js/blob/886037b1bac4bdbfeb689b032c1612750fb593f7/packages/next/client/dev/error-overlay/eventsource.js
// https://github.com/zeit/next.js/blob/886037b1bac4bdbfeb689b032c1612750fb593f7/packages/next/client/dev/dev-build-watcher.js
// more details about this API at https://www.html5rocks.com/en/tutorials/eventsource/basics/
let source = new window.EventSource('/__webpack_hmr')
// source.addEventListener('open', () => { console.log('open!!!!!') })
source.addEventListener('message', (e) => {
// console.log('message!!!!!', e)
// console.log(e.data.action)
// console.log('ACTION', e.data.action);
// if (e.data && e.data.action)
if (event.data === '\uD83D\uDC93') {
// heartbeat
} else {
const obj = JSON.parse(event.data)
// console.log('obj.action', obj.action);
if (obj.action === 'building') {
app.ports.fromJsPort.send({ thingy: 'hmr-check' });
} else if (obj.action === 'built') {
// console.log('httpGet start');
let currentPath = window.location.pathname.replace(/(\w)$/, "$1/")
httpGet(`${window.location.origin}${currentPath}content.json`).then(function (/** @type JSON */ contentJson) {
// console.log('httpGet received');
app.ports.fromJsPort.send({ contentJson: contentJson });
});
}
}
})
} }
return app
}); });
return app
});
} }
function setupLinkPrefetching() { function setupLinkPrefetching() {
@ -132,7 +182,7 @@ function setupLinkPrefetchingHelp(
const links = document.querySelectorAll("a"); const links = document.querySelectorAll("a");
links.forEach(link => { links.forEach(link => {
// console.log(link.pathname); // console.log(link.pathname);
link.addEventListener("mouseenter", function(event) { link.addEventListener("mouseenter", function (event) {
if ( if (
event && event &&
event.target && event.target &&
@ -166,7 +216,9 @@ function prefetchIfNeeded(/** @type {HTMLAnchorElement} */ target) {
} }
} }
/** @typedef {{ name: string; attributes: string[][]; }} HeadTag */ /** @typedef {HeadTag | JsonLdTag} SeoTag */
/** @typedef {{ name: string; attributes: string[][]; type: 'head' }} HeadTag */
function appendTag(/** @type {HeadTag} */ tagDetails) { function appendTag(/** @type {HeadTag} */ tagDetails) {
const meta = document.createElement(tagDetails.name); const meta = document.createElement(tagDetails.name);
tagDetails.attributes.forEach(([name, value]) => { tagDetails.attributes.forEach(([name, value]) => {
@ -175,15 +227,36 @@ function appendTag(/** @type {HeadTag} */ tagDetails) {
document.getElementsByTagName("head")[0].appendChild(meta); document.getElementsByTagName("head")[0].appendChild(meta);
} }
/** @typedef {{ contents: Object; type: 'json-ld' }} JsonLdTag */
function appendJsonLdTag(/** @type {JsonLdTag} */ tagDetails) {
let jsonLdScript = document.createElement('script');
jsonLdScript.type = "application/ld+json";
jsonLdScript.innerHTML = JSON.stringify(tagDetails.contents);
document.getElementsByTagName("head")[0].appendChild(jsonLdScript);
}
function httpGet(/** @type string */ theUrl) { function httpGet(/** @type string */ theUrl) {
return new Promise(function(resolve, reject) { return new Promise(function (resolve, reject) {
const xmlHttp = new XMLHttpRequest(); const xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() { xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
resolve(JSON.parse(xmlHttp.responseText)); resolve(JSON.parse(xmlHttp.responseText));
} }
xmlHttp.onerror = reject; xmlHttp.onerror = reject;
xmlHttp.open("GET", theUrl, true); // true for asynchronous xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.send(null); xmlHttp.send(null);
}) })
} }
/**
* @returns { Promise<DevServerConfig?>}
*/
function getConfig() {
if (module.hot) {
return httpGet(`/elm-pages-dev-server-options`)
} else {
return Promise.resolve(null)
}
}
/** @typedef { { elmDebugger : boolean } } DevServerConfig */

5026
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,9 +22,9 @@
"@babel/core": "^7.5.5", "@babel/core": "^7.5.5",
"@babel/preset-env": "^7.5.5", "@babel/preset-env": "^7.5.5",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"chokidar": "^2.1.5",
"closure-webpack-plugin": "^2.0.1", "closure-webpack-plugin": "^2.0.1",
"copy-webpack-plugin": "^5.0.4", "copy-webpack-plugin": "^5.0.4",
"cross-spawn": "6.0.5",
"css-loader": "^3.2.0", "css-loader": "^3.2.0",
"elm": "^0.19.1-3", "elm": "^0.19.1-3",
"elm-hot-webpack-loader": "^1.1.2", "elm-hot-webpack-loader": "^1.1.2",
@ -32,30 +32,29 @@
"express": "^4.17.1", "express": "^4.17.1",
"favicons-webpack-plugin": "^3.0.0", "favicons-webpack-plugin": "^3.0.0",
"file-loader": "^4.2.0", "file-loader": "^4.2.0",
"find-elm-dependencies": "2.0.2",
"globby": "^10.0.1", "globby": "^10.0.1",
"google-closure-compiler": "^20190909.0.0", "google-closure-compiler": "^20190909.0.0",
"gray-matter": "^4.0.2", "gray-matter": "^4.0.2",
"html-webpack-plugin": "^4.0.0-beta.11", "html-webpack-plugin": "^4.2.0",
"imagemin-mozjpeg": "^8.0.0", "imagemin-mozjpeg": "^8.0.0",
"imagemin-webpack-plugin": "^2.4.2", "imagemin-webpack-plugin": "^2.4.2",
"lodash": "4.17.15",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"prerender-spa-plugin": "^3.4.0", "prerender-spa-plugin": "^3.4.0",
"raw-loader": "^4.0.0",
"sass-loader": "^8.0.0", "sass-loader": "^8.0.0",
"script-ext-html-webpack-plugin": "^2.1.4", "script-ext-html-webpack-plugin": "^2.1.4",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"webpack": "^4.41.5", "temp": "^0.9.0",
"webpack": "4.42.1",
"webpack-dev-middleware": "^3.7.0", "webpack-dev-middleware": "^3.7.0",
"webpack-hot-middleware": "^2.25.0", "webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.1", "webpack-merge": "^4.2.1",
"workbox-webpack-plugin": "^4.3.1", "workbox-webpack-plugin": "^4.3.1",
"xhr2": "^0.2.0", "xhr2": "^0.2.0"
"cross-spawn": "6.0.5",
"find-elm-dependencies": "2.0.2",
"lodash": "4.17.15",
"temp": "^0.9.0"
}, },
"devDependencies": { "devDependencies": {
"@types/chokidar": "^2.1.3",
"@types/express": "^4.17.0", "@types/express": "^4.17.0",
"@types/node": "^12.7.7", "@types/node": "^12.7.7",
"@types/webpack": "^4.32.1", "@types/webpack": "^4.32.1",

View File

@ -1,6 +1,7 @@
module Head exposing module Head exposing
( Tag, metaName, metaProperty ( Tag, metaName, metaProperty
, rssLink, sitemapLink , rssLink, sitemapLink
, structuredData
, AttributeValue , AttributeValue
, currentPageFullUrl, fullImageUrl, fullPageUrl, raw , currentPageFullUrl, fullImageUrl, fullPageUrl, raw
, toJson, canonicalLink , toJson, canonicalLink
@ -19,6 +20,11 @@ writing a plugin package to extend `elm-pages`.
@docs rssLink, sitemapLink @docs rssLink, sitemapLink
## Structured Data
@docs structuredData
## `AttributeValue`s ## `AttributeValue`s
@docs AttributeValue @docs AttributeValue
@ -42,6 +48,7 @@ through the `head` function.
-} -}
type Tag pathKey type Tag pathKey
= Tag (Details pathKey) = Tag (Details pathKey)
| StructuredData Json.Encode.Value
type alias Details pathKey = type alias Details pathKey =
@ -50,6 +57,100 @@ type alias Details pathKey =
} }
{-| You can learn more about structured data in [Google's intro to structured data](https://developers.google.com/search/docs/guides/intro-structured-data).
When you add a `structuredData` item to one of your pages in `elm-pages`, it will add `json-ld` data to your document that looks like this:
```html
<script type="application/ld+json">
{
"@context":"http://schema.org/",
"@type":"Article",
"headline":"Extensible Markdown Parsing in Pure Elm",
"description":"Introducing a new parser that extends your palette with no additional syntax",
"image":"https://elm-pages.com/images/article-covers/extensible-markdown-parsing.jpg",
"author":{
"@type":"Person",
"name":"Dillon Kearns"
},
"publisher":{
"@type":"Person",
"name":"Dillon Kearns"
},
"url":"https://elm-pages.com/blog/extensible-markdown-parsing-in-elm",
"datePublished":"2019-10-08",
"mainEntityOfPage":{
"@type":"SoftwareSourceCode",
"codeRepository":"https://github.com/dillonkearns/elm-pages",
"description":"A statically typed site generator for Elm.",
"author":"Dillon Kearns",
"programmingLanguage":{
"@type":"ComputerLanguage",
"url":"http://elm-lang.org/",
"name":"Elm",
"image":"http://elm-lang.org/",
"identifier":"http://elm-lang.org/"
}
}
}
</script>
```
To get that data, you would write this in your `elm-pages` head tags:
import Json.Encode as Encode
{-| <https://schema.org/Article>
-}
encodeArticle :
{ title : String
, description : String
, author : StructuredData { authorMemberOf | personOrOrganization : () } authorPossibleFields
, publisher : StructuredData { publisherMemberOf | personOrOrganization : () } publisherPossibleFields
, url : String
, imageUrl : String
, datePublished : String
, mainEntityOfPage : Encode.Value
}
-> Head.Tag pathKey
encodeArticle info =
Encode.object
[ ( "@context", Encode.string "http://schema.org/" )
, ( "@type", Encode.string "Article" )
, ( "headline", Encode.string info.title )
, ( "description", Encode.string info.description )
, ( "image", Encode.string info.imageUrl )
, ( "author", encode info.author )
, ( "publisher", encode info.publisher )
, ( "url", Encode.string info.url )
, ( "datePublished", Encode.string info.datePublished )
, ( "mainEntityOfPage", info.mainEntityOfPage )
]
|> Head.structuredData
Take a look at this [Google Search Gallery](https://developers.google.com/search/docs/guides/search-gallery)
to see some examples of how structured data can be used by search engines to give rich search results. It can help boost
your rankings, get better engagement for your content, and also make your content more accessible. For example,
voice assistant devices can make use of structured data. If you're hosting a conference and want to make the event
date and location easy for attendees to find, this can make that information more accessible.
For the current version of API, you'll need to make sure that the format is correct and contains the required and recommended
structure.
Check out <https://schema.org> for a comprehensive listing of possible data types and fields. And take a look at
Google's [Structured Data Testing Tool](https://search.google.com/structured-data/testing-tool)
too make sure that your structured data is valid and includes the recommended values.
In the future, `elm-pages` will likely support a typed API, but schema.org is a massive spec, and changes frequently.
And there are multiple sources of information on the possible and recommended structure. So it will take some time
for the right API design to evolve. In the meantime, this allows you to make use of this for SEO purposes.
-}
structuredData : Json.Encode.Value -> Tag pathKey
structuredData value =
StructuredData value
{-| Create a raw `AttributeValue` (as opposed to some kind of absolute URL). {-| Create a raw `AttributeValue` (as opposed to some kind of absolute URL).
-} -}
raw : String -> AttributeValue pathKey raw : String -> AttributeValue pathKey
@ -196,11 +297,20 @@ node name attributes =
code will run this for you to generate your `manifest.json` file automatically! code will run this for you to generate your `manifest.json` file automatically!
-} -}
toJson : String -> String -> Tag pathKey -> Json.Encode.Value toJson : String -> String -> Tag pathKey -> Json.Encode.Value
toJson canonicalSiteUrl currentPagePath (Tag tag) = toJson canonicalSiteUrl currentPagePath tag =
Json.Encode.object case tag of
[ ( "name", Json.Encode.string tag.name ) Tag headTag ->
, ( "attributes", Json.Encode.list (encodeProperty canonicalSiteUrl currentPagePath) tag.attributes ) Json.Encode.object
] [ ( "name", Json.Encode.string headTag.name )
, ( "attributes", Json.Encode.list (encodeProperty canonicalSiteUrl currentPagePath) headTag.attributes )
, ( "type", Json.Encode.string "head" )
]
StructuredData value ->
Json.Encode.object
[ ( "contents", value )
, ( "type", Json.Encode.string "json-ld" )
]
encodeProperty : String -> String -> ( String, AttributeValue pathKey ) -> Json.Encode.Value encodeProperty : String -> String -> ( String, AttributeValue pathKey ) -> Json.Encode.Value

View 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
View 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

View 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

View File

@ -21,16 +21,12 @@ import Html exposing (Html)
import Html.Attributes as Attr import Html.Attributes as Attr
import Http import Http
import Json.Decode as Decode import Json.Decode as Decode
import Mark
import Mark.Error
import Pages.Document as Document exposing (Document) import Pages.Document as Document exposing (Document)
import Pages.Internal.String as String import Pages.Internal.String as String
import Pages.PagePath as PagePath exposing (PagePath) import Pages.PagePath as PagePath exposing (PagePath)
import Result.Extra
import Task exposing (Task) import Task exposing (Task)
import TerminalText as Terminal import TerminalText as Terminal
import Url exposing (Url) import Url exposing (Url)
import Url.Builder
type alias Content = type alias Content =
@ -283,16 +279,6 @@ type alias Page metadata view pathKey =
} }
renderErrors : ( List String, List Mark.Error.Error ) -> Html msg
renderErrors ( path, errors ) =
Html.div []
[ Html.text (String.join "/" path)
, errors
|> List.map (Mark.Error.toHtml Mark.Error.Light)
|> Html.div []
]
combineTupleResults : combineTupleResults :
List ( List String, Result error success ) List ( List String, Result error success )
-> Result (List error) (List ( List String, success )) -> Result (List error) (List ( List String, success ))

View File

@ -1,6 +1,6 @@
module Pages.Document exposing module Pages.Document exposing
( Document, DocumentHandler ( Document, DocumentHandler
, parser, markupParser , parser
, fromList, get , fromList, get
) )
@ -80,7 +80,7 @@ Hello!!!
) )
@docs Document, DocumentHandler @docs Document, DocumentHandler
@docs parser, markupParser @docs parser
## Functions for use by generated code ## Functions for use by generated code
@ -92,8 +92,6 @@ Hello!!!
import Dict exposing (Dict) import Dict exposing (Dict)
import Html exposing (Html) import Html exposing (Html)
import Json.Decode import Json.Decode
import Mark
import Mark.Error
{-| Represents all of the `DocumentHandler`s. You register a handler for each {-| Represents all of the `DocumentHandler`s. You register a handler for each
@ -157,54 +155,3 @@ parser { extension, body, metadata } =
|> Result.mapError Json.Decode.errorToString |> Result.mapError Json.Decode.errorToString
} }
) )
{-| Register an [`elm-markup`](https://github.com/mdgriffith/elm-markup/)
parser for your `.emu` files.
-}
markupParser :
Mark.Document metadata
-> Mark.Document view
-> ( String, DocumentHandler metadata view )
markupParser metadataParser markBodyParser =
( "emu"
, DocumentHandler
{ contentParser = renderMarkup markBodyParser
, frontmatterParser =
\frontMatter ->
Mark.compile metadataParser
frontMatter
|> (\outcome ->
case outcome of
Mark.Success parsedMetadata ->
Ok parsedMetadata
Mark.Failure failure ->
Err "Failure"
Mark.Almost failure ->
Err "Almost failure"
)
}
)
renderMarkup : Mark.Document view -> String -> Result String view
renderMarkup markBodyParser markupBody =
Mark.compile
markBodyParser
(markupBody |> String.trimLeft)
|> (\outcome ->
case outcome of
Mark.Success renderedView ->
Ok renderedView
Mark.Failure failures ->
failures
|> List.map Mark.Error.toString
|> String.join "\n"
|> Err
Mark.Almost failure ->
Err "TODO almost failure"
)

View File

@ -13,11 +13,12 @@ that is in the generated `Pages` module (see <Pages.Platform>).
-} -}
import Json.Decode
import Json.Encode import Json.Encode
import Pages.Internal.Platform import Pages.Internal.Platform
{-| Internal detial to track whether to run the CLI step or the runtime step in the browser. {-| Internal detail to track whether to run the CLI step or the runtime step in the browser.
-} -}
type ApplicationType type ApplicationType
= Browser = Browser
@ -31,4 +32,5 @@ type alias Internal pathKey =
, content : Pages.Internal.Platform.Content , content : Pages.Internal.Platform.Content
, pathKey : pathKey , pathKey : pathKey
, toJsPort : Json.Encode.Value -> Cmd Never , toJsPort : Json.Encode.Value -> Cmd Never
, fromJsPort : Sub Json.Decode.Value
} }

View File

@ -0,0 +1,6 @@
module Pages.Internal.ApplicationType exposing (ApplicationType(..))
type ApplicationType
= Browser
| Cli

View 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"
]
]

View File

@ -1,4 +1,4 @@
module Pages.Internal.Platform exposing (Content, Flags, Model, Msg, Page, Parser, Program, application, cliApplication) module Pages.Internal.Platform exposing (Content, Flags, Model, Msg, Page, Program, application, cliApplication)
import Browser import Browser
import Browser.Dom as Dom import Browser.Dom as Dom
@ -6,14 +6,15 @@ import Browser.Navigation
import Dict exposing (Dict) import Dict exposing (Dict)
import Head import Head
import Html exposing (Html) import Html exposing (Html)
import Html.Attributes import Html.Attributes exposing (style)
import Html.Lazy
import Http import Http
import Json.Decode as Decode import Json.Decode as Decode
import Json.Encode import Json.Encode
import List.Extra
import Mark
import Pages.ContentCache as ContentCache exposing (ContentCache) import Pages.ContentCache as ContentCache exposing (ContentCache)
import Pages.Document import Pages.Document
import Pages.Internal.ApplicationType as ApplicationType
import Pages.Internal.HotReloadLoadingIndicator as HotReloadLoadingIndicator
import Pages.Internal.Platform.Cli import Pages.Internal.Platform.Cli
import Pages.Internal.String as String import Pages.Internal.String as String
import Pages.Manifest as Manifest import Pages.Manifest as Manifest
@ -122,7 +123,7 @@ pageViewOrError pathKey viewFn model cache =
-- TODO handle error better -- TODO handle error better
) )
|> (\request -> |> (\request ->
StaticHttpRequest.resolve request viewResult.staticData StaticHttpRequest.resolve ApplicationType.Browser request viewResult.staticData
) )
in in
case viewResult.body of case viewResult.body of
@ -146,6 +147,12 @@ pageViewOrError pathKey viewFn model cache =
[ Html.text "I'm missing some StaticHttp data for this page:" [ Html.text "I'm missing some StaticHttp data for this page:"
, Html.pre [] [ Html.text missingKey ] , Html.pre [] [ Html.text missingKey ]
] ]
StaticHttpRequest.UserCalledStaticHttpFail message ->
Html.div []
[ Html.text "I ran into a call to `Pages.StaticHttp.fail` with message:"
, Html.pre [] [ Html.text message ]
]
} }
Err error -> Err error ->
@ -198,10 +205,28 @@ view pathKey content viewFn model =
, body = , body =
[ onViewChangeElement model.url [ onViewChangeElement model.url
, body |> Html.map UserMsg |> Html.map AppMsg , body |> Html.map UserMsg |> Html.map AppMsg
, Html.Lazy.lazy2 loadingView model.phase model.hmrStatus
] ]
} }
loadingView : Phase -> HmrStatus -> Html msg
loadingView phase hmrStatus =
case phase of
DevClient isDebugMode ->
(case hmrStatus of
HmrLoading ->
True
_ ->
False
)
|> HotReloadLoadingIndicator.view isDebugMode
_ ->
Html.text ""
onViewChangeElement currentUrl = onViewChangeElement currentUrl =
-- this is a hidden tag -- this is a hidden tag
-- it is used from the JS-side to reliably -- it is used from the JS-side to reliably
@ -224,6 +249,13 @@ type alias ContentJson =
} }
contentJsonDecoder : Decode.Decoder ContentJson
contentJsonDecoder =
Decode.map2 ContentJson
(Decode.field "body" Decode.string)
(Decode.field "staticData" (Decode.dict Decode.string))
init : init :
pathKey pathKey
-> String -> String
@ -273,12 +305,6 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder) |> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder)
|> Result.toMaybe |> Result.toMaybe
contentJsonDecoder : Decode.Decoder ContentJson
contentJsonDecoder =
Decode.map2 ContentJson
(Decode.field "body" Decode.string)
(Decode.field "staticData" (Decode.dict Decode.string))
baseUrl = baseUrl =
flags flags
|> Decode.decodeValue (Decode.field "baseUrl" Decode.string) |> Decode.decodeValue (Decode.field "baseUrl" Decode.string)
@ -295,15 +321,26 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
Ok okCache -> Ok okCache ->
let let
phase = phase =
case Decode.decodeValue (Decode.field "isPrerendering" Decode.bool) flags of case
Ok True -> Decode.decodeValue
(Decode.map3 (\a b c -> ( a, b, c ))
(Decode.field "isPrerendering" Decode.bool)
(Decode.field "isDevServer" Decode.bool)
(Decode.field "isElmDebugMode" Decode.bool)
)
flags
of
Ok ( True, _, _ ) ->
Prerender Prerender
Ok False -> Ok ( False, True, isElmDebugMode ) ->
Client DevClient isElmDebugMode
Ok ( False, False, _ ) ->
ProdClient
Err _ -> Err _ ->
Client DevClient False
( userModel, userCmd ) = ( userModel, userCmd ) =
maybePagePath maybePagePath
@ -347,6 +384,7 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
, userModel = userModel , userModel = userModel
, contentCache = contentCache , contentCache = contentCache
, phase = phase , phase = phase
, hmrStatus = HmrLoaded
} }
, cmd , cmd
) )
@ -361,7 +399,8 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
, baseUrl = baseUrl , baseUrl = baseUrl
, userModel = userModel , userModel = userModel
, contentCache = contentCache , contentCache = contentCache
, phase = Client , phase = DevClient False
, hmrStatus = HmrLoaded
} }
, Cmd.batch , Cmd.batch
[ userCmd |> Cmd.map UserMsg [ userCmd |> Cmd.map UserMsg
@ -389,7 +428,10 @@ type AppMsg userMsg metadata view
| UserMsg userMsg | UserMsg userMsg
| UpdateCache (Result Http.Error (ContentCache metadata view)) | UpdateCache (Result Http.Error (ContentCache metadata view))
| UpdateCacheAndUrl Url (Result Http.Error (ContentCache metadata view)) | UpdateCacheAndUrl Url (Result Http.Error (ContentCache metadata view))
| UpdateCacheForHotReload (Result Http.Error (ContentCache metadata view))
| PageScrollComplete | PageScrollComplete
| HotReloadComplete ContentJson
| StartingHotReload
type Model userModel userMsg metadata view type Model userModel userMsg metadata view
@ -404,16 +446,19 @@ type alias ModelDetails userModel metadata view =
, contentCache : ContentCache metadata view , contentCache : ContentCache metadata view
, userModel : userModel , userModel : userModel
, phase : Phase , phase : Phase
, hmrStatus : HmrStatus
} }
type Phase type Phase
= Prerender = Prerender
| Client | DevClient Bool
| ProdClient
update : update :
List String Content
-> List String
-> String -> String
-> ->
(List ( PagePath pathKey, metadata ) (List ( PagePath pathKey, metadata )
@ -442,7 +487,7 @@ update :
-> Msg userMsg metadata view -> Msg userMsg metadata view
-> ModelDetails userModel metadata view -> ModelDetails userModel metadata view
-> ( ModelDetails userModel metadata view, Cmd (AppMsg userMsg metadata view) ) -> ( ModelDetails userModel metadata view, Cmd (AppMsg userMsg metadata view) )
update allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJsPort document userUpdate msg model = update content allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJsPort document userUpdate msg model =
case msg of case msg of
AppMsg appMsg -> AppMsg appMsg ->
case appMsg of case appMsg of
@ -535,7 +580,7 @@ update allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJs
) )
{ path = pagePath, frontmatter = frontmatter } { path = pagePath, frontmatter = frontmatter }
|> (\request -> |> (\request ->
StaticHttpRequest.resolve request staticDataThing StaticHttpRequest.resolve ApplicationType.Browser request staticDataThing
) )
in in
( { model | contentCache = updatedCache } ( { model | contentCache = updatedCache }
@ -584,18 +629,42 @@ update allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChangeMsg toJs
-- TODO handle error -- TODO handle error
( { model | url = url }, Cmd.none ) ( { model | url = url }, Cmd.none )
UpdateCacheForHotReload cacheUpdateResult ->
case cacheUpdateResult of
Ok updatedCache ->
( { model | contentCache = updatedCache }, Cmd.none )
Err _ ->
-- TODO handle error
( model, Cmd.none )
PageScrollComplete -> PageScrollComplete ->
( model, Cmd.none ) ( model, Cmd.none )
HotReloadComplete contentJson ->
( { model
| contentCache = ContentCache.init document content (Just { contentJson = contentJson, initialUrl = model.url })
, hmrStatus = HmrLoaded
}
, Cmd.none
-- ContentCache.init document content (Maybe.map (\cj -> { contentJson = contentJson, initialUrl = model.url }) Nothing)
--|> ContentCache.lazyLoad document
-- { currentUrl = model.url
-- , baseUrl = model.baseUrl
-- }
--|> Task.attempt UpdateCacheForHotReload
)
StartingHotReload ->
( { model | hmrStatus = HmrLoading }, Cmd.none )
CliMsg _ -> CliMsg _ ->
( model, Cmd.none ) ( model, Cmd.none )
type alias Parser metadata view = type HmrStatus
Dict String String = HmrLoading
-> List String | HmrLoaded
-> List ( List String, metadata )
-> Mark.Document view
application : application :
@ -622,6 +691,7 @@ application :
, document : Pages.Document.Document metadata view , document : Pages.Document.Document metadata view
, content : Content , content : Content
, toJsPort : Json.Encode.Value -> Cmd Never , toJsPort : Json.Encode.Value -> Cmd Never
, fromJsPort : Sub Decode.Value
, manifest : Manifest.Config pathKey , manifest : Manifest.Config pathKey
, generateFiles : , generateFiles :
List List
@ -678,7 +748,7 @@ application config =
Prerender -> Prerender ->
noOpUpdate noOpUpdate
Client -> _ ->
config.update config.update
noOpUpdate = noOpUpdate =
@ -690,7 +760,7 @@ application config =
|> List.map Tuple.first |> List.map Tuple.first
|> List.map (String.join "/") |> List.map (String.join "/")
in in
update allRoutes config.canonicalSiteUrl config.view config.pathKey config.onPageChange config.toJsPort config.document userUpdate msg model update config.content allRoutes config.canonicalSiteUrl config.view config.pathKey config.onPageChange config.toJsPort config.document userUpdate msg model
|> Tuple.mapFirst Model |> Tuple.mapFirst Model
|> Tuple.mapSecond (Cmd.map AppMsg) |> Tuple.mapSecond (Cmd.map AppMsg)
@ -700,9 +770,27 @@ application config =
\outerModel -> \outerModel ->
case outerModel of case outerModel of
Model model -> Model model ->
config.subscriptions model.userModel Sub.batch
|> Sub.map UserMsg [ config.subscriptions model.userModel
|> Sub.map AppMsg |> Sub.map UserMsg
|> Sub.map AppMsg
, config.fromJsPort
|> Sub.map
(\decodeValue ->
case decodeValue |> Decode.decodeValue (Decode.field "thingy" Decode.string) of
Ok "hmr-check" ->
AppMsg StartingHotReload
_ ->
case decodeValue |> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder) of
Ok contentJson ->
AppMsg (HotReloadComplete contentJson)
Err error ->
-- TODO should be no message here
AppMsg StartingHotReload
)
]
CliModel _ -> CliModel _ ->
Sub.none Sub.none
@ -735,6 +823,7 @@ cliApplication :
, document : Pages.Document.Document metadata view , document : Pages.Document.Document metadata view
, content : Content , content : Content
, toJsPort : Json.Encode.Value -> Cmd Never , toJsPort : Json.Encode.Value -> Cmd Never
, fromJsPort : Sub Decode.Value
, manifest : Manifest.Config pathKey , manifest : Manifest.Config pathKey
, generateFiles : , generateFiles :
List List

View File

@ -26,6 +26,7 @@ import Pages.ContentCache as ContentCache exposing (ContentCache)
import Pages.Document import Pages.Document
import Pages.Http import Pages.Http
import Pages.ImagePath as ImagePath import Pages.ImagePath as ImagePath
import Pages.Internal.ApplicationType as ApplicationType exposing (ApplicationType)
import Pages.Internal.StaticHttpBody as StaticHttpBody import Pages.Internal.StaticHttpBody as StaticHttpBody
import Pages.Manifest as Manifest import Pages.Manifest as Manifest
import Pages.PagePath as PagePath exposing (PagePath) import Pages.PagePath as PagePath exposing (PagePath)
@ -47,6 +48,7 @@ type alias ToJsSuccessPayload pathKey =
{ pages : Dict String (Dict String String) { pages : Dict String (Dict String String)
, manifest : Manifest.Config pathKey , manifest : Manifest.Config pathKey
, filesToGenerate : List FileToGenerate , filesToGenerate : List FileToGenerate
, staticHttpCache : Dict String String
, errors : List String , errors : List String
} }
@ -65,8 +67,8 @@ toJsCodec =
Errors errorList -> Errors errorList ->
errorsTag errorList errorsTag errorList
Success { pages, manifest, filesToGenerate, errors } -> Success { pages, manifest, filesToGenerate, errors, staticHttpCache } ->
success (ToJsSuccessPayload pages manifest filesToGenerate errors) success (ToJsSuccessPayload pages manifest filesToGenerate staticHttpCache errors)
) )
|> Codec.variant1 "Errors" Errors Codec.string |> Codec.variant1 "Errors" Errors Codec.string
|> Codec.variant1 "Success" |> Codec.variant1 "Success"
@ -115,6 +117,9 @@ successCodec =
) )
(Decode.succeed []) (Decode.succeed [])
) )
|> Codec.field "staticHttpCache"
.staticHttpCache
(Codec.dict Codec.string)
|> Codec.field "errors" .errors (Codec.list Codec.string) |> Codec.field "errors" .errors (Codec.list Codec.string)
|> Codec.buildObject |> Codec.buildObject
@ -178,6 +183,7 @@ type alias Config pathKey userMsg userModel metadata view =
, document : Pages.Document.Document metadata view , document : Pages.Document.Document metadata view
, content : Content , content : Content
, toJsPort : Json.Encode.Value -> Cmd Never , toJsPort : Json.Encode.Value -> Cmd Never
, fromJsPort : Sub Decode.Value
, manifest : Manifest.Config pathKey , manifest : Manifest.Config pathKey
, generateFiles : , generateFiles :
List List
@ -320,13 +326,20 @@ init :
init toModel contentCache siteMetadata config flags = init toModel contentCache siteMetadata config flags =
case case
Decode.decodeValue Decode.decodeValue
(Decode.map2 Tuple.pair (Decode.map3 (\a b c -> ( a, b, c ))
(Decode.field "secrets" SecretsDict.decoder) (Decode.field "secrets" SecretsDict.decoder)
(Decode.field "mode" modeDecoder) (Decode.field "mode" modeDecoder)
(Decode.field "staticHttpCache"
(Decode.dict
(Decode.string
|> Decode.map Just
)
)
)
) )
flags flags
of of
Ok ( secrets, mode ) -> Ok ( secrets, mode, staticHttpCache ) ->
case contentCache of case contentCache of
Ok _ -> Ok _ ->
case ContentCache.pagesWithErrors contentCache of case ContentCache.pagesWithErrors contentCache of
@ -343,14 +356,14 @@ init toModel contentCache siteMetadata config flags =
staticResponses = staticResponses =
case requests of case requests of
Ok okRequests -> Ok okRequests ->
staticResponsesInit siteMetadata config okRequests staticResponsesInit staticHttpCache siteMetadata config okRequests
Err errors -> Err errors ->
-- TODO need to handle errors better? -- TODO need to handle errors better?
staticResponsesInit siteMetadata config [] staticResponsesInit staticHttpCache siteMetadata config []
( updatedRawResponses, effect ) = ( updatedRawResponses, effect ) =
sendStaticResponsesIfDone config siteMetadata mode secrets Dict.empty [] staticResponses sendStaticResponsesIfDone config siteMetadata mode secrets staticHttpCache [] staticResponses
in in
( Model staticResponses secrets [] updatedRawResponses mode |> toModel ( Model staticResponses secrets [] updatedRawResponses mode |> toModel
, effect , effect
@ -369,11 +382,11 @@ init toModel contentCache siteMetadata config flags =
staticResponses = staticResponses =
case requests of case requests of
Ok okRequests -> Ok okRequests ->
staticResponsesInit siteMetadata config okRequests staticResponsesInit staticHttpCache siteMetadata config okRequests
Err errors -> Err errors ->
-- TODO need to handle errors better? -- TODO need to handle errors better?
staticResponsesInit siteMetadata config [] staticResponsesInit staticHttpCache siteMetadata config []
in in
updateAndSendPortIfDone updateAndSendPortIfDone
config config
@ -382,7 +395,7 @@ init toModel contentCache siteMetadata config flags =
staticResponses staticResponses
secrets secrets
pageErrors pageErrors
Dict.empty staticHttpCache
mode mode
) )
toModel toModel
@ -394,7 +407,7 @@ init toModel contentCache siteMetadata config flags =
(Model Dict.empty (Model Dict.empty
secrets secrets
(metadataParserErrors |> List.map Tuple.second) (metadataParserErrors |> List.map Tuple.second)
Dict.empty staticHttpCache
mode mode
) )
toModel toModel
@ -528,7 +541,7 @@ performStaticHttpRequests allRawResponses secrets staticRequests =
(\( pagePath, request ) -> (\( pagePath, request ) ->
allRawResponses allRawResponses
|> dictCompact |> dictCompact
|> StaticHttpRequest.resolveUrls request |> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|> Tuple.second |> Tuple.second
) )
|> List.concat |> List.concat
@ -580,18 +593,23 @@ cliDictKey =
"////elm-pages-CLI////" "////elm-pages-CLI////"
staticResponsesInit : Result (List BuildError) (List ( PagePath pathKey, metadata )) -> Config pathKey userMsg userModel metadata view -> List ( PagePath pathKey, StaticHttp.Request value ) -> StaticResponses staticResponsesInit : Dict String (Maybe String) -> Result (List BuildError) (List ( PagePath pathKey, metadata )) -> Config pathKey userMsg userModel metadata view -> List ( PagePath pathKey, StaticHttp.Request value ) -> StaticResponses
staticResponsesInit siteMetadata config list = staticResponsesInit staticHttpCache siteMetadataResult config list =
let let
foo : StaticHttp.Request (List (Result String { path : List String, content : String })) generateFilesRequest : StaticHttp.Request (List (Result String { path : List String, content : String }))
foo = generateFilesRequest =
config.generateFiles thing2 config.generateFiles siteMetadataWithContent
generateFilesStaticRequest = generateFilesStaticRequest =
( cliDictKey, NotFetched (foo |> StaticHttp.map (\_ -> ())) Dict.empty ) ( -- we don't want to include the CLI-only StaticHttp responses in the production bundle
-- since that data is only needed to run these functions during the build step
-- in the future, this could be refactored to have a type to represent this more clearly
cliDictKey
, NotFetched (generateFilesRequest |> StaticHttp.map (\_ -> ())) Dict.empty
)
thing2 = siteMetadataWithContent =
siteMetadata siteMetadataResult
|> Result.withDefault [] |> Result.withDefault []
|> List.map |> List.map
(\( pagePath, metadata ) -> (\( pagePath, metadata ) ->
@ -625,8 +643,26 @@ staticResponsesInit siteMetadata config list =
list list
|> List.map |> List.map
(\( path, staticRequest ) -> (\( path, staticRequest ) ->
let
entry =
NotFetched (staticRequest |> StaticHttp.map (\_ -> ())) Dict.empty
updatedEntry =
staticHttpCache
|> dictCompact
|> Dict.toList
|> List.foldl
(\( hashedRequest, response ) entrySoFar ->
entrySoFar
|> addEntry
staticHttpCache
hashedRequest
(Ok response)
)
entry
in
( PagePath.toString path ( PagePath.toString path
, NotFetched (staticRequest |> StaticHttp.map (\_ -> ())) Dict.empty , updatedEntry
) )
) )
|> List.append [ generateFilesStaticRequest ] |> List.append [ generateFilesStaticRequest ]
@ -654,7 +690,7 @@ staticResponsesUpdate newEntry model =
realUrls = realUrls =
updatedAllResponses updatedAllResponses
|> dictCompact |> dictCompact
|> StaticHttpRequest.resolveUrls request |> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|> Tuple.second |> Tuple.second
|> List.map Secrets.maskedLookup |> List.map Secrets.maskedLookup
|> List.map HashRequest.hash |> List.map HashRequest.hash
@ -681,6 +717,36 @@ staticResponsesUpdate newEntry model =
} }
addEntry : Dict String (Maybe String) -> String -> Result () String -> StaticHttpResult -> StaticHttpResult
addEntry globalRawResponses hashedRequest rawResponse ((NotFetched request rawResponses) as entry) =
let
realUrls =
globalRawResponses
|> dictCompact
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|> Tuple.second
|> List.map Secrets.maskedLookup
|> List.map HashRequest.hash
includesUrl =
List.member
hashedRequest
realUrls
in
if includesUrl then
let
updatedRawResponses =
Dict.insert
hashedRequest
rawResponse
rawResponses
in
NotFetched request updatedRawResponses
else
entry
isJust : Maybe a -> Bool isJust : Maybe a -> Bool
isJust maybeValue = isJust maybeValue =
case maybeValue of case maybeValue of
@ -721,7 +787,7 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
hasPermanentError = hasPermanentError =
usableRawResponses usableRawResponses
|> StaticHttpRequest.permanentError request |> StaticHttpRequest.permanentError ApplicationType.Cli request
|> isJust |> isJust
hasPermanentHttpError = hasPermanentHttpError =
@ -737,7 +803,9 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
-- False -- False
-- ) -- )
( allUrlsKnown, knownUrlsToFetch ) = ( allUrlsKnown, knownUrlsToFetch ) =
StaticHttpRequest.resolveUrls request StaticHttpRequest.resolveUrls
ApplicationType.Cli
request
(rawResponses |> Dict.map (\key value -> value |> Result.withDefault "")) (rawResponses |> Dict.map (\key value -> value |> Result.withDefault ""))
fetchedAllKnownUrls = fetchedAllKnownUrls =
@ -773,7 +841,9 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
) )
maybePermanentError = maybePermanentError =
StaticHttpRequest.permanentError request StaticHttpRequest.permanentError
ApplicationType.Cli
request
usableRawResponses usableRawResponses
decoderErrors = decoderErrors =
@ -893,7 +963,8 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
mythingy2 : Result StaticHttpRequest.Error (List (Result String { path : List String, content : String })) mythingy2 : Result StaticHttpRequest.Error (List (Result String { path : List String, content : String }))
mythingy2 = mythingy2 =
StaticHttpRequest.resolve (config.generateFiles metadataForGenerateFiles) StaticHttpRequest.resolve ApplicationType.Cli
(config.generateFiles metadataForGenerateFiles)
(allRawResponses |> Dict.Extra.filterMap (\key value -> value)) (allRawResponses |> Dict.Extra.filterMap (\key value -> value))
generatedOkayFiles : List { path : List String, content : String } generatedOkayFiles : List { path : List String, content : String }
@ -938,11 +1009,19 @@ sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses error
(encodeStaticResponses mode staticResponses) (encodeStaticResponses mode staticResponses)
config.manifest config.manifest
generatedOkayFiles generatedOkayFiles
allRawResponses
allErrors allErrors
) )
toJsPayload encodedStatic manifest generated allErrors = toJsPayload :
Dict String (Dict String String)
-> Manifest.Config pathKey
-> List FileToGenerate
-> Dict String (Maybe String)
-> List { title : String, message : List Terminal.Text, fatal : Bool }
-> Effect pathKey
toJsPayload encodedStatic manifest generated allRawResponses allErrors =
SendJsData <| SendJsData <|
if allErrors |> List.filter .fatal |> List.isEmpty then if allErrors |> List.filter .fatal |> List.isEmpty then
Success Success
@ -950,6 +1029,15 @@ toJsPayload encodedStatic manifest generated allErrors =
encodedStatic encodedStatic
manifest manifest
generated generated
(allRawResponses
|> Dict.toList
|> List.filterMap
(\( key, maybeValue ) ->
maybeValue
|> Maybe.map (\value -> ( key, value ))
)
|> Dict.fromList
)
(List.map BuildError.errorToString allErrors) (List.map BuildError.errorToString allErrors)
) )
@ -980,7 +1068,7 @@ encodeStaticResponses mode staticResponses =
strippedResponses : Dict String String strippedResponses : Dict String String
strippedResponses = strippedResponses =
-- TODO should this return an Err and handle that here? -- TODO should this return an Err and handle that here?
StaticHttpRequest.strippedResponses request relevantResponses StaticHttpRequest.strippedResponses ApplicationType.Cli request relevantResponses
in in
case mode of case mode of
Dev -> Dev ->

View File

@ -322,6 +322,7 @@ application config =
, content = config.internals.content , content = config.internals.content
, generateFiles = config.generateFiles , generateFiles = config.generateFiles
, toJsPort = config.internals.toJsPort , toJsPort = config.internals.toJsPort
, fromJsPort = config.internals.fromJsPort
, manifest = config.manifest , manifest = config.manifest
, canonicalSiteUrl = config.canonicalSiteUrl , canonicalSiteUrl = config.canonicalSiteUrl
, onPageChange = config.onPageChange , onPageChange = config.onPageChange

View File

@ -1,7 +1,7 @@
module Pages.StaticHttp exposing module Pages.StaticHttp exposing
( Request, RequestDetails ( Request, RequestDetails
, get, request , get, request
, map, succeed , map, succeed, fail
, Body, emptyBody, stringBody, jsonBody , Body, emptyBody, stringBody, jsonBody
, andThen, resolve, combine , andThen, resolve, combine
, map2, map3, map4, map5, map6, map7, map8, map9 , map2, map3, map4, map5, map6, map7, map8, map9
@ -40,7 +40,7 @@ in [this article introducing StaticHttp requests and some concepts around it](ht
@docs Request, RequestDetails @docs Request, RequestDetails
@docs get, request @docs get, request
@docs map, succeed @docs map, succeed, fail
## Building a StaticHttp Request Body ## Building a StaticHttp Request Body
@ -76,9 +76,12 @@ your decoders. This can significantly reduce download sizes for your StaticHttp
import Dict exposing (Dict) import Dict exposing (Dict)
import Dict.Extra import Dict.Extra
import Internal.OptimizedDecoder
import Json.Decode import Json.Decode
import Json.Decode.Exploration as Decode exposing (Decoder) import Json.Decode.Exploration
import Json.Encode as Encode import Json.Encode as Encode
import OptimizedDecoder as Decode exposing (Decoder)
import Pages.Internal.ApplicationType as ApplicationType exposing (ApplicationType)
import Pages.Internal.StaticHttpBody as Body import Pages.Internal.StaticHttpBody as Body
import Pages.Secrets import Pages.Secrets
import Pages.StaticHttp.Request as HashRequest import Pages.StaticHttp.Request as HashRequest
@ -155,8 +158,8 @@ map fn requestInfo =
Request ( urls, lookupFn ) -> Request ( urls, lookupFn ) ->
Request Request
( urls ( urls
, \rawResponses -> , \appType rawResponses ->
lookupFn rawResponses lookupFn appType rawResponses
|> Result.map (\( partiallyStripped, nextRequest ) -> ( partiallyStripped, map fn nextRequest )) |> Result.map (\( partiallyStripped, nextRequest ) -> ( partiallyStripped, map fn nextRequest ))
) )
@ -238,24 +241,24 @@ map2 fn request1 request2 =
case ( request1, request2 ) of case ( request1, request2 ) of
( Request ( urls1, lookupFn1 ), Request ( urls2, lookupFn2 ) ) -> ( Request ( urls1, lookupFn1 ), Request ( urls2, lookupFn2 ) ) ->
let let
value : Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, Request c ) value : ApplicationType -> Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, Request c )
value rawResponses = value appType rawResponses =
let let
value1 = value1 =
lookupFn1 rawResponses lookupFn1 appType rawResponses
|> Result.map Tuple.second |> Result.map Tuple.second
value2 = value2 =
lookupFn2 rawResponses lookupFn2 appType rawResponses
|> Result.map Tuple.second |> Result.map Tuple.second
dict1 = dict1 =
lookupFn1 rawResponses lookupFn1 appType rawResponses
|> Result.map Tuple.first |> Result.map Tuple.first
|> Result.withDefault Dict.empty |> Result.withDefault Dict.empty
dict2 = dict2 =
lookupFn2 rawResponses lookupFn2 appType rawResponses
|> Result.map Tuple.first |> Result.map Tuple.first
|> Result.withDefault Dict.empty |> Result.withDefault Dict.empty
in in
@ -274,14 +277,14 @@ map2 fn request1 request2 =
( Request ( urls1, lookupFn1 ), Done value2 ) -> ( Request ( urls1, lookupFn1 ), Done value2 ) ->
Request Request
( urls1 ( urls1
, \rawResponses -> , \appType rawResponses ->
let let
value1 = value1 =
lookupFn1 rawResponses lookupFn1 appType rawResponses
|> Result.map Tuple.second |> Result.map Tuple.second
dict1 = dict1 =
lookupFn1 rawResponses lookupFn1 appType rawResponses
|> Result.map Tuple.first |> Result.map Tuple.first
|> Result.withDefault Dict.empty |> Result.withDefault Dict.empty
in in
@ -296,14 +299,14 @@ map2 fn request1 request2 =
( Done value2, Request ( urls1, lookupFn1 ) ) -> ( Done value2, Request ( urls1, lookupFn1 ) ) ->
Request Request
( urls1 ( urls1
, \rawResponses -> , \appType rawResponses ->
let let
value1 = value1 =
lookupFn1 rawResponses lookupFn1 appType rawResponses
|> Result.map Tuple.second |> Result.map Tuple.second
dict1 = dict1 =
lookupFn1 rawResponses lookupFn1 appType rawResponses
|> Result.map Tuple.first |> Result.map Tuple.first
|> Result.withDefault Dict.empty |> Result.withDefault Dict.empty
in in
@ -336,14 +339,14 @@ combineReducedDicts dict1 dict2 =
) )
lookup : Pages.StaticHttpRequest.Request value -> Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, value ) lookup : ApplicationType -> Pages.StaticHttpRequest.Request value -> Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, value )
lookup requestInfo rawResponses = lookup appType requestInfo rawResponses =
case requestInfo of case requestInfo of
Request ( urls, lookupFn ) -> Request ( urls, lookupFn ) ->
lookupFn rawResponses lookupFn appType rawResponses
|> Result.andThen |> Result.andThen
(\( strippedResponses, nextRequest ) -> (\( strippedResponses, nextRequest ) ->
lookup lookup appType
(addUrls urls nextRequest) (addUrls urls nextRequest)
strippedResponses strippedResponses
) )
@ -393,8 +396,8 @@ andThen : (a -> Request b) -> Request a -> Request b
andThen fn requestInfo = andThen fn requestInfo =
Request Request
( lookupUrls requestInfo ( lookupUrls requestInfo
, \rawResponses -> , \appType rawResponses ->
lookup lookup appType
requestInfo requestInfo
rawResponses rawResponses
|> (\result -> |> (\result ->
@ -436,11 +439,22 @@ succeed : a -> Request a
succeed value = succeed value =
Request Request
( [] ( []
, \rawResponses -> , \appType rawResponses ->
Ok ( rawResponses, Done value ) Ok ( rawResponses, Done value )
) )
{-| TODO
-}
fail : String -> Request a
fail errorMessage =
Request
( []
, \appType rawResponses ->
Err (Pages.StaticHttpRequest.UserCalledStaticHttpFail errorMessage)
)
{-| A simplified helper around [`StaticHttp.request`](#request), which builds up a StaticHttp GET request. {-| A simplified helper around [`StaticHttp.request`](#request), which builds up a StaticHttp GET request.
import Json.Decode as Decode exposing (Decoder) import Json.Decode as Decode exposing (Decoder)
@ -575,70 +589,104 @@ unoptimizedRequest requestWithSecrets expect =
ExpectJson decoder -> ExpectJson decoder ->
Request Request
( [ requestWithSecrets ] ( [ requestWithSecrets ]
, \rawResponseDict -> , \appType rawResponseDict ->
rawResponseDict case appType of
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash) ApplicationType.Cli ->
|> (\maybeResponse -> rawResponseDict
case maybeResponse of |> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
Just rawResponse -> |> (\maybeResponse ->
Ok case maybeResponse of
( rawResponseDict Just rawResponse ->
-- |> Dict.update url (\maybeValue -> Just """{"fake": 123}""") Ok
, rawResponse ( rawResponseDict
) , rawResponse
)
Nothing -> Nothing ->
Secrets.maskedLookup requestWithSecrets Secrets.maskedLookup requestWithSecrets
|> requestToString |> requestToString
|> Pages.StaticHttpRequest.MissingHttpResponse |> Pages.StaticHttpRequest.MissingHttpResponse
|> Err |> Err
) )
|> Result.andThen |> Result.andThen
(\( strippedResponses, rawResponse ) -> (\( strippedResponses, rawResponse ) ->
let let
reduced = reduced =
Decode.stripString decoder rawResponse Json.Decode.Exploration.stripString (Internal.OptimizedDecoder.jde decoder) rawResponse
|> Result.withDefault "TODO" |> Result.withDefault "TODO"
in in
rawResponse rawResponse
|> Decode.decodeString decoder |> Json.Decode.Exploration.decodeString (decoder |> Internal.OptimizedDecoder.jde)
-- |> Result.mapError Json.Decode.Exploration.errorsToString |> (\decodeResult ->
|> (\decodeResult -> case decodeResult of
case decodeResult of Json.Decode.Exploration.BadJson ->
Decode.BadJson -> Pages.StaticHttpRequest.DecoderError "Payload sent back invalid JSON" |> Err
Pages.StaticHttpRequest.DecoderError "Payload sent back invalid JSON" |> Err
Decode.Errors errors -> Json.Decode.Exploration.Errors errors ->
errors errors
|> Decode.errorsToString |> Json.Decode.Exploration.errorsToString
|> Pages.StaticHttpRequest.DecoderError |> Pages.StaticHttpRequest.DecoderError
|> Err |> Err
Decode.WithWarnings warnings a -> Json.Decode.Exploration.WithWarnings warnings a ->
-- Pages.StaticHttpRequest.DecoderError "" |> Err Ok a
Ok a
Decode.Success a -> Json.Decode.Exploration.Success a ->
Ok a Ok a
) )
-- |> Result.mapError Pages.StaticHttpRequest.DecoderError |> Result.map Done
|> Result.map Done |> Result.map
|> Result.map (\finalRequest ->
(\finalRequest -> ( strippedResponses
( strippedResponses |> Dict.insert
|> Dict.insert (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
(Secrets.maskedLookup requestWithSecrets |> HashRequest.hash) reduced
reduced , finalRequest
, finalRequest )
) )
) )
)
ApplicationType.Browser ->
rawResponseDict
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|> (\maybeResponse ->
case maybeResponse of
Just rawResponse ->
Ok
( rawResponseDict
, rawResponse
)
Nothing ->
Secrets.maskedLookup requestWithSecrets
|> requestToString
|> Pages.StaticHttpRequest.MissingHttpResponse
|> Err
)
|> Result.andThen
(\( strippedResponses, rawResponse ) ->
rawResponse
|> Json.Decode.decodeString (decoder |> Internal.OptimizedDecoder.jd)
|> (\decodeResult ->
case decodeResult of
Err _ ->
Pages.StaticHttpRequest.DecoderError "Payload sent back invalid JSON" |> Err
Ok a ->
Ok a
)
|> Result.map Done
|> Result.map
(\finalRequest ->
( strippedResponses, finalRequest )
)
)
) )
ExpectUnoptimizedJson decoder -> ExpectUnoptimizedJson decoder ->
Request Request
( [ requestWithSecrets ] ( [ requestWithSecrets ]
, \rawResponseDict -> , \appType rawResponseDict ->
rawResponseDict rawResponseDict
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash) |> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|> (\maybeResponse -> |> (\maybeResponse ->
@ -664,7 +712,10 @@ unoptimizedRequest requestWithSecrets expect =
|> (\decodeResult -> |> (\decodeResult ->
case decodeResult of case decodeResult of
Err error -> Err error ->
Pages.StaticHttpRequest.DecoderError "Payload sent back invalid JSON" |> Err error
|> Decode.errorToString
|> Pages.StaticHttpRequest.DecoderError
|> Err
Ok a -> Ok a ->
Ok a Ok a
@ -685,7 +736,7 @@ unoptimizedRequest requestWithSecrets expect =
ExpectString mapStringFn -> ExpectString mapStringFn ->
Request Request
( [ requestWithSecrets ] ( [ requestWithSecrets ]
, \rawResponseDict -> , \appType rawResponseDict ->
rawResponseDict rawResponseDict
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash) |> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|> (\maybeResponse -> |> (\maybeResponse ->

View File

@ -2,26 +2,27 @@ module Pages.StaticHttpRequest exposing (Error(..), Request(..), permanentError,
import BuildError exposing (BuildError) import BuildError exposing (BuildError)
import Dict exposing (Dict) import Dict exposing (Dict)
import Pages.Internal.ApplicationType as ApplicationType exposing (ApplicationType)
import Pages.StaticHttp.Request import Pages.StaticHttp.Request
import Secrets import Secrets
import TerminalText as Terminal import TerminalText as Terminal
type Request value type Request value
= Request ( List (Secrets.Value Pages.StaticHttp.Request.Request), Dict String String -> Result Error ( Dict String String, Request value ) ) = Request ( List (Secrets.Value Pages.StaticHttp.Request.Request), ApplicationType -> Dict String String -> Result Error ( Dict String String, Request value ) )
| Done value | Done value
strippedResponses : Request value -> Dict String String -> Dict String String strippedResponses : ApplicationType -> Request value -> Dict String String -> Dict String String
strippedResponses request rawResponses = strippedResponses appType request rawResponses =
case request of case request of
Request ( list, lookupFn ) -> Request ( list, lookupFn ) ->
case lookupFn rawResponses of case lookupFn appType rawResponses of
Err error -> Err error ->
rawResponses rawResponses
Ok ( partiallyStrippedResponses, followupRequest ) -> Ok ( partiallyStrippedResponses, followupRequest ) ->
strippedResponses followupRequest partiallyStrippedResponses strippedResponses appType followupRequest partiallyStrippedResponses
Done value -> Done value ->
rawResponses rawResponses
@ -30,6 +31,7 @@ strippedResponses request rawResponses =
type Error type Error
= MissingHttpResponse String = MissingHttpResponse String
| DecoderError String | DecoderError String
| UserCalledStaticHttpFail String
urls : Request value -> List (Secrets.Value Pages.StaticHttp.Request.Request) urls : Request value -> List (Secrets.Value Pages.StaticHttp.Request.Request)
@ -65,14 +67,24 @@ toBuildError path error =
, fatal = True , fatal = True
} }
UserCalledStaticHttpFail decodeErrorMessage ->
{ title = "Called Static Http Fail"
, message =
[ Terminal.text path
, Terminal.text "\n\n"
, Terminal.text <| "I ran into a call to `Pages.StaticHttp.fail` with message: " ++ decodeErrorMessage
]
, fatal = True
}
permanentError : Request value -> Dict String String -> Maybe Error
permanentError request rawResponses = permanentError : ApplicationType -> Request value -> Dict String String -> Maybe Error
permanentError appType request rawResponses =
case request of case request of
Request ( urlList, lookupFn ) -> Request ( urlList, lookupFn ) ->
case lookupFn rawResponses of case lookupFn appType rawResponses of
Ok ( partiallyStrippedResponses, nextRequest ) -> Ok ( partiallyStrippedResponses, nextRequest ) ->
permanentError nextRequest rawResponses permanentError appType nextRequest rawResponses
Err error -> Err error ->
case error of case error of
@ -82,17 +94,20 @@ permanentError request rawResponses =
DecoderError _ -> DecoderError _ ->
Just error Just error
UserCalledStaticHttpFail string ->
Just error
Done value -> Done value ->
Nothing Nothing
resolve : Request value -> Dict String String -> Result Error value resolve : ApplicationType -> Request value -> Dict String String -> Result Error value
resolve request rawResponses = resolve appType request rawResponses =
case request of case request of
Request ( urlList, lookupFn ) -> Request ( urlList, lookupFn ) ->
case lookupFn rawResponses of case lookupFn appType rawResponses of
Ok ( partiallyStrippedResponses, nextRequest ) -> Ok ( partiallyStrippedResponses, nextRequest ) ->
resolve nextRequest rawResponses resolve appType nextRequest rawResponses
Err error -> Err error ->
Err error Err error
@ -101,13 +116,13 @@ resolve request rawResponses =
Ok value Ok value
resolveUrls : Request value -> Dict String String -> ( Bool, List (Secrets.Value Pages.StaticHttp.Request.Request) ) resolveUrls : ApplicationType -> Request value -> Dict String String -> ( Bool, List (Secrets.Value Pages.StaticHttp.Request.Request) )
resolveUrls request rawResponses = resolveUrls appType request rawResponses =
case request of case request of
Request ( urlList, lookupFn ) -> Request ( urlList, lookupFn ) ->
case lookupFn rawResponses of case lookupFn appType rawResponses of
Ok ( partiallyStrippedResponses, nextRequest ) -> Ok ( partiallyStrippedResponses, nextRequest ) ->
resolveUrls nextRequest rawResponses resolveUrls appType nextRequest rawResponses
|> Tuple.mapSecond ((++) urlList) |> Tuple.mapSecond ((++) urlList)
Err error -> Err error ->

234
src/StructuredData.elm Normal file
View 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" )
]
)
]

View File

@ -68,10 +68,10 @@ colorToString color =
"[34m" "[34m"
Green -> Green ->
"[32;1m" "[32m"
Yellow -> Yellow ->
"[33;1m" "[33m"
Cyan -> Cyan ->
"[36m" "[36m"

View File

@ -5,7 +5,9 @@ import Dict exposing (Dict)
import Expect import Expect
import Html import Html
import Json.Decode as JD import Json.Decode as JD
import Json.Decode.Exploration as Decode exposing (Decoder) import Json.Decode.Exploration
import Json.Encode as Encode
import OptimizedDecoder as Decode exposing (Decoder)
import Pages.ContentCache as ContentCache import Pages.ContentCache as ContentCache
import Pages.Document as Document import Pages.Document as Document
import Pages.Http import Pages.Http
@ -604,11 +606,68 @@ Body: """)
] ]
) )
] ]
, describe "staticHttpCache"
[ test "it doesn't perform http requests that are provided in the http cache flag" <|
\() ->
startWithHttpCache
[ ( { url = "https://api.github.com/repos/dillonkearns/elm-pages"
, method = "GET"
, headers = []
, body = StaticHttpBody.EmptyBody
}
, """{"stargazer_count":86}"""
)
]
[ ( []
, StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") starDecoder
)
]
|> expectSuccess
[ ( ""
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
)
]
, test "it ignores unused cache" <|
\() ->
startWithHttpCache
[ ( { url = "https://this-is-never-used.example.com/"
, method = "GET"
, headers = []
, body = StaticHttpBody.EmptyBody
}
, """{"stargazer_count":86}"""
)
]
[ ( []
, StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") starDecoder
)
]
|> ProgramTest.simulateHttpOk
"GET"
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86 }"""
|> expectSuccess
[ ( ""
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
)
]
]
] ]
start : List ( List String, StaticHttp.Request a ) -> ProgramTest Main.Model Main.Msg (Main.Effect PathKey) start : List ( List String, StaticHttp.Request a ) -> ProgramTest Main.Model Main.Msg (Main.Effect PathKey)
start pages = start pages =
startWithHttpCache [] pages
startWithHttpCache : List ( Request.Request, String ) -> List ( List String, StaticHttp.Request a ) -> ProgramTest Main.Model Main.Msg (Main.Effect PathKey)
startWithHttpCache staticHttpCache pages =
let let
document = document =
Document.fromList Document.fromList
@ -637,6 +696,7 @@ start pages =
config = config =
{ toJsPort = toJsPort { toJsPort = toJsPort
, fromJsPort = fromJsPort
, manifest = manifest , manifest = manifest
, generateFiles = \_ -> StaticHttp.succeed [] , generateFiles = \_ -> StaticHttp.succeed []
, init = \_ -> ( (), Cmd.none ) , init = \_ -> ( (), Cmd.none )
@ -669,6 +729,30 @@ start pages =
, pathKey = PathKey , pathKey = PathKey
, onPageChange = \_ -> () , onPageChange = \_ -> ()
} }
encodedFlags =
--{"secrets":
-- {"API_KEY": "ABCD1234","BEARER": "XYZ789"}, "mode": "prod", "staticHttpCache": {}
-- }
Encode.object
[ ( "secrets"
, [ ( "API_KEY", "ABCD1234" )
, ( "BEARER", "XYZ789" )
]
|> Dict.fromList
|> Encode.dict identity Encode.string
)
, ( "mode", Encode.string "prod" )
, ( "staticHttpCache", encodedStaticHttpCache )
]
encodedStaticHttpCache =
staticHttpCache
|> List.map
(\( request, httpResponseString ) ->
( Request.hash request, Encode.string httpResponseString )
)
|> Encode.object
in in
{- {-
(Model -> model) (Model -> model)
@ -684,9 +768,7 @@ start pages =
, view = \_ -> { title = "", body = [] } , view = \_ -> { title = "", body = [] }
} }
|> ProgramTest.withSimulatedEffects simulateEffects |> ProgramTest.withSimulatedEffects simulateEffects
|> ProgramTest.start (flags """{"secrets": |> ProgramTest.start (flags (Encode.encode 0 encodedFlags))
{"API_KEY": "ABCD1234","BEARER": "XYZ789"}, "mode": "prod"
}""")
flags : String -> JD.Value flags : String -> JD.Value
@ -780,6 +862,10 @@ toJsPort foo =
Cmd.none Cmd.none
fromJsPort =
Sub.none
type PathKey type PathKey
= PathKey = PathKey
@ -831,27 +917,31 @@ expectSuccess expectedRequests previous =
|> ProgramTest.expectOutgoingPortValues |> ProgramTest.expectOutgoingPortValues
"toJsPort" "toJsPort"
(Codec.decoder Main.toJsCodec) (Codec.decoder Main.toJsCodec)
(Expect.equal (\value ->
[ Main.Success case value of
{ pages = [ Main.Success portPayload ] ->
expectedRequests portPayload.pages
|> List.map |> Expect.equal
(\( url, requests ) -> (expectedRequests
( url |> List.map
, requests (\( url, requests ) ->
|> List.map ( url
(\( request, response ) -> , requests
( Request.hash request, response ) |> List.map
(\( request, response ) ->
( Request.hash request, response )
)
|> Dict.fromList
) )
|> Dict.fromList )
) |> Dict.fromList
) )
|> Dict.fromList
, manifest = manifest [ _ ] ->
, filesToGenerate = [] Expect.fail "Expected success port."
, errors = []
} _ ->
] Expect.fail ("Expected ports to be called once, but instead there were " ++ String.fromInt (List.length value) ++ " calls.")
) )

View File

@ -2,7 +2,9 @@ module StaticHttpUnitTests exposing (all)
import Dict exposing (Dict) import Dict exposing (Dict)
import Expect import Expect
import Json.Decode.Exploration as Decode import Json.Decode.Exploration
import OptimizedDecoder as Decode
import Pages.Internal.ApplicationType as ApplicationType
import Pages.StaticHttp as StaticHttp import Pages.StaticHttp as StaticHttp
import Pages.StaticHttp.Request as Request import Pages.StaticHttp.Request as Request
import Pages.StaticHttpRequest as StaticHttpRequest import Pages.StaticHttpRequest as StaticHttpRequest
@ -42,11 +44,11 @@ all =
StaticHttp.get (Secrets.succeed "first") (Decode.succeed "NEXT") StaticHttp.get (Secrets.succeed "first") (Decode.succeed "NEXT")
|> StaticHttp.andThen |> StaticHttp.andThen
(\continueUrl -> (\continueUrl ->
-- StaticHttp.get continueUrl (Decode.succeed ())
getWithoutSecrets "NEXT" (Decode.succeed ()) getWithoutSecrets "NEXT" (Decode.succeed ())
) )
|> (\request -> |> (\request ->
StaticHttpRequest.resolveUrls request StaticHttpRequest.resolveUrls ApplicationType.Cli
request
(requestsDict (requestsDict
[ ( get "first", "null" ) [ ( get "first", "null" )
, ( get "NEXT", "null" ) , ( get "NEXT", "null" )
@ -63,7 +65,8 @@ all =
getWithoutSecrets "NEXT" (Decode.succeed ()) getWithoutSecrets "NEXT" (Decode.succeed ())
) )
|> (\request -> |> (\request ->
StaticHttpRequest.resolveUrls request StaticHttpRequest.resolveUrls ApplicationType.Cli
request
(requestsDict (requestsDict
[ ( get "NEXT", "null" ) [ ( get "NEXT", "null" )
] ]
@ -81,7 +84,8 @@ all =
) )
|> StaticHttp.map (\_ -> ()) |> StaticHttp.map (\_ -> ())
|> (\request -> |> (\request ->
StaticHttpRequest.resolveUrls request StaticHttpRequest.resolveUrls ApplicationType.Cli
request
(requestsDict (requestsDict
[ ( get "first", "null" ) [ ( get "first", "null" )
, ( get "NEXT", "null" ) , ( get "NEXT", "null" )
@ -98,7 +102,8 @@ all =
getWithoutSecrets "NEXT" (Decode.succeed ()) getWithoutSecrets "NEXT" (Decode.succeed ())
) )
|> (\request -> |> (\request ->
StaticHttpRequest.resolveUrls request StaticHttpRequest.resolveUrls ApplicationType.Cli
request
(requestsDict (requestsDict
[ ( get "first", "null" ) [ ( get "first", "null" )
] ]
@ -119,7 +124,8 @@ all =
) )
) )
|> (\request -> |> (\request ->
StaticHttpRequest.resolveUrls request StaticHttpRequest.resolveUrls ApplicationType.Cli
request
(requestsDict (requestsDict
[ ( get "first", "1" ) [ ( get "first", "1" )
] ]