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

View File

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

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/)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/dillonkearns/elm-pages-starter)
@ -165,12 +163,12 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://citric.id"><img src="https://avatars1.githubusercontent.com/u/296665?v=4" width="100px;" alt=""/><br /><sub><b>Steven Vandevelde</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=icidasset" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Y0hy0h"><img src="https://avatars0.githubusercontent.com/u/11377826?v=4" width="100px;" alt=""/><br /><sub><b>Johannes Maas</b></sub></a><br /><a href="#userTesting-Y0hy0h" title="User Testing">📓</a></td>
<td align="center"><a href="https://github.com/vViktorPL"><img src="https://avatars1.githubusercontent.com/u/2961541?v=4" width="100px;" alt=""/><br /><sub><b>Wiktor Toporek</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=vViktorPL" title="Code">💻</a></td>
<td align="center"><a href="https://sunrisemovement.com"><img src="https://avatars1.githubusercontent.com/u/1508245?v=4" width="100px;" alt=""/><br /><sub><b>Luke Westby</b></sub></a><br /><a href="https://github.com/dillonkearns/elm-pages/commits?author=lukewestby" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

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

View File

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

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",
"justinmimbs/date": "3.2.0",
"lukewestby/elm-string-interpolate": "1.0.4",
"mdgriffith/elm-markup": "3.0.1",
"mdgriffith/elm-ui": "1.1.5",
"miniBill/elm-codec": "1.2.0",
"noahzgordon/elm-color-extra": "1.0.2",

View File

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

View File

@ -91,7 +91,7 @@ decoder =
|> 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.Font
import FontAwesome
import Json.Decode.Exploration as Decode
import OptimizedDecoder as Decode
import Pages.Secrets as Secrets
import Pages.StaticHttp as StaticHttp
import Palette

View File

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

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 = markdown, markup = markup }
{ markdown = markdown }
markdown : List ( List String, { frontMatter : String, body : Maybe String } )
markdown =
[ {1}
]
markup : List ( List String, String )
markup =
[
{0}
]
"""
[ List.map generatePage content |> String.join "\n ,"
, List.map generateMarkdownPage markdownContent |> String.join "\n ,"

View File

@ -2,6 +2,7 @@ const path = require("path");
const fs = require("fs");
const globby = require("globby");
const parseFrontmatter = require("./frontmatter.js");
const webpack = require('webpack')
function unpackFile(filePath) {
const { content, data } = parseFrontmatter(
@ -15,20 +16,33 @@ function unpackFile(filePath) {
return {
baseRoute,
content
content,
filePath
};
}
module.exports = class AddFilesPlugin {
constructor(data, filesToGenerate) {
this.pagesWithRequests = data;
this.filesToGenerate = filesToGenerate;
apply(/** @type {webpack.Compiler} */ compiler) {
(global.mode === "dev" ? compiler.hooks.emit : compiler.hooks.make).tapAsync("AddFilesPlugin", (compilation, callback) => {
const files = globby.sync("content").map(unpackFile);
let staticRequestData = {}
global.pagesWithRequests.then(payload => {
if (payload.type === 'error') {
compilation.errors.push(new Error(payload.message))
} else if (payload.errors && payload.errors.length > 0) {
compilation.errors.push(new Error(payload.errors[0]))
}
apply(compiler) {
compiler.hooks.emit.tap("AddFilesPlugin", compilation => {
const files = globby
.sync(["content/**/*.*", "!content/**/*.emu"], {})
.map(unpackFile);
else {
staticRequestData = payload.pages
}
})
.finally(() => {
files.forEach(file => {
// Couldn't find this documented in the webpack docs,
@ -36,10 +50,16 @@ module.exports = class AddFilesPlugin {
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
let route = file.baseRoute.replace(/\/$/, '');
const staticRequests = this.pagesWithRequests[route];
const staticRequests = staticRequestData[route];
const filename = path.join(file.baseRoute, "content.json");
compilation.fileDependencies.add(filename);
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 || {}
@ -51,7 +71,7 @@ module.exports = class AddFilesPlugin {
};
});
(this.filesToGenerate || []).forEach(file => {
(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
@ -61,6 +81,9 @@ module.exports = class AddFilesPlugin {
};
});
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");
module.exports = runElm;
function runElm(/** @type string */ mode, /** @type any */ callback) {
function runElm(/** @type string */ mode) {
return new Promise((resolve, reject) => {
const elmBaseDirectory = "./elm-stuff/elm-pages";
const mainElmFile = "../../src/Main.elm";
const startingDir = process.cwd();
process.chdir(elmBaseDirectory);
compileToString([mainElmFile], {}).then(function(data) {
(function() {
const data = compileToStringSync([mainElmFile], {});
process.chdir(startingDir);
(function () {
const warnOriginal = console.warn;
console.warn = function() {};
console.warn = function () { };
eval(data.toString());
const app = Elm.Main.init({
flags: { secrets: process.env, mode }
flags: { secrets: process.env, mode, staticHttpCache: global.staticHttpCache }
});
app.ports.toJsPort.subscribe(payload => {
process.chdir(startingDir);
if (payload.tag === "Success") {
callback(payload.args[0]);
global.staticHttpCache = payload.args[0].staticHttpCache;
resolve(payload.args[0])
} else {
console.log(payload.args[0]);
process.exit(1);
reject(payload.args[0])
}
delete Elm;
console.warn = warnOriginal;
});
})();
});
}

View File

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

View File

@ -41,7 +41,7 @@ isValidRoute route =
`;
}
function elmPagesUiFile(staticRoutes, markdownContent, markupContent) {
function elmPagesUiFile(staticRoutes, markdownContent) {
return `port module Pages exposing ${exposingList}
import Color exposing (Color)
@ -50,7 +50,6 @@ import Head
import Html exposing (Html)
import Json.Decode
import Json.Encode
import Mark
import Pages.Platform
import Pages.Manifest exposing (DisplayMode, Orientation)
import Pages.Manifest.Category as Category exposing (Category)
@ -92,22 +91,25 @@ directoryWithoutIndex path =
port toJsPort : Json.Encode.Value -> Cmd msg
port fromJsPort : (Json.Decode.Value -> msg) -> Sub msg
internals : Pages.Internal.Internal PathKey
internals =
{ applicationType = Pages.Internal.Browser
, toJsPort = toJsPort
, fromJsPort = fromJsPort identity
, content = content
, pathKey = PathKey
}
${staticRouteStuff(staticRoutes)}
${generateRawContent(markdownContent, markupContent, false)}
${generateRawContent(markdownContent, false)}
`;
}
function elmPagesCliFile(staticRoutes, markdownContent, markupContent) {
function elmPagesCliFile(staticRoutes, markdownContent) {
return `port module Pages exposing ${exposingList}
import Color exposing (Color)
@ -116,7 +118,6 @@ import Head
import Html exposing (Html)
import Json.Decode
import Json.Encode
import Mark
import Pages.Platform
import Pages.Manifest exposing (DisplayMode, Orientation)
import Pages.Manifest.Category as Category exposing (Category)
@ -159,10 +160,14 @@ directoryWithoutIndex path =
port toJsPort : Json.Encode.Value -> Cmd msg
port fromJsPort : (Json.Decode.Value -> msg) -> Sub msg
internals : Pages.Internal.Internal PathKey
internals =
{ applicationType = Pages.Internal.Cli
, toJsPort = toJsPort
, fromJsPort = fromJsPort identity
, content = content
, pathKey = PathKey
}
@ -170,7 +175,7 @@ internals =
${staticRouteStuff(staticRoutes)}
${generateRawContent(markdownContent, markupContent, true)}
${generateRawContent(markdownContent, true)}
`;
}
module.exports = { elmPagesUiFile, elmPagesCliFile };

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
const path = require("path");
const matter = require("gray-matter");
const dir = "content/";
const glob = require("glob");
const fs = require("fs");
@ -138,14 +137,14 @@ function allImageAssetNames() {
});
}
function toPascalCase(str) {
var pascal = str.replace(/(\-\w)/g, function(m) {
var pascal = str.replace(/(\-\w)/g, function (m) {
return m[1].toUpperCase();
});
return pascal.charAt(0).toUpperCase() + pascal.slice(1);
}
function toCamelCase(str) {
var pascal = str.replace(/(\-\w)/g, function(m) {
var pascal = str.replace(/(\-\w)/g, function (m) {
return m[1].toUpperCase();
});
return pascal.charAt(0).toLowerCase() + pascal.slice(1);

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>

View File

@ -11,9 +11,9 @@ module.exports = function pagesInit(
prefetchedPages = [window.location.pathname];
initialLocationHash = document.location.hash.replace(/^#/, "");
return new Promise(function(resolve, reject) {
return new Promise(function (resolve, reject) {
document.addEventListener("DOMContentLoaded", _ => {
new MutationObserver(function() {
new MutationObserver(function () {
elmViewRendered = true;
if (headTagsAdded) {
document.dispatchEvent(new Event("prerender-trigger"));
@ -32,7 +32,10 @@ function loadContentAndInitializeApp(/** @type { init: any } */ mainElmModule)
const isPrerendering = navigator.userAgent.indexOf("Headless") >= 0
const path = window.location.pathname.replace(/(\w)$/, "$1/")
return httpGet(`${window.location.origin}${path}content.json`).then(function(/** @type JSON */ contentJson) {
return Promise.all([
getConfig(),
httpGet(`${window.location.origin}${path}content.json`)]).then(function (/** @type {[DevServerConfig?, JSON]} */[devServerConfig, contentJson]) {
console.log('devServerConfig', devServerConfig);
const app = mainElmModule.init({
flags: {
@ -41,14 +44,17 @@ function loadContentAndInitializeApp(/** @type { init: any } */ mainElmModule)
? window.location.origin
: document.baseURI,
isPrerendering: isPrerendering,
contentJson
isDevServer: !!module.hot,
isElmDebugMode: devServerConfig ? devServerConfig.elmDebugger : false,
contentJson,
}
});
app.ports.toJsPort.subscribe((
/** @type { { head: HeadTag[], allRoutes: string[] } } */ fromElm
/** @type { { head: SeoTag[], allRoutes: string[] } } */ fromElm
) => {
appendTag({
type: 'head',
name: "meta",
attributes: [
["name", "generator"],
@ -60,7 +66,13 @@ function loadContentAndInitializeApp(/** @type { init: any } */ mainElmModule)
if (navigator.userAgent.indexOf("Headless") >= 0) {
fromElm.head.forEach(headTag => {
if (headTag.type === 'head') {
appendTag(headTag);
} else if (headTag.type === 'json-ld') {
appendJsonLdTag(headTag);
} else {
throw new Error(`Unknown tag type #{headTag}`)
}
});
headTagsAdded = true;
if (elmViewRendered) {
@ -71,8 +83,46 @@ function loadContentAndInitializeApp(/** @type { init: any } */ mainElmModule)
}
});
return app
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
});
}
@ -132,7 +182,7 @@ function setupLinkPrefetchingHelp(
const links = document.querySelectorAll("a");
links.forEach(link => {
// console.log(link.pathname);
link.addEventListener("mouseenter", function(event) {
link.addEventListener("mouseenter", function (event) {
if (
event &&
event.target &&
@ -166,7 +216,9 @@ function prefetchIfNeeded(/** @type {HTMLAnchorElement} */ target) {
}
}
/** @typedef {{ name: string; attributes: string[][]; }} HeadTag */
/** @typedef {HeadTag | JsonLdTag} SeoTag */
/** @typedef {{ name: string; attributes: string[][]; type: 'head' }} HeadTag */
function appendTag(/** @type {HeadTag} */ tagDetails) {
const meta = document.createElement(tagDetails.name);
tagDetails.attributes.forEach(([name, value]) => {
@ -175,10 +227,18 @@ function appendTag(/** @type {HeadTag} */ tagDetails) {
document.getElementsByTagName("head")[0].appendChild(meta);
}
/** @typedef {{ contents: Object; type: 'json-ld' }} JsonLdTag */
function appendJsonLdTag(/** @type {JsonLdTag} */ tagDetails) {
let jsonLdScript = document.createElement('script');
jsonLdScript.type = "application/ld+json";
jsonLdScript.innerHTML = JSON.stringify(tagDetails.contents);
document.getElementsByTagName("head")[0].appendChild(jsonLdScript);
}
function httpGet(/** @type string */ theUrl) {
return new Promise(function(resolve, reject) {
return new Promise(function (resolve, reject) {
const xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
resolve(JSON.parse(xmlHttp.responseText));
}
@ -187,3 +247,16 @@ function httpGet(/** @type string */ theUrl) {
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 */

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

234
src/StructuredData.elm Normal file
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"
Green ->
"[32;1m"
"[32m"
Yellow ->
"[33;1m"
"[33m"
Cyan ->
"[36m"

View File

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

View File

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