Merge pull request #196 from dillonkearns/worker-threads

Worker threads
This commit is contained in:
Dillon Kearns 2021-07-28 10:59:29 -07:00 committed by GitHub
commit 0dfdf9f7ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 9673 additions and 3201 deletions

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -1,3 +1,4 @@
{
"defaultCommandTimeout": 4000
}
"defaultCommandTimeout": 4000,
"baseUrl": "http://localhost:1234"
}

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,44 @@
context("dev server with base path", () => {
it("404 message", () => {
cy.visit("/qwer/asdf", { failOnStatusCode: false });
cy.contains("No route found for /asdf");
});
it("navigates to root page", () => {
cy.visit("/qwer/");
cy.contains("This is the index page");
});
it("loads file data sources", () => {
cy.writeFile(
"examples/end-to-end/my-json-data.json",
JSON.stringify({
greeting: "Hello, World!",
})
);
cy.visit("/qwer/file-data");
cy.contains("Greeting: Hello, World!");
cy.writeFile(
"examples/end-to-end/my-json-data.json",
JSON.stringify({
greeting: "Goodbye, World!",
})
);
cy.contains("Greeting: Goodbye, World!");
cy.writeFile(
"examples/end-to-end/my-json-data.json",
JSON.stringify({
greeting: null,
})
);
cy.contains(`I encountered some errors while decoding this JSON:
At path /jsonFile/greeting
I expected a string here, but instead found this value:
null`);
});
});

View File

@ -0,0 +1,7 @@
context("dev server with base path", () => {
it("navigating with a link successfully resolves data sources", () => {
cy.visit("/links");
cy.contains("Root page").click();
cy.contains("This is the index page.");
});
});

22
cypress/plugins/index.js Normal file
View File

@ -0,0 +1,22 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

20
cypress/support/index.js Normal file
View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -41,7 +41,9 @@
"elm-community/list-extra": "8.3.0 <= v < 9.0.0",
"miniBill/elm-codec": "2.0.0 <= v < 3.0.0",
"noahzgordon/elm-color-extra": "1.0.2 <= v < 2.0.0",
"robinheghan/murmur3": "1.0.0 <= v < 2.0.0",
"tripokey/elm-fuzzy": "5.2.1 <= v < 6.0.0",
"vito/elm-ansi": "10.0.1 <= v < 11.0.0",
"zwilias/json-decode-exploration": "6.0.0 <= v < 7.0.0"
},
"test-dependencies": {

View File

@ -32,8 +32,10 @@
"miniBill/elm-codec": "2.0.0",
"noahzgordon/elm-color-extra": "1.0.2",
"pablohirafuji/elm-syntax-highlight": "3.4.0",
"robinheghan/murmur3": "1.0.0",
"rtfeldman/elm-css": "16.1.1",
"tripokey/elm-fuzzy": "5.2.1",
"vito/elm-ansi": "10.0.1",
"zwilias/json-decode-exploration": "6.0.0"
},
"indirect": {

View File

@ -29,6 +29,7 @@
"commander": "^7.2.0",
"connect": "^3.7.0",
"cross-spawn": "7.0.3",
"elm-doc-preview": "^5.0.5",
"elm-hot": "^1.1.6",
"elm-optimize-level-2": "^0.1.5",
"fs-extra": "^10.0.0",
@ -36,6 +37,7 @@
"gray-matter": "^4.0.3",
"kleur": "^4.1.4",
"micromatch": "^4.0.4",
"node-worker-threads-pool": "^1.5.0",
"serve-static": "^1.14.1",
"terser": "^5.7.0",
"xhr2": "^0.2.1"
@ -49,7 +51,8 @@
"@types/micromatch": "^4.0.1",
"@types/node": "12.20.12",
"@types/serve-static": "^1.13.9",
"elm-review": "^2.5.1",
"cypress": "^7.4.0",
"elm-review": "^2.5.3",
"elm-test": "^0.19.1-revision7",
"elm-tooling": "^1.3.0",
"elm-verify-examples": "^5.0.0",
@ -1091,9 +1094,11 @@
"commander": "^7.2.0",
"connect": "^3.7.0",
"cross-spawn": "7.0.3",
"cypress": "^7.4.0",
"elm-doc-preview": "^5.0.5",
"elm-hot": "^1.1.6",
"elm-optimize-level-2": "^0.1.5",
"elm-review": "^2.5.1",
"elm-review": "^2.5.3",
"elm-test": "^0.19.1-revision7",
"elm-tooling": "^1.3.0",
"elm-verify-examples": "^5.0.0",
@ -1103,6 +1108,7 @@
"kleur": "^4.1.4",
"micromatch": "^4.0.4",
"mocha": "^8.4.0",
"node-worker-threads-pool": "^1.5.0",
"serve-static": "^1.14.1",
"terser": "^5.7.0",
"typescript": "^4.2.4",

View File

@ -1,4 +1,4 @@
module Shared exposing (Data, Model, Msg, SharedMsg(..), template)
module Shared exposing (Data, Model, Msg, template)
import Browser.Navigation
import DataSource
@ -8,13 +8,14 @@ import Html.Styled
import Pages.Flags
import Pages.PageUrl exposing (PageUrl)
import Path exposing (Path)
import Route exposing (Route)
import SharedTemplate exposing (SharedTemplate)
import TableOfContents
import View exposing (View)
import View.Header
template : SharedTemplate Msg Model Data SharedMsg msg
template : SharedTemplate Msg Model Data msg
template =
{ init = init
, update = update
@ -22,7 +23,6 @@ template =
, data = data
, subscriptions = subscriptions
, onPageChange = Just OnPageChange
, sharedMsg = SharedMsg
}
@ -33,17 +33,13 @@ type Msg
, fragment : Maybe String
}
| ToggleMobileMenu
| SharedMsg SharedMsg
| IncrementFromChild
type alias Data =
TableOfContents.TableOfContents TableOfContents.Data
type SharedMsg
= IncrementFromChild
type alias Model =
{ showMobileMenu : Bool
, counter : Int
@ -83,10 +79,8 @@ update msg model =
ToggleMobileMenu ->
( { model | showMobileMenu = not model.showMobileMenu }, Cmd.none )
SharedMsg globalMsg ->
case globalMsg of
IncrementFromChild ->
( { model | counter = model.counter + 1 }, Cmd.none )
IncrementFromChild ->
( { model | counter = model.counter + 1 }, Cmd.none )
subscriptions : Path -> Model -> Sub Msg
@ -103,7 +97,7 @@ view :
Data
->
{ path : Path
, frontmatter : route
, route : Maybe Route
}
-> Model
-> (Msg -> msg)

6
examples/end-to-end/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules/
elm-stuff/
dist/
.cache/
.elm-pages/
functions/render/elm-pages-cli.js

View File

@ -0,0 +1 @@
# README

View File

@ -0,0 +1,8 @@
{
"name": "dmy/elm-doc-preview",
"summary": "Offline documentation previewer",
"version": "5.0.0",
"exposed-modules": [
"Page"
]
}

View File

@ -0,0 +1,8 @@
{
"tools": {
"elm": "0.19.1",
"elm-format": "0.8.4",
"elm-json": "0.2.10",
"elm-test-rs": "1.0.0"
}
}

View File

@ -0,0 +1,56 @@
{
"type": "application",
"source-directories": [
"src",
"../../src",
".elm-pages",
"../../plugins"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"MartinSStewart/elm-serialize": "1.2.5",
"avh4/elm-color": "1.0.0",
"danyx23/elm-mimetype": "4.0.1",
"dillonkearns/elm-bcp47-language-tag": "1.0.1",
"dillonkearns/elm-markdown": "6.0.1",
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/regex": "1.0.0",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm-community/dict-extra": "2.4.0",
"elm-community/list-extra": "8.3.0",
"matheus23/elm-default-tailwind-modules": "2.0.1",
"miniBill/elm-codec": "2.0.0",
"noahzgordon/elm-color-extra": "1.0.2",
"pablohirafuji/elm-syntax-highlight": "3.4.0",
"robinheghan/murmur3": "1.0.0",
"rtfeldman/elm-css": "16.1.1",
"tripokey/elm-fuzzy": "5.2.1",
"vito/elm-ansi": "10.0.1",
"zwilias/json-decode-exploration": "6.0.0"
},
"indirect": {
"bburdette/toop": "1.0.1",
"danfishgold/base64-bytes": "1.1.0",
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/parser": "1.1.0",
"elm/random": "1.0.0",
"elm/virtual-dom": "1.0.2",
"fredcy/elm-parseint": "2.0.1",
"mgold/elm-nonempty-list": "4.2.0",
"rtfeldman/elm-hex": "1.0.0"
}
},
"test-dependencies": {
"direct": {
"elm-explorations/test": "1.2.2"
},
"indirect": {}
}
}

View File

@ -0,0 +1 @@
Hello there!

View File

@ -0,0 +1,3 @@
{
"greeting": "Hello, World!"
}

2562
examples/end-to-end/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
{
"name": "elm-pages-example",
"version": "1.0.0",
"description": "Example site built with elm-pages.",
"scripts": {
"start": "elm-pages dev",
"serve": "npm run build && http-server ./dist -a localhost -p 3000 -c-1",
"build": "elm-pages build"
},
"author": "Dillon Kearns",
"license": "BSD-3",
"devDependencies": {
"elm-oembed": "0.0.6",
"elm-pages": "file:../..",
"elm-tooling": "^1.3.0",
"http-server": "^0.11.1"
}
}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 323.141 322.95" enable-background="new 0 0 323.141 322.95" xml:space="preserve">
<g>
<polygon
fill="#F0AD00"
points="161.649,152.782 231.514,82.916 91.783,82.916"/>
<polygon
fill="#7FD13B"
points="8.867,0 79.241,70.375 232.213,70.375 161.838,0"/>
<rect
fill="#7FD13B"
x="192.99"
y="107.392"
transform="matrix(0.7071 0.7071 -0.7071 0.7071 186.4727 -127.2386)"
width="107.676"
height="108.167"/>
<polygon
fill="#60B5CC"
points="323.298,143.724 323.298,0 179.573,0"/>
<polygon
fill="#5A6378"
points="152.781,161.649 0,8.868 0,314.432"/>
<polygon
fill="#F0AD00"
points="255.522,246.655 323.298,314.432 323.298,178.879"/>
<polygon
fill="#60B5CC"
points="161.649,170.517 8.869,323.298 314.43,323.298"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub icon</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

View File

@ -0,0 +1,2 @@
<svg version="1.1" viewBox="251.0485 144.52063 56.114286 74.5" width="50px" height="74.5"><defs><linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="10%" style="stop-color:rgba(1.96%,45.88%,90.2%,1);stop-opacity:1"></stop><stop offset="100%" style="stop-color:rgba(0%,94.9%,37.65%,1);stop-opacity:1"></stop></linearGradient></defs><metadata></metadata><g id="Canvas_11" stroke="none" fill="url(#grad1)" stroke-opacity="1" fill-opacity="1" stroke-dasharray="none"><g id="Canvas_11: Layer 1"><g id="Group_38"><g id="Graphic_32"><path d="M 252.5485 146.02063 L 252.5485 217.52063 L 305.66277 217.52063 L 305.66277 161.68254 L 290.00087 146.02063 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"></path></g><g id="Line_34"><line x1="266.07286" y1="182.8279" x2="290.75465" y2="183.00997" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_35"><line x1="266.07286" y1="191.84156" x2="290.75465" y2="192.02363" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_36"><line x1="266.07286" y1="200.85522" x2="290.75465" y2="201.0373" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_37"><line x1="266.07286" y1="164.80058" x2="278.3874" y2="164.94049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,6 @@
export default {
load: function (elmLoaded) {},
flags: function () {
return null;
},
};

View File

@ -0,0 +1,79 @@
@import url("https://rsms.me/inter/inter.css");
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap");
body {
font-family: "Inter var" !important;
}
pre.elmsh {
padding: 10px;
margin: 0;
text-align: left;
overflow: auto;
height: 100%;
width: 500px;
font-size: 14px;
font-family: "IBM Plex Mono" !important;
}
code.elmsh {
padding: 0;
}
.elmsh-line:before {
content: attr(data-elmsh-lc);
display: inline-block;
text-align: right;
width: 40px;
padding: 0 20px 0 0;
opacity: 0.3;
}
.elmsh {
color: #f8f8f2;
background: #1e1e1e;
}
.elmsh-hl {
background: #4864aa;
}
.elmsh-add {
background: #003800;
}
.elmsh-del {
background: #380000;
}
.elmsh-comm {
color: #d4d4d4;
}
.elmsh1 {
color: #74b0df;
}
.elmsh2 {
color: #ce9178;
}
.elmsh3 {
color: #ff00ff;
}
.elmsh4 {
color: #4f76ac;
}
.elmsh5 {
color: #3dc9b0;
}
.elmsh6 {
color: #74b0df;
}
.elmsh7 {
color: #ce9178;
}
.elmsh-elm-ts,
.elmsh-js-dk,
.elmsh-css-p {
font-style: italic;
color: #4f76ac;
}
.elmsh-js-ce {
font-style: italic;
color: #5bb498;
}
.elmsh-css-ar-i {
font-weight: bold;
color: #ff0000;
}

View File

@ -0,0 +1,43 @@
pre.elmsh {
padding: 10px;
margin: 0;
text-align: left;
overflow: auto;
padding: 20px !important;
}
code.elmsh {
padding: 0;
}
code {
font-family: 'Roboto Mono' !important;
font-size: 20px !important;
line-height: 28px;
}
.elmsh-line:before {
/* content: attr(data-elmsh-lc); */
display: inline-block;
text-align: right;
width: 40px;
padding: 0 20px 0 0;
opacity: 0.3;
}
.elmsh {
color: #f8f8f2;
background: #000;
}
.elmsh-hl {background: #343434;}
.elmsh-add {background: #003800;}
.elmsh-del {background: #380000;}
.elmsh-comm {color: #75715e;}
.elmsh1 {color: #ae81ff;}
.elmsh2 {color: #e6db74;}
.elmsh3 {color: #66d9ef;}
.elmsh4 {color: #f92672;}
.elmsh5 {color: #a6e22e;}
.elmsh6 {color: #ae81ff;}
.elmsh7 {color: #fd971f;}

View File

@ -0,0 +1,18 @@
module Api exposing (routes)
import ApiRoute
import DataSource exposing (DataSource)
import DataSource.Http
import Html exposing (Html)
import Json.Encode
import OptimizedDecoder as Decode
import Route exposing (Route)
import Secrets
routes :
DataSource (List Route)
-> (Html Never -> String)
-> List (ApiRoute.Done ApiRoute.Response)
routes getStaticRoutes htmlToString =
[]

View File

@ -0,0 +1,233 @@
module MarkdownRenderer exposing (renderer)
import Html.Styled as Html
import Html.Styled.Attributes as Attr exposing (css)
import Markdown.Block as Block exposing (ListItem(..), Task(..))
import Markdown.Html
import Markdown.Renderer
import SyntaxHighlight
import Tailwind.Utilities as Tw
renderer : Markdown.Renderer.Renderer (Html.Html msg)
renderer =
{ heading = heading
, paragraph = Html.p []
, thematicBreak = Html.hr [] []
, text = Html.text
, strong = \content -> Html.strong [ css [ Tw.font_bold ] ] content
, emphasis = \content -> Html.em [ css [ Tw.italic ] ] content
, blockQuote = Html.blockquote []
, codeSpan =
\content ->
Html.code
[ css
[ Tw.font_semibold
, Tw.font_medium
]
]
[ Html.text content ]
--, codeSpan = code
, link =
\{ destination } body ->
Html.a
[ Attr.href destination
, css
[ Tw.underline
]
]
body
, hardLineBreak = Html.br [] []
, image =
\image ->
case image.title of
Just _ ->
Html.img [ Attr.src image.src, Attr.alt image.alt ] []
Nothing ->
Html.img [ Attr.src image.src, Attr.alt image.alt ] []
, unorderedList =
\items ->
Html.ul []
(items
|> List.map
(\item ->
case item of
Block.ListItem task children ->
let
checkbox =
case task of
Block.NoTask ->
Html.text ""
Block.IncompleteTask ->
Html.input
[ Attr.disabled True
, Attr.checked False
, Attr.type_ "checkbox"
]
[]
Block.CompletedTask ->
Html.input
[ Attr.disabled True
, Attr.checked True
, Attr.type_ "checkbox"
]
[]
in
Html.li [] (checkbox :: children)
)
)
, orderedList =
\startingIndex items ->
Html.ol
(case startingIndex of
1 ->
[ Attr.start startingIndex ]
_ ->
[]
)
(items
|> List.map
(\itemBlocks ->
Html.li []
itemBlocks
)
)
, html = Markdown.Html.oneOf []
, codeBlock = codeBlock
--\{ body, language } ->
-- let
-- classes =
-- -- Only the first word is used in the class
-- case Maybe.map String.words language of
-- Just (actualLanguage :: _) ->
-- [ Attr.class <| "language-" ++ actualLanguage ]
--
-- _ ->
-- []
-- in
-- Html.pre []
-- [ Html.code classes
-- [ Html.text body
-- ]
-- ]
, table = Html.table []
, tableHeader = Html.thead []
, tableBody = Html.tbody []
, tableRow = Html.tr []
, strikethrough =
\children -> Html.del [] children
, tableHeaderCell =
\maybeAlignment ->
let
attrs =
maybeAlignment
|> Maybe.map
(\alignment ->
case alignment of
Block.AlignLeft ->
"left"
Block.AlignCenter ->
"center"
Block.AlignRight ->
"right"
)
|> Maybe.map Attr.align
|> Maybe.map List.singleton
|> Maybe.withDefault []
in
Html.th attrs
, tableCell =
\maybeAlignment ->
let
attrs =
maybeAlignment
|> Maybe.map
(\alignment ->
case alignment of
Block.AlignLeft ->
"left"
Block.AlignCenter ->
"center"
Block.AlignRight ->
"right"
)
|> Maybe.map Attr.align
|> Maybe.map List.singleton
|> Maybe.withDefault []
in
Html.td attrs
}
rawTextToId : String -> String
rawTextToId rawText =
rawText
|> String.split " "
|> String.join "-"
|> String.toLower
heading : { level : Block.HeadingLevel, rawText : String, children : List (Html.Html msg) } -> Html.Html msg
heading { level, rawText, children } =
(case level of
Block.H1 ->
Html.h1
Block.H2 ->
Html.h2
Block.H3 ->
Html.h3
Block.H4 ->
Html.h4
Block.H5 ->
Html.h5
Block.H6 ->
Html.h6
)
[ Attr.id (rawTextToId rawText)
, Attr.attribute "name" (rawTextToId rawText)
, css
[ Tw.font_bold
, Tw.text_2xl
, Tw.mt_8
, Tw.mb_4
]
]
children
--code : String -> Element msg
--code snippet =
-- Element.el
-- [ Element.Background.color
-- (Element.rgba255 50 50 50 0.07)
-- , Element.Border.rounded 2
-- , Element.paddingXY 5 3
-- , Font.family [ Font.typeface "Roboto Mono", Font.monospace ]
-- ]
-- (Element.text snippet)
--
--
codeBlock : { body : String, language : Maybe String } -> Html.Html msg
codeBlock details =
SyntaxHighlight.elm details.body
|> Result.map (SyntaxHighlight.toBlockHtml (Just 1))
|> Result.map Html.fromUnstyled
|> Result.withDefault (Html.pre [] [ Html.code [] [ Html.text details.body ] ])

View File

@ -0,0 +1,79 @@
module Page.FileData exposing (Data, Model, Msg, page)
import DataSource exposing (DataSource)
import DataSource.File
import Head
import Head.Seo as Seo
import Html.Styled exposing (text)
import OptimizedDecoder as Decode
import Page exposing (Page, PageWithState, StaticPayload)
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Shared
import View exposing (View)
type alias Model =
()
type alias Msg =
Never
type alias RouteParams =
{}
page : Page RouteParams Data
page =
Page.single
{ head = head
, data = data
}
|> Page.buildNoState { view = view }
type alias Data =
{ greeting : String
}
data : DataSource Data
data =
"my-json-data.json"
|> DataSource.File.jsonFile (Decode.field "greeting" Decode.string)
|> DataSource.map Data
head :
StaticPayload Data RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = Pages.Url.external "TODO"
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
view :
Maybe PageUrl
-> Shared.Model
-> StaticPayload Data RouteParams
-> View Msg
view maybeUrl sharedModel static =
{ title = "Index page"
, body =
[ text <| "Greeting: " ++ static.data.greeting
]
}

View File

@ -0,0 +1,76 @@
module Page.Index exposing (Data, Model, Msg, page)
import DataSource exposing (DataSource)
import DataSource.File
import Head
import Head.Seo as Seo
import Html.Styled exposing (text)
import Page exposing (Page, PageWithState, StaticPayload)
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Shared
import View exposing (View)
type alias Model =
()
type alias Msg =
Never
type alias RouteParams =
{}
page : Page RouteParams Data
page =
Page.single
{ head = head
, data = data
}
|> Page.buildNoState { view = view }
type alias Data =
String
data : DataSource Data
data =
DataSource.File.rawFile "greeting.txt"
head :
StaticPayload Data RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = Pages.Url.external "TODO"
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
view :
Maybe PageUrl
-> Shared.Model
-> StaticPayload Data RouteParams
-> View Msg
view maybeUrl sharedModel static =
{ title = "Index page"
, body =
[ text "This is the index page."
, text <| "Greeting: " ++ static.data
]
}

View File

@ -0,0 +1,76 @@
module Page.Links exposing (Data, Model, Msg, page)
import DataSource exposing (DataSource)
import Head
import Head.Seo as Seo
import Html.Styled as Html exposing (text)
import Html.Styled.Attributes as Attr
import Page exposing (Page, PageWithState, StaticPayload)
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Shared
import View exposing (View)
type alias Model =
()
type alias Msg =
Never
type alias RouteParams =
{}
page : Page RouteParams Data
page =
Page.single
{ head = head
, data = data
}
|> Page.buildNoState { view = view }
type alias Data =
()
data : DataSource Data
data =
DataSource.succeed ()
head :
StaticPayload Data RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = Pages.Url.external "TODO"
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
view :
Maybe PageUrl
-> Shared.Model
-> StaticPayload Data RouteParams
-> View Msg
view maybeUrl sharedModel static =
{ title = "Links"
, body =
[ Html.a [ Attr.href "/" ]
[ text "Root page" ]
]
}

View File

@ -0,0 +1,105 @@
module Shared exposing (Data, Model, Msg(..), SharedMsg(..), template)
import Browser.Navigation
import Css.Global
import DataSource
import DataSource.Http
import Html exposing (Html)
import Html.Styled
import OptimizedDecoder as D
import Pages.Flags
import Pages.PageUrl exposing (PageUrl)
import Path exposing (Path)
import Route exposing (Route)
import Secrets
import SharedTemplate exposing (SharedTemplate)
import Tailwind.Utilities
import View exposing (View)
template : SharedTemplate Msg Model Data msg
template =
{ init = init
, update = update
, view = view
, data = data
, subscriptions = subscriptions
, onPageChange = Just OnPageChange
}
type Msg
= OnPageChange
{ path : Path
, query : Maybe String
, fragment : Maybe String
}
type alias Data =
()
type SharedMsg
= NoOp
type alias Model =
{ showMobileMenu : Bool
}
init :
Maybe Browser.Navigation.Key
-> Pages.Flags.Flags
->
Maybe
{ path :
{ path : Path
, query : Maybe String
, fragment : Maybe String
}
, metadata : route
, pageUrl : Maybe PageUrl
}
-> ( Model, Cmd Msg )
init _ flags maybePagePath =
( { showMobileMenu = False }
, Cmd.none
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnPageChange _ ->
( { model | showMobileMenu = False }, Cmd.none )
subscriptions : Path -> Model -> Sub Msg
subscriptions _ _ =
Sub.none
data : DataSource.DataSource Data
data =
DataSource.succeed ()
view :
Data
->
{ path : Path
, route : Maybe Route
}
-> Model
-> (Msg -> msg)
-> View msg
-> { body : Html msg, title : String }
view stars page model toMsg pageView =
{ body =
Html.Styled.div []
pageView.body
|> Html.Styled.toUnstyled
, title = pageView.title
}

View File

@ -0,0 +1,93 @@
module Site exposing (config)
import Cloudinary
import DataSource
import Head
import MimeType
import Pages.Manifest as Manifest
import Pages.Url
import Route exposing (Route)
import SiteConfig exposing (SiteConfig)
config : SiteConfig Data
config =
\routes ->
{ data = data
, canonicalUrl = canonicalUrl
, manifest = manifest
, head = head
}
type alias Data =
{ siteName : String
}
data : DataSource.DataSource Data
data =
DataSource.map Data
--(StaticFile.request "site-name.txt" StaticFile.body)
(DataSource.succeed "site-name")
head : Data -> List Head.Tag
head static =
[ Head.icon [ ( 32, 32 ) ] MimeType.Png (cloudinaryIcon MimeType.Png 32)
, Head.icon [ ( 16, 16 ) ] MimeType.Png (cloudinaryIcon MimeType.Png 16)
, Head.appleTouchIcon (Just 180) (cloudinaryIcon MimeType.Png 180)
, Head.appleTouchIcon (Just 192) (cloudinaryIcon MimeType.Png 192)
, Head.sitemapLink "/sitemap.xml"
]
canonicalUrl : String
canonicalUrl =
"https://elm-pages.com"
manifest : Data -> Manifest.Config
manifest static =
Manifest.init
{ name = static.siteName
, description = "elm-pages - " ++ tagline
, startUrl = Route.Index |> Route.toPath
, icons =
[ icon webp 192
, icon webp 512
, icon MimeType.Png 192
, icon MimeType.Png 512
]
}
|> Manifest.withShortName "elm-pages"
tagline : String
tagline =
"A statically typed site generator"
webp : MimeType.MimeImage
webp =
MimeType.OtherImage "webp"
icon :
MimeType.MimeImage
-> Int
-> Manifest.Icon
icon format width =
{ src = cloudinaryIcon format width
, sizes = [ ( width, width ) ]
, mimeType = format |> Just
, purposes = [ Manifest.IconPurposeAny, Manifest.IconPurposeMaskable ]
}
cloudinaryIcon :
MimeType.MimeImage
-> Int
-> Pages.Url.Url
cloudinaryIcon mimeType width =
Cloudinary.urlSquare "v1603234028/elm-pages/elm-pages-icon" (Just mimeType) width

View File

@ -0,0 +1,23 @@
module View exposing (View, map, placeholder)
import Html.Styled exposing (text)
type alias View msg =
{ title : String
, body : List (Html.Styled.Html msg)
}
map : (msg1 -> msg2) -> View msg1 -> View msg2
map fn doc =
{ title = doc.title
, body = List.map (Html.Styled.map fn) doc.body
}
placeholder : String -> View msg
placeholder moduleName =
{ title = "Placeholder - " ++ moduleName
, body = [ text moduleName ]
}

View File

@ -29,8 +29,10 @@
"miniBill/elm-codec": "1.2.0",
"noahzgordon/elm-color-extra": "1.0.2",
"pablohirafuji/elm-syntax-highlight": "3.4.0",
"robinheghan/murmur3": "1.0.0",
"rtfeldman/elm-css": "16.1.1",
"tripokey/elm-fuzzy": "5.2.1",
"vito/elm-ansi": "10.0.1",
"zwilias/json-decode-exploration": "6.0.0"
},
"indirect": {

View File

@ -6,11 +6,12 @@ import Html exposing (Html)
import Pages.Flags
import Pages.PageUrl exposing (PageUrl)
import Path exposing (Path)
import Route exposing (Route)
import SharedTemplate exposing (SharedTemplate)
import View exposing (View)
template : SharedTemplate Msg Model Data SharedMsg msg
template : SharedTemplate Msg Model Data msg
template =
{ init = init
, update = update
@ -18,7 +19,6 @@ template =
, data = data
, subscriptions = subscriptions
, onPageChange = Just OnPageChange
, sharedMsg = SharedMsg
}
@ -88,7 +88,7 @@ view :
Data
->
{ path : Path
, frontmatter : route
, route : Maybe Route
}
-> Model
-> (Msg -> msg)

View File

@ -28,8 +28,10 @@
"miniBill/elm-codec": "2.0.0",
"noahzgordon/elm-color-extra": "1.0.2",
"pablohirafuji/elm-syntax-highlight": "3.4.0",
"robinheghan/murmur3": "1.0.0",
"rtfeldman/elm-css": "16.1.1",
"tripokey/elm-fuzzy": "5.2.1",
"vito/elm-ansi": "10.0.1",
"zwilias/json-decode-exploration": "6.0.0"
},
"indirect": {

View File

@ -9,6 +9,7 @@
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"MartinSStewart/elm-serialize": "1.2.5",
"avh4/elm-color": "1.0.0",
"billstclair/elm-xml-eeue56": "1.0.1",
"danyx23/elm-mimetype": "4.0.1",
@ -46,6 +47,8 @@
"zwilias/json-decode-exploration": "6.0.0"
},
"indirect": {
"bburdette/toop": "1.0.1",
"danfishgold/base64-bytes": "1.1.0",
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/parser": "1.1.0",
@ -63,4 +66,4 @@
},
"indirect": {}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -15,4 +15,4 @@
"elm-tooling": "^1.3.0",
"http-server": "^0.11.1"
}
}
}

View File

@ -70,7 +70,7 @@ type alias PageWithState routeParams templateData templateModel templateMsg =
StaticPayload templateData routeParams
-> List Head.Tag
, init : Maybe PageUrl -> Shared.Model -> StaticPayload templateData routeParams -> ( templateModel, Cmd templateMsg )
, update : PageUrl -> StaticPayload templateData routeParams -> Maybe Browser.Navigation.Key -> templateMsg -> templateModel -> Shared.Model -> ( templateModel, Cmd templateMsg, Maybe Shared.SharedMsg )
, update : PageUrl -> StaticPayload templateData routeParams -> Maybe Browser.Navigation.Key -> templateMsg -> templateModel -> Shared.Model -> ( templateModel, Cmd templateMsg, Maybe Shared.Msg )
, subscriptions : Maybe PageUrl -> routeParams -> Path -> templateModel -> Shared.Model -> Sub templateMsg
, handleRoute : { moduleName : List String, routePattern : RoutePattern } -> (routeParams -> List ( String, String )) -> routeParams -> DataSource (Maybe NotFoundReason)
, kind : String
@ -188,7 +188,7 @@ buildWithSharedState :
-> StaticPayload templateData routeParams
-> View templateMsg
, init : Maybe PageUrl -> Shared.Model -> StaticPayload templateData routeParams -> ( templateModel, Cmd templateMsg )
, update : PageUrl -> Maybe Browser.Navigation.Key -> Shared.Model -> StaticPayload templateData routeParams -> templateMsg -> templateModel -> ( templateModel, Cmd templateMsg, Maybe Shared.SharedMsg )
, update : PageUrl -> Maybe Browser.Navigation.Key -> Shared.Model -> StaticPayload templateData routeParams -> templateMsg -> templateModel -> ( templateModel, Cmd templateMsg, Maybe Shared.Msg )
, subscriptions : Maybe PageUrl -> routeParams -> Path -> templateModel -> Shared.Model -> Sub templateMsg
}
-> Builder routeParams templateData

View File

@ -10,7 +10,7 @@ import Route exposing (Route)
import View exposing (View)
type alias SharedTemplate msg sharedModel sharedData sharedMsg mappedMsg =
type alias SharedTemplate msg sharedModel sharedData mappedMsg =
{ init :
Maybe Browser.Navigation.Key
-> Flags
@ -30,7 +30,7 @@ type alias SharedTemplate msg sharedModel sharedData sharedMsg mappedMsg =
sharedData
->
{ path : Path
, frontmatter : Maybe Route
, route : Maybe Route
}
-> sharedModel
-> (msg -> mappedMsg)
@ -46,5 +46,4 @@ type alias SharedTemplate msg sharedModel sharedData sharedMsg mappedMsg =
}
-> msg
)
, sharedMsg : sharedMsg -> msg
}

View File

@ -0,0 +1,38 @@
const parseUrl = require("url").parse;
// this middleware is only active when (config.base !== '/')
module.exports = function baseMiddleware(base) {
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteBaseMiddleware(req, res, next) {
const url = req.url;
const parsed = parseUrl(url);
const path = parsed.pathname || "/";
if (path.startsWith(base)) {
// rewrite url to remove base.. this ensures that other middleware does
// not need to consider base being prepended or not
req.url = url.replace(base, "/");
return next();
}
if (path === "/" || path === "/index.html") {
// redirect root visit to based url
res.writeHead(302, {
Location: base,
});
res.end();
return;
} else if (req.headers.accept && req.headers.accept.includes("text/html")) {
// non-based page visit
res.statusCode = 404;
res.end(
`The server is configured with a public base URL of ${base} - ` +
`did you mean to visit ${base}${url.slice(1)} instead?`
);
return;
}
next();
};
};

View File

@ -1,18 +1,25 @@
const fs = require("./dir-helpers.js");
const fsPromises = require("fs").promises;
const { restoreColor } = require("./error-formatter");
const path = require("path");
const spawnCallback = require("cross-spawn").spawn;
const codegen = require("./codegen.js");
const terser = require("terser");
const matter = require("gray-matter");
const globby = require("globby");
const preRenderHtml = require("./pre-render-html.js");
const os = require("os");
const { Worker, SHARE_ENV } = require("worker_threads");
const { ensureDirSync } = require("./file-helpers.js");
let pool = [];
let pagesReady;
let pages = new Promise((resolve, reject) => {
pagesReady = resolve;
});
const DIR_PATH = path.join(process.cwd());
const OUTPUT_FILE_NAME = "elm.js";
let foundErrors = false;
process.on("unhandledRejection", (error) => {
console.error(error);
console.error("Unhandled: ", error);
process.exitCode = 1;
});
@ -23,146 +30,103 @@ const ELM_FILE_PATH = path.join(
);
async function ensureRequiredDirs() {
await fs.tryMkdir(`dist`);
ensureDirSync(`dist`);
ensureDirSync(path.join(process.cwd(), ".elm-pages", "http-response-cache"));
}
async function run(options) {
await ensureRequiredDirs();
XMLHttpRequest = require("xhr2");
// since init/update are never called in pre-renders, and DataSource.Http is called using undici
// we can provide a fake HTTP instead of xhr2 (which is otherwise needed for Elm HTTP requests from Node)
XMLHttpRequest = {};
const generateCode = codegen.generate();
const generateCode = codegen.generate(options.base);
const copyDone = copyAssets();
await generateCode;
const cliDone = runCli(options);
const compileClientDone = compileElm(options);
await Promise.all([copyDone, cliDone, compileClientDone]);
try {
await Promise.all([copyDone, cliDone, compileClientDone]);
} catch (error) {
console.log(error);
}
}
/**
* @param {string} basePath
*/
function initWorker(basePath) {
return new Promise((resolve, reject) => {
let newWorker = {
worker: new Worker(path.join(__dirname, "./render-worker.js"), {
env: SHARE_ENV,
workerData: { basePath },
}),
};
newWorker.worker.once("online", () => {
newWorker.worker.on("message", (message) => {
if (message.tag === "all-paths") {
pagesReady(JSON.parse(message.data));
} else if (message.tag === "error") {
process.exitCode = 1;
console.error(restoreColor(message.data.errorsJson));
buildNextPage(newWorker);
} else if (message.tag === "done") {
buildNextPage(newWorker);
} else {
throw `Unhandled tag ${message.tag}`;
}
});
newWorker.worker.on("error", (error) => {
console.error("Unhandled worker exception", error);
process.exitCode = 1;
buildNextPage(newWorker);
});
resolve(newWorker);
});
});
}
/**
*/
function prepareStaticPathsNew(thread) {
thread.worker.postMessage({
mode: "build",
tag: "render",
pathname: "/all-paths.json",
});
}
async function buildNextPage(thread) {
let nextPage = (await pages).pop();
if (nextPage) {
thread.worker.postMessage({
mode: "build",
tag: "render",
pathname: nextPage,
});
} else {
thread.worker.terminate();
}
}
async function runCli(options) {
await compileCliApp(options);
runElmApp();
}
const cpuCount = os.cpus().length;
console.log("Threads: ", cpuCount);
function runElmApp() {
process.on("beforeExit", (code) => {
if (foundErrors) {
process.exitCode = 1;
} else {
}
const getPathsWorker = initWorker(options.base);
getPathsWorker.then(prepareStaticPathsNew);
const threadsToCreate = Math.max(1, cpuCount / 2 - 1);
pool.push(getPathsWorker);
for (let index = 0; index < threadsToCreate - 1; index++) {
pool.push(initWorker(options.base));
}
pool.forEach((threadPromise) => {
threadPromise.then(buildNextPage);
});
return new Promise((resolve, _) => {
const mode /** @type { "dev" | "prod" } */ = "elm-to-html-beta";
const staticHttpCache = {};
const app = require(ELM_FILE_PATH).Elm.TemplateModulesBeta.init({
flags: { secrets: process.env, mode, staticHttpCache },
});
app.ports.toJsPort.subscribe(async (/** @type { FromElm } */ fromElm) => {
// console.log({ fromElm });
if (fromElm.command === "log") {
console.log(fromElm.value);
} else if (fromElm.tag === "InitialData") {
generateFiles(fromElm.args[0].filesToGenerate);
} else if (fromElm.tag === "PageProgress") {
outputString(fromElm);
} else if (fromElm.tag === "ReadFile") {
const filePath = fromElm.args[0];
try {
const fileContents = (await fs.readFile(filePath)).toString();
const parsedFile = matter(fileContents);
app.ports.fromJsPort.send({
tag: "GotFile",
data: {
filePath,
parsedFrontmatter: parsedFile.data,
withoutFrontmatter: parsedFile.content,
rawFile: fileContents,
jsonFile: jsonOrNull(fileContents),
},
});
} catch (error) {
app.ports.fromJsPort.send({
tag: "BuildError",
data: { filePath },
});
}
} else if (fromElm.tag === "Glob") {
const globPattern = fromElm.args[0];
const matchedPaths = await globby(globPattern);
app.ports.fromJsPort.send({
tag: "GotGlob",
data: { pattern: globPattern, result: matchedPaths },
});
} else if (fromElm.tag === "Errors") {
console.error(fromElm.args[0].errorString);
foundErrors = true;
} else {
console.log(fromElm);
throw "Unknown port tag.";
}
});
});
}
/**
* @param {{ path: string; content: string; }[]} filesToGenerate
*/
async function generateFiles(filesToGenerate) {
filesToGenerate.forEach(async ({ path: pathToGenerate, content }) => {
const fullPath = `dist/${pathToGenerate}`;
console.log(`Generating file /${pathToGenerate}`);
await fs.tryMkdir(path.dirname(fullPath));
fs.writeFile(fullPath, content);
});
}
/**
* @param {string} route
*/
function cleanRoute(route) {
return route.replace(/(^\/|\/$)/, "");
}
/**
* @param {string} cleanedRoute
*/
function pathToRoot(cleanedRoute) {
return cleanedRoute === ""
? cleanedRoute
: cleanedRoute
.split("/")
.map((_) => "..")
.join("/")
.replace(/\.$/, "./");
}
/**
* @param {string} route
*/
function baseRoute(route) {
const cleanedRoute = cleanRoute(route);
return cleanedRoute === "" ? "./" : pathToRoot(route);
}
async function outputString(/** @type { PageProgress } */ fromElm) {
const args = fromElm.args[0];
console.log(`Pre-rendered /${args.route}`);
const normalizedRoute = args.route.replace(/index$/, "");
// await fs.mkdir(`./dist/${normalizedRoute}`, { recursive: true });
await fs.tryMkdir(`./dist/${normalizedRoute}`);
const contentJsonString = JSON.stringify({
is404: args.is404,
staticData: args.contentJson,
});
fs.writeFile(
`dist/${normalizedRoute}/index.html`,
preRenderHtml(args, contentJsonString, false)
);
fs.writeFile(`dist/${normalizedRoute}/content.json`, contentJsonString);
}
async function compileElm(options) {
@ -175,19 +139,33 @@ async function compileElm(options) {
}
}
function spawnElmMake(options, elmEntrypointPath, outputPath, cwd) {
function elmOptimizeLevel2(elmEntrypointPath, outputPath, cwd) {
return new Promise((resolve, reject) => {
const fullOutputPath = cwd ? path.join(cwd, outputPath) : outputPath;
if (fs.existsSync(fullOutputPath)) {
fs.rmSync(fullOutputPath, {
force: true /* ignore errors if file doesn't exist */,
});
}
const subprocess = runElm(options, elmEntrypointPath, outputPath, cwd);
const subprocess = spawnCallback(
`elm-optimize-level-2`,
[elmEntrypointPath, "--output", outputPath],
{
// ignore stdout
// stdio: ["inherit", "ignore", "inherit"],
cwd: cwd,
}
);
let commandOutput = "";
subprocess.stderr.on("data", function (data) {
commandOutput += data;
});
subprocess.on("exit", async (code) => {
if (code !== 0) {
process.exitCode = 1;
reject(commandOutput);
}
});
subprocess.on("close", async (code) => {
const fileOutputExists = await fs.exists(fullOutputPath);
if (code == 0 && fileOutputExists) {
if (code == 0 && (await fs.fileExists(fullOutputPath))) {
resolve();
} else {
process.exitCode = 1;
@ -202,30 +180,55 @@ function spawnElmMake(options, elmEntrypointPath, outputPath, cwd) {
* @param {string} outputPath
* @param {string} cwd
*/
function runElm(options, elmEntrypointPath, outputPath, cwd) {
async function spawnElmMake(options, elmEntrypointPath, outputPath, cwd) {
if (options.debug) {
console.log("Running elm make");
return spawnCallback(
`elm`,
["make", elmEntrypointPath, "--output", outputPath, "--debug"],
{
// ignore stdout
stdio: ["inherit", "ignore", "inherit"],
cwd: cwd,
}
);
await runElmMake(elmEntrypointPath, outputPath, cwd);
} else {
console.log("Running elm-optimize-level-2");
return spawnCallback(
`elm-optimize-level-2`,
[elmEntrypointPath, "--output", outputPath],
await elmOptimizeLevel2(elmEntrypointPath, outputPath, cwd);
}
}
function runElmMake(elmEntrypointPath, outputPath, cwd) {
return new Promise(async (resolve, reject) => {
const subprocess = spawnCallback(
`elm`,
[
"make",
elmEntrypointPath,
"--output",
outputPath,
"--debug",
"--report",
"json",
],
{
// ignore stdout
stdio: ["inherit", "ignore", "inherit"],
// stdio: ["inherit", "ignore", "inherit"],
cwd: cwd,
}
);
}
const fullOutputPath = cwd ? path.join(cwd, outputPath) : outputPath;
if (await fs.fileExists(fullOutputPath)) {
await fsPromises.unlink(fullOutputPath, {
force: true /* ignore errors if file doesn't exist */,
});
}
let commandOutput = "";
subprocess.stderr.on("data", function (data) {
commandOutput += data;
});
subprocess.on("close", async (code) => {
if (code == 0 && (await fs.fileExists(fullOutputPath))) {
resolve();
} else {
process.exitCode = 1;
reject(restoreColor(JSON.parse(commandOutput).errors));
}
});
});
}
/**
@ -234,7 +237,7 @@ function runElm(options, elmEntrypointPath, outputPath, cwd) {
async function runTerser(filePath) {
console.log("Running terser");
const minifiedElm = await terser.minify(
(await fs.readFile(filePath)).toString(),
(await fsPromises.readFile(filePath)).toString(),
{
ecma: 5,
@ -268,16 +271,18 @@ async function runTerser(filePath) {
}
);
if (minifiedElm.code) {
await fs.writeFile(filePath, minifiedElm.code);
await fsPromises.writeFile(filePath, minifiedElm.code);
} else {
throw "Error running terser.";
}
}
async function copyAssets() {
fs.writeFile(
await fsPromises.writeFile(
"dist/elm-pages.js",
fs.readFileSync(path.join(__dirname, "../static-code/elm-pages.js"))
await fsPromises.readFile(
path.join(__dirname, "../static-code/elm-pages.js")
)
);
fs.copyDirFlat("public", "dist");
}
@ -290,13 +295,25 @@ async function compileCliApp(options) {
"./elm-stuff/elm-pages"
);
const elmFileContent = await fs.readFile(ELM_FILE_PATH, "utf-8");
await fs.writeFile(
const elmFileContent = await fsPromises.readFile(ELM_FILE_PATH, "utf-8");
await fsPromises.writeFile(
ELM_FILE_PATH,
elmFileContent.replace(
/return \$elm\$json\$Json\$Encode\$string\(.REPLACE_ME_WITH_JSON_STRINGIFY.\)/g,
"return " + (options.debug ? "_Json_wrap(x)" : "x")
)
elmFileContent
.replace(
/return \$elm\$json\$Json\$Encode\$string\(.REPLACE_ME_WITH_JSON_STRINGIFY.\)/g,
"return " + (options.debug ? "_Json_wrap(x)" : "x")
)
.replace(
"return ports ? { ports: ports } : {};",
`const die = function() {
managers = null
model = null
stepper = null
ports = null
}
return ports ? { ports: ports, die: die } : { die: die };`
)
);
}
@ -316,14 +333,3 @@ async function compileCliApp(options) {
*/
module.exports = { run };
/**
* @param {string} string
*/
function jsonOrNull(string) {
try {
return JSON.parse(string);
} catch (e) {
return { invalidJson: e.toString() };
}
}

View File

@ -18,8 +18,14 @@ async function main() {
program
.command("build")
.option("--debug", "Skip terser and run elm make with --debug")
.option(
"--base <basePath>",
"build site to be served under a base path",
"/"
)
.description("run a full site build")
.action(async (options) => {
options.base = normalizeUrl(options.base);
await build.run(options);
});
@ -27,8 +33,9 @@ async function main() {
.command("dev")
.description("start a dev server")
.option("--port <number>", "serve site at localhost:<port>", "1234")
.option("--base <basePath>", "serve site under a base path", "/")
.action(async (options) => {
console.log({ options });
options.base = normalizeUrl(options.base);
await dev.start(options);
});
@ -51,7 +58,7 @@ async function main() {
.description("open the docs for locally generated modules")
.option("--port <number>", "serve site at localhost:<port>", "8000")
.action(async (options) => {
await codegen.generate();
await codegen.generate("/");
const DocServer = require("elm-doc-preview");
const server = new DocServer({
port: options.port,
@ -65,4 +72,17 @@ async function main() {
program.parse(process.argv);
}
/**
* @param {string} pagePath
*/
function normalizeUrl(pagePath) {
if (!pagePath.startsWith("/")) {
pagePath = "/" + pagePath;
}
if (!pagePath.endsWith("/")) {
pagePath = pagePath + "/";
}
return pagePath;
}
main();

View File

@ -8,64 +8,66 @@ const path = require("path");
const { ensureDirSync, deleteIfExists } = require("./file-helpers.js");
global.builtAt = new Date();
async function generate() {
await writeFiles();
}
async function writeFiles() {
const cliCode = generateTemplateModuleConnector("cli");
const browserCode = generateTemplateModuleConnector("browser");
/**
* @param {string} basePath
*/
async function generate(basePath) {
const cliCode = generateTemplateModuleConnector(basePath, "cli");
const browserCode = generateTemplateModuleConnector(basePath, "browser");
ensureDirSync("./elm-stuff");
ensureDirSync("./.elm-pages");
ensureDirSync("./elm-stuff/elm-pages/.elm-pages");
fs.copyFileSync(path.join(__dirname, `./Page.elm`), `./.elm-pages/Page.elm`);
fs.copyFileSync(
path.join(__dirname, `./elm-application.json`),
`./elm-stuff/elm-pages/elm-application.json`
);
fs.copyFileSync(
path.join(__dirname, `./Page.elm`),
`./elm-stuff/elm-pages/.elm-pages/Page.elm`
);
fs.copyFileSync(
path.join(__dirname, `./SharedTemplate.elm`),
`./.elm-pages/SharedTemplate.elm`
);
fs.copyFileSync(
path.join(__dirname, `./SharedTemplate.elm`),
`./elm-stuff/elm-pages/.elm-pages/SharedTemplate.elm`
);
fs.copyFileSync(
path.join(__dirname, `./SiteConfig.elm`),
`./.elm-pages/SiteConfig.elm`
);
fs.copyFileSync(
path.join(__dirname, `./SiteConfig.elm`),
`./elm-stuff/elm-pages/.elm-pages/SiteConfig.elm`
);
const uiFileContent = elmPagesUiFile();
fs.writeFileSync("./.elm-pages/Pages.elm", uiFileContent);
// write `Pages.elm` with cli interface
fs.writeFileSync(
"./elm-stuff/elm-pages/.elm-pages/Pages.elm",
elmPagesCliFile()
);
fs.writeFileSync(
"./elm-stuff/elm-pages/.elm-pages/TemplateModulesBeta.elm",
cliCode.mainModule
);
fs.writeFileSync(
"./elm-stuff/elm-pages/.elm-pages/Route.elm",
cliCode.routesModule
);
fs.writeFileSync(
"./.elm-pages/TemplateModulesBeta.elm",
browserCode.mainModule
);
fs.writeFileSync("./.elm-pages/Route.elm", browserCode.routesModule);
await Promise.all([
fs.promises.copyFile(
path.join(__dirname, `./Page.elm`),
`./.elm-pages/Page.elm`
),
fs.promises.copyFile(
path.join(__dirname, `./elm-application.json`),
`./elm-stuff/elm-pages/elm-application.json`
),
fs.promises.copyFile(
path.join(__dirname, `./Page.elm`),
`./elm-stuff/elm-pages/.elm-pages/Page.elm`
),
fs.promises.copyFile(
path.join(__dirname, `./SharedTemplate.elm`),
`./.elm-pages/SharedTemplate.elm`
),
fs.promises.copyFile(
path.join(__dirname, `./SharedTemplate.elm`),
`./elm-stuff/elm-pages/.elm-pages/SharedTemplate.elm`
),
fs.promises.copyFile(
path.join(__dirname, `./SiteConfig.elm`),
`./.elm-pages/SiteConfig.elm`
),
fs.promises.copyFile(
path.join(__dirname, `./SiteConfig.elm`),
`./elm-stuff/elm-pages/.elm-pages/SiteConfig.elm`
),
fs.promises.writeFile("./.elm-pages/Pages.elm", uiFileContent),
// write `Pages.elm` with cli interface
fs.promises.writeFile(
"./elm-stuff/elm-pages/.elm-pages/Pages.elm",
elmPagesCliFile()
),
fs.promises.writeFile(
"./elm-stuff/elm-pages/.elm-pages/TemplateModulesBeta.elm",
cliCode.mainModule
),
fs.promises.writeFile(
"./elm-stuff/elm-pages/.elm-pages/Route.elm",
cliCode.routesModule
),
fs.promises.writeFile(
"./.elm-pages/TemplateModulesBeta.elm",
browserCode.mainModule
),
fs.promises.writeFile("./.elm-pages/Route.elm", browserCode.routesModule),
]);
// write modified elm.json to elm-stuff/elm-pages/
copyModifiedElmJson();

View File

@ -37,7 +37,10 @@ async function spawnElmMake(elmEntrypointPath, outputPath, cwd) {
async function compileElmForBrowser() {
await runElm("./.elm-pages/TemplateModulesBeta.elm", pathToClientElm);
return inject(await fs.promises.readFile(pathToClientElm, "utf-8"));
return fs.promises.writeFile(
"./.elm-pages/cache/elm.js",
inject(await fs.promises.readFile(pathToClientElm, "utf-8"))
);
}
/**

View File

@ -1,8 +1,6 @@
const path = require("path");
const fs = require("fs");
const chokidar = require("chokidar");
const compiledElmPath = path.join(process.cwd(), "elm-stuff/elm-pages/elm.js");
const renderer = require("../../generator/src/render");
const { spawnElmMake, compileElmForBrowser } = require("./compile-elm.js");
const http = require("http");
const codegen = require("./codegen.js");
@ -10,14 +8,26 @@ const kleur = require("kleur");
const serveStatic = require("serve-static");
const connect = require("connect");
const { restoreColor } = require("./error-formatter");
let Elm;
const { Worker, SHARE_ENV } = require("worker_threads");
const os = require("os");
const { ensureDirSync } = require("./file-helpers.js");
const baseMiddleware = require("./basepath-middleware.js");
/**
* @param {{ port: string; base: string }} options
*/
async function start(options) {
let threadReadyQueue = [];
let pool = [];
ensureDirSync(path.join(process.cwd(), ".elm-pages", "http-response-cache"));
const cpuCount = os.cpus().length;
const port = options.port;
global.staticHttpCache = {};
let elmMakeRunning = true;
const serve = serveStatic("public/", { index: false });
fs.mkdirSync(".elm-pages/cache", { recursive: true });
const serveCachedFiles = serveStatic(".elm-pages/cache", { index: false });
const generatedFilesDirectory = "elm-stuff/elm-pages/generated-files";
fs.mkdirSync(generatedFilesDirectory, { recursive: true });
const serveStaticCode = serveStatic(
@ -33,17 +43,15 @@ async function start(options) {
ignored: [/\.swp$/],
ignoreInitial: true,
});
watchElmSourceDirs();
await codegen.generate();
await codegen.generate(options.base);
let clientElmMakeProcess = compileElmForBrowser();
let pendingCliCompile = compileCliApp();
watchElmSourceDirs(true);
async function setup() {
await codegen.generate();
await Promise.all([clientElmMakeProcess, pendingCliCompile])
.then(() => {
console.log("Dev server ready");
elmMakeRunning = false;
})
.catch(() => {
@ -54,24 +62,33 @@ async function start(options) {
`<http://localhost:${port}>`
)}`
);
const poolSize = Math.max(1, cpuCount / 2 - 1);
for (let index = 0; index < poolSize; index++) {
pool.push(initWorker(options.base));
}
runPendingWork();
}
setup();
function watchElmSourceDirs() {
console.log("elm.json changed - reloading watchers");
watcher.removeAllListeners();
const sourceDirs = JSON.parse(fs.readFileSync("./elm.json").toString())[
"source-directories"
];
console.log("Watching...", { sourceDirs });
/**
* @param {boolean} initialRun
*/
async function watchElmSourceDirs(initialRun) {
if (initialRun) {
} else {
console.log("elm.json changed - reloading watchers");
watcher.removeAllListeners();
}
const sourceDirs = JSON.parse(
(await fs.promises.readFile("./elm.json")).toString()
)["source-directories"].filter(
(sourceDir) => path.resolve(sourceDir) !== path.resolve(".elm-pages")
);
watcher.add(sourceDirs);
watcher.add("./public/*.css");
}
function requireUncached() {
delete require.cache[require.resolve(compiledElmPath)];
Elm = require(compiledElmPath);
watcher.add("./port-data-source.js");
}
async function compileCliApp() {
@ -80,11 +97,13 @@ async function start(options) {
"elm.js",
"elm-stuff/elm-pages/"
);
requireUncached();
}
const app = connect()
.use(timeMiddleware())
.use(baseMiddleware(options.base))
.use(awaitElmMiddleware)
.use(serveCachedFiles)
.use(serveStaticCode)
.use(serve)
.use(processRequest);
@ -95,27 +114,17 @@ async function start(options) {
* @param {connect.NextHandleFunction} next
*/
async function processRequest(request, response, next) {
if (request.url && request.url.startsWith("/elm.js")) {
try {
await pendingCliCompile;
const clientElmJs = await clientElmMakeProcess;
response.writeHead(200, { "Content-Type": "text/javascript" });
response.end(clientElmJs);
} catch (elmCompilerError) {
response.writeHead(500, { "Content-Type": "application/json" });
response.end(elmCompilerError);
}
} else if (request.url && request.url.startsWith("/stream")) {
if (request.url && request.url.startsWith("/stream")) {
handleStream(request, response);
} else {
handleNavigationRequest(request, response, next);
await handleNavigationRequest(request, response, next);
}
}
watcher.on("all", async function (eventName, pathThatChanged) {
console.log({ pathThatChanged });
// console.log({ pathThatChanged });
if (pathThatChanged === "elm.json") {
watchElmSourceDirs();
watchElmSourceDirs(false);
} else if (pathThatChanged.endsWith(".css")) {
clients.forEach((client) => {
client.response.write(`data: style.css\n\n`);
@ -126,7 +135,7 @@ async function start(options) {
let codegenError = null;
if (needToRerunCodegen(eventName, pathThatChanged)) {
try {
await codegen.generate();
await codegen.generate(options.base);
clientElmMakeProcess = compileElmForBrowser();
pendingCliCompile = compileCliApp();
@ -156,7 +165,6 @@ async function start(options) {
clientElmMakeProcess = compileElmForBrowser();
pendingCliCompile = compileCliApp();
}
let timestamp = Date.now();
Promise.all([clientElmMakeProcess, pendingCliCompile])
.then(() => {
@ -170,22 +178,23 @@ async function start(options) {
});
}
} else {
const changedPathRelative = path.relative(process.cwd(), pathThatChanged);
Object.keys(global.staticHttpCache).forEach((dataSourceKey) => {
if (dataSourceKey.includes(`file://${changedPathRelative}`)) {
delete global.staticHttpCache[dataSourceKey];
} else if (
(eventName === "add" ||
eventName === "unlink" ||
eventName === "change" ||
eventName === "addDir" ||
eventName === "unlinkDir") &&
dataSourceKey.startsWith("glob://")
) {
delete global.staticHttpCache[dataSourceKey];
}
});
// TODO use similar logic in the workers? Or don't use cache at all?
// const changedPathRelative = path.relative(process.cwd(), pathThatChanged);
//
// Object.keys(global.staticHttpCache).forEach((dataSourceKey) => {
// if (dataSourceKey.includes(`file://${changedPathRelative}`)) {
// delete global.staticHttpCache[dataSourceKey];
// } else if (
// (eventName === "add" ||
// eventName === "unlink" ||
// eventName === "change" ||
// eventName === "addDir" ||
// eventName === "unlinkDir") &&
// dataSourceKey.startsWith("glob://")
// ) {
// delete global.staticHttpCache[dataSourceKey];
// }
// });
clients.forEach((client) => {
client.response.write(`data: content.json\n\n`);
});
@ -219,6 +228,55 @@ async function start(options) {
);
}
/**
* @param {string} pathname
* @param {((value: any) => any) | null | undefined} onOk
* @param {((reason: any) => PromiseLike<never>) | null | undefined} onErr
*/
function runRenderThread(pathname, onOk, onErr) {
let cleanUpThread = () => {};
return new Promise(async (resolve, reject) => {
const readyThread = await waitForThread();
console.log(`Rendering ${pathname}`, readyThread.worker.threadId);
cleanUpThread = () => {
cleanUp(readyThread);
};
readyThread.ready = false;
readyThread.worker.postMessage({
mode: "dev-server",
pathname,
});
readyThread.worker.on("message", (message) => {
if (message.tag === "done") {
resolve(message.data);
} else if (message.tag === "watch") {
// console.log("@@@ WATCH", message.data);
message.data.forEach((pattern) => watcher.add(pattern));
} else if (message.tag === "error") {
reject(message.data);
} else {
throw `Unhandled message: ${message}`;
}
});
readyThread.worker.on("error", (error) => {
reject(error.context);
});
})
.then(onOk)
.catch(onErr)
.finally(() => {
cleanUpThread();
});
}
function cleanUp(thread) {
thread.worker.removeAllListeners("message");
thread.worker.removeAllListeners("error");
thread.ready = true;
runPendingWork();
}
/**
* @param {http.IncomingMessage} req
* @param {http.ServerResponse} res
@ -229,46 +287,56 @@ async function start(options) {
const pathname = urlParts.pathname || "";
try {
await pendingCliCompile;
const renderResult = await renderer(
Elm,
await runRenderThread(
pathname,
req,
function (pattern) {
console.log(`Watching data source ${pattern}`);
watcher.add(pattern);
function (renderResult) {
const is404 = renderResult.is404;
switch (renderResult.kind) {
case "json": {
res.writeHead(is404 ? 404 : 200, {
"Content-Type": "application/json",
});
res.end(renderResult.contentJson);
break;
}
case "html": {
res.writeHead(is404 ? 404 : 200, {
"Content-Type": "text/html",
});
res.end(renderResult.htmlString);
break;
}
case "api-response": {
let mimeType = serveStatic.mime.lookup(pathname || "text/html");
mimeType =
mimeType === "application/octet-stream"
? "text/html"
: mimeType;
res.writeHead(renderResult.statusCode, {
"Content-Type": mimeType,
});
res.end(renderResult.body);
// TODO - if route is static, write file to api-route-cache/ directory
// TODO - get 404 or other status code from elm-pages renderer
break;
}
}
},
function (error) {
console.log(restoreColor(error.errorsJson));
if (req.url.includes("content.json")) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify(error.errorsJson));
} else {
res.writeHead(500, { "Content-Type": "text/html" });
res.end(errorHtml());
}
}
);
const is404 = renderResult.is404;
switch (renderResult.kind) {
case "json": {
res.writeHead(is404 ? 404 : 200, {
"Content-Type": "application/json",
});
res.end(renderResult.contentJson);
break;
}
case "html": {
res.writeHead(is404 ? 404 : 200, {
"Content-Type": "text/html",
});
res.end(renderResult.htmlString);
break;
}
case "api-response": {
let mimeType = serveStatic.mime.lookup(pathname || "text/html");
mimeType =
mimeType === "application/octet-stream" ? "text/html" : mimeType;
res.writeHead(renderResult.statusCode, {
"Content-Type": mimeType,
});
res.end(renderResult.body);
// TODO - if route is static, write file to api-route-cache/ directory
// TODO - get 404 or other status code from elm-pages renderer
break;
}
}
} catch (error) {
console.log(restoreColor(error));
console.log(restoreColor(error.errorsJson));
if (req.url.includes("content.json")) {
res.writeHead(500, { "Content-Type": "application/json" });
@ -279,6 +347,62 @@ async function start(options) {
}
}
}
async function awaitElmMiddleware(req, res, next) {
if (req.url && req.url.startsWith("/elm.js")) {
try {
await pendingCliCompile;
await clientElmMakeProcess;
next();
} catch (elmCompilerError) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(elmCompilerError);
}
} else {
next();
}
}
/**
* @returns {Promise<{ ready:boolean; worker: Worker }>}
* */
function waitForThread() {
return new Promise((resolve, reject) => {
threadReadyQueue.push(resolve);
runPendingWork();
});
}
function runPendingWork() {
const readyThreads = pool.filter((thread) => thread.ready);
readyThreads.forEach((readyThread) => {
const startTask = threadReadyQueue.shift();
if (startTask) {
// if we don't use setImmediate here, the remaining work will be done sequentially by a single worker
// using setImmediate delegates a ready thread to each pending task until it runs out of ready workers
// so the delegation is done sequentially, and the actual work is then executed
setImmediate(() => {
startTask(readyThread);
});
}
});
}
/**
* @param {string} basePath
*/
function initWorker(basePath) {
let newWorker = {
worker: new Worker(path.join(__dirname, "./render-worker.js"), {
env: SHARE_ENV,
workerData: { basePath },
}),
ready: false,
};
newWorker.worker.once("online", () => {
newWorker.ready = true;
});
return newWorker;
}
}
function timeMiddleware() {

View File

@ -2,6 +2,7 @@ const util = require("util");
const fsSync = require("fs");
const fs = {
writeFile: util.promisify(fsSync.writeFile),
writeFileSync: fsSync.writeFileSync,
rm: util.promisify(fsSync.unlinkSync),
mkdir: util.promisify(fsSync.mkdir),
readFile: util.promisify(fsSync.readFile),
@ -21,6 +22,22 @@ async function tryMkdir(dirName) {
}
}
function fileExists(file) {
return fsSync.promises
.access(file, fsSync.constants.F_OK)
.then(() => true)
.catch(() => false);
}
/**
* @param {string} filePath
* @param {string} data
*/
function writeFileSyncSafe(filePath, data) {
fsSync.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, data);
}
const path = require("path");
/**
@ -61,14 +78,17 @@ async function copyDirNested(src, dest) {
module.exports = {
writeFile: fs.writeFile,
writeFileSync: fs.writeFileSync,
readFile: fs.readFile,
readFileSync: fsSync.readFileSync,
copyFile: fs.copyFile,
exists: fs.exists,
writeFileSyncSafe,
tryMkdir,
copyDirFlat,
copyDirNested,
rmSync: fs.rm,
rm: fs.rm,
existsSync: fs.existsSync,
fileExists: fileExists,
};

View File

@ -24,7 +24,9 @@ function parseMsg(msg) {
if (typeof msg === "string") {
return msg;
} else {
if (msg.underline) {
if (msg.underline && msg.color) {
return kleur[msg.color.toLowerCase()]().underline(msg.string);
} else if (msg.underline) {
return kleur.underline(msg.string);
} else if (msg.color) {
return kleur[msg.color.toLowerCase()](msg.string);
@ -39,11 +41,10 @@ function parseMsg(msg) {
*
* This function takes in the array of compiler errors and maps over them to generate a formatted compiler error
**/
const restoreColor = (error) => {
console.log(error);
const restoreColor = (errors) => {
try {
return JSON.parse(error)
.errors.map(({ problems, path }) =>
return errors
.map(({ problems, path }) =>
problems.map(restoreProblem(path)).join("\n\n\n")
)
.join("\n\n\n\n\n");
@ -57,9 +58,7 @@ const restoreColor = (error) => {
*
* This function takes in the array of compiler errors and maps over them to generate a formatted compiler error
**/
const restoreProblem =
(path) =>
({ title, message }) =>
[parseHeader(title, path), ...message.map(parseMsg)].join("");
const restoreProblem = (path) => ({ title, message }) =>
[parseHeader(title, path), ...message.map(parseMsg)].join("");
module.exports = { restoreColor };

View File

@ -4,9 +4,10 @@ const mm = require("micromatch");
const routeHelpers = require("./route-codegen-helpers");
/**
* @param {string} basePath
* @param {'browser' | 'cli'} phase
*/
function generateTemplateModuleConnector(phase) {
function generateTemplateModuleConnector(basePath, phase) {
const templates = globby.sync(["src/Page/**/*.elm"], {}).map((file) => {
const captures = mm.capture("src/Page/**/*.elm", file);
if (captures) {
@ -128,7 +129,7 @@ type PageData
view :
{ path : Path
, frontmatter : Maybe Route
, route : Maybe Route
}
-> Maybe PageUrl
-> Shared.Data
@ -138,7 +139,7 @@ view :
, head : List Head.Tag
}
view page maybePageUrl globalData pageData =
case ( page.frontmatter, pageData ) of
case ( page.route, pageData ) of
${templates
.map(
(name) =>
@ -350,7 +351,7 @@ update sharedData pageData navigationKey msg model =
|> (\\( a, b, c ) ->
case c of
Just sharedMsg ->
( a, b, Shared.template.update (Shared.template.sharedMsg sharedMsg) model.global )
( a, b, Shared.template.update sharedMsg model.global )
Nothing ->
( a, b, ( model.global, Cmd.none ) )
@ -430,8 +431,13 @@ main =
, fromJsPort = fromJsPort identity
, data = dataForRoute
, sharedData = Shared.template.data
, apiRoutes = \\htmlToString -> routePatterns :: manifestHandler :: Api.routes getStaticRoutes htmlToString
, apiRoutes = \\htmlToString -> pathsToGenerateHandler :: routePatterns :: manifestHandler :: Api.routes getStaticRoutes htmlToString
, pathPatterns = routePatterns3
, basePath = [ ${basePath
.split("/")
.filter((segment) => segment !== "")
.map((segment) => `"${segment}"`)
.join(", ")} ]
}
dataForRoute : Maybe Route -> DataSource PageData
@ -593,6 +599,37 @@ getStaticRoutes =
|> DataSource.map List.concat
pathsToGenerateHandler : ApiRoute.Done ApiRoute.Response
pathsToGenerateHandler =
ApiRoute.succeed
(DataSource.map2
(\\pageRoutes apiRoutes ->
{ body =
(pageRoutes ++ (apiRoutes |> List.map (\\api -> "/" ++ api)))
|> Json.Encode.list Json.Encode.string
|> Json.Encode.encode 0
}
)
(DataSource.map
(List.map
(\\route ->
route
|> Route.toPath
|> Path.toAbsolute
)
)
getStaticRoutes
)
((manifestHandler :: Api.routes getStaticRoutes (\\_ -> ""))
|> List.map ApiRoute.getBuildTimeRoutes
|> DataSource.combine
|> DataSource.map List.concat
)
)
|> ApiRoute.literal "all-paths.json"
|> ApiRoute.single
manifestHandler : ApiRoute.Done ApiRoute.Response
manifestHandler =
ApiRoute.succeed
@ -655,9 +692,29 @@ type Route
{-| -}
urlToRoute : { url | path : String } -> Maybe Route
urlToRoute url =
Router.firstMatch matchers url.path
url.path
|> withoutBaseUrl
|> Router.firstMatch matchers
baseUrl : String
baseUrl =
"${basePath}"
baseUrlAsPath : List String
baseUrlAsPath =
baseUrl
|> String.split "/"
|> List.filter (not << String.isEmpty)
withoutBaseUrl path =
if (path |> String.startsWith baseUrl) then
String.dropLeft (String.length baseUrl) path
else
path
{-| -}
matchers : List (Router.Matcher Route)
matchers =
@ -711,13 +768,20 @@ routeToPath route =
{-| -}
toPath : Route -> Path
toPath route =
route |> routeToPath |> String.join "/" |> Path.fromString
(baseUrlAsPath ++ (route |> routeToPath)) |> String.join "/" |> Path.fromString
{-| -}
toString : Route -> String
toString route =
route |> toPath |> Path.toAbsolute
{-| -}
toLink : (List (Attribute msg) -> tag) -> Route -> tag
toLink toAnchorTag route =
toAnchorTag
[ Attr.href ("/" ++ (routeToPath route |> String.join "/"))
[ route |> toString |> Attr.href
, Attr.attribute "elm-pages:prefetch" ""
]

View File

@ -1,18 +1,20 @@
const cliVersion = require("../../package.json").version;
const seo = require("./seo-renderer.js");
const elmPagesJsMinified = require("./elm-pages-js-minified.js");
const path = require("path");
/** @typedef { { head: any[]; errors: any[]; contentJson: any[]; html: string; route: string; title: string; } } Arg */
/** @typedef { { tag : 'PageProgress'; args : Arg[] } } PageProgress */
module.exports =
/**
* @param {string} basePath
* @param {Arg} fromElm
* @param {string} contentJsonString
* @param {boolean} devServer
* @returns {string}
*/
function wrapHtml(fromElm, contentJsonString, devServer) {
function wrapHtml(basePath, fromElm, contentJsonString, devServer) {
const devServerOnly = (/** @type {string} */ devServerOnlyString) =>
devServer ? devServerOnlyString : "";
const seoData = seo.gather(fromElm.head);
@ -20,24 +22,29 @@ module.exports =
return `<!DOCTYPE html>
${seoData.rootElement}
<head>
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="${path.join(basePath, "style.css")}">
${devServerOnly(devServerStyleTag())}
<link rel="preload" href="/elm.js" as="script">
<link rel="modulepreload" href="/index.js">
<link rel="preload" href="${path.join(basePath, "elm.js")}" as="script">
<link rel="modulepreload" href="${path.join(basePath, "index.js")}">
${devServerOnly(
/* html */ `<script defer="defer" src="/hmr.js" type="text/javascript"></script>`
/* html */ `<script defer="defer" src="${path.join(
basePath,
"hmr.js"
)}" type="text/javascript"></script>`
)}
<script defer="defer" src="/elm.js" type="text/javascript"></script>
<base href="${baseRoute(fromElm.route)}">
<script defer="defer" src="${path.join(
basePath,
"elm.js"
)}" type="text/javascript"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script type="module">
import userInit from"/index.js";
import userInit from"${path.join(basePath, "index.js")}";
${elmPagesJsMinified}
</script>
<title>${fromElm.title}</title>
<meta name="generator" content="elm-pages v${cliVersion}">
<link rel="manifest" href="manifest.json">
<link rel="manifest" href="/manifest.json">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#ffffff">
<meta name="apple-mobile-web-app-capable" content="yes">
@ -53,14 +60,6 @@ ${elmPagesJsMinified}
`;
};
/**
* @param {string} route
*/
function baseRoute(route) {
const cleanedRoute = cleanRoute(route);
return cleanedRoute === "" ? "./" : pathToRoot(route);
}
/**
* @param {string} route
*/

View File

@ -0,0 +1,97 @@
const renderer = require("../../generator/src/render");
const path = require("path");
const fs = require("./dir-helpers.js");
const compiledElmPath = path.join(process.cwd(), "elm-stuff/elm-pages/elm.js");
const { parentPort, threadId, workerData } = require("worker_threads");
let Elm;
global.staticHttpCache = {};
async function run({ mode, pathname }) {
console.time(`${threadId} ${pathname}`);
try {
const req = null;
const renderResult = await renderer(
workerData.basePath,
requireElm(mode),
mode,
pathname,
req,
function (patterns) {
if (mode === "dev-server" && patterns.size > 0) {
parentPort.postMessage({ tag: "watch", data: [...patterns] });
}
}
);
if (mode === "dev-server") {
parentPort.postMessage({ tag: "done", data: renderResult });
} else if (mode === "build") {
outputString(renderResult, pathname);
} else {
throw `Unknown mode ${mode}`;
}
} catch (error) {
parentPort.postMessage({ tag: "error", data: error });
}
console.timeEnd(`${threadId} ${pathname}`);
}
function requireElm(mode) {
if (mode === "build") {
if (!Elm) {
const warnOriginal = console.warn;
console.warn = function () {};
Elm = require(compiledElmPath);
console.warn = warnOriginal;
}
return Elm;
} else {
delete require.cache[require.resolve(compiledElmPath)];
return require(compiledElmPath);
}
}
async function outputString(
/** @type { { kind: 'page'; data: PageProgress } | { kind: 'api'; data: Object } } */ fromElm,
/** @type string */ pathname
) {
switch (fromElm.kind) {
case "html": {
const args = fromElm;
console.log(`Pre-rendered /${args.route}`);
const normalizedRoute = args.route.replace(/index$/, "");
await fs.tryMkdir(`./dist/${normalizedRoute}`);
const contentJsonString = JSON.stringify({
is404: args.is404,
staticData: args.contentJson,
path: args.route,
});
fs.writeFileSync(`dist/${normalizedRoute}/index.html`, args.htmlString);
fs.writeFileSync(
`dist/${normalizedRoute}/content.json`,
contentJsonString
);
// parentPort.postMessage({ tag: "done" });
parentPort.postMessage({ tag: "done" });
break;
}
case "api-response": {
const body = fromElm.body;
console.log(`Generated ${pathname}`);
fs.writeFileSyncSafe(path.join("dist", pathname), body);
if (pathname === "/all-paths.json") {
parentPort.postMessage({ tag: "all-paths", data: body });
} else {
parentPort.postMessage({ tag: "done" });
}
break;
}
}
}
parentPort.on("message", run);
/** @typedef { { tag : 'PageProgress'; args : Arg[] } } PageProgress */

View File

@ -5,25 +5,45 @@ const matter = require("gray-matter");
const globby = require("globby");
const fsPromises = require("fs").promises;
const preRenderHtml = require("./pre-render-html.js");
const { lookupOrPerform } = require("./request-cache.js");
const kleur = require("kleur");
kleur.enabled = true;
let foundErrors = false;
process.on("unhandledRejection", (error) => {
console.error(error);
});
let foundErrors;
let pendingDataSourceResponses;
let pendingDataSourceCount;
module.exports =
/**
*
* @param {string} basePath
* @param {Object} elmModule
* @param {string} path
* @param {import('aws-lambda').APIGatewayProxyEvent} request
* @param {(pattern: string) => void} addDataSourceWatcher
* @returns
*/
async function run(elmModule, path, request, addDataSourceWatcher) {
XMLHttpRequest = require("xhr2");
async function run(
basePath,
elmModule,
mode,
path,
request,
addDataSourceWatcher
) {
foundErrors = false;
pendingDataSourceResponses = [];
pendingDataSourceCount = 0;
// since init/update are never called in pre-renders, and DataSource.Http is called using undici
// we can provide a fake HTTP instead of xhr2 (which is otherwise needed for Elm HTTP requests from Node)
XMLHttpRequest = {};
const result = await runElmApp(
basePath,
elmModule,
mode,
path,
request,
addDataSourceWatcher
@ -32,27 +52,36 @@ module.exports =
};
/**
* @param {string} basePath
* @param {Object} elmModule
* @param {string} pagePath
* @param {string} mode
* @param {import('aws-lambda').APIGatewayProxyEvent} request
* @param {(pattern: string) => void} addDataSourceWatcher
* @returns {Promise<({is404: boolean} & ( { kind: 'json'; contentJson: string} | { kind: 'html'; htmlString: string } | { kind: 'api-response'; body: string; }) )>}
*/
function runElmApp(elmModule, pagePath, request, addDataSourceWatcher) {
function runElmApp(
basePath,
elmModule,
mode,
pagePath,
request,
addDataSourceWatcher
) {
const isDevServer = mode !== "build";
let patternsToWatch = new Set();
let app = null;
let killApp;
return new Promise((resolve, reject) => {
const isJson = pagePath.match(/content\.json\/?$/);
const route = pagePath.replace(/content\.json\/?$/, "");
const mode = "elm-to-html-beta";
const modifiedRequest = { ...request, path: route };
// console.log("StaticHttp cache keys", Object.keys(global.staticHttpCache));
app = elmModule.Elm.TemplateModulesBeta.init({
flags: {
secrets: process.env,
mode,
staticHttpCache: global.staticHttpCache,
staticHttpCache: global.staticHttpCache || {},
request: {
payload: modifiedRequest,
kind: "single-page",
@ -62,7 +91,7 @@ function runElmApp(elmModule, pagePath, request, addDataSourceWatcher) {
});
killApp = () => {
// app.ports.toJsPort.unsubscribe(portHandler);
app.ports.toJsPort.unsubscribe(portHandler);
app.die();
app = null;
// delete require.cache[require.resolve(compiledElmPath)];
@ -71,13 +100,11 @@ function runElmApp(elmModule, pagePath, request, addDataSourceWatcher) {
async function portHandler(/** @type { FromElm } */ fromElm) {
if (fromElm.command === "log") {
console.log(fromElm.value);
} else if (fromElm.tag === "InitialData") {
const args = fromElm.args[0];
// console.log(`InitialData`, args);
writeGeneratedFiles(args.filesToGenerate);
} else if (fromElm.tag === "ApiResponse") {
const args = fromElm.args[0];
global.staticHttpCache = args.staticHttpCache;
if (mode === "build") {
global.staticHttpCache = args.staticHttpCache;
}
resolve({
kind: "api-response",
@ -87,9 +114,10 @@ function runElmApp(elmModule, pagePath, request, addDataSourceWatcher) {
});
} else if (fromElm.tag === "PageProgress") {
const args = fromElm.args[0];
global.staticHttpCache = args.staticHttpCache;
if (mode === "build") {
global.staticHttpCache = args.staticHttpCache;
}
// delete require.cache[require.resolve(compiledElmPath)];
if (isJson) {
resolve({
kind: "json",
@ -100,50 +128,29 @@ function runElmApp(elmModule, pagePath, request, addDataSourceWatcher) {
}),
});
} else {
resolve(outputString(fromElm));
resolve(outputString(basePath, fromElm, isDevServer));
}
} else if (fromElm.tag === "ReadFile") {
const filePath = fromElm.args[0];
try {
addDataSourceWatcher(filePath);
patternsToWatch.add(filePath);
const fileContents = (
await fsPromises.readFile(path.join(process.cwd(), filePath))
).toString();
const parsedFile = matter(fileContents);
app.ports.fromJsPort.send({
tag: "GotFile",
data: {
filePath,
parsedFrontmatter: parsedFile.data,
withoutFrontmatter: parsedFile.content,
rawFile: fileContents,
jsonFile: jsonOrNull(fileContents),
},
});
runJob(app, filePath);
} catch (error) {
app.ports.fromJsPort.send({
tag: "BuildError",
data: { filePath },
sendError(app, {
title: "DataSource.File Error",
message: `A DataSource.File read failed because I couldn't find this file: ${kleur.yellow(
filePath
)}`,
});
}
} else if (fromElm.tag === "DoHttp") {
const requestToPerform = fromElm.args[0];
runHttpJob(app, mode, requestToPerform);
} else if (fromElm.tag === "Glob") {
const globPattern = fromElm.args[0];
addDataSourceWatcher(globPattern);
const matchedPaths = await globby(globPattern);
app.ports.fromJsPort.send({
tag: "GotGlob",
data: { pattern: globPattern, result: matchedPaths },
});
} else if (fromElm.tag === "Port") {
const portName = fromElm.args[0];
console.log({ portName });
app.ports.fromJsPort.send({
tag: "GotPort",
data: { portName, portResponse: "Hello from ports!" },
});
patternsToWatch.add(globPattern);
runGlobJob(app, globPattern);
} else if (fromElm.tag === "Errors") {
foundErrors = true;
reject(fromElm.args[0]);
@ -153,23 +160,34 @@ function runElmApp(elmModule, pagePath, request, addDataSourceWatcher) {
}
app.ports.toJsPort.subscribe(portHandler);
}).finally(() => {
addDataSourceWatcher(patternsToWatch);
killApp();
killApp = null;
});
}
async function outputString(/** @type { PageProgress } */ fromElm) {
/**
* @param {string} basePath
* @param {PageProgress} fromElm
* @param {boolean} isDevServer
*/
async function outputString(
basePath,
/** @type { PageProgress } */ fromElm,
isDevServer
) {
const args = fromElm.args[0];
let contentJson = {};
contentJson["staticData"] = args.contentJson;
contentJson["is404"] = args.is404;
contentJson["path"] = args.route;
const normalizedRoute = args.route.replace(/index$/, "");
const contentJsonString = JSON.stringify(contentJson);
return {
is404: args.is404,
route: normalizedRoute,
htmlString: preRenderHtml(args, contentJsonString, true),
htmlString: preRenderHtml(basePath, args, contentJsonString, isDevServer),
contentJson: args.contentJson,
kind: "html",
};
}
@ -193,3 +211,206 @@ function jsonOrNull(string) {
return { invalidJson: e.toString() };
}
}
async function runJob(app, filePath) {
pendingDataSourceCount += 1;
try {
const fileContents = (
await fsPromises.readFile(path.join(process.cwd(), filePath))
).toString();
const parsedFile = matter(fileContents);
pendingDataSourceResponses.push({
request: {
masked: {
url: `file://${filePath}`,
method: "GET",
headers: [],
body: { tag: "EmptyBody", args: [] },
},
unmasked: {
url: `file://${filePath}`,
method: "GET",
headers: [],
body: { tag: "EmptyBody", args: [] },
},
},
response: JSON.stringify({
parsedFrontmatter: parsedFile.data,
withoutFrontmatter: parsedFile.content,
rawFile: fileContents,
jsonFile: jsonOrNull(fileContents),
}),
});
} catch (e) {
sendError(app, {
title: "Error reading file",
message: `A DataSource.File read failed because I couldn't find this file: ${kleur.yellow(
filePath
)}`,
});
} finally {
pendingDataSourceCount -= 1;
flushIfDone(app);
}
}
async function runHttpJob(app, mode, requestToPerform) {
pendingDataSourceCount += 1;
try {
const responseFilePath = await lookupOrPerform(
mode,
requestToPerform.unmasked
);
pendingDataSourceResponses.push({
request: requestToPerform,
response: (
await fsPromises.readFile(responseFilePath, "utf8")
).toString(),
});
} catch (error) {
sendError(app, error);
} finally {
pendingDataSourceCount -= 1;
flushIfDone(app);
}
}
async function runGlobJob(app, globPattern) {
try {
// if (pendingDataSourceCount > 0) {
// console.log(`Waiting for ${pendingDataSourceCount} pending data sources`);
// }
pendingDataSourceCount += 1;
pendingDataSourceResponses.push(await globTask(globPattern));
} catch (error) {
console.log(`Error running glob pattern ${globPattern}`);
throw error;
} finally {
pendingDataSourceCount -= 1;
flushIfDone(app);
}
}
function flushIfDone(app) {
if (foundErrors) {
pendingDataSourceResponses = [];
} else if (pendingDataSourceCount === 0) {
// console.log(
// `Flushing ${pendingDataSourceResponses.length} items in ${timeUntilThreshold}ms`
// );
flushQueue(app);
}
}
function flushQueue(app) {
const temp = pendingDataSourceResponses;
pendingDataSourceResponses = [];
// console.log("@@@ FLUSHING", temp.length);
app.ports.fromJsPort.send({
tag: "GotBatch",
data: temp,
});
}
/**
* @param {string} filePath
* @returns {Promise<Object>}
*/
async function readFileTask(app, filePath) {
// console.log(`Read file ${filePath}`);
try {
const fileContents = (
await fsPromises.readFile(path.join(process.cwd(), filePath))
).toString();
// console.log(`DONE reading file ${filePath}`);
const parsedFile = matter(fileContents);
return {
request: {
masked: {
url: `file://${filePath}`,
method: "GET",
headers: [],
body: { tag: "EmptyBody", args: [] },
},
unmasked: {
url: `file://${filePath}`,
method: "GET",
headers: [],
body: { tag: "EmptyBody", args: [] },
},
},
response: JSON.stringify({
parsedFrontmatter: parsedFile.data,
withoutFrontmatter: parsedFile.content,
rawFile: fileContents,
jsonFile: jsonOrNull(fileContents),
}),
};
} catch (e) {
sendError(app, {
title: "Error reading file",
message: `A DataSource.File read failed because I couldn't find this file: ${kleur.yellow(
filePath
)}`,
});
}
}
/**
* @param {string} globPattern
* @returns {Promise<Object>}
*/
async function globTask(globPattern) {
try {
const matchedPaths = await globby(globPattern);
// console.log("Got glob path", matchedPaths);
return {
request: {
masked: {
url: `glob://${globPattern}`,
method: "GET",
headers: [],
body: { tag: "EmptyBody", args: [] },
},
unmasked: {
url: `glob://${globPattern}`,
method: "GET",
headers: [],
body: { tag: "EmptyBody", args: [] },
},
},
response: JSON.stringify(matchedPaths),
};
} catch (e) {
console.log(`Error performing glob '${globPattern}'`);
throw e;
}
}
function requireUncached(mode, filePath) {
if (mode === "dev-server") {
// for the build command, we can skip clearing the cache because it won't change while the build is running
// in the dev server, we want to clear the cache to get a the latest code each time it runs
delete require.cache[require.resolve(filePath)];
}
return require(filePath);
}
/**
* @param {{ ports: { fromJsPort: { send: (arg0: { tag: string; data: any; }) => void; }; }; }} app
* @param {{ message: string; title: string; }} error
*/
function sendError(app, error) {
foundErrors = true;
app.ports.fromJsPort.send({
tag: "BuildError",
data: error,
});
}

View File

@ -0,0 +1,180 @@
const path = require("path");
const undici = require("undici");
const fs = require("fs");
const objectHash = require("object-hash");
const kleur = require("kleur");
/**
* To cache HTTP requests on disk with quick lookup and insertion, we store the hashed request.
* This uses SHA1 hashes. They are uni-directional hashes, which works for this use case. Most importantly,
* they're unique enough and can be expressed in a case-insensitive way so it works on Windows filesystems.
* And they are 40 hex characters, so the length won't be too long no matter what the request payload.
* @param {Object} request
*/
function requestToString(request) {
return objectHash(request);
}
/**
* @param {Object} request
*/
function fullPath(request) {
return path.join(
process.cwd(),
".elm-pages",
"http-response-cache",
requestToString(request)
);
}
/**
* @param {string} mode
* @param {{url: string; headers: {[x: string]: string}; method: string; body: Body } } rawRequest
* @returns {Promise<string>}
*/
function lookupOrPerform(mode, rawRequest) {
return new Promise(async (resolve, reject) => {
const request = toRequest(rawRequest);
const responsePath = fullPath(request);
if (fs.existsSync(responsePath)) {
// console.log("Skipping request, found file.");
resolve(responsePath);
} else {
let portDataSource = {};
let portDataSourceFound = false;
try {
portDataSource = requireUncached(
mode,
path.join(process.cwd(), "port-data-source.js")
);
portDataSourceFound = true;
} catch (e) {}
if (request.url.startsWith("port://")) {
try {
const portName = request.url.replace(/^port:\/\//, "");
// console.time(JSON.stringify(request.url));
if (!portDataSource[portName]) {
if (portDataSourceFound) {
throw `DataSource.Port.send "${portName}" is not defined. Be sure to export a function with that name from port-data-source.js`;
} else {
throw `DataSource.Port.send "${portName}" was called, but I couldn't find the port definitions file 'port-data-source.js'.`;
}
} else if (typeof portDataSource[portName] !== "function") {
throw `DataSource.Port.send "${portName}" is not a function. Be sure to export a function with that name from port-data-source.js`;
}
await fs.promises.writeFile(
responsePath,
JSON.stringify(
await portDataSource[portName](rawRequest.body.args[0])
)
);
resolve(responsePath);
// console.timeEnd(JSON.stringify(requestToPerform.masked));
} catch (error) {
console.trace(error);
reject({
title: "DataSource.Port Error",
message: error.toString(),
});
}
} else {
undici
.stream(
request.url,
{
method: request.method,
body: request.body,
headers: {
"User-Agent": "request",
...request.headers,
},
},
(response) => {
const writeStream = fs.createWriteStream(responsePath);
writeStream.on("finish", async () => {
resolve(responsePath);
});
return writeStream;
}
)
.catch((error) => {
let errorMessage = error.toString();
if (error.code === "ENOTFOUND") {
errorMessage = `Could not reach URL.`;
}
reject({
title: "DataSource.Http Error",
message: `${kleur
.yellow()
.underline(request.url)} ${errorMessage}`,
});
});
}
}
});
}
/**
* @param {{url: string; headers: {[x: string]: string}; method: string; body: Body } } elmRequest
*/
function toRequest(elmRequest) {
const elmHeaders = Object.fromEntries(elmRequest.headers);
let contentType = toContentType(elmRequest.body);
let headers = { ...contentType, ...elmHeaders };
return {
url: elmRequest.url,
method: elmRequest.method,
headers,
body: toBody(elmRequest.body),
};
}
/**
* @param {Body} body
*/
function toBody(body) {
switch (body.tag) {
case "EmptyBody": {
return null;
}
case "StringBody": {
return body.args[1];
}
case "JsonBody": {
return JSON.stringify(body.args[0]);
}
}
}
/**
* @param {Body} body
* @returns Object
*/
function toContentType(body) {
switch (body.tag) {
case "EmptyBody": {
return {};
}
case "StringBody": {
return { "Content-Type": body.args[0] };
}
case "JsonBody": {
return { "Content-Type": "application/json" };
}
}
}
/** @typedef { { tag: 'EmptyBody'} | { tag: 'StringBody'; args: [string, string] } | {tag: 'JsonBody'; args: [ Object ] } } Body */
function requireUncached(mode, filePath) {
if (mode === "dev-server") {
// for the build command, we can skip clearing the cache because it won't change while the build is running
// in the dev server, we want to clear the cache to get a the latest code each time it runs
delete require.cache[require.resolve(filePath)];
}
return require(filePath);
}
module.exports = { lookupOrPerform };

View File

@ -1,10 +1,13 @@
const fs = require("fs");
module.exports = function () {
var elmJson = JSON.parse(fs.readFileSync("./elm.json").toString());
module.exports = async function () {
var elmJson = JSON.parse(
(await fs.promises.readFile("./elm.json")).toString()
);
// write new elm.json
fs.writeFileSync(
await writeFileIfChanged(
"./elm-stuff/elm-pages/elm.json",
JSON.stringify(rewriteElmJson(elmJson))
);
@ -28,3 +31,18 @@ function rewriteElmJson(elmJson) {
elmJson["source-directories"].push(".elm-pages");
return elmJson;
}
async function writeFileIfChanged(filePath, content) {
if (
!(await fileExists(filePath)) ||
(await fs.promises.readFile(filePath, "utf8")) !== content
) {
await fs.promises.writeFile(filePath, content);
}
}
function fileExists(file) {
return fs.promises
.access(file, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
}

View File

@ -14,7 +14,6 @@ function loadContentAndInitializeApp() {
const app = Elm.TemplateModulesBeta.init({
flags: {
secrets: null,
baseUrl: document.baseURI,
isPrerendering: false,
isDevServer: false,
isElmDebugMode: false,
@ -45,7 +44,6 @@ function prefetchIfNeeded(/** @type {HTMLAnchorElement} */ target) {
!prefetchedPages.includes(target.pathname)
) {
prefetchedPages.push(target.pathname);
console.log("Preloading...", target.pathname);
const link = document.createElement("link");
link.setAttribute("as", "fetch");

View File

@ -41,6 +41,7 @@ async function handleEvent(sendContentJsonPort, evt) {
showCompiling("");
elmJsFetch().then(thenApplyHmr);
} else if (evt.data === "style.css") {
// https://stackoverflow.com/a/43161591
const links = document.getElementsByTagName("link");
for (var i = 0; i < links.length; i++) {
const link = links[i];

View File

@ -6,11 +6,12 @@ import Html exposing (Html)
import Pages.Flags
import Pages.PageUrl exposing (PageUrl)
import Path exposing (Path)
import Route exposing (Route)
import SharedTemplate exposing (SharedTemplate)
import View exposing (View)
template : SharedTemplate Msg Model Data SharedMsg msg
template : SharedTemplate Msg Model Data msg
template =
{ init = init
, update = update
@ -18,7 +19,6 @@ template =
, data = data
, subscriptions = subscriptions
, onPageChange = Just OnPageChange
, sharedMsg = SharedMsg
}
@ -88,7 +88,7 @@ view :
Data
->
{ path : Path
, frontmatter : route
, route : Maybe Route
}
-> Model
-> (Msg -> msg)

1667
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,35 +22,36 @@
"author": "Dillon Kearns",
"license": "BSD-3-Clause",
"dependencies": {
"chokidar": "^3.5.1",
"commander": "^7.2.0",
"chokidar": "3.5.2",
"commander": "8.0.0",
"connect": "^3.7.0",
"cross-spawn": "7.0.3",
"elm-doc-preview": "^5.0.5",
"elm-hot": "^1.1.6",
"elm-optimize-level-2": "^0.1.5",
"fs-extra": "^10.0.0",
"globby": "^11.0.3",
"globby": "11.0.4",
"gray-matter": "^4.0.3",
"kleur": "^4.1.4",
"micromatch": "^4.0.4",
"object-hash": "^2.2.0",
"serve-static": "^1.14.1",
"terser": "^5.7.0",
"xhr2": "^0.2.1"
"terser": "5.7.1",
"undici": "4.2.1"
},
"devDependencies": {
"@types/cross-spawn": "^6.0.2",
"@types/fs-extra": "^9.0.11",
"@types/fs-extra": "9.0.12",
"@types/micromatch": "^4.0.1",
"@types/node": "12.20.12",
"@types/serve-static": "^1.13.9",
"cypress": "^7.4.0",
"@types/serve-static": "1.13.10",
"cypress": "^8.0.0",
"elm-review": "^2.5.3",
"elm-test": "^0.19.1-revision7",
"elm-tooling": "^1.3.0",
"elm-verify-examples": "^5.0.0",
"mocha": "^8.4.0",
"typescript": "^4.2.4"
"typescript": "4.3.5"
},
"files": [
"generator/src/",

File diff suppressed because it is too large Load Diff

102
plugins/Shiki.elm Normal file
View File

@ -0,0 +1,102 @@
module Shiki exposing (Highlighted, decoder, view)
import Html exposing (Html)
import Html.Attributes as Attr exposing (class)
import Html.Lazy
import OptimizedDecoder as Decode exposing (Decoder)
type alias ShikiToken =
{ content : String
, color : Maybe String
, fontStyle : Maybe ( String, String )
}
type alias Highlighted =
{ lines : List (List ShikiToken)
, fg : String
, bg : String
}
decoder : Decoder Highlighted
decoder =
Decode.map3 Highlighted
(Decode.field "tokens" (Decode.list (Decode.list shikiTokenDecoder)))
(Decode.field "fg" Decode.string)
(Decode.field "bg" Decode.string)
shikiTokenDecoder : Decode.Decoder ShikiToken
shikiTokenDecoder =
Decode.map3 ShikiToken
(Decode.field "content" Decode.string)
(Decode.optionalField "color" Decode.string)
(Decode.optionalField "fontStyle" fontStyleDecoder |> Decode.map (Maybe.andThen identity))
fontStyleDecoder : Decoder (Maybe ( String, String ))
fontStyleDecoder =
Decode.int
|> Decode.map
(\styleNumber ->
case styleNumber of
1 ->
Just ( "font-style", "italic" )
2 ->
Just ( "font-style", "bold" )
4 ->
Just ( "font-style", "underline" )
_ ->
Nothing
)
{-| <https://github.com/shikijs/shiki/blob/2a31dc50f4fbdb9a63990ccd15e08cccc9c1566a/packages/shiki/src/renderer.ts#L16>
-}
view : List (Html.Attribute msg) -> Highlighted -> Html msg
view attrs highlighted =
highlighted.lines
|> List.indexedMap
(\lineIndex line ->
let
isLastLine =
List.length highlighted.lines == (lineIndex + 1)
in
Html.span [ class "line" ]
((line
|> List.map
(\token ->
Html.span
[ Attr.style "color" (token.color |> Maybe.withDefault highlighted.fg)
, token.fontStyle
|> Maybe.map
(\( key, value ) ->
Attr.style key value
)
|> Maybe.withDefault (Attr.title "")
]
[ Html.text token.content ]
)
)
++ [ if isLastLine then
Html.text ""
else
Html.text "\n"
]
)
)
|> Html.code []
|> List.singleton
|> Html.pre
([ Attr.style "background-color" highlighted.bg
, Attr.style "white-space" "pre-wrap"
, Attr.style "overflow-wrap" "break-word"
]
++ attrs
)

64
plugins/Timestamps.elm Normal file
View File

@ -0,0 +1,64 @@
module Timestamps exposing (Timestamps, data, format)
import DataSource exposing (DataSource)
import DataSource.Port
import DateFormat
import Json.Encode
import List.Extra
import OptimizedDecoder as Decode exposing (Decoder)
import Result.Extra
import Time
type alias Timestamps =
{ updated : Time.Posix
, created : Time.Posix
}
data : String -> DataSource Timestamps
data filePath =
DataSource.Port.send "gitTimestamps"
(Json.Encode.string filePath)
(Decode.string
|> Decode.map (String.trim >> String.split "\n")
|> Decode.map (List.map secondsStringToPosix)
|> Decode.map Result.Extra.combine
|> Decode.andThen Decode.fromResult
|> Decode.map (firstAndLast Timestamps >> Result.fromMaybe "Error")
|> Decode.andThen Decode.fromResult
)
firstAndLast : (a -> a -> b) -> List a -> Maybe b
firstAndLast constructor list =
Maybe.map2 constructor
(List.head list)
(List.Extra.last list)
secondsStringToPosix : String -> Result String Time.Posix
secondsStringToPosix posixTime =
posixTime
|> String.trim
|> String.toInt
|> Maybe.map (\unixTimeInSeconds -> (unixTimeInSeconds * 1000) |> Time.millisToPosix)
|> Result.fromMaybe "Expected int"
format : Time.Posix -> String
format posix =
DateFormat.format
[ DateFormat.monthNameFull
, DateFormat.text " "
, DateFormat.dayOfMonthNumber
, DateFormat.text ", "
, DateFormat.yearNumber
]
pacificZone
posix
pacificZone : Time.Zone
pacificZone =
Time.customZone (-60 * 7) []

View File

@ -9,13 +9,13 @@
"elm/core": "1.0.5",
"elm/json": "1.1.3",
"elm/project-metadata-utils": "1.0.2",
"jfmengels/elm-review": "2.4.2",
"jfmengels/elm-review": "2.5.0",
"jfmengels/elm-review-common": "1.0.4",
"jfmengels/elm-review-debug": "1.0.6",
"jfmengels/elm-review-performance": "1.0.0",
"jfmengels/elm-review-unused": "1.1.11",
"jfmengels/elm-review-performance": "1.0.1",
"jfmengels/elm-review-unused": "1.1.15",
"sparksp/elm-review-imports": "1.0.1",
"stil4m/elm-syntax": "7.2.5"
"stil4m/elm-syntax": "7.2.6"
},
"indirect": {
"elm/html": "1.0.0",
@ -23,7 +23,7 @@
"elm/random": "1.0.0",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2",
"elm-community/list-extra": "8.3.0",
"elm-community/list-extra": "8.3.1",
"elm-explorations/test": "1.2.2",
"miniBill/elm-unicode": "1.0.2",
"rtfeldman/elm-hex": "1.0.0",

View File

@ -1,4 +1,7 @@
module ApiRoute exposing (Done, Handler, Response, buildTimeRoutes, capture, int, literal, single, slash, succeed)
module ApiRoute exposing
( Done, Handler, Response, buildTimeRoutes, capture, int, literal, single, slash, succeed
, getBuildTimeRoutes
)
{-|
@ -104,13 +107,8 @@ slash (Handler pattern handler toString constructor) =
{-| -}
capture :
Handler
(String -> a)
constructor
->
Handler
a
(String -> constructor)
Handler (String -> a) constructor
-> Handler a (String -> constructor)
capture (Handler pattern previousHandler toString constructor) =
Handler
(pattern ++ "(.*)")
@ -138,13 +136,8 @@ capture (Handler pattern previousHandler toString constructor) =
{-| -}
int :
Handler
(Int -> a)
constructor
->
Handler
a
(Int -> constructor)
Handler (Int -> a) constructor
-> Handler a (Int -> constructor)
int (Handler pattern previousHandler toString constructor) =
Handler
(pattern ++ "(\\d+)")
@ -170,6 +163,11 @@ int (Handler pattern previousHandler toString constructor) =
)
getBuildTimeRoutes : Done response -> DataSource (List String)
getBuildTimeRoutes (Done handler) =
handler.buildTimeRoutes
--captureRest : Handler (List String -> a) b -> Handler a b
--captureRest previousHandler =

View File

@ -29,7 +29,7 @@ errorToString error =
banner : String -> List Terminal.Text
banner title =
[ Terminal.cyan <|
Terminal.text ("-- " ++ String.toUpper title ++ " ----------------------------------------------------- elm-pages")
("-- " ++ String.toUpper title ++ " ----------------------------------------------------- elm-pages")
, Terminal.text "\n\n"
]

View File

@ -418,7 +418,7 @@ resolve =
-}
combine : List (DataSource value) -> DataSource (List value)
combine =
List.foldl (map2 (::)) (succeed [])
List.foldr (map2 (::)) (succeed [])
{-| Like map, but it takes in two `Request`s.

View File

@ -59,7 +59,7 @@ frontmatter frontmatterDecoder =
import DataSource.File as File
import OptimizedDecoder as Decode exposing (Decoder)
blogPost : DataSource ( String, BlogPostMetadata )
blogPost : DataSource BlogPostMetadata
blogPost =
File.bodyWithFrontmatter blogPostDecoder
"blog/hello-world.md"
@ -70,7 +70,7 @@ frontmatter frontmatterDecoder =
, tags : List String
}
blogPostDecoder : Decoder BlogPostMetadata
blogPostDecoder : String -> Decoder BlogPostMetadata
blogPostDecoder body =
Decode.map2 (BlogPostMetadata body)
(Decode.field "title" Decode.string)

20
src/DataSource/Port.elm Normal file
View File

@ -0,0 +1,20 @@
module DataSource.Port exposing (send)
import DataSource
import DataSource.Http
import Json.Encode
import OptimizedDecoder exposing (Decoder)
import Secrets
send : String -> Json.Encode.Value -> Decoder b -> DataSource.DataSource b
send portName input decoder =
DataSource.Http.request
(Secrets.succeed
{ url = "port://" ++ portName
, method = "GET"
, headers = []
, body = DataSource.jsonBody input
}
)
decoder

View File

@ -148,14 +148,17 @@ prerenderedOptionsView moduleContext routes =
Html.li
[ Attr.style "list-style" "inside"
]
[ Html.a
[ Attr.href "/blog/extensible-markdown-parsing-in-elm"
]
[ Html.code
[]
[ Html.text (recordToString record)
]
[ --Html.a
-- [-- Attr.href "/blog/extensible-markdown-parsing-in-elm"
-- -- TODO get href data
-- ]
-- [
Html.code
[]
[ Html.text (recordToString record)
]
--]
]
)
)

View File

@ -53,26 +53,22 @@ type alias Path =
init :
Maybe ( { currentUrl : Url, baseUrl : Url }, ContentJson )
Maybe ( Path, ContentJson )
-> ContentCache
init maybeInitialPageContent =
Dict.fromList []
|> (\dict ->
case maybeInitialPageContent of
Nothing ->
dict
case maybeInitialPageContent of
Nothing ->
Dict.empty
Just ( urls, contentJson ) ->
dict
|> Dict.insert (pathForUrl urls) (Parsed contentJson)
)
Just ( urls, contentJson ) ->
Dict.singleton urls (Parsed contentJson)
{-| Get from the Cache... if it's not already parsed, it will
parse it before returning it and store the parsed version in the Cache
-}
lazyLoad :
{ currentUrl : Url, baseUrl : Url }
{ currentUrl : Url, basePath : List String }
-> ContentCache
-> Task Http.Error ( Url, ContentJson, ContentCache )
lazyLoad urls cache =
@ -141,6 +137,7 @@ httpTask url =
type alias ContentJson =
{ staticData : RequestsAndPending
, is404 : Bool
, path : Maybe String
, notFoundReason : Maybe NotFoundReason.Payload
}
@ -151,9 +148,10 @@ contentJsonDecoder =
|> Decode.andThen
(\is404Value ->
if is404Value then
Decode.map3 ContentJson
Decode.map4 ContentJson
(Decode.succeed Dict.empty)
(Decode.succeed is404Value)
(Decode.field "path" Decode.string |> Decode.map Just)
(Decode.at [ "staticData", "notFoundReason" ]
(Decode.string
|> Decode.andThen
@ -176,16 +174,17 @@ contentJsonDecoder =
)
else
Decode.map3 ContentJson
Decode.map4 ContentJson
(Decode.field "staticData" RequestsAndPending.decoder)
(Decode.succeed is404Value)
(Decode.succeed Nothing)
(Decode.succeed Nothing)
)
update :
ContentCache
-> { currentUrl : Url, baseUrl : Url }
-> { currentUrl : Url, basePath : List String }
-> ContentJson
-> ContentCache
update cache urls rawContent =
@ -199,6 +198,7 @@ update cache urls rawContent =
Nothing ->
{ staticData = rawContent.staticData
, is404 = rawContent.is404
, path = rawContent.path
, notFoundReason = rawContent.notFoundReason
}
|> Parsed
@ -207,18 +207,18 @@ update cache urls rawContent =
cache
pathForUrl : { currentUrl : Url, baseUrl : Url } -> Path
pathForUrl { currentUrl, baseUrl } =
pathForUrl : { currentUrl : Url, basePath : List String } -> Path
pathForUrl { currentUrl, basePath } =
currentUrl.path
|> String.dropLeft (String.length baseUrl.path)
|> String.chopForwardSlashes
|> String.split "/"
|> List.filter ((/=) "")
|> List.drop (List.length basePath)
is404 :
ContentCache
-> { currentUrl : Url, baseUrl : Url }
-> { currentUrl : Url, basePath : List String }
-> Bool
is404 dict urls =
dict
@ -234,7 +234,7 @@ is404 dict urls =
notFoundReason :
ContentCache
-> { currentUrl : Url, baseUrl : Url }
-> { currentUrl : Url, basePath : List String }
-> Maybe NotFoundReason.Payload
notFoundReason dict urls =
dict

View File

@ -19,7 +19,6 @@ import Pages.ProgramConfig exposing (ProgramConfig)
import Pages.StaticHttpRequest as StaticHttpRequest
import Path exposing (Path)
import QueryParams
import RequestsAndPending exposing (RequestsAndPending)
import Task
import Url exposing (Url)
@ -34,10 +33,10 @@ mainView :
-> { title : String, body : Html userMsg }
mainView config model =
let
urls : { currentUrl : Url, baseUrl : Url }
urls : { currentUrl : Url, basePath : List String }
urls =
{ currentUrl = model.url
, baseUrl = model.baseUrl
, basePath = config.basePath
}
in
case ContentCache.notFoundReason model.contentCache urls of
@ -49,7 +48,7 @@ mainView config model =
Ok pageData ->
(config.view
{ path = ContentCache.pathForUrl urls |> Path.join
, frontmatter = config.urlToRoute model.url
, route = config.urlToRoute model.url
}
Nothing
pageData.sharedData
@ -65,27 +64,15 @@ mainView config model =
}
urlToPath : Url -> Url -> Path
urlToPath url baseUrl =
url.path
|> String.dropLeft (String.length baseUrl.path)
|> String.chopForwardSlashes
|> String.split "/"
|> List.filter ((/=) "")
|> Path.join
urlsToPagePath :
{ currentUrl : Url
, baseUrl : Url
}
{ currentUrl : Url, basePath : List String }
-> Path
urlsToPagePath urls =
urls.currentUrl.path
|> String.dropLeft (String.length urls.baseUrl.path)
|> String.chopForwardSlashes
|> String.split "/"
|> List.filter ((/=) "")
|> List.drop (List.length urls.basePath)
|> Path.join
@ -137,33 +124,37 @@ init config flags url key =
ContentCache.init
(Maybe.map
(\cj ->
-- TODO parse the page path to a list here
( urls
( currentPath
, cj
)
)
contentJson
)
currentPath : List String
currentPath =
flags
|> Decode.decodeValue
(Decode.at [ "contentJson", "path" ]
(Decode.string
|> Decode.map Path.fromString
|> Decode.map Path.toSegments
)
)
|> Result.mapError Decode.errorToString
|> Result.toMaybe
|> Maybe.withDefault []
contentJson : Maybe ContentJson
contentJson =
flags
|> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder)
|> Result.toMaybe
baseUrl : Url
baseUrl =
flags
|> Decode.decodeValue (Decode.field "baseUrl" Decode.string)
|> Result.toMaybe
|> Maybe.andThen Url.fromString
|> Maybe.withDefault url
urls : { currentUrl : Url, baseUrl : Url }
urls : { currentUrl : Url, basePath : List String }
urls =
-- @@@
{ currentUrl = url -- |> normalizeUrl baseUrl
, baseUrl = baseUrl
{ currentUrl = url
, basePath = config.basePath
}
in
case contentJson |> Maybe.map .staticData of
@ -231,10 +222,10 @@ init config flags url key =
|> List.filterMap identity
|> Cmd.batch
initialModel : Model userModel pageData sharedData
initialModel =
{ key = key
, url = url
, baseUrl = baseUrl
, contentCache = contentCache
, pageData =
Ok
@ -243,6 +234,7 @@ init config flags url key =
, userModel = userModel
}
, ariaNavigationAnnouncement = ""
, userFlags = flags
}
in
( { initialModel
@ -254,10 +246,10 @@ init config flags url key =
Err error ->
( { key = key
, url = url
, baseUrl = baseUrl
, contentCache = contentCache
, pageData = BuildError.errorToString error |> Err
, ariaNavigationAnnouncement = "Error"
, userFlags = flags
}
, Cmd.none
)
@ -265,10 +257,10 @@ init config flags url key =
Nothing ->
( { key = key
, url = url
, baseUrl = baseUrl
, contentCache = contentCache
, pageData = Err "TODO"
, ariaNavigationAnnouncement = "Error"
, userFlags = flags
}
, Cmd.none
)
@ -288,16 +280,15 @@ type Msg userMsg
type alias Model userModel pageData sharedData =
{ key : Browser.Navigation.Key
, url : Url
, baseUrl : Url
, contentCache : ContentCache
, ariaNavigationAnnouncement : String
, pageData :
Result
String
Result String
{ userModel : userModel
, pageData : pageData
, sharedData : sharedData
}
, userFlags : Decode.Value
}
@ -335,10 +326,10 @@ update config appMsg model =
navigatingToSamePage =
(url.path == model.url.path) && (url /= model.url)
urls : { currentUrl : Url, baseUrl : Url }
urls : { currentUrl : Url, basePath : List String }
urls =
{ currentUrl = url
, baseUrl = model.baseUrl
, basePath = config.basePath
}
in
if navigatingToSamePage then
@ -368,7 +359,7 @@ update config appMsg model =
{ protocol = model.url.protocol
, host = model.url.host
, port_ = model.url.port_
, path = urlToPath url model.baseUrl
, path = urlPathToPath config urls.currentUrl
, query = url.query
, fragment = url.fragment
, metadata = config.urlToRoute url
@ -449,7 +440,12 @@ update config appMsg model =
StaticHttpRequest.resolve ApplicationType.Browser
(config.data (config.urlToRoute url))
contentJson.staticData
|> Result.mapError (\_ -> "Http error")
|> Result.mapError
(\error ->
error
|> StaticHttpRequest.toBuildError ""
|> BuildError.errorToString
)
( userModel, userCmd ) =
config.update
@ -460,7 +456,7 @@ update config appMsg model =
{ protocol = model.url.protocol
, host = model.url.host
, port_ = model.url.port_
, path = urlToPath url model.baseUrl
, path = url |> urlPathToPath config
, query = url.query
, fragment = url.fragment
, metadata = config.urlToRoute url
@ -484,18 +480,36 @@ update config appMsg model =
]
)
Err _ ->
-- TODO handle error
( { model | url = url }, Cmd.none )
Err error ->
{-
When there is an error loading the content.json, we are either
1) in the dev server, and should show the relevant DataSource error for the page
we're navigating to. This could be done more cleanly, but it's simplest to just
do a fresh page load and use the code path for presenting an error for a fresh page.
2) In a production app. That means we had a successful build, so there were no DataSource failures,
so the app must be stale (unless it's in some unexpected state from a bug). In the future,
it probably makes sense to include some sort of hash of the app version we are fetching, match
it with the current version that's running, and perform this logic when we see there is a mismatch.
But for now, if there is any error we do a full page load (not a single-page navigation), which
gives us a fresh version of the app to make sure things are in sync.
-}
( model
, url
|> Url.toString
|> Browser.Navigation.load
)
PageScrollComplete ->
( model, Cmd.none )
HotReloadComplete contentJson ->
let
urls : { currentUrl : Url, baseUrl : Url }
urls : { currentUrl : Url, basePath : List String }
urls =
{ currentUrl = model.url, baseUrl = model.baseUrl }
{ currentUrl = model.url
, basePath = config.basePath
}
pageDataResult : Result BuildError pageData
pageDataResult =
@ -536,7 +550,7 @@ update config appMsg model =
{ protocol = model.url.protocol
, host = model.url.host
, port_ = model.url.port_
, path = urlToPath model.url model.baseUrl
, path = model.url |> urlPathToPath config
, query = model.url.query
, fragment = model.url.fragment
, metadata = config.urlToRoute model.url
@ -554,7 +568,15 @@ update config appMsg model =
case updateResult of
Just ( userModel, userCmd ) ->
( { model
| contentCache = ContentCache.init (Just ( urls, contentJson ))
| contentCache =
ContentCache.init
(Just
( urls.currentUrl
|> config.urlToRoute
|> config.routeToPath
, contentJson
)
)
, pageData =
Ok
{ pageData = pageData
@ -568,9 +590,49 @@ update config appMsg model =
)
Nothing ->
let
pagePath : Path
pagePath =
urlsToPagePath urls
userFlags : Pages.Flags.Flags
userFlags =
model.userFlags
|> Decode.decodeValue
(Decode.field "userFlags" Decode.value)
|> Result.withDefault Json.Encode.null
|> Pages.Flags.BrowserFlags
( userModel, userCmd ) =
Just
{ path =
{ path = pagePath
, query = model.url.query
, fragment = model.url.fragment
}
, metadata = config.urlToRoute model.url
, pageUrl =
Just
{ protocol = model.url.protocol
, host = model.url.host
, port_ = model.url.port_
, path = pagePath
, query = model.url.query |> Maybe.map QueryParams.fromString
, fragment = model.url.fragment
}
}
|> config.init userFlags sharedData pageData (Just model.key)
in
( { model
| contentCache =
ContentCache.init (Just ( urls, contentJson ))
ContentCache.init
(Just
( urls.currentUrl
|> config.urlToRoute
|> config.routeToPath
, contentJson
)
)
, pageData =
model.pageData
|> Result.map
@ -580,14 +642,27 @@ update config appMsg model =
, userModel = previousPageData.userModel
}
)
|> Result.withDefault
{ pageData = pageData
, sharedData = sharedData
, userModel = userModel
}
|> Ok
}
, Cmd.none
, userCmd |> Cmd.map UserMsg
)
Err error ->
( { model
| contentCache =
ContentCache.init (Just ( urls, contentJson ))
ContentCache.init
(Just
( urls.currentUrl
|> config.urlToRoute
|> config.routeToPath
, contentJson
)
)
}
, Cmd.none
)
@ -609,18 +684,16 @@ application config =
, subscriptions =
\model ->
let
urls : { currentUrl : Url, baseUrl : Url }
urls : { currentUrl : Url }
urls =
{ currentUrl = model.url, baseUrl = model.baseUrl }
pagePath : Path
pagePath =
urlsToPagePath urls
{ currentUrl = model.url }
in
case model.pageData of
Ok pageData ->
Sub.batch
[ config.subscriptions (model.url |> config.urlToRoute) pagePath pageData.userModel
[ config.subscriptions (model.url |> config.urlToRoute)
(urls.currentUrl |> config.urlToRoute |> config.routeToPath |> Path.join)
pageData.userModel
|> Sub.map UserMsg
, config.fromJsPort
|> Sub.map
@ -650,3 +723,8 @@ application config =
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
urlPathToPath : ProgramConfig userMsg userModel route siteData pageData sharedData -> Url -> Path
urlPathToPath config urls =
urls.path |> Path.fromString

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,11 @@
module Pages.Internal.Platform.Effect exposing (Effect(..))
import DataSource.Http exposing (RequestDetails)
import Pages.Internal.Platform.ToJsPayload exposing (ToJsPayload, ToJsSuccessPayloadNewCombined)
import Pages.Internal.Platform.ToJsPayload exposing (ToJsSuccessPayloadNewCombined)
type Effect
= NoEffect
| SendJsData ToJsPayload
| FetchHttp { masked : RequestDetails, unmasked : RequestDetails }
| ReadFile String
| GetGlob String

View File

@ -1,25 +0,0 @@
module Pages.Internal.Platform.Mode exposing (Mode(..), modeDecoder)
import Json.Decode as Decode exposing (Decoder)
type Mode
= Prod
| Dev
| ElmToHtmlBeta
modeDecoder : Decoder Mode
modeDecoder =
Decode.string
|> Decode.andThen
(\mode ->
if mode == "prod" then
Decode.succeed Prod
else if mode == "elm-to-html-beta" then
Decode.succeed ElmToHtmlBeta
else
Decode.succeed Dev
)

View File

@ -1,4 +1,4 @@
module Pages.Internal.Platform.StaticResponses exposing (NextStep(..), StaticResponses, error, init, nextStep, renderApiRequest, renderSingleRoute, update)
module Pages.Internal.Platform.StaticResponses exposing (FinishKind(..), NextStep(..), StaticResponses, batchUpdate, error, nextStep, renderApiRequest, renderSingleRoute)
import ApiRoute
import BuildError exposing (BuildError)
@ -11,8 +11,6 @@ import HtmlPrinter exposing (htmlToString)
import Internal.ApiRoute exposing (Done(..))
import NotFoundReason exposing (NotFoundReason)
import Pages.Internal.ApplicationType as ApplicationType
import Pages.Internal.Platform.Mode exposing (Mode)
import Pages.Internal.Platform.ToJsPayload as ToJsPayload exposing (ToJsPayload)
import Pages.SiteConfig exposing (SiteConfig)
import Pages.StaticHttp.Request as HashRequest
import Pages.StaticHttpRequest as StaticHttpRequest
@ -25,8 +23,7 @@ import TerminalText as Terminal
type StaticResponses
= GettingInitialData StaticHttpResult
| ApiRequest StaticHttpResult
= ApiRequest StaticHttpResult
| StaticResponses (Dict String StaticHttpResult)
| CheckIfHandled (DataSource (Maybe NotFoundReason)) StaticHttpResult (Dict String StaticHttpResult)
@ -40,32 +37,6 @@ error =
StaticResponses Dict.empty
init :
{ config
| getStaticRoutes : DataSource (List route)
, site : SiteConfig route siteData
, data : route -> DataSource pageData
, sharedData : DataSource sharedData
, apiRoutes :
(Html Never -> String) -> List (ApiRoute.Done ApiRoute.Response)
}
-> StaticResponses
init config =
NotFetched
(DataSource.map3 (\_ _ _ -> ())
(config.getStaticRoutes
|> DataSource.andThen
(\resolvedRoutes ->
config.site resolvedRoutes |> .data
)
)
(buildTimeFilesRequest config)
config.sharedData
)
Dict.empty
|> GettingInitialData
buildTimeFilesRequest :
{ config
| apiRoutes :
@ -136,11 +107,12 @@ renderApiRequest request =
)
update :
{ request :
{ masked : RequestDetails, unmasked : RequestDetails }
, response : Result () String
}
batchUpdate :
List
{ request :
{ masked : RequestDetails, unmasked : RequestDetails }
, response : String
}
->
{ model
| staticResponses : StaticResponses
@ -151,23 +123,33 @@ update :
| staticResponses : StaticResponses
, allRawResponses : Dict String (Maybe String)
}
update newEntry model =
batchUpdate newEntries model =
let
newResponses =
newEntries
|> List.map
(\newEntry ->
( HashRequest.hash newEntry.request.masked, newEntry.response )
)
|> Dict.fromList
updatedAllResponses : Dict String (Maybe String)
updatedAllResponses =
-- @@@@@@@@@ TODO handle errors here, change Dict to have `Result` instead of `Maybe`
Dict.insert
(HashRequest.hash newEntry.request.masked)
(Just <| Result.withDefault "TODO" newEntry.response)
Dict.merge
(\key a -> Dict.insert key (Just a))
(\key a _ -> Dict.insert key (Just a))
(\key b -> Dict.insert key b)
newResponses
model.allRawResponses
Dict.empty
in
{ model
| allRawResponses = updatedAllResponses
}
encode : RequestsAndPending -> Mode -> Dict String StaticHttpResult -> Result (List BuildError) (Dict String (Dict String String))
encode requestsAndPending _ staticResponses =
encode : RequestsAndPending -> Dict String StaticHttpResult -> Result (List BuildError) (Dict String (Dict String String))
encode requestsAndPending staticResponses =
staticResponses
|> Dict.filter
(\key _ ->
@ -191,7 +173,13 @@ cliDictKey =
type NextStep route
= Continue (Dict String (Maybe String)) (List { masked : RequestDetails, unmasked : RequestDetails }) (Maybe (List route))
| Finish ToJsPayload
| Finish (FinishKind route)
type FinishKind route
= ApiResponse
| Errors (List BuildError)
| Page (Dict String String)
nextStep :
@ -209,11 +197,10 @@ nextStep :
, secrets : SecretsDict
, errors : List BuildError
, allRawResponses : Dict String (Maybe String)
, mode : Mode
}
-> Maybe (List route)
-> ( StaticResponses, NextStep route )
nextStep config ({ mode, secrets, allRawResponses, errors } as model) maybeRoutes =
nextStep config ({ secrets, allRawResponses, errors } as model) maybeRoutes =
let
staticResponses : Dict String StaticHttpResult
staticResponses =
@ -221,9 +208,6 @@ nextStep config ({ mode, secrets, allRawResponses, errors } as model) maybeRoute
StaticResponses s ->
s
GettingInitialData initialData ->
Dict.singleton cliDictKey initialData
ApiRequest staticHttpResult ->
Dict.singleton cliDictKey staticHttpResult
@ -240,19 +224,6 @@ nextStep config ({ mode, secrets, allRawResponses, errors } as model) maybeRoute
(buildTimeFilesRequest config)
(allRawResponses |> Dict.Extra.filterMap (\_ value -> Just value))
generatedOkayFiles : List { path : List String, content : String }
generatedOkayFiles =
generatedFiles
|> List.filterMap
(\result ->
case result of
Ok ok ->
Just ok
Err _ ->
Nothing
)
generatedFileErrors : List BuildError
generatedFileErrors =
generatedFiles
@ -429,73 +400,10 @@ nextStep config ({ mode, secrets, allRawResponses, errors } as model) maybeRoute
( model.staticResponses, Continue newAllRawResponses newThing maybeRoutes )
Err error_ ->
( model.staticResponses, Finish (ToJsPayload.Errors <| (error_ ++ failedRequests ++ errors)) )
( model.staticResponses, Finish (Errors <| (error_ ++ failedRequests ++ errors)) )
else
case model.staticResponses of
GettingInitialData (NotFetched _ _) ->
let
resolvedRoutes : Result StaticHttpRequest.Error (List route)
resolvedRoutes =
StaticHttpRequest.resolve ApplicationType.Cli
(DataSource.map3
(\routes _ _ ->
routes
)
config.getStaticRoutes
(buildTimeFilesRequest config)
config.sharedData
)
(allRawResponses |> Dict.Extra.filterMap (\_ value -> Just value))
in
case resolvedRoutes of
Ok staticRoutes ->
let
newState : StaticResponses
newState =
staticRoutes
|> List.map
(\route ->
let
entry : StaticHttpResult
entry =
NotFetched
(DataSource.map2 (\_ _ -> ())
config.sharedData
(config.data route)
)
Dict.empty
in
( config.routeToPath route |> String.join "/"
, entry
)
)
|> Dict.fromList
|> StaticResponses
newThing : List { masked : RequestDetails, unmasked : RequestDetails }
newThing =
[]
in
( newState
, Continue allRawResponses newThing (Just staticRoutes)
)
Err error_ ->
( model.staticResponses
, Finish
(ToJsPayload.Errors <|
([ StaticHttpRequest.toBuildError
-- TODO give more fine-grained error reference
"get static routes"
error_
]
++ failedRequests
++ errors
)
)
)
StaticResponses _ ->
--let
-- siteStaticData =
@ -517,29 +425,27 @@ nextStep config ({ mode, secrets, allRawResponses, errors } as model) maybeRoute
--
-- Ok okSiteStaticData ->
( model.staticResponses
, case encode allRawResponses mode staticResponses of
, case encode allRawResponses staticResponses of
Ok encodedResponses ->
ToJsPayload.toJsPayload
encodedResponses
generatedOkayFiles
allRawResponses
-- TODO send all global head tags on initial call
if List.length allErrors > 0 then
allErrors
-- TODO send all global head tags on initial call
|> Finish
|> Errors
|> Finish
else
Page (encodedResponses |> Dict.values |> List.head |> Maybe.withDefault Dict.empty)
|> Finish
Err buildErrors ->
ToJsPayload.toJsPayload
Dict.empty
generatedOkayFiles
allRawResponses
(allErrors ++ buildErrors)
-- TODO send all global head tags on initial call
(allErrors ++ buildErrors)
|> Errors
|> Finish
)
ApiRequest _ ->
( model.staticResponses
, ToJsPayload.ApiResponse
, ApiResponse
|> Finish
)
@ -557,14 +463,14 @@ nextStep config ({ mode, secrets, allRawResponses, errors } as model) maybeRoute
Ok (Just _) ->
( StaticResponses Dict.empty
, Finish ToJsPayload.ApiResponse
, Finish ApiResponse
-- TODO should there be a new type for 404response? Or something else?
)
Err error_ ->
( model.staticResponses
, Finish
(ToJsPayload.Errors <|
(Errors <|
([ StaticHttpRequest.toBuildError
-- TODO give more fine-grained error reference
"get static routes"

View File

@ -1,13 +1,7 @@
module Pages.Internal.Platform.ToJsPayload exposing
( FileToGenerate
, InitialDataRecord
, ToJsPayload(..)
, ToJsSuccessPayload
, ToJsSuccessPayloadNew
( ToJsSuccessPayloadNew
, ToJsSuccessPayloadNewCombined(..)
, successCodecNew2
, toJsCodec
, toJsPayload
)
import BuildError exposing (BuildError)
@ -16,20 +10,7 @@ import Dict exposing (Dict)
import Head
import Json.Decode as Decode
import Json.Encode
type ToJsPayload
= Errors (List BuildError)
| Success ToJsSuccessPayload
| ApiResponse
type alias ToJsSuccessPayload =
{ pages : Dict String (Dict String String)
, filesToGenerate : List FileToGenerate
, staticHttpCache : Dict String String
, errors : List BuildError
}
import Pages.StaticHttp.Request
type alias ToJsSuccessPayloadNew =
@ -44,60 +25,6 @@ type alias ToJsSuccessPayloadNew =
}
type alias FileToGenerate =
{ path : List String
, content : String
}
toJsPayload :
Dict String (Dict String String)
-> List FileToGenerate
-> Dict String (Maybe String)
-> List BuildError
-> ToJsPayload
toJsPayload encodedStatic generated allRawResponses allErrors =
if allErrors |> List.filter .fatal |> List.isEmpty then
Success
(ToJsSuccessPayload
encodedStatic
generated
(allRawResponses
|> Dict.toList
|> List.filterMap
(\( key, maybeValue ) ->
maybeValue
|> Maybe.map (\value -> ( key, value ))
)
|> Dict.fromList
)
allErrors
)
else
Errors <| allErrors
toJsCodec : Codec ToJsPayload
toJsCodec =
Codec.custom
(\errorsTag success vApiResponse value ->
case value of
Errors errorList ->
errorsTag errorList
Success { pages, filesToGenerate, errors, staticHttpCache } ->
success (ToJsSuccessPayload pages filesToGenerate staticHttpCache errors)
ApiResponse ->
vApiResponse
)
|> Codec.variant1 "Errors" Errors errorCodec
|> Codec.variant1 "Success" Success successCodec
|> Codec.variant0 "ApiResponse" ApiResponse
|> Codec.buildCustom
errorCodec : Codec (List BuildError)
errorCodec =
Codec.object (\errorString _ -> errorString)
@ -117,39 +44,6 @@ errorCodec =
|> Codec.buildObject
successCodec : Codec ToJsSuccessPayload
successCodec =
Codec.object ToJsSuccessPayload
|> Codec.field "pages"
.pages
(Codec.dict (Codec.dict Codec.string))
|> Codec.field "filesToGenerate"
.filesToGenerate
(Codec.build
(\list ->
list
|> Json.Encode.list
(\item ->
Json.Encode.object
[ ( "path", item.path |> String.join "/" |> Json.Encode.string )
, ( "content", item.content |> Json.Encode.string )
]
)
)
(Decode.list
(Decode.map2 (\path content -> { path = path, content = content })
(Decode.string |> Decode.map (String.split "/") |> Decode.field "path")
(Decode.string |> Decode.field "content")
)
)
)
|> Codec.field "staticHttpCache"
.staticHttpCache
(Codec.dict Codec.string)
|> Codec.field "errors" .errors errorCodec
|> Codec.buildObject
successCodecNew : String -> String -> Codec ToJsSuccessPayloadNew
successCodecNew canonicalSiteUrl currentPagePath =
Codec.object ToJsSuccessPayloadNew
@ -180,45 +74,56 @@ headCodec canonicalSiteUrl currentPagePath =
type ToJsSuccessPayloadNewCombined
= PageProgress ToJsSuccessPayloadNew
| InitialData InitialDataRecord
| SendApiResponse { body : String, staticHttpCache : Dict String String, statusCode : Int }
| ReadFile String
| Glob String
| DoHttp { masked : Pages.StaticHttp.Request.Request, unmasked : Pages.StaticHttp.Request.Request }
| Port String
type alias InitialDataRecord =
{ filesToGenerate : List FileToGenerate
}
| Errors (List BuildError)
| ApiResponse
successCodecNew2 : String -> String -> Codec ToJsSuccessPayloadNewCombined
successCodecNew2 canonicalSiteUrl currentPagePath =
Codec.custom
(\success initialData vReadFile vGlob vSendApiResponse vPort value ->
(\errorsTag vApiResponse success vReadFile vGlob vDoHttp vSendApiResponse vPort value ->
case value of
ApiResponse ->
vApiResponse
Errors errorList ->
errorsTag errorList
PageProgress payload ->
success payload
InitialData payload ->
initialData payload
ReadFile filePath ->
vReadFile filePath
Glob globPattern ->
vGlob globPattern
DoHttp requestUrl ->
vDoHttp requestUrl
SendApiResponse record ->
vSendApiResponse record
Port string ->
vPort string
)
|> Codec.variant1 "Errors" Errors errorCodec
|> Codec.variant0 "ApiResponse" ApiResponse
|> Codec.variant1 "PageProgress" PageProgress (successCodecNew canonicalSiteUrl currentPagePath)
|> Codec.variant1 "InitialData" InitialData initialDataCodec
|> Codec.variant1 "ReadFile" ReadFile Codec.string
|> Codec.variant1 "Glob" Glob Codec.string
|> Codec.variant1 "DoHttp"
DoHttp
(Codec.object (\masked unmasked -> { masked = masked, unmasked = unmasked })
|> Codec.field "masked" .masked Pages.StaticHttp.Request.codec
|> Codec.field "unmasked" .unmasked Pages.StaticHttp.Request.codec
|> Codec.buildObject
)
|> Codec.variant1 "ApiResponse"
SendApiResponse
(Codec.object (\body staticHttpCache statusCode -> { body = body, staticHttpCache = staticHttpCache, statusCode = statusCode })
@ -231,33 +136,3 @@ successCodecNew2 canonicalSiteUrl currentPagePath =
)
|> Codec.variant1 "Port" Port Codec.string
|> Codec.buildCustom
filesToGenerateCodec : Codec (List { path : List String, content : String })
filesToGenerateCodec =
Codec.build
(\list ->
list
|> Json.Encode.list
(\item ->
Json.Encode.object
[ ( "path", item.path |> String.join "/" |> Json.Encode.string )
, ( "content", item.content |> Json.Encode.string )
]
)
)
(Decode.list
(Decode.map2 (\path content -> { path = path, content = content })
(Decode.string |> Decode.map (String.split "/") |> Decode.field "path")
(Decode.string |> Decode.field "content")
)
)
initialDataCodec : Codec InitialDataRecord
initialDataCodec =
Codec.object InitialDataRecord
|> Codec.field "filesToGenerate"
.filesToGenerate
filesToGenerateCodec
|> Codec.buildObject

View File

@ -1,5 +1,6 @@
module Pages.Internal.StaticHttpBody exposing (Body(..), encode)
module Pages.Internal.StaticHttpBody exposing (Body(..), codec, encode)
import Codec exposing (Codec)
import Json.Encode as Encode
@ -31,3 +32,23 @@ encodeWithType typeName otherFields =
Encode.object <|
( "type", Encode.string typeName )
:: otherFields
codec : Codec Body
codec =
Codec.custom
(\vEmpty vString vJson value ->
case value of
EmptyBody ->
vEmpty
StringBody a b ->
vString a b
JsonBody body ->
vJson body
)
|> Codec.variant0 "EmptyBody" EmptyBody
|> Codec.variant2 "StringBody" StringBody Codec.string Codec.string
|> Codec.variant1 "JsonBody" JsonBody Codec.value
|> Codec.buildCustom

View File

@ -39,7 +39,7 @@ type alias ProgramConfig userMsg userModel route siteData pageData sharedData =
, data : route -> DataSource.DataSource pageData
, view :
{ path : Path
, frontmatter : route
, route : route
}
-> Maybe PageUrl
-> sharedData
@ -69,4 +69,5 @@ type alias ProgramConfig userMsg userModel route siteData pageData sharedData =
(Html Never -> String)
-> List (ApiRoute.Done ApiRoute.Response)
, pathPatterns : List RoutePattern
, basePath : List String
}

View File

@ -1,6 +1,8 @@
module Pages.StaticHttp.Request exposing (Request, hash)
module Pages.StaticHttp.Request exposing (Request, codec, hash)
import Codec exposing (Codec)
import Json.Encode as Encode
import Murmur3
import Pages.Internal.StaticHttpBody as StaticHttpBody exposing (Body)
@ -21,8 +23,20 @@ hash requestDetails =
, ( "body", StaticHttpBody.encode requestDetails.body )
]
|> Encode.encode 0
|> Murmur3.hashString 0
|> String.fromInt
hashHeader : ( String, String ) -> Encode.Value
hashHeader ( name, value ) =
Encode.string <| name ++ ": " ++ value
codec : Codec Request
codec =
Codec.object Request
|> Codec.field "url" .url Codec.string
|> Codec.field "method" .method Codec.string
|> Codec.field "headers" .headers (Codec.list (Codec.tuple Codec.string Codec.string))
|> Codec.field "body" .body StaticHttpBody.codec
|> Codec.buildObject

View File

@ -16,11 +16,7 @@ import TerminalText as Terminal
type RawRequest value
= Request
(Dict String WhatToDo)
( List (Secrets.Value Pages.StaticHttp.Request.Request)
, KeepOrDiscard -> ApplicationType -> RequestsAndPending -> RawRequest value
)
= Request (Dict String WhatToDo) ( List (Secrets.Value Pages.StaticHttp.Request.Request), KeepOrDiscard -> ApplicationType -> RequestsAndPending -> RawRequest value )
| RequestError Error
| Done (Dict String WhatToDo) value
@ -70,14 +66,14 @@ merge key whatToDo1 whatToDo2 =
, message =
[ Terminal.text "I encountered DataSource.distill with two matching keys that had differing encoded values.\n\n"
, Terminal.text "Look for "
, Terminal.red <| Terminal.text "DataSource.distill"
, Terminal.red <| "DataSource.distill"
, Terminal.text " with the key "
, Terminal.red <| Terminal.text ("\"" ++ key ++ "\"")
, Terminal.red <| ("\"" ++ key ++ "\"")
, Terminal.text "\n\n"
, Terminal.yellow <| Terminal.text "The first encoded value was:\n"
, Terminal.yellow <| "The first encoded value was:\n"
, Terminal.text <| Json.Encode.encode 2 distilled1
, Terminal.text "\n\n-------------------------------\n\n"
, Terminal.yellow <| Terminal.text "The second encoded value was:\n"
, Terminal.yellow <| "The second encoded value was:\n"
, Terminal.text <| Json.Encode.encode 2 distilled2
]
, path = "" -- TODO wire in path here?
@ -263,10 +259,6 @@ resolveUrlsHelp appType request rawResponses soFar =
RequestError error ->
case error of
MissingHttpResponse _ next ->
let
thing =
next |> List.map Secrets.maskedLookup
in
(soFar ++ next)
|> List.Extra.uniqueBy (Secrets.maskedLookup >> Pages.StaticHttp.Request.hash)
@ -311,7 +303,7 @@ cacheRequestResolutionHelp foundUrls appType request rawResponses =
case request of
RequestError error ->
case error of
MissingHttpResponse key _ ->
MissingHttpResponse _ _ ->
-- TODO do I need to pass through continuation URLs here? -- Incomplete (urlList ++ foundUrls)
Incomplete foundUrls

View File

@ -72,7 +72,7 @@ fromString path =
{-| -}
toSegments : Path -> List String
toSegments (Path path) =
path |> String.split "/"
path |> String.split "/" |> List.filter ((/=) "")
{-| Turn a Path to an absolute URL (with no trailing slash).

View File

@ -3,6 +3,7 @@ module RenderRequest exposing
, RenderRequest(..)
, RequestPayload(..)
, decoder
, default
, maybeRequestPayload
)
@ -10,6 +11,7 @@ import ApiRoute
import HtmlPrinter
import Internal.ApiRoute
import Json.Decode as Decode
import Json.Encode as Encode
import Pages.ProgramConfig exposing (ProgramConfig)
import Path exposing (Path)
import Regex
@ -24,15 +26,19 @@ type RequestPayload route
type RenderRequest route
= SinglePage IncludeHtml (RequestPayload route) Decode.Value
| FullBuild
default : RenderRequest route
default =
SinglePage
HtmlAndJson
(NotFound (Path.fromString "/error"))
Encode.null
maybeRequestPayload : RenderRequest route -> Maybe Decode.Value
maybeRequestPayload renderRequest =
case renderRequest of
FullBuild ->
Nothing
SinglePage _ _ rawJson ->
Just rawJson
@ -46,7 +52,7 @@ decoder :
ProgramConfig userMsg userModel (Maybe route) siteData pageData sharedData
-> Decode.Decoder (RenderRequest (Maybe route))
decoder config =
optionalField "request"
Decode.field "request"
(Decode.map3
(\includeHtml requestThing payload ->
SinglePage includeHtml requestThing payload
@ -73,15 +79,6 @@ decoder config =
(requestPayloadDecoder config)
(Decode.field "payload" Decode.value)
)
|> Decode.map
(\maybeRequest ->
case maybeRequest of
Just request ->
request
Nothing ->
FullBuild
)

View File

@ -32,6 +32,7 @@ fromOptionalSplat : Maybe String -> List String
fromOptionalSplat maybeMatch =
maybeMatch
|> Maybe.map (\match -> match |> String.split "/")
|> Maybe.map (List.filter (\item -> item /= ""))
|> Maybe.withDefault []

View File

@ -50,11 +50,11 @@ buildError secretName secretsDict =
[ Terminal.text "I expected to find this Secret in your environment variables but didn't find a match:\n\nSecrets.get \""
, Terminal.text secretName
, Terminal.text "\"\n "
, Terminal.red <| Terminal.text (underlineText (secretName |> String.length))
, Terminal.red <| underlineText (secretName |> String.length)
, Terminal.text "\n\nSo maybe "
, Terminal.yellow <| Terminal.text secretName
, Terminal.yellow <| secretName
, Terminal.text " should be "
, Terminal.green <| Terminal.text (sortMatches secretName availableEnvironmentVariables |> List.head |> Maybe.withDefault "")
, Terminal.green <| (sortMatches secretName availableEnvironmentVariables |> List.head |> Maybe.withDefault "")
]
, path = "" -- TODO wire in path here?
, fatal = True

View File

@ -1,13 +1,12 @@
module TerminalText exposing
( Color(..)
, Text(..)
( Text(..)
, ansi
, ansiPrefix
, blue
, colorToString
, cyan
, encoder
, getString
, fromAnsiString
, green
, red
, resetColors
@ -17,50 +16,42 @@ module TerminalText exposing
, yellow
)
import Ansi
import Json.Encode as Encode
type Text
= RawText String
| Style Color Text
type Color
= Red
| Blue
| Green
| Yellow
| Cyan
= Style AnsiStyle String
text : String -> Text
text value =
RawText value
Style blankStyle value
cyan : Text -> Text
cyan : String -> Text
cyan inner =
Style Cyan inner
Style { blankStyle | color = Just Ansi.Cyan } inner
green : Text -> Text
green : String -> Text
green inner =
Style Green inner
Style { blankStyle | color = Just Ansi.Green } inner
yellow : Text -> Text
yellow : String -> Text
yellow inner =
Style Yellow inner
Style { blankStyle | color = Just Ansi.Yellow } inner
red : Text -> Text
red : String -> Text
red inner =
Style Red inner
Style { blankStyle | color = Just Ansi.Red } inner
blue : Text -> Text
blue : String -> Text
blue inner =
Style Blue inner
Style { blankStyle | color = Just Ansi.Blue } inner
resetColors : String
@ -78,25 +69,29 @@ ansiPrefix =
"\u{001B}"
colorToString : Color -> String
colorToString : Ansi.Color -> String
colorToString color =
ansi <|
case color of
Red ->
Ansi.Red ->
"[31m"
Blue ->
Ansi.Blue ->
"[34m"
Green ->
Ansi.Green ->
"[32m"
Yellow ->
Ansi.Yellow ->
"[33m"
Cyan ->
Ansi.Cyan ->
"[36m"
_ ->
-- TODO
""
toString : List Text -> String
toString list =
@ -106,57 +101,142 @@ toString list =
toString_ : Text -> String
toString_ textValue =
-- elm-review: known-unoptimized-recursion
case textValue of
RawText content ->
content
Style color innerText ->
String.concat
[ colorToString color
, toString_ innerText
, resetColors
]
encoder : Text -> Encode.Value
encoder node =
Encode.object
[ ( "bold", Encode.bool False )
, ( "underline", Encode.bool False )
, ( "color"
, Encode.string <|
case node of
RawText _ ->
"WHITE"
Style color _ ->
case color of
Red ->
"red"
Blue ->
"blue"
Green ->
"green"
Yellow ->
"yellow"
Cyan ->
"cyan"
)
, ( "string", Encode.string (getString node) )
toString_ (Style ansiStyle innerText) =
String.concat
[ ansiStyle.color |> Maybe.withDefault Ansi.White |> colorToString
, innerText
, resetColors
]
getString : Text -> String
getString node =
case node of
RawText string ->
string
fromAnsiString : String -> List Text
fromAnsiString ansiString =
Ansi.parseInto ( blankStyle, [] ) parseInto ansiString
|> Tuple.second
|> List.reverse
Style _ innerNode ->
getString innerNode
type alias AnsiStyle =
{ bold : Bool
, underline : Bool
, color : Maybe Ansi.Color
}
blankStyle : AnsiStyle
blankStyle =
{ bold = False
, underline = False
, color = Nothing
}
parseInto : Ansi.Action -> ( AnsiStyle, List Text ) -> ( AnsiStyle, List Text )
parseInto action ( pendingStyle, soFar ) =
case action of
Ansi.Print string ->
( blankStyle, Style pendingStyle string :: soFar )
Ansi.Remainder _ ->
( pendingStyle, soFar )
Ansi.SetForeground maybeColor ->
case maybeColor of
Just newColor ->
( { pendingStyle
| color = Just newColor
}
, soFar
)
Nothing ->
( blankStyle, soFar )
Ansi.SetBold bool ->
( { pendingStyle | bold = bool }, soFar )
Ansi.SetFaint _ ->
( pendingStyle, soFar )
Ansi.SetItalic _ ->
( pendingStyle, soFar )
Ansi.SetUnderline bool ->
( { pendingStyle | underline = bool }, soFar )
Ansi.SetBackground _ ->
( pendingStyle, soFar )
Ansi.Linebreak ->
case soFar of
next :: rest ->
( pendingStyle, Style blankStyle "\n" :: next :: rest )
[] ->
( pendingStyle, soFar )
_ ->
( pendingStyle, soFar )
encoder : Text -> Encode.Value
encoder (Style ansiStyle string) =
Encode.object
[ ( "bold", Encode.bool ansiStyle.bold )
, ( "underline", Encode.bool ansiStyle.underline )
, ( "color"
, Encode.string <|
case ansiStyle.color |> Maybe.withDefault Ansi.White of
Ansi.Red ->
"red"
Ansi.Blue ->
"blue"
Ansi.Green ->
"green"
Ansi.Yellow ->
"yellow"
Ansi.Cyan ->
"cyan"
Ansi.Black ->
"black"
Ansi.Magenta ->
"magenta"
Ansi.White ->
"white"
Ansi.BrightBlack ->
"BLACK"
Ansi.BrightRed ->
"RED"
Ansi.BrightGreen ->
"GREEN"
Ansi.BrightYellow ->
"YELLOW"
Ansi.BrightBlue ->
"BLUE"
Ansi.BrightMagenta ->
"MAGENTA"
Ansi.BrightCyan ->
"CYAN"
Ansi.BrightWhite ->
"WHITE"
Ansi.Custom _ _ _ ->
""
)
, ( "string", Encode.string string )
]

View File

@ -11,13 +11,12 @@ import Expect
import Html
import Json.Decode as JD
import Json.Encode as Encode
import List.Extra
import NotFoundReason
import OptimizedDecoder as Decode exposing (Decoder)
import Pages.ContentCache as ContentCache exposing (ContentCache)
import Pages.Internal.Platform.Cli exposing (..)
import Pages.Internal.Platform.Effect as Effect exposing (Effect)
import Pages.Internal.Platform.ToJsPayload as ToJsPayload exposing (ToJsPayload)
import Pages.Internal.Platform.ToJsPayload as ToJsPayload
import Pages.Internal.StaticHttpBody as StaticHttpBody
import Pages.Manifest as Manifest
import Pages.ProgramConfig exposing (ProgramConfig)
@ -28,12 +27,11 @@ import ProgramTest exposing (ProgramTest)
import Regex
import RenderRequest
import Secrets
import Serialize
import SimulatedEffect.Cmd
import SimulatedEffect.Http as Http
import SimulatedEffect.Ports
import SimulatedEffect.Task
import Test exposing (Test, describe, only, test)
import Test exposing (Test, describe, test)
import Test.Http
@ -52,11 +50,8 @@ all =
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86 }"""
|> expectSuccess
[ ( ""
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
[ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
, test "StaticHttp request for initial are resolved" <|
@ -76,11 +71,8 @@ all =
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86 }"""
|> expectSuccess
[ ( "post-1"
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
[ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
, describe "single page renders"
@ -151,11 +143,8 @@ all =
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86, "language": "Elm" }"""
|> expectSuccess
[ ( "post-1"
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86,"language":"Elm"}"""
)
]
[ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86,"language":"Elm"}"""
)
]
, test "andThen" <|
@ -178,14 +167,11 @@ all =
"NEXT-REQUEST"
"""null"""
|> expectSuccess
[ ( "elm-pages"
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """null"""
)
, ( get "NEXT-REQUEST"
, """null"""
)
]
[ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """null"""
)
, ( get "NEXT-REQUEST"
, """null"""
)
]
, test "andThen chain avoids repeat requests" <|
@ -272,75 +258,73 @@ all =
"url10"
"""{"image": "image10.jpg"}"""
|> expectSuccess
[ ( "elm-pages"
, [ ( get "https://pokeapi.co/api/v2/pokemon/"
, """[{"url":"url1"},{"url":"url2"},{"url":"url3"},{"url":"url4"},{"url":"url5"},{"url":"url6"},{"url":"url7"},{"url":"url8"},{"url":"url9"},{"url":"url10"}]"""
)
, ( get "url1"
, """{"image":"image1.jpg"}"""
)
, ( get "url2"
, """{"image":"image2.jpg"}"""
)
, ( get "url3"
, """{"image":"image3.jpg"}"""
)
, ( get "url4"
, """{"image":"image4.jpg"}"""
)
, ( get "url5"
, """{"image":"image5.jpg"}"""
)
, ( get "url6"
, """{"image":"image6.jpg"}"""
)
, ( get "url7"
, """{"image":"image7.jpg"}"""
)
, ( get "url8"
, """{"image":"image8.jpg"}"""
)
, ( get "url9"
, """{"image":"image9.jpg"}"""
)
, ( get "url10"
, """{"image":"image10.jpg"}"""
)
]
)
]
, test "port is sent out once all requests are finished" <|
\() ->
start
[ ( [ "elm-pages" ]
, DataSource.Http.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") starDecoder
)
, ( [ "elm-pages-starter" ]
, DataSource.Http.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages-starter") starDecoder
)
]
|> ProgramTest.simulateHttpOk
"GET"
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86 }"""
|> ProgramTest.simulateHttpOk
"GET"
"https://api.github.com/repos/dillonkearns/elm-pages-starter"
"""{ "stargazer_count": 22 }"""
|> expectSuccess
[ ( "elm-pages"
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
)
, ( "elm-pages-starter"
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages-starter"
, """{"stargazer_count":22}"""
)
]
[ ( get "https://pokeapi.co/api/v2/pokemon/"
, """[{"url":"url1"},{"url":"url2"},{"url":"url3"},{"url":"url4"},{"url":"url5"},{"url":"url6"},{"url":"url7"},{"url":"url8"},{"url":"url9"},{"url":"url10"}]"""
)
, ( get "url1"
, """{"image":"image1.jpg"}"""
)
, ( get "url2"
, """{"image":"image2.jpg"}"""
)
, ( get "url3"
, """{"image":"image3.jpg"}"""
)
, ( get "url4"
, """{"image":"image4.jpg"}"""
)
, ( get "url5"
, """{"image":"image5.jpg"}"""
)
, ( get "url6"
, """{"image":"image6.jpg"}"""
)
, ( get "url7"
, """{"image":"image7.jpg"}"""
)
, ( get "url8"
, """{"image":"image8.jpg"}"""
)
, ( get "url9"
, """{"image":"image9.jpg"}"""
)
, ( get "url10"
, """{"image":"image10.jpg"}"""
)
]
--, test "port is sent out once all requests are finished" <|
-- \() ->
-- start
-- [ ( [ "elm-pages" ]
-- , DataSource.Http.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") starDecoder
-- )
-- , ( [ "elm-pages-starter" ]
-- , DataSource.Http.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages-starter") starDecoder
-- )
-- ]
-- |> ProgramTest.simulateHttpOk
-- "GET"
-- "https://api.github.com/repos/dillonkearns/elm-pages"
-- """{ "stargazer_count": 86 }"""
-- |> ProgramTest.simulateHttpOk
-- "GET"
-- "https://api.github.com/repos/dillonkearns/elm-pages-starter"
-- """{ "stargazer_count": 22 }"""
-- |> expectSuccess
-- [ ( "elm-pages"
-- , [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
-- , """{"stargazer_count":86}"""
-- )
-- ]
-- )
-- , ( "elm-pages-starter"
-- , [ ( get "https://api.github.com/repos/dillonkearns/elm-pages-starter"
-- , """{"stargazer_count":22}"""
-- )
-- ]
-- )
-- ]
, test "reduced JSON is sent out" <|
\() ->
start
@ -353,11 +337,8 @@ all =
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86, "unused_field": 123 }"""
|> expectSuccess
[ ( ""
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
[ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
, test "you can use elm/json decoders with StaticHttp.unoptimizedRequest" <|
@ -382,11 +363,8 @@ all =
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86, "unused_field": 123 }"""
|> expectSuccess
[ ( ""
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{ "stargazer_count": 86, "unused_field": 123 }"""
)
]
[ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{ "stargazer_count": 86, "unused_field": 123 }"""
)
]
, test "plain string" <|
@ -409,11 +387,8 @@ all =
"https://example.com/file.txt"
"This is a raw text file."
|> expectSuccess
[ ( ""
, [ ( get "https://example.com/file.txt"
, "This is a raw text file."
)
]
[ ( get "https://example.com/file.txt"
, "This is a raw text file."
)
]
, test "Err in String to Result function turns into decode error" <|
@ -445,7 +420,7 @@ all =
"This is a raw text file."
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.successCodecNew2 "" ""))
(expectErrorsPort
"""-- STATIC HTTP DECODING ERROR ----------------------------------------------------- elm-pages
@ -473,15 +448,12 @@ String was not uppercased"""
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86, "unused_field": 123 }"""
|> expectSuccess
[ ( ""
, [ ( { method = "POST"
, url = "https://api.github.com/repos/dillonkearns/elm-pages"
, headers = []
, body = DataSource.emptyBody
}
, """{"stargazer_count":86}"""
)
]
[ ( { method = "POST"
, url = "https://api.github.com/repos/dillonkearns/elm-pages"
, headers = []
, body = DataSource.emptyBody
}
, """{"stargazer_count":86}"""
)
]
, test "json is reduced from andThen chains" <|
@ -504,14 +476,11 @@ String was not uppercased"""
"https://api.github.com/repos/dillonkearns/elm-pages-starter"
"""{ "stargazer_count": 50, "unused_field": 456 }"""
|> expectSuccess
[ ( ""
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":100}"""
)
, ( get "https://api.github.com/repos/dillonkearns/elm-pages-starter"
, """{"stargazer_count":50}"""
)
]
[ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":100}"""
)
, ( get "https://api.github.com/repos/dillonkearns/elm-pages-starter"
, """{"stargazer_count":50}"""
)
]
, test "reduced json is preserved by StaticHttp.map2" <|
@ -532,14 +501,11 @@ String was not uppercased"""
"https://api.github.com/repos/dillonkearns/elm-pages-starter"
"""{ "stargazer_count": 50, "unused_field": 456 }"""
|> expectSuccess
[ ( ""
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":100}"""
)
, ( get "https://api.github.com/repos/dillonkearns/elm-pages-starter"
, """{"stargazer_count":50}"""
)
]
[ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":100}"""
)
, ( get "https://api.github.com/repos/dillonkearns/elm-pages-starter"
, """{"stargazer_count":50}"""
)
]
, test "the port sends out even if there are no http requests" <|
@ -549,7 +515,7 @@ String was not uppercased"""
, DataSource.succeed ()
)
]
|> expectSuccess [ ( "", [] ) ]
|> expectSuccess []
, test "the port sends out when there are duplicate http requests for the same page" <|
\() ->
start
@ -564,11 +530,8 @@ String was not uppercased"""
"http://example.com"
"""null"""
|> expectSuccess
[ ( ""
, [ ( get "http://example.com"
, """null"""
)
]
[ ( get "http://example.com"
, """null"""
)
]
, test "an error is sent out for decoder failures" <|
@ -584,7 +547,7 @@ String was not uppercased"""
"""{ "stargazer_count": 86 }"""
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.successCodecNew2 "" ""))
(expectErrorsPort
"""-- STATIC HTTP DECODING ERROR ----------------------------------------------------- elm-pages
@ -627,7 +590,7 @@ I encountered some errors while decoding this JSON:
""" "continuation-url" """
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.successCodecNew2 "" ""))
(expectErrorsPort
"""-- MISSING SECRET ----------------------------------------------------- elm-pages
@ -656,21 +619,8 @@ So maybe MISSING should be API_KEY"""
)
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.successCodecNew2 "" ""))
(expectErrorsPort """-- STATIC HTTP ERROR ----------------------------------------------------- elm-pages
I got an error making an HTTP request to this URL: https://api.github.com/repos/dillonkearns/elm-pages
Bad status: 404
Status message: TODO: if you need this, please report to https://github.com/avh4/elm-program-test/issues
Body:
-- STATIC HTTP DECODING ERROR ----------------------------------------------------- elm-pages
Payload sent back invalid JSON
TODO
""")
, test "uses real secrets to perform request and masked secrets to store and lookup response" <|
\() ->
@ -707,17 +657,14 @@ TODO
}
)
|> expectSuccess
[ ( ""
, [ ( { method = "GET"
, url = "https://api.github.com/repos/dillonkearns/elm-pages?apiKey=<API_KEY>"
, headers =
[ ( "Authorization", "Bearer <BEARER>" )
]
, body = DataSource.emptyBody
}
, """{}"""
)
]
[ ( { method = "GET"
, url = "https://api.github.com/repos/dillonkearns/elm-pages?apiKey=<API_KEY>"
, headers =
[ ( "Authorization", "Bearer <BEARER>" )
]
, body = DataSource.emptyBody
}
, """{}"""
)
]
, describe "staticHttpCache"
@ -737,11 +684,8 @@ TODO
)
]
|> expectSuccess
[ ( ""
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
[ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
, test "it ignores unused cache" <|
@ -764,11 +708,8 @@ TODO
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86 }"""
|> expectSuccess
[ ( ""
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
[ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
, test "validate DataSource is not stored for any pages" <|
@ -941,7 +882,25 @@ TODO
[ {- ToJsPayload.Glob _, ToJsPayload.ReadFile _ -} ToJsPayload.PageProgress portData ] ->
portData.contentJson
|> Expect.equalDicts
(Dict.fromList [ ( "{\"method\":\"GET\",\"url\":\"file://content/glossary/hello.md\",\"headers\":[],\"body\":{\"type\":\"empty\"}}", "{\"withoutFrontmatter\":\"BODY\"}" ), ( "{\"method\":\"GET\",\"url\":\"glob://content/glossary/*.md\",\"headers\":[],\"body\":{\"type\":\"empty\"}}", "[\"content/glossary/hello.md\"]" ) ])
(Dict.fromList
[ ( Request.hash
{ method = "GET"
, url = "file://content/glossary/hello.md"
, headers = []
, body = DataSource.Http.emptyBody
}
, "{\"withoutFrontmatter\":\"BODY\"}"
)
, ( Request.hash
{ method = "GET"
, url = "glob://content/glossary/*.md"
, headers = []
, body = DataSource.Http.emptyBody
}
, "[\"content/glossary/hello.md\"]"
)
]
)
_ ->
Expect.fail <|
@ -1009,7 +968,7 @@ TODO
"""{ "stargazer_count": 123 }"""
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.successCodecNew2 "" ""))
(expectErrorsPort """-- NON-UNIQUE DISTILL KEYS ----------------------------------------------------- elm-pages
I encountered DataSource.distill with two matching keys that had differing encoded values.
@ -1022,91 +981,92 @@ The second encoded value was:
123""")
]
, describe "generateFiles"
[ test "initial requests are sent out" <|
\() ->
startLowLevel
[ ApiRoute.succeed
(DataSource.Http.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
(starDecoder
|> Decode.map
(\starCount ->
{ body = "Star count: " ++ String.fromInt starCount
}
)
)
)
|> ApiRoute.literal "test.txt"
|> ApiRoute.single
]
[]
[]
|> ProgramTest.simulateHttpOk
"GET"
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86 }"""
|> expectSuccessNew
[]
[ \success ->
success.filesToGenerate
|> Expect.equal
[ { path = [ "test.txt" ]
, content = "Star count: 86"
}
]
]
, test "it sends success port when no HTTP requests are needed because they're all cached" <|
\() ->
startLowLevel
[ ApiRoute.succeed
(DataSource.Http.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages-starter")
(starDecoder
|> Decode.map
(\starCount ->
{ body = "Star count: " ++ String.fromInt starCount
}
)
)
)
|> ApiRoute.literal "test.txt"
|> ApiRoute.single
]
[ ( { url = "https://api.github.com/repos/dillonkearns/elm-pages"
, method = "GET"
, headers = []
, body = StaticHttpBody.EmptyBody
}
, """{"stargazer_count":86}"""
)
, ( { url = "https://api.github.com/repos/dillonkearns/elm-pages-starter"
, method = "GET"
, headers = []
, body = StaticHttpBody.EmptyBody
}
, """{"stargazer_count":23}"""
)
]
[ ( []
, DataSource.Http.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") starDecoder
)
]
|> expectSuccessNew
[ ( ""
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
)
]
[ \success ->
success.filesToGenerate
|> Expect.equal
[ { path = [ "test.txt" ]
, content = "Star count: 23"
}
]
]
]
--, describe "generateFiles"
-- [ test "initial requests are sent out" <|
-- \() ->
-- startLowLevel
-- [ ApiRoute.succeed
-- (DataSource.Http.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
-- (starDecoder
-- |> Decode.map
-- (\starCount ->
-- { body = "Star count: " ++ String.fromInt starCount
-- }
-- )
-- )
-- )
-- |> ApiRoute.literal "test.txt"
-- |> ApiRoute.single
-- ]
-- []
-- []
-- |> ProgramTest.simulateHttpOk
-- "GET"
-- "https://api.github.com/repos/dillonkearns/elm-pages"
-- """{ "stargazer_count": 86 }"""
-- |> expectSuccessNew
-- []
-- [ \success ->
-- success.filesToGenerate
-- |> Expect.equal
-- [ { path = [ "test.txt" ]
-- , content = "Star count: 86"
-- }
-- ]
-- ]
-- , test "it sends success port when no HTTP requests are needed because they're all cached" <|
-- \() ->
-- startLowLevel
-- [ ApiRoute.succeed
-- (DataSource.Http.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages-starter")
-- (starDecoder
-- |> Decode.map
-- (\starCount ->
-- { body = "Star count: " ++ String.fromInt starCount
-- }
-- )
-- )
-- )
-- |> ApiRoute.literal "test.txt"
-- |> ApiRoute.single
-- ]
-- [ ( { url = "https://api.github.com/repos/dillonkearns/elm-pages"
-- , method = "GET"
-- , headers = []
-- , body = StaticHttpBody.EmptyBody
-- }
-- , """{"stargazer_count":86}"""
-- )
-- , ( { url = "https://api.github.com/repos/dillonkearns/elm-pages-starter"
-- , method = "GET"
-- , headers = []
-- , body = StaticHttpBody.EmptyBody
-- }
-- , """{"stargazer_count":23}"""
-- )
-- ]
-- [ ( []
-- , DataSource.Http.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") starDecoder
-- )
-- ]
-- |> expectSuccessNew
-- [ ( ""
-- , [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
-- , """{"stargazer_count":86}"""
-- )
-- ]
-- )
-- ]
-- [ \success ->
-- success.filesToGenerate
-- |> Expect.equal
-- [ { path = [ "test.txt" ]
-- , content = "Star count: 23"
-- }
-- ]
-- ]
-- ]
]
@ -1138,6 +1098,15 @@ startLowLevel apiRoutes staticHttpCache pages =
contentCache =
ContentCache.init Nothing
pageToLoad : List String
pageToLoad =
case pages |> List.head |> Maybe.map Tuple.first of
Just justPageToLoad ->
justPageToLoad
Nothing ->
Debug.todo "Error - no pages"
config : ProgramConfig Msg () Route () () ()
config =
{ toJsPort = toJsPort
@ -1154,6 +1123,7 @@ startLowLevel apiRoutes staticHttpCache pages =
, handleRoute = \_ -> DataSource.succeed Nothing
, urlToRoute = .path >> Route
, update = \_ _ _ _ _ -> ( (), Cmd.none )
, basePath = []
, data =
\(Route pageRoute) ->
let
@ -1239,7 +1209,19 @@ startLowLevel apiRoutes staticHttpCache pages =
-> ( model, Effect pathKey )
-}
ProgramTest.createDocument
{ init = init RenderRequest.FullBuild contentCache config
{ init =
init
(RenderRequest.SinglePage
RenderRequest.OnlyJson
(RenderRequest.Page
{ path = Path.fromString (pageToLoad |> String.join "/")
, frontmatter = Route (pageToLoad |> String.join "/")
}
)
(Encode.object [])
)
contentCache
config
, update = update contentCache config
, view = \_ -> { title = "", body = [] }
}
@ -1284,6 +1266,7 @@ startWithRoutes pageToLoad staticRoutes staticHttpCache pages =
|> DataSource.succeed
, urlToRoute = .path >> Route
, update = \_ _ _ _ _ -> ( (), Cmd.none )
, basePath = []
, data =
\(Route pageRoute) ->
let
@ -1347,7 +1330,6 @@ startWithRoutes pageToLoad staticRoutes staticHttpCache pages =
|> Dict.fromList
|> Encode.dict identity Encode.string
)
, ( "mode", Encode.string "elm-to-html-beta" )
, ( "staticHttpCache", encodedStaticHttpCache )
]
@ -1410,20 +1392,13 @@ simulateEffects effect =
Effect.NoEffect ->
SimulatedEffect.Cmd.none
Effect.SendJsData value ->
SimulatedEffect.Ports.send "toJsPort" (value |> Codec.encoder ToJsPayload.toJsCodec)
-- toJsPort value |> Cmd.map never
Effect.Batch list ->
list
|> List.map simulateEffects
|> SimulatedEffect.Cmd.batch
Effect.FetchHttp ({ unmasked } as requests) ->
let
_ =
Debug.log "Fetching " unmasked.url
in
Effect.FetchHttp { unmasked } ->
if unmasked.url |> String.startsWith "file://" then
let
filePath : String
@ -1455,10 +1430,6 @@ simulateEffects effect =
|> SimulatedEffect.Cmd.map never
else
let
_ =
Debug.log "Fetching" unmasked.url
in
Http.request
{ method = unmasked.method
, url = unmasked.url
@ -1476,10 +1447,24 @@ simulateEffects effect =
, expect =
PagesHttp.expectString
(\response ->
GotStaticHttpResponse
{ request = requests
, response = response
}
case response of
Ok okResponse ->
GotDataBatch
[ { request =
{ unmasked = unmasked
, masked = unmasked -- TODO use masked
}
, response = okResponse
}
]
Err _ ->
GotBuildError
{ title = "Static HTTP Error"
, message = []
, fatal = True
, path = ""
}
)
, timeout = Nothing
, tracker = Nothing
@ -1508,7 +1493,7 @@ simulateEffects effect =
SimulatedEffect.Cmd.none
expectErrorsPort : String -> List ToJsPayload -> Expect.Expectation
expectErrorsPort : String -> List ToJsPayload.ToJsSuccessPayloadNewCombined -> Expect.Expectation
expectErrorsPort expectedPlainString actualPorts =
case actualPorts of
[ ToJsPayload.Errors actualRichTerminalString ] ->
@ -1528,10 +1513,12 @@ normalizeErrorExpectEqual : String -> String -> Expect.Expectation
normalizeErrorExpectEqual expectedPlainString actualRichTerminalString =
actualRichTerminalString
|> Regex.replace
(Regex.fromString "\u{001B}\\[[0-9;]+m"
-- strip out all possible ANSI sequences
(Regex.fromString "(\\x9B|\\x1B\\[)[0-?]*[ -/]*[@-~]"
|> Maybe.withDefault Regex.never
)
(\_ -> "")
|> String.replace "\u{001B}" ""
|> normalizeNewlines
|> Expect.equal
(expectedPlainString |> normalizeNewlines)
@ -1546,6 +1533,10 @@ normalizeNewlines string =
|> Regex.replace
(Regex.fromString "( )+" |> Maybe.withDefault Regex.never)
(\_ -> " ")
|> String.replace "\u{000D}" ""
|> Regex.replace
(Regex.fromString "\\s" |> Maybe.withDefault Regex.never)
(\_ -> "")
toJsPort : a -> Cmd msg
@ -1573,40 +1564,37 @@ starDecoder =
Decode.field "stargazer_count" Decode.int
expectSuccess : List ( String, List ( Request.Request, String ) ) -> ProgramTest model msg effect -> Expect.Expectation
expectSuccess : List ( Request.Request, String ) -> ProgramTest model msg effect -> Expect.Expectation
expectSuccess expectedRequests previous =
expectSuccessNew expectedRequests [] previous
expectSuccessNew : List ( String, List ( Request.Request, String ) ) -> List (ToJsPayload.ToJsSuccessPayload -> Expect.Expectation) -> ProgramTest model msg effect -> Expect.Expectation
expectSuccessNew expectedRequests expectations previous =
expectSuccessNew : List ( Request.Request, String ) -> List (ToJsPayload.ToJsSuccessPayloadNew -> Expect.Expectation) -> ProgramTest model msg effect -> Expect.Expectation
expectSuccessNew expectedRequest expectations previous =
previous
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.successCodecNew2 "" ""))
(\value ->
case value of
(ToJsPayload.Success portPayload) :: _ ->
portPayload
|> Expect.all
((\subject ->
subject.pages
|> Expect.equalDicts
(expectedRequests
(ToJsPayload.PageProgress portPayload) :: _ ->
let
singleExpectation : ToJsPayload.ToJsSuccessPayloadNew -> Expect.Expectation
singleExpectation =
\subject ->
subject.contentJson
|> Expect.equal
(expectedRequest
|> List.map
(\( url, requests ) ->
( url
, requests
|> List.map
(\( request, response ) ->
( Request.hash request, response )
)
|> Dict.fromList
)
(\( request, response ) ->
( Request.hash request, response )
)
|> Dict.fromList
)
)
in
portPayload
|> Expect.all
(singleExpectation
:: expectations
)
@ -1631,7 +1619,27 @@ simulateSubscriptions _ =
(JD.field "pattern" JD.string)
(JD.field "result" JD.value)
)
|> JD.map GotGlob
|> JD.map
(\( globPattern, response ) ->
GotDataBatch
[ { request =
{ masked =
{ url = "glob://" ++ globPattern
, method = "GET"
, headers = []
, body = StaticHttpBody.EmptyBody
}
, unmasked =
{ url = "glob://" ++ globPattern
, method = "GET"
, headers = []
, body = StaticHttpBody.EmptyBody
}
}
, response = Encode.encode 0 response
}
]
)
"GotFile" ->
JD.field "data"
@ -1639,7 +1647,27 @@ simulateSubscriptions _ =
(JD.field "filePath" JD.string)
JD.value
)
|> JD.map GotStaticFile
|> JD.map
(\( filePath, response ) ->
GotDataBatch
[ { request =
{ masked =
{ url = "file://" ++ filePath
, method = "GET"
, headers = []
, body = StaticHttpBody.EmptyBody
}
, unmasked =
{ url = "file://" ++ filePath
, method = "GET"
, headers = []
, body = StaticHttpBody.EmptyBody
}
}
, response = Encode.encode 0 response
}
]
)
_ ->
JD.fail "Unexpected subscription tag."