version 4 bb

This commit is contained in:
Ryan Haskell-Glatz 2020-03-26 23:43:08 -05:00
parent b0af558f38
commit 4d015a2faf
214 changed files with 4405 additions and 18500 deletions

6
.gitignore vendored
View File

@ -1,4 +1,4 @@
.DS_Store
./dist
elm-stuff
node_modules
node_modules
dist
Generated

30
LICENSE
View File

@ -1,30 +0,0 @@
Copyright (c) 2019-present, Ryan Haskell-Glatz
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Ryan Haskell-Glatz nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,24 +1,20 @@
# elm-spa
single page apps made easy
## deprecated
### try it out
this is a deprecated version of elm-spa, so please check out the latest API
docs at [elm-spa.dev](https://elm-spa.dev)
```
npx elm-spa init new-project
```
(the old docs site is available at [v3.elm-spa.dev](https://v3.elm-spa.dev))
### or just install the elm package
### what is elm-spa?
```
elm install ryannhg/elm-spa
```
__elm-spa__ is a framework for building web apps in Elm. It's not a "framework" like React or VueJS, it's more of a framework like NextJS, Rails, or .NET MVC.
### or run the example
It provides simple tooling and a convention for creating single page apps, so you can focus on the cool thing you want to build!
__Learn more at [elm-spa.dev](https://v3.elm-spa.dev)__
### looking for the website's code?
the website project is available over here:
https://github.com/ryannhg/elm-spa-dev
(that's the one we build together at [elm-spa.dev/guide](https://v3.elm-spa.dev/guide))
```
cd example && npm start
```

3
cli/.gitignore vendored
View File

@ -1,3 +0,0 @@
dist
elm-stuff
node_modules

View File

@ -1,5 +1,5 @@
elm-stuff
src/elm
tests
/src
/tests
/elm.json
!.gitignore

View File

@ -1,3 +0,0 @@
# elm-spa/cli
> the thing that types the stuff

View File

@ -1,18 +1,17 @@
{
"type": "application",
"source-directories": [
"src/elm"
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.2",
"elm/html": "1.0.0",
"elm/json": "1.1.3",
"elm-community/list-extra": "8.2.2"
"elm/core": "1.0.5",
"elm/html": "1.0.0"
},
"indirect": {
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.2"

184
cli/index.js Executable file
View File

@ -0,0 +1,184 @@
#!/usr/bin/env node
const path = require('path')
const fs = require('fs')
const { Elm } = require('./dist/elm.worker.js')
const package = require('./package.json')
// File stuff
const folders = {
src: (dir) => path.join(process.cwd(), dir, 'src'),
pages: (dir) => path.join(process.cwd(), dir, 'src', 'Pages'),
generated: (dir) => path.join(process.cwd(), dir, 'src', 'Generated')
}
const rejectIfMissing = (dir) => new Promise((resolve, reject) =>
fs.existsSync(dir) ? resolve(true) : reject(false)
)
const cp = (src, dest) => {
const exists = fs.existsSync(src)
const stats = exists && fs.statSync(src)
if (stats && stats.isDirectory()) {
fs.mkdirSync(dest)
fs.readdirSync(src).forEach(child =>
cp(path.join(src, child), path.join(dest, child))
)
} else {
fs.copyFileSync(src, dest)
}
}
const listFiles = (dir) =>
fs.readdirSync(dir)
.reduce((files, file) =>
fs.statSync(path.join(dir, file)).isDirectory() ?
files.concat(listFiles(path.join(dir, file))) :
files.concat(path.join(dir, file)),
[])
const ensureDirectory = (dir) =>
fs.mkdirSync(dir, { recursive: true })
const saveToFolder = (prefix) => ({ filepath, content }) =>
fs.writeFileSync(path.join(prefix, filepath), content, { encoding: 'utf8' })
// Formatting output
const bold = (str) => '\033[1m' + str + '\033[0m'
const toFilepath = name => path.join(folders.pages('.'), `${name.split('.').join('/')}.elm`)
// Flags + Validation
const flags = { command: '', name: '', pageType: '', filepaths: [] }
const isValidPageType = type =>
[ 'static', 'sandbox', 'element', 'component' ].some(x => x === type)
const isValidModuleName = (name = '') => {
const isAlphaOnly = word => word.match(/[A-Z|a-z]+/)[0] === word
const isCapitalized = word => word[0].toUpperCase() === word[0]
return name &&
name.length &&
name.split('.').every(word => isAlphaOnly(word) && isCapitalized(word))
}
// Help commands
const help = {
general: `
${bold('elm-spa')} version ${package.version}
${bold('elm-spa init')} create a new project
${bold('elm-spa add')} add a new page
${bold('elm-spa build')} generate files
${bold('elm-spa <command> help')} get detailed help for a command
${bold('elm-spa -v')} print version number
`,
init: `
${bold('elm-spa init')} <directory>
Create a new elm-spa app in the <directory>
folder specified.
${bold('examples:')}
elm-spa init .
elm-spa init my-app
`,
add: `
${bold('elm-spa add')} <static|sandbox|element|component> <name>
Create a new page of type <static|sandbox|element|component>
with the module name <name>.
${bold('examples:')}
elm-spa add static Top
elm-spa add sandbox Posts.Top
elm-spa add element Posts.Dynamic
elm-spa add component SignIn
`,
build: `
${bold('elm-spa build')} [dir]
Generate "Generated.Route" and "Generated.Pages" for
this project, based on the files in src/Pages/*
Optionally, you can specify a different directory.
${bold('examples:')}
elm-spa build
elm-spa build ../some/other-folder
elm-spa build ./help
`
}
// Available commands
const commands = {
'init': ([ folder ]) =>
folder && folder !== 'help'
? Promise.resolve()
.then(_ => cp(path.join(__dirname, 'projects', 'new'), path.join(process.cwd(), folder)))
.then(_ => `\ncreated a new project in ${path.join(process.cwd(), folder)}\n`)
.catch(_ => `\nUnable to initialize a project at ${path.join(process.cwd(), folder)}\n`)
: Promise.resolve(help.init),
'add': ([ type, name ]) =>
(type && name) && type !== 'help' && isValidPageType(type) && isValidModuleName(name)
? rejectIfMissing(folders.pages('.'))
.then(_ => new Promise(
Elm.Main.init({ flags: { ...flags, command: 'add', name: name, pageType: type } }).ports.addPort.subscribe)
)
.then(file => {
const containingFolder = path.join(folders.pages('.'), file.filepath.split('/').slice(0, -1).join('/'))
ensureDirectory(containingFolder)
saveToFolder((folders.pages('.')))(file)
})
.then(_ => `\nadded a new ${bold(type)} page at:\n${toFilepath(name)}\n`)
.catch(_ => `\nplease run ${bold('elm-spa add')} in the folder with ${bold('elm.json')}\n`)
: Promise.resolve(help.add),
'build': ([ dir = '.' ] = []) =>
dir !== 'help'
? Promise.resolve(folders.pages(dir))
.then(listFiles)
.then(names => names.filter(name => name.endsWith('.elm')))
.then(names => names.map(name => name.substring(folders.pages(dir).length)))
.then(filepaths => new Promise(
Elm.Main.init({ flags: { ...flags, command: 'build', filepaths } }).ports.buildPort.subscribe
))
.then(files => {
ensureDirectory(folders.generated(dir))
files.forEach(saveToFolder(folders.src(dir)))
return files
})
.then(files => `\nelm-spa generated two files:\n${files.map(({ filepath }) => ' - ' + path.join(folders.src(dir), filepath)).join('\n')}\n`)
.catch(_ => `\nplease run ${bold('elm-spa build')} in the folder with ${bold('elm.json')}\n`)
: Promise.resolve(help.build),
'-v': _ => Promise.resolve(package.version),
'help': _ => Promise.resolve(help.general)
}
const main = ([ command, ...args ] = []) =>
(commands[command] || commands['help'])(args)
// .then(_ => args.data.slice)
.then(console.info)
.catch(reason => {
console.info(`\n${bold('Congratulations!')} - you've found a bug!
If you'd like, open an issue here with the following output:
https://github.com/ryannhg/elm-spa/issues/new?labels=cli
${bold(`### terminal output`)}
`)
console.log('```')
console.error(reason)
console.log('```\n')
})
main([...process.argv.slice(2)])

View File

@ -1,24 +0,0 @@
# your elm-spa
> learn more at [https://elm-spa.dev](https://elm-spa.dev)
### local development
```
npm run dev
```
## folder structure
```elm
README.md -- this file you're reading 👀
elm.json -- has project dependencies
src/
Main.elm -- the entrypoint to the app
Global.elm -- share state across pages
Transitions.elm -- smoothly animate between pages
Ports.elm -- communicate with JS
Pages/ -- where all your pages go
Layouts/ -- reusable views around pages
Components/ -- views shared across the site
Utils/ -- a place for helper functions
```

View File

@ -1,3 +0,0 @@
{
"ui": "Element"
}

View File

@ -1,27 +0,0 @@
{
"type": "application",
"source-directories": [
"src",
"elm-stuff/.elm-spa"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/core": "1.0.4",
"elm/html": "1.0.0",
"elm/json": "1.1.3",
"elm/url": "1.0.0",
"mdgriffith/elm-ui": "1.1.5",
"ryannhg/elm-spa": "3.0.0"
},
"indirect": {
"elm/browser": "1.0.2",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -1,6 +0,0 @@
# sends all routes to /index.html
# (so you can handle 404s there!)
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

View File

@ -1,26 +0,0 @@
{
"name": "my-elm-spa-project",
"version": "1.0.0",
"description": "learn more at https://elm-spa.dev",
"scripts": {
"start": "npm install && npm run dev",
"dev": "npm run elm:spa:build && npm run build:watch",
"build": "npm run elm:spa:build && npm run elm:compile",
"build:watch": "concurrently --raw --kill-others \"npm run elm:spa:watch\" \"npm run elm:live\"",
"elm:compile": "elm make src/Main.elm --output=public/dist/elm.compiled.js --optimize",
"elm:live": "elm-live src/Main.elm --dir=public --start-page=index.html --open --pushstate --port=1234 -- --output=public/dist/elm.compiled.js --debug",
"elm:spa:build": "elm-spa build .",
"elm:spa:watch": "chokidar src/Pages -c \"npm run elm:spa:build\""
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"chokidar-cli": "2.1.0",
"concurrently": "5.0.0",
"elm": "0.19.1-3",
"elm-live": "4.0.1",
"elm-spa": "3.0.3"
}
}

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>our-elm-spa</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<script src="/dist/elm.compiled.js"></script>
<script src="/ports.js"></script>
<script>
window.addEventListener('load', _ =>
window.ports.init(Elm.Main.init())
)
</script>
</body>
</html>

View File

@ -1,17 +0,0 @@
// On load, listen to Elm!
window.addEventListener('load', _ => {
window.ports = {
init: (app) =>
app.ports.outgoing.subscribe(({ action, data }) =>
actions[action]
? actions[action](data)
: console.warn(`I didn't recognize action "${action}".`)
)
}
})
// maps actions to functions!
const actions = {
'LOG': (message) =>
console.log(`From Elm:`, message)
}

View File

@ -1,5 +0,0 @@
/* you can include CSS here */
html, body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
height: 100%;
}

View File

@ -1,2 +0,0 @@
# src/Components
> views shared across the site

View File

@ -1,49 +0,0 @@
module Global exposing
( Flags
, Model
, Msg(..)
, init
, subscriptions
, update
)
import Generated.Routes as Routes exposing (Route)
import Ports
type alias Flags =
()
type alias Model =
{}
type Msg
= Msg
type alias Commands msg =
{ navigate : Route -> Cmd msg
}
init : Commands msg -> Flags -> ( Model, Cmd Msg, Cmd msg )
init _ _ =
( {}
, Cmd.none
, Ports.log "Hello!"
)
update : Commands msg -> Msg -> Model -> ( Model, Cmd Msg, Cmd msg )
update _ _ model =
( model
, Cmd.none
, Cmd.none
)
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none

View File

@ -1,50 +0,0 @@
module Layout exposing (view)
import Element exposing (..)
import Element.Font as Font
import Generated.Routes as Routes exposing (Route, routes)
import Utils.Spa as Spa
view : Spa.LayoutContext msg -> Element msg
view { page, route } =
column [ height fill, width fill ]
[ viewHeader route
, page
]
viewHeader : Route -> Element msg
viewHeader currentRoute =
row
[ spacing 24
, paddingEach { top = 32, left = 16, right = 16, bottom = 0 }
, centerX
, width (fill |> maximum 480)
]
[ viewLink currentRoute ( "home", routes.top )
, viewLink currentRoute ( "nowhere", routes.notFound )
]
viewLink : Route -> ( String, Route ) -> Element msg
viewLink currentRoute ( label, route ) =
if currentRoute == route then
el
[ Font.underline
, Font.color (rgb255 204 75 75)
, alpha 0.5
, Font.size 16
]
(text label)
else
link
[ Font.underline
, Font.color (rgb255 204 75 75)
, Font.size 16
, mouseOver [ alpha 0.5 ]
]
{ label = text label
, url = Routes.toPath route
}

View File

@ -1,2 +0,0 @@
# src/Layouts
> where all your pages go

View File

@ -1,27 +0,0 @@
module Main exposing (main)
import Generated.Pages as Pages
import Generated.Routes as Routes exposing (routes)
import Global
import Spa
import Transitions
main : Spa.Program Global.Flags Global.Model Global.Msg Pages.Model Pages.Msg
main =
Spa.create
{ ui = Spa.usingElmUi
, transitions = Transitions.transitions
, routing =
{ routes = Routes.parsers
, toPath = Routes.toPath
, notFound = routes.notFound
, afterNavigate = Nothing
}
, global =
{ init = Global.init
, update = Global.update
, subscriptions = Global.subscriptions
}
, page = Pages.page
}

View File

@ -1,39 +0,0 @@
module Pages.NotFound exposing (Model, Msg, page)
import Element exposing (..)
import Element.Font as Font
import Generated.Params as Params
import Generated.Routes as Routes exposing (routes)
import Spa.Page
import Utils.Spa exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Params.NotFound Model Msg model msg appMsg
page =
Spa.Page.static
{ title = always "not found | elm-spa"
, view = always view
}
-- VIEW
view : Element Msg
view =
column [ centerX, centerY, spacing 16 ]
[ el [ Font.size 32, Font.semiBold ] (text "404 is life.")
, link [ Font.size 16, Font.underline, centerX, Font.color (rgb255 204 75 75), mouseOver [ alpha 0.5 ] ]
{ label = text "back home?"
, url = Routes.toPath routes.top
}
]

View File

@ -1,3 +0,0 @@
# src/Pages
> where all your pages go

View File

@ -1,51 +0,0 @@
module Pages.Top exposing (Model, Msg, page)
import Element exposing (..)
import Element.Font as Font
import Generated.Params as Params
import Spa.Page
import Utils.Spa exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Params.Top Model Msg model msg appMsg
page =
Spa.Page.static
{ title = always "homepage"
, view = always view
}
-- VIEW
view : Element Msg
view =
column
[ centerX
, centerY
, spacing 24
]
[ column [ spacing 14 ]
[ el [ centerX, Font.size 48, Font.semiBold ] (text "elm-spa")
, el [ alpha 0.5 ] (text "(you're doing great already!)")
]
, newTabLink
[ Font.underline
, centerX
, Font.color (rgb255 204 75 75)
, mouseOver [ alpha 0.5 ]
, Font.size 16
]
{ label = text "learn more"
, url = "https://elm-spa.dev"
}
]

View File

@ -1,14 +0,0 @@
port module Ports exposing (log)
import Json.Encode as Json
port outgoing : { action : String, data : Json.Value } -> Cmd msg
log : String -> Cmd msg
log message =
outgoing
{ action = "LOG"
, data = Json.string message
}

View File

@ -1,12 +0,0 @@
module Transitions exposing (transitions)
import Spa.Transition as Transition
import Utils.Spa as Spa
transitions : Spa.Transitions msg
transitions =
{ layout = Transition.fadeElmUi 500
, page = Transition.fadeElmUi 300
, pages = []
}

View File

@ -1,2 +0,0 @@
# src/Utils
> a place for helper functions

View File

@ -1,72 +0,0 @@
module Utils.Spa exposing
( Bundle
, Init
, LayoutContext
, Page
, PageContext
, Recipe
, Transitions
, Update
, layout
, recipe
)
import Element exposing (Element)
import Generated.Routes as Routes exposing (Route)
import Global
import Spa.Page
import Spa.Types
type alias Page params model msg layoutModel layoutMsg appMsg =
Spa.Types.Page Route params model msg (Element msg) layoutModel layoutMsg (Element layoutMsg) Global.Model Global.Msg appMsg (Element appMsg)
type alias Recipe params model msg layoutModel layoutMsg appMsg =
Spa.Types.Recipe Route params model msg layoutModel layoutMsg (Element layoutMsg) Global.Model Global.Msg appMsg (Element appMsg)
type alias Init model msg =
Spa.Types.Init Route model msg Global.Model Global.Msg
type alias Update model msg =
Spa.Types.Update Route model msg Global.Model Global.Msg
type alias Bundle msg appMsg =
Spa.Types.Bundle Route msg (Element msg) Global.Model Global.Msg appMsg (Element appMsg)
type alias LayoutContext msg =
Spa.Types.LayoutContext Route msg (Element msg) Global.Model Global.Msg
type alias PageContext =
Spa.Types.PageContext Route Global.Model
type alias Layout params model msg appMsg =
Spa.Types.Layout Route params model msg (Element msg) Global.Model Global.Msg appMsg (Element appMsg)
layout :
Layout params model msg appMsg
-> Page params model msg layoutModel layoutMsg appMsg
layout =
Spa.Page.layout Element.map
type alias Upgrade params model msg layoutModel layoutMsg appMsg =
Spa.Types.Upgrade Route params model msg (Element msg) layoutModel layoutMsg (Element layoutMsg) Global.Model Global.Msg appMsg (Element appMsg)
recipe :
Upgrade params model msg layoutModel layoutMsg appMsg
-> Recipe params model msg layoutModel layoutMsg appMsg
recipe =
Spa.Page.recipe Element.map
type alias Transitions msg =
Spa.Types.Transitions (Element msg)

View File

@ -1,4 +0,0 @@
.DS_Store
dist
elm-stuff
node_modules

View File

@ -1,24 +0,0 @@
# your elm-spa
> learn more at [https://elm-spa.dev](https://elm-spa.dev)
### local development
```
npm run dev
```
## folder structure
```elm
README.md -- this file you're reading 👀
elm.json -- has project dependencies
src/
Main.elm -- the entrypoint to the app
Global.elm -- share state across pages
Transitions.elm -- smoothly animate between pages
Ports.elm -- communicate with JS
Pages/ -- where all your pages go
Layouts/ -- reusable views around pages
Components/ -- views shared across the site
Utils/ -- a place for helper functions
```

View File

@ -1,3 +0,0 @@
{
"ui": "Html"
}

View File

@ -1,6 +0,0 @@
# sends all routes to /index.html
# (so you can handle 404s there!)
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
{
"name": "my-elm-spa-project",
"version": "1.0.0",
"description": "learn more at https://elm-spa.dev",
"scripts": {
"start": "npm install && npm run dev",
"dev": "npm run elm:spa:build && npm run build:watch",
"build": "npm run elm:spa:build && npm run elm:compile",
"build:watch": "concurrently --raw --kill-others \"npm run elm:spa:watch\" \"npm run elm:live\"",
"elm:compile": "elm make src/Main.elm --output=public/dist/elm.compiled.js --optimize",
"elm:live": "elm-live src/Main.elm --dir=public --start-page=index.html --open --pushstate --port=1234 -- --output=public/dist/elm.compiled.js --debug",
"elm:spa:build": "elm-spa build .",
"elm:spa:watch": "chokidar src/Pages -c \"npm run elm:spa:build\""
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"chokidar-cli": "2.1.0",
"concurrently": "5.0.0",
"elm": "0.19.1-3",
"elm-live": "4.0.1",
"elm-spa": "3.0.3"
}
}

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>our-elm-spa</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<script src="/dist/elm.compiled.js"></script>
<script src="/ports.js"></script>
<script>
window.addEventListener('load', _ =>
window.ports.init(Elm.Main.init())
)
</script>
</body>
</html>

View File

@ -1,17 +0,0 @@
// On load, listen to Elm!
window.addEventListener('load', _ => {
window.ports = {
init: (app) =>
app.ports.outgoing.subscribe(({ action, data }) =>
actions[action]
? actions[action](data)
: console.warn(`I didn't recognize action "${action}".`)
)
}
})
// maps actions to functions!
const actions = {
'LOG': (message) =>
console.log(`From Elm:`, message)
}

View File

@ -1,5 +0,0 @@
/* you can include CSS here */
html, body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
height: 100%;
}

View File

@ -1,2 +0,0 @@
# src/Components
> views shared across the site

View File

@ -1,49 +0,0 @@
module Global exposing
( Flags
, Model
, Msg(..)
, init
, subscriptions
, update
)
import Generated.Routes as Routes exposing (Route)
import Ports
type alias Flags =
()
type alias Model =
{}
type Msg
= Msg
type alias Commands msg =
{ navigate : Route -> Cmd msg
}
init : Commands msg -> Flags -> ( Model, Cmd Msg, Cmd msg )
init _ _ =
( {}
, Cmd.none
, Ports.log "Hello!"
)
update : Commands msg -> Msg -> Model -> ( Model, Cmd Msg, Cmd msg )
update _ _ model =
( model
, Cmd.none
, Cmd.none
)
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none

View File

@ -1,39 +0,0 @@
module Layout exposing (view)
import Generated.Routes as Routes exposing (Route, routes)
import Html exposing (..)
import Html.Attributes as Attr
import Utils.Spa as Spa
view : Spa.LayoutContext msg -> Html msg
view { page, route } =
div [ Attr.class "app" ]
[ viewHeader route
, page
]
viewHeader : Route -> Html msg
viewHeader currentRoute =
header
[ Attr.class "navbar"
]
[ viewLink currentRoute ( "home", routes.top )
, viewLink currentRoute ( "nowhere", routes.notFound )
]
viewLink : Route -> ( String, Route ) -> Html msg
viewLink currentRoute ( label, route ) =
if currentRoute == route then
span
[ Attr.class "link link--active" ]
[ text label ]
else
a
[ Attr.class "link"
, Attr.href (Routes.toPath route)
]
[ text label ]

View File

@ -1,2 +0,0 @@
# src/Layouts
> where all your pages go

View File

@ -1,27 +0,0 @@
module Main exposing (main)
import Generated.Pages as Pages
import Generated.Routes as Routes exposing (routes)
import Global
import Spa
import Transitions
main : Spa.Program Global.Flags Global.Model Global.Msg Pages.Model Pages.Msg
main =
Spa.create
{ ui = Spa.usingHtml
, transitions = Transitions.transitions
, routing =
{ routes = Routes.parsers
, toPath = Routes.toPath
, notFound = routes.notFound
, afterNavigate = Nothing
}
, global =
{ init = Global.init
, update = Global.update
, subscriptions = Global.subscriptions
}
, page = Pages.page
}

View File

@ -1,42 +0,0 @@
module Pages.NotFound exposing (Model, Msg, page)
import Generated.Params as Params
import Generated.Routes as Routes exposing (routes)
import Html exposing (..)
import Html.Attributes as Attr
import Spa.Page
import Utils.Spa exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Params.NotFound Model Msg model msg appMsg
page =
Spa.Page.static
{ title = always "not found | elm-spa"
, view = always view
}
-- VIEW
view : Html Msg
view =
div [ Attr.class "page" ]
[ h1
[ Attr.class "page__title" ]
[ text "404 is life." ]
, a
[ Attr.class "page__link"
, Attr.href (Routes.toPath routes.top)
]
[ text "back home?" ]
]

View File

@ -1,3 +0,0 @@
# src/Pages
> where all your pages go

View File

@ -1,45 +0,0 @@
module Pages.Top exposing (Model, Msg, page)
import Generated.Params as Params
import Html exposing (..)
import Html.Attributes as Attr
import Spa.Page
import Utils.Spa exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Params.Top Model Msg model msg appMsg
page =
Spa.Page.static
{ title = always "homepage"
, view = always view
}
-- VIEW
view : Html Msg
view =
div [ Attr.class "page" ]
[ h1
[ Attr.class "page__title" ]
[ text "404 is life." ]
, p [ Attr.class "page__subtitle" ]
[ text "(you're doing great already!)" ]
, a
[ Attr.class "page__link"
, Attr.target "_blank"
, Attr.href "https://elm-spa.dev"
]
[ text "learn more"
]
]

View File

@ -1,14 +0,0 @@
port module Ports exposing (log)
import Json.Encode as Json
port outgoing : { action : String, data : Json.Value } -> Cmd msg
log : String -> Cmd msg
log message =
outgoing
{ action = "LOG"
, data = Json.string message
}

View File

@ -1,12 +0,0 @@
module Transitions exposing (transitions)
import Spa.Transition as Transition
import Utils.Spa as Spa
transitions : Spa.Transitions msg
transitions =
{ layout = Transition.fadeHtml 500
, page = Transition.fadeHtml 300
, pages = []
}

View File

@ -1,2 +0,0 @@
# src/Utils
> a place for helper functions

View File

@ -1,72 +0,0 @@
module Utils.Spa exposing
( Bundle
, Init
, LayoutContext
, Page
, PageContext
, Recipe
, Transitions
, Update
, layout
, recipe
)
import Html exposing (Html)
import Generated.Routes as Routes exposing (Route)
import Global
import Spa.Page
import Spa.Types
type alias Page params model msg layoutModel layoutMsg appMsg =
Spa.Types.Page Route params model msg (Html msg) layoutModel layoutMsg (Html layoutMsg) Global.Model Global.Msg appMsg (Html appMsg)
type alias Recipe params model msg layoutModel layoutMsg appMsg =
Spa.Types.Recipe Route params model msg layoutModel layoutMsg (Html layoutMsg) Global.Model Global.Msg appMsg (Html appMsg)
type alias Init model msg =
Spa.Types.Init Route model msg Global.Model Global.Msg
type alias Update model msg =
Spa.Types.Update Route model msg Global.Model Global.Msg
type alias Bundle msg appMsg =
Spa.Types.Bundle Route msg (Html msg) Global.Model Global.Msg appMsg (Html appMsg)
type alias LayoutContext msg =
Spa.Types.LayoutContext Route msg (Html msg) Global.Model Global.Msg
type alias PageContext =
Spa.Types.PageContext Route Global.Model
type alias Layout params model msg appMsg =
Spa.Types.Layout Route params model msg (Html msg) Global.Model Global.Msg appMsg (Html appMsg)
layout :
Layout params model msg appMsg
-> Page params model msg layoutModel layoutMsg appMsg
layout =
Spa.Page.layout Html.map
type alias Upgrade params model msg layoutModel layoutMsg appMsg =
Spa.Types.Upgrade Route params model msg (Html msg) layoutModel layoutMsg (Html layoutMsg) Global.Model Global.Msg appMsg (Html appMsg)
recipe :
Upgrade params model msg layoutModel layoutMsg appMsg
-> Recipe params model msg layoutModel layoutMsg appMsg
recipe =
Spa.Page.recipe Html.map
type alias Transitions msg =
Spa.Types.Transitions (Html msg)

1106
cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,18 @@
{
"name": "elm-spa",
"version": "3.0.3",
"description": "the cli companion tool for ryannhg/elm-spa",
"main": "src/index.js",
"bin": "src/index.js",
"version": "4.0.0",
"description": "single page apps made easy",
"main": "index.js",
"bin": "./index.js",
"directories": {
"test": "tests"
},
"scripts": {
"start": "npm run build && node src/index.js",
"build": "npm run elm:compile",
"build": "elm make src/Main.elm --optimize --output dist/elm.worker.js",
"publish": "npm run build && npm publish",
"test": "elm-test",
"test:watch": "elm-test --watch",
"elm:compile": "elm make src/elm/Main.elm --output=dist/elm.compiled.js --optimize"
"dev": "chokidar src -c \"(npm run build || true)\""
},
"repository": {
"type": "git",
@ -18,7 +21,7 @@
"keywords": [
"elm",
"spa",
"cli",
"web",
"framework"
],
"author": "Ryan Haskell-Glatz",
@ -27,8 +30,5 @@
"url": "https://github.com/ryannhg/elm-spa/issues"
},
"homepage": "https://github.com/ryannhg/elm-spa#readme",
"devDependencies": {
"elm": "0.19.1-3",
"elm-test": "0.19.1-revision2"
}
"dependencies": {}
}

View File

@ -2,22 +2,20 @@
"type": "application",
"source-directories": [
"src",
"elm-stuff/.elm-spa"
"../../../src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.4",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/json": "1.1.3",
"elm/url": "1.0.0",
"ryannhg/elm-spa": "3.0.0"
"elm/url": "1.0.0"
},
"indirect": {
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2",
"mdgriffith/elm-ui": "1.1.5"
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {

View File

@ -0,0 +1,26 @@
{
"name": "elm-spa-app",
"version": "1.0.0",
"description": "my new elm-spa application",
"main": "public/index.html",
"scripts": {
"start": "npm install && npm run build && npm run dev",
"build": "npm run build:elm-spa && npm run build:elm",
"build:elm-spa": "elm-spa build .",
"build:elm": "elm make src/Main.elm --optimize --output public/dist/elm.js",
"dev": "npm run dev:elm-spa & npm run dev:elm",
"dev:elm-spa": "chokidar src/Pages -c \"npm run build:elm-spa\"",
"dev:elm": "elm-live src/Main.elm -u -d public -- --debug --output public/dist/elm.js"
},
"keywords": [
"elm",
"spa"
],
"author": "",
"license": "ISC",
"devDependencies": {
"chokidar-cli": "2.1.0",
"elm": "0.19.1-3",
"elm-live": "4.0.2"
}
}

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://not-much-css.netlify.com/not-much.css" />
<title>elm-spa</title>
</head>
<body>
<script src="/dist/elm.js"></script>
<script>
Elm.Main.init()
</script>
</body>
</html>

View File

@ -0,0 +1,36 @@
module Components exposing (layout)
import Browser exposing (Document)
import Generated.Route as Route exposing (Route)
import Html exposing (..)
import Html.Attributes as Attr exposing (class, href, style)
layout : { page : Document msg } -> Document msg
layout { page } =
{ title = page.title
, body =
[ div [ class "column spacing--large pad--medium container h--fill" ]
[ navbar
, div [ class "column", style "flex" "1 0 auto" ] page.body
, footer
]
]
}
navbar : Html msg
navbar =
header [ class "row center-y spacing--between" ]
[ a [ class "link font--h5", href (Route.toHref Route.Top) ] [ text "home" ]
, div [ class "row center-y spacing--medium" ]
[ a [ class "link", href (Route.toHref Route.Docs) ] [ text "docs" ]
, a [ class "link", href (Route.toHref Route.NotFound) ] [ text "a broken link" ]
, a [ class "button", href "https://twitter.com/intent/tweet?text=elm-spa is ez pz" ] [ text "tweet about it" ]
]
]
footer : Html msg
footer =
Html.footer [] [ text "built with elm " ]

View File

@ -0,0 +1,98 @@
module Global exposing
( Flags
, Model
, Msg
, init
, navigate
, subscriptions
, update
, view
)
import Browser exposing (Document)
import Browser.Navigation as Nav
import Components
import Generated.Route as Route exposing (Route)
import Task
import Url exposing (Url)
-- INIT
type alias Flags =
()
type alias Model =
{ flags : Flags
, url : Url
, key : Nav.Key
}
init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model
flags
url
key
, Cmd.none
)
-- UPDATE
type Msg
= Navigate Route
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Navigate route ->
( model
, Nav.pushUrl model.key (Route.toHref route)
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view :
{ page : Document msg
, global : Model
, toMsg : Msg -> msg
}
-> Document msg
view { page, global, toMsg } =
Components.layout
{ page = page
}
-- COMMANDS
send : msg -> Cmd msg
send =
Task.succeed >> Task.perform identity
navigate : Route -> Cmd Msg
navigate route =
send (Navigate route)

View File

@ -0,0 +1,141 @@
module Main exposing (main)
import Browser exposing (Document)
import Browser.Navigation as Nav exposing (Key)
import Generated.Pages as Pages
import Generated.Route as Route exposing (Route)
import Global
import Html
import Url exposing (Url)
main : Program Flags Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlRequest = LinkClicked
, onUrlChange = UrlChanged
}
-- INIT
type alias Flags =
()
type alias Model =
{ key : Key
, url : Url
, global : Global.Model
, page : Pages.Model
}
init : Flags -> Url -> Key -> ( Model, Cmd Msg )
init flags url key =
let
( global, globalCmd ) =
Global.init flags url key
( page, pageCmd, pageGlobalCmd ) =
Pages.init (fromUrl url) global
in
( Model key url global page
, Cmd.batch
[ Cmd.map Global globalCmd
, Cmd.map Global pageGlobalCmd
, Cmd.map Page pageCmd
]
)
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url
| Global Global.Msg
| Page Pages.Msg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked (Browser.Internal url) ->
( model, Nav.pushUrl model.key (Url.toString url) )
LinkClicked (Browser.External href) ->
( model, Nav.load href )
UrlChanged url ->
let
( page, pageCmd, globalCmd ) =
Pages.init (fromUrl url) model.global
in
( { model | url = url, page = page }
, Cmd.batch
[ Cmd.map Page pageCmd
, Cmd.map Global globalCmd
]
)
Global globalMsg ->
let
( global, globalCmd ) =
Global.update globalMsg model.global
in
( { model | global = global }
, Cmd.map Global globalCmd
)
Page pageMsg ->
let
( page, pageCmd, globalCmd ) =
Pages.update pageMsg model.page model.global
in
( { model | page = page }
, Cmd.batch
[ Cmd.map Page pageCmd
, Cmd.map Global globalCmd
]
)
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ model.global
|> Global.subscriptions
|> Sub.map Global
, model.page
|> (\page -> Pages.subscriptions page model.global)
|> Sub.map Page
]
view : Model -> Browser.Document Msg
view model =
let
documentMap :
(msg1 -> msg2)
-> Document msg1
-> Document msg2
documentMap fn doc =
{ title = doc.title
, body = List.map (Html.map fn) doc.body
}
in
Global.view
{ page = Pages.view model.page model.global |> documentMap Page
, global = model.global
, toMsg = Global
}
fromUrl : Url -> Route
fromUrl =
Route.fromUrl >> Maybe.withDefault Route.NotFound

View File

@ -0,0 +1,32 @@
module Pages.Docs exposing (Flags, Model, Msg, page)
import Browser exposing (Document)
import Global
import Html
import Spa
type alias Flags =
()
type alias Model =
()
type alias Msg =
Never
page : Spa.Page Flags Model Msg Global.Model Global.Msg
page =
Spa.static
{ view = view
}
view : Document Msg
view =
{ title = "Docs"
, body = [ Html.text "Docs" ]
}

View File

@ -0,0 +1,32 @@
module Pages.NotFound exposing (Flags, Model, Msg, page)
import Browser exposing (Document)
import Global
import Html
import Spa
type alias Flags =
()
type alias Model =
()
type alias Msg =
Never
page : Spa.Page Flags Model Msg Global.Model Global.Msg
page =
Spa.static
{ view = view
}
view : Document Msg
view =
{ title = "NotFound"
, body = [ Html.text "NotFound" ]
}

View File

@ -0,0 +1,32 @@
module Pages.Top exposing (Flags, Model, Msg, page)
import Browser exposing (Document)
import Global
import Html
import Spa
type alias Flags =
()
type alias Model =
()
type alias Msg =
Never
page : Spa.Page Flags Model Msg Global.Model Global.Msg
page =
Spa.static
{ view = view
}
view : Document Msg
view =
{ title = "Top"
, body = [ Html.text "Top" ]
}

64
cli/src/Add/Component.elm Normal file
View File

@ -0,0 +1,64 @@
module Add.Component exposing (create)
import Path exposing (Path)
create : Path -> String
create path =
"""
module Pages.{{name}} exposing (Flags, Model, Msg, page)
import Browser exposing (Document)
import Global
import Html
import Spa
type alias Flags =
{{flags}}
type alias Model =
{}
type Msg
= NoOp
page : Spa.Page Flags Model Msg Global.Model Global.Msg
page =
Spa.component
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
init : Global.Model -> Flags -> ( Model, Cmd Msg, Cmd Global.Msg )
init global flags =
( {}, Cmd.none, Cmd.none )
update : Global.Model -> Msg -> Model -> ( Model, Cmd Msg, Cmd Global.Msg )
update global msg model =
case msg of
NoOp ->
( model, Cmd.none, Cmd.none )
subscriptions : Global.Model -> Model -> Sub Msg
subscriptions global model =
Sub.none
view : Global.Model -> Model -> Document Msg
view global model =
{ title = "{{name}}"
, body = [ Html.text "{{name}}" ]
}
"""
|> String.replace "{{name}}" (Path.toModulePath path)
|> String.replace "{{flags}}" (Path.toFlags path)
|> String.trim

64
cli/src/Add/Element.elm Normal file
View File

@ -0,0 +1,64 @@
module Add.Element exposing (create)
import Path exposing (Path)
create : Path -> String
create path =
"""
module Pages.{{name}} exposing (Flags, Model, Msg, page)
import Browser exposing (Document)
import Global
import Html
import Spa
type alias Flags =
{{flags}}
type alias Model =
{}
type Msg
= NoOp
page : Spa.Page Flags Model Msg Global.Model Global.Msg
page =
Spa.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
init : Flags -> ( Model, Cmd Msg )
init flags =
( {}, Cmd.none )
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
view : Model -> Document Msg
view model =
{ title = "{{name}}"
, body = [ Html.text "{{name}}" ]
}
"""
|> String.replace "{{name}}" (Path.toModulePath path)
|> String.replace "{{flags}}" (Path.toFlags path)
|> String.trim

58
cli/src/Add/Sandbox.elm Normal file
View File

@ -0,0 +1,58 @@
module Add.Sandbox exposing (create)
import Path exposing (Path)
create : Path -> String
create path =
"""
module Pages.{{name}} exposing (Flags, Model, Msg, page)
import Browser exposing (Document)
import Global
import Html
import Spa
type alias Flags =
{{flags}}
type alias Model =
{}
type Msg
= NoOp
page : Spa.Page Flags Model Msg Global.Model Global.Msg
page =
Spa.sandbox
{ init = init
, update = update
, view = view
}
init : Model
init =
{}
update : Msg -> Model -> Model
update msg model =
case msg of
NoOp ->
{}
view : Model -> Document Msg
view model =
{ title = "{{name}}"
, body = [ Html.text "{{name}}" ]
}
"""
|> String.replace "{{name}}" (Path.toModulePath path)
|> String.replace "{{flags}}" (Path.toFlags path)
|> String.trim

44
cli/src/Add/Static.elm Normal file
View File

@ -0,0 +1,44 @@
module Add.Static exposing (create)
import Path exposing (Path)
create : Path -> String
create path =
"""
module Pages.{{name}} exposing (Flags, Model, Msg, page)
import Browser exposing (Document)
import Global
import Html
import Spa
type alias Flags =
{{flags}}
type alias Model =
()
type alias Msg =
Never
page : Spa.Page Flags Model Msg Global.Model Global.Msg
page =
Spa.static
{ view = view
}
view : Document Msg
view =
{ title = "{{name}}"
, body = [ Html.text "{{name}}" ]
}
"""
|> String.replace "{{name}}" (Path.toModulePath path)
|> String.replace "{{flags}}" (Path.toFlags path)
|> String.trim

View File

@ -0,0 +1,299 @@
module Generators.Pages exposing
( generate
, pagesBundle
, pagesCustomType
, pagesImports
, pagesInit
, pagesUpdate
, pagesUpgradedTypes
, pagesUpgradedValues
)
import Path exposing (Path)
import Utils.Generate as Utils
generate : List Path -> String
generate paths =
String.trim """
module Generated.Pages exposing
( Model
, Msg
, init
, subscriptions
, update
, view
)
import Browser exposing (Document)
import Generated.Route as Route exposing (Route)
import Global
import Spa
{{pagesImports}}
-- TYPES
{{pagesModels}}
{{pagesMsgs}}
-- PAGES
type alias UpgradedPage flags model msg =
{ init : flags -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
, update : msg -> model -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
, bundle : model -> Global.Model -> Spa.Bundle Msg
}
type alias UpgradedPages =
{{pagesUpgradedTypes}}
pages : UpgradedPages
pages =
{{pagesUpgradedValues}}
-- INIT
{{pagesInit}}
-- UPDATE
{{pagesUpdate}}
-- BUNDLE - (view + subscriptions)
{{pagesBundle}}
view : Model -> Global.Model -> Document Msg
view model =
bundle model >> .view
subscriptions : Model -> Global.Model -> Sub Msg
subscriptions model =
bundle model >> .subscriptions
"""
|> String.replace "{{pagesImports}}" (pagesImports paths)
|> String.replace "{{pagesModels}}" (pagesModels paths)
|> String.replace "{{pagesMsgs}}" (pagesMsgs paths)
|> String.replace "{{pagesUpgradedTypes}}" (pagesUpgradedTypes paths)
|> String.replace "{{pagesUpgradedValues}}" (pagesUpgradedValues paths)
|> String.replace "{{pagesInit}}" (pagesInit paths)
|> String.replace "{{pagesUpdate}}" (pagesUpdate paths)
|> String.replace "{{pagesBundle}}" (pagesBundle paths)
pagesImports : List Path -> String
pagesImports paths =
paths
|> List.map Path.toModulePath
|> List.map ((++) "import Pages.")
|> String.join "\n"
pagesModels : List Path -> String
pagesModels =
pagesCustomType "Model"
pagesMsgs : List Path -> String
pagesMsgs =
pagesCustomType "Msg"
pagesCustomType : String -> List Path -> String
pagesCustomType name paths =
Utils.customType
{ name = name
, variants =
List.map
(\path ->
Path.toTypeName path
++ "_"
++ name
++ " Pages."
++ Path.toModulePath path
++ "."
++ name
)
paths
}
pagesUpgradedTypes : List Path -> String
pagesUpgradedTypes paths =
paths
|> List.map
(\path ->
let
name =
"Pages." ++ Path.toModulePath path
in
( Path.toVariableName path
, "UpgradedPage "
++ name
++ ".Flags "
++ name
++ ".Model "
++ name
++ ".Msg"
)
)
|> Utils.recordType
|> Utils.indent 1
pagesUpgradedValues : List Path -> String
pagesUpgradedValues paths =
paths
|> List.map
(\path ->
( Path.toVariableName path
, "Pages."
++ Path.toModulePath path
++ ".page |> Spa.upgrade "
++ Path.toTypeName path
++ "_Model "
++ Path.toTypeName path
++ "_Msg"
)
)
|> Utils.recordValue
|> Utils.indent 1
pagesInit : List Path -> String
pagesInit paths =
Utils.function
{ name = "init"
, annotation = [ "Route", "Global.Model", "( Model, Cmd Msg, Cmd Global.Msg )" ]
, inputs = [ "route" ]
, body =
Utils.caseExpression
{ variable = "route"
, cases =
paths
|> List.map
(\path ->
( "Route."
++ Path.toTypeName path
++ (if Path.hasParams path then
" params"
else
""
)
, "pages."
++ Path.toVariableName path
++ ".init"
++ (if Path.hasParams path then
" params"
else
" ()"
)
)
)
}
}
pagesUpdate : List Path -> String
pagesUpdate paths =
Utils.function
{ name = "update"
, annotation = [ "Msg", "Model", "Global.Model", "( Model, Cmd Msg, Cmd Global.Msg )" ]
, inputs = [ "bigMsg bigModel" ]
, body =
Utils.caseExpression
{ variable = "( bigMsg, bigModel )"
, cases =
paths
|> List.map
(\path ->
let
typeName =
Path.toTypeName path
in
( "( "
++ typeName
++ "_Msg msg, "
++ typeName
++ "_Model model )"
, "pages." ++ Path.toVariableName path ++ ".update msg model"
)
)
|> (\cases ->
if List.length paths == 1 then
cases
else
cases ++ [ ( "_", "always ( bigModel, Cmd.none, Cmd.none )" ) ]
)
}
}
pagesBundle : List Path -> String
pagesBundle paths =
Utils.function
{ name = "bundle"
, annotation = [ "Model", "Global.Model", "Spa.Bundle Msg" ]
, inputs = [ "bigModel" ]
, body =
Utils.caseExpression
{ variable = "bigModel"
, cases =
paths
|> List.map
(\path ->
let
typeName =
Path.toTypeName path
in
( typeName ++ "_Model model"
, "pages." ++ Path.toVariableName path ++ ".bundle model"
)
)
}
}
-- bundle : Model -> Global.Model -> Spa.Bundle Msg
-- bundle appModel =
-- case appModel of
-- Top_Model model ->
-- pages.top.bundle model
-- Profile_Model model ->
-- pages.profile.bundle model
-- About_Model model ->
-- pages.about.bundle model
-- Authors_Dynamic_Posts_Dynamic_Model model ->
-- pages.authors_dynamic_posts_dynamic.bundle model
-- Posts_Top_Model model ->
-- pages.posts_top.bundle model
-- Posts_Dynamic_Model model ->
-- pages.posts_dynamic.bundle model
-- NotFound_Model model ->
-- pages.notFound.bundle model

View File

@ -0,0 +1,111 @@
module Generators.Route exposing
( generate
, routeCustomType
, routeParsers
, routeSegments
)
import Path exposing (Path)
import Utils.Generate as Utils
generate : List Path -> String
generate paths =
String.trim """
module Generated.Route exposing
( Route(..)
, fromUrl
, toHref
)
import Url exposing (Url)
import Url.Parser as Parser exposing ((</>), Parser)
{{routeCustomType}}
fromUrl : Url -> Maybe Route
fromUrl =
Parser.parse routes
routes : Parser (Route -> a) a
routes =
Parser.oneOf
{{routeParsers}}
toHref : Route -> String
toHref route =
let
segments : List String
segments =
{{routeSegments}}
in
segments
|> String.join "/"
|> String.append "/"
"""
|> String.replace "{{routeCustomType}}" (routeCustomType paths)
|> String.replace "{{routeParsers}}" (routeParsers paths)
|> String.replace "{{routeSegments}}" (routeSegments paths)
routeCustomType : List Path -> String
routeCustomType paths =
Utils.customType
{ name = "Route"
, variants =
List.map
(\path -> Path.toTypeName path ++ Path.optionalParams path)
paths
}
routeParsers : List Path -> String
routeParsers paths =
paths
|> List.map Path.toParser
|> Utils.list
|> Utils.indent 2
routeSegments : List Path -> String
routeSegments paths =
case paths of
[] ->
""
_ ->
Utils.caseExpression
{ variable = "route"
, cases =
paths
|> List.map
(\path ->
( Path.toTypeName path ++ Path.toParamInputs path
, Path.toParamList path
)
)
}
|> Utils.indent 3
-- case route of
-- Top ->
-- []
-- About ->
-- [ "about" ]
-- Authors_Dynamic_Posts_Dynamic { param1, param2 } ->
-- [ "authors", param1, "posts", param2 ]
-- Posts_Top ->
-- [ "posts" ]
-- Posts_Dynamic { param1 } ->
-- [ "posts", param1 ]
-- Profile ->
-- [ "profile" ]
-- NotFound ->
-- [ "not-found" ]
-- """

33
cli/src/Main.elm Normal file
View File

@ -0,0 +1,33 @@
module Main exposing (main)
import Platform
import Ports
main :
Program
{ command : String
, name : String
, pageType : String
, filepaths : List String
}
()
Never
main =
Platform.worker
{ init =
\{ command, filepaths, name, pageType } ->
( ()
, case command of
"build" ->
Ports.build filepaths
"add" ->
Ports.add { pageType = pageType, name = name }
_ ->
Ports.uhhh ()
)
, update = \_ model -> ( model, Cmd.none )
, subscriptions = \_ -> Sub.none
}

266
cli/src/Path.elm Normal file
View File

@ -0,0 +1,266 @@
module Path exposing
( Path
, fromFilepath
, fromModuleName
, hasParams
, optionalParams
, toFilepath
, toFlags
, toList
, toModulePath
, toParamInputs
, toParamList
, toParser
, toTypeName
, toVariableName
)
import Utils.Generate as Utils
type Path
= Internals (List String)
fromFilepath : String -> Path
fromFilepath filepath =
filepath
|> String.replace ".elm" ""
|> String.split "/"
|> List.filter ((/=) "")
|> Internals
fromModuleName : String -> Path
fromModuleName name =
name
|> String.split "."
|> Internals
toFilepath : Path -> String
toFilepath (Internals list) =
String.join "/" list ++ ".elm"
toList : Path -> List String
toList (Internals list) =
list
toModulePath : Path -> String
toModulePath =
join "."
toTypeName : Path -> String
toTypeName =
join "_"
toVariableName : Path -> String
toVariableName (Internals list) =
let
lowercaseFirstLetter : String -> String
lowercaseFirstLetter str =
String.left 1 (String.toLower str) ++ String.dropLeft 1 str
in
list |> List.map lowercaseFirstLetter |> String.join "_"
optionalParams : Path -> String
optionalParams =
toSingleLineParamRecord
Utils.singleLineRecordType
(\num -> ( "param" ++ String.fromInt num, "String" ))
toFlags : Path -> String
toFlags path =
let
params =
optionalParams path
in
if params == "" then
"()"
else
String.dropLeft 1 params
dynamicCount : Path -> Int
dynamicCount (Internals list) =
list
|> List.filter ((==) "Dynamic")
|> List.length
toParser : Path -> String
toParser (Internals list) =
let
count : Int
count =
dynamicCount (Internals list)
toUrlSegment : String -> String
toUrlSegment piece =
if piece == "Dynamic" then
"Parser.string"
else
"Parser.s \"" ++ sluggify piece ++ "\""
toUrlParser : List String -> String
toUrlParser list_ =
"("
++ (list_ |> stripEndingTop |> List.map toUrlSegment |> String.join " </> ")
++ ")"
toStaticParser : List String -> String
toStaticParser list_ =
"Parser.map "
++ toTypeName (Internals list_)
++ " "
++ toUrlParser list_
toParamMap : Path -> String
toParamMap =
toSingleLineParamRecord
Utils.singleLineRecordValue
(\num -> ( "param" ++ String.fromInt num, "param" ++ String.fromInt num ))
dynamicParamsFn : List String -> String
dynamicParamsFn list_ =
"\\" ++ (List.range 1 count |> List.map (\num -> "param" ++ String.fromInt num) |> String.join " ") ++ " ->" ++ toParamMap (Internals list_)
toDynamicParser : List String -> String
toDynamicParser list_ =
String.join "\n"
[ toUrlParser list_
, " |> Parser.map (" ++ dynamicParamsFn list_ ++ ")"
, " |> Parser.map " ++ toTypeName (Internals list_)
]
in
case list of
[ "Top" ] ->
"Parser.map Top Parser.top"
_ ->
if count > 0 then
toDynamicParser list
else
toStaticParser list
stripEndingTop : List String -> List String
stripEndingTop list_ =
List.reverse list_
|> (\l ->
if List.head l == Just "Top" then
List.drop 1 l
else
l
)
|> List.reverse
sluggify : String -> String
sluggify =
String.toList
>> List.map
(\char ->
if Char.isUpper char then
String.fromList [ ' ', char ]
else
String.fromList [ char ]
)
>> String.concat
>> String.trim
>> String.replace " " "-"
>> String.toLower
-- { param1, param2 }
toParamInputs : Path -> String
toParamInputs path =
let
count =
dynamicCount path
in
if count == 0 then
""
else
" { " ++ (List.range 1 count |> List.map (\num -> "param" ++ String.fromInt num) |> String.join ", ") ++ " }"
-- [ "authors", param1, "posts", param2 ]
toParamList : Path -> String
toParamList (Internals list) =
let
helper : String -> ( List String, Int ) -> ( List String, Int )
helper piece ( names, num ) =
if piece == "Dynamic" then
( names ++ [ "param" ++ String.fromInt num ]
, num + 1
)
else
( names ++ [ "\"" ++ sluggify piece ++ "\"" ]
, num
)
in
list
|> stripEndingTop
|> List.foldl helper ( [], 1 )
|> Tuple.first
|> (\items ->
if List.length items == 0 then
"[]"
else
"[ " ++ String.join ", " items ++ " ]"
)
hasParams : Path -> Bool
hasParams path =
dynamicCount path > 0
-- HELPERS
join : String -> Path -> String
join separator (Internals list) =
String.join separator list
toSingleLineParamRecord :
(List ( String, String ) -> String)
-> (Int -> ( String, String ))
-> Path
-> String
toSingleLineParamRecord toRecord toItem path =
let
count =
dynamicCount path
in
if count == 0 then
""
else
List.range 1 count
|> List.map toItem
|> toRecord
|> String.append " "

72
cli/src/Ports.elm Normal file
View File

@ -0,0 +1,72 @@
port module Ports exposing (add, build, uhhh)
import Add.Component
import Add.Element
import Add.Sandbox
import Add.Static
import Generators.Pages as Pages
import Generators.Route as Route
import Path
port addPort : { filepath : String, content : String } -> Cmd msg
port buildPort : List { filepath : String, content : String } -> Cmd msg
port uhhh : () -> Cmd msg
add : { name : String, pageType : String } -> Cmd msg
add { name, pageType } =
let
path =
Path.fromModuleName name
sendItBro : String -> Cmd msg
sendItBro content =
addPort
{ filepath = Path.toFilepath path
, content = content
}
in
case pageType of
"static" ->
path
|> Add.Static.create
|> sendItBro
"sandbox" ->
path
|> Add.Sandbox.create
|> sendItBro
"element" ->
path
|> Add.Element.create
|> sendItBro
"component" ->
path
|> Add.Component.create
|> sendItBro
_ ->
uhhh ()
build : List String -> Cmd msg
build filepaths =
filepaths
|> List.map Path.fromFilepath
|> (\paths ->
[ { filepath = "Generated/Route.elm"
, content = Route.generate paths
}
, { filepath = "Generated/Pages.elm"
, content = Pages.generate paths
}
]
)
|> buildPort

256
cli/src/Utils/Generate.elm Normal file
View File

@ -0,0 +1,256 @@
module Utils.Generate exposing
( caseExpression
, customType
, function
, import_
, indent
, list
, recordType
, recordValue
, singleLineRecordType
, singleLineRecordValue
, tuple
)
indent : Int -> String -> String
indent count string =
String.lines string
|> List.map (String.append (" " |> List.repeat count |> String.concat))
|> String.join "\n"
customType :
{ name : String
, variants : List String
}
-> String
customType options =
case options.variants of
[] ->
""
first :: [] ->
"type " ++ options.name ++ " = " ++ first
first :: rest ->
multilineIndentedThing
{ header = "type " ++ options.name
, items = { first = first, rest = rest }
, prefixes = { first = "= ", rest = "| " }
, suffix = []
}
module_ :
{ name : String
, exposing_ : List String
}
-> String
module_ options =
case options.exposing_ of
[] ->
"module " ++ options.name
first :: [] ->
"module " ++ options.name ++ " exposing " ++ "(" ++ first ++ ")"
first :: rest ->
multilineIndentedThing
{ header = "module " ++ options.name
, items = { first = first, rest = rest }
, prefixes = { first = "( ", rest = ", " }
, suffix = [ ")" ]
}
singleLineRecordType : List ( String, String ) -> String
singleLineRecordType =
singleLineRecord " : "
singleLineRecordValue : List ( String, String ) -> String
singleLineRecordValue =
singleLineRecord " = "
recordType : List ( String, String ) -> String
recordType =
record (\( key, value ) -> key ++ " : " ++ value)
recordValue : List ( String, String ) -> String
recordValue =
record (\( key, value ) -> key ++ " = " ++ value)
list : List String -> String
list values =
case values of
[] ->
"[]"
first :: [] ->
"[ " ++ first ++ " ]"
first :: rest ->
multilineThing
{ items = { first = first, rest = rest }
, prefixes = { first = "[ ", rest = ", " }
, suffix = [ "]" ]
}
tuple : List String -> String
tuple values =
case values of
[] ->
"()"
first :: [] ->
"( " ++ first ++ " )"
first :: rest ->
multilineThing
{ items = { first = first, rest = rest }
, prefixes = { first = "( ", rest = ", " }
, suffix = [ ")" ]
}
import_ :
{ name : String
, alias : Maybe String
, exposing_ : List String
}
-> String
import_ options =
case ( options.alias, options.exposing_ ) of
( Nothing, [] ) ->
"import " ++ options.name
( Just alias_, [] ) ->
"import " ++ options.name ++ " as " ++ alias_
( Nothing, _ ) ->
"import " ++ options.name ++ " exposing (" ++ String.join ", " options.exposing_ ++ ")"
( Just alias_, _ ) ->
"import " ++ options.name ++ " as " ++ alias_ ++ " exposing (" ++ String.join ", " options.exposing_ ++ ")"
function :
{ name : String
, inputs : List String
, annotation : List String
, body : String
}
-> String
function options =
case options.annotation of
[] ->
""
_ ->
String.join "\n"
[ options.name ++ " : " ++ String.join " -> " options.annotation
, options.name ++ " " ++ List.foldl (\arg str -> str ++ arg ++ " ") "" options.inputs ++ "="
, options.body |> indent 1
]
caseExpression :
{ variable : String
, cases : List ( String, String )
}
-> String
caseExpression options =
let
toBranch : ( String, String ) -> String
toBranch ( value, result ) =
String.join "\n"
[ value ++ " ->"
, indent 1 result
]
in
case options.cases of
[] ->
""
_ ->
[ "case " ++ options.variable ++ " of"
, options.cases
|> List.map toBranch
|> String.join "\n\n"
|> indent 1
]
|> String.join "\n"
-- HELPERS
multilineIndentedThing :
{ header : String
, items : { first : String, rest : List String }
, prefixes : { first : String, rest : String }
, suffix : List String
}
-> String
multilineIndentedThing options =
String.join "\n"
[ options.header
, multilineThing options |> indent 1
]
multilineThing :
{ options
| items : { first : String, rest : List String }
, prefixes : { first : String, rest : String }
, suffix : List String
}
-> String
multilineThing { items, prefixes, suffix } =
[ [ prefixes.first ++ items.first ]
, List.map (String.append prefixes.rest) items.rest
, suffix
]
|> List.concat
|> String.join "\n"
record :
(( String, String ) -> String)
-> List ( String, String )
-> String
record fromTuple properties =
case properties of
[] ->
"{}"
first :: [] ->
"{ " ++ fromTuple first ++ " }"
first :: rest ->
multilineThing
{ items = { first = fromTuple first, rest = List.map fromTuple rest }
, prefixes = { first = "{ ", rest = ", " }
, suffix = [ "}" ]
}
singleLineRecord : String -> List ( String, String ) -> String
singleLineRecord separator properties =
case properties of
[] ->
"{}"
_ ->
"{ "
++ (properties
|> List.map (\( k, v ) -> k ++ separator ++ v)
|> String.join ", "
)
++ " }"

View File

@ -1,85 +0,0 @@
module Add exposing
( PageType
, generate
, modulePathDecoder
, pageTypeDecoder
)
import Json.Decode as D exposing (Decoder)
import Templates.Component
import Templates.Element
import Templates.Sandbox
import Templates.Static
type PageType
= Static
| Sandbox
| Element
| Component
pageTypeDecoder : Decoder PageType
pageTypeDecoder =
D.string
|> D.andThen
(\pageType ->
case pageType of
"static" ->
D.succeed Static
"sandbox" ->
D.succeed Sandbox
"element" ->
D.succeed Element
"component" ->
D.succeed Component
_ ->
D.fail <| "Did not recognize page type: " ++ pageType
)
modulePathDecoder : Decoder (List String)
modulePathDecoder =
let
isValidModuleName : String -> Bool
isValidModuleName name =
String.split "." name
|> List.all
(\str ->
case String.toList str of
[] ->
False
first :: rest ->
Char.isUpper first && List.all Char.isAlpha rest
)
in
D.string
|> D.andThen
(\name ->
if isValidModuleName name then
D.succeed (String.split "." name)
else
D.fail "That module name isn't valid."
)
generate : PageType -> { modulePath : List String, ui : String } -> String
generate pageType =
case pageType of
Static ->
Templates.Static.contents
Sandbox ->
Templates.Sandbox.contents
Element ->
Templates.Element.contents
Component ->
Templates.Component.contents

File diff suppressed because it is too large Load Diff

View File

@ -1,243 +0,0 @@
module Main exposing (main)
import Add
import Dict exposing (Dict)
import File exposing (File)
import Json.Decode as D exposing (Decoder)
import Json.Encode as Json
import Ports
import Set exposing (Set)
import Templates.Layout
import Utils
type alias Flags =
{ command : String
, data : Json.Value
}
type Args
= BuildArgs BuildConfig
| AddArgs AddConfig
type alias BuildConfig =
{ paths : List Filepath
}
type alias AddConfig =
{ ui : String
, pageType : Add.PageType
, modulePath : List String
, layoutPaths : List Filepath
}
argsDecoder : String -> Decoder Args
argsDecoder command =
case command of
"add" ->
D.map AddArgs <|
D.map4 AddConfig
(D.field "ui" D.string)
(D.field "pageType" Add.pageTypeDecoder)
(D.field "moduleName" Add.modulePathDecoder)
(D.field "layoutPaths" (D.list (D.list D.string)))
"build" ->
D.map BuildArgs <|
D.map BuildConfig
(D.list (D.list D.string))
_ ->
D.fail <| "Couldn't recognize command: " ++ command
type alias Filepath =
List String
main : Program Flags () Never
main =
Platform.worker
{ init = \flags -> ( (), handle flags )
, update = \_ model -> ( model, Cmd.none )
, subscriptions = always Sub.none
}
handle : Flags -> Cmd msg
handle flags =
D.decodeValue
(argsDecoder flags.command)
flags.data
|> (\result ->
case result of
Ok args ->
case args of
BuildArgs config ->
build config
AddArgs config ->
add config
Err error ->
case error of
D.Failure reason _ ->
Ports.error reason
_ ->
Cmd.none
)
build : BuildConfig -> Cmd msg
build { paths } =
List.concat
[ [ File [ "Routes" ]
(File.routes
{ paths = Utils.allSubsets paths
, pathsWithFiles = paths
}
)
]
, paths
|> List.foldl groupByFolder Dict.empty
|> Utils.addInMissingFolders
|> toDetails
|> (\items ->
let
itemsWithFiles : List File.Details
itemsWithFiles =
List.filter
(.files >> List.isEmpty >> not)
items
shouldImportParams : String -> Bool
shouldImportParams filepath =
List.member filepath
(List.map .moduleName itemsWithFiles)
in
List.concat
[ List.map File.params itemsWithFiles
, List.map (File.route { shouldImportParams = shouldImportParams }) items
, List.map (File.pages { shouldImportParams = shouldImportParams }) items
]
)
]
|> List.map (\file -> { file | filepath = List.append [ "elm-stuff", ".elm-spa", "Generated" ] file.filepath })
|> Ports.createFiles
add : AddConfig -> Cmd msg
add config =
Ports.createFiles <|
List.concat
[ layoutsToCreate
{ path = config.modulePath
, existingLayouts = config.layoutPaths
}
|> List.map
(\path ->
{ filepath = List.append [ "src", "Layouts" ] path
, contents = Templates.Layout.contents { ui = config.ui, modulePath = path }
}
)
, [ { filepath = List.append [ "src", "Pages" ] config.modulePath
, contents =
Add.generate
config.pageType
{ modulePath = config.modulePath
, ui = config.ui
}
}
]
]
-- UTILS
layoutsToCreate : { path : Filepath, existingLayouts : List Filepath } -> List Filepath
layoutsToCreate { path, existingLayouts } =
let
subLists : List a -> List (List a)
subLists list =
list
|> List.repeat (List.length list - 1)
|> List.indexedMap (\i -> List.take (1 + i))
in
subLists path
|> List.filter (\list -> not (List.member list existingLayouts))
dropLast : List a -> List a
dropLast =
List.reverse
>> List.drop 1
>> List.reverse
fileWithin : Filepath -> String
fileWithin =
dropLast
>> String.join "."
folderWithin : Filepath -> String
folderWithin =
List.reverse
>> List.drop 2
>> List.reverse
>> String.join "."
type alias Items =
{ files : Set Filepath
, folders : Set Filepath
}
groupByFolder :
Filepath
-> Dict String Items
-> Dict String Items
groupByFolder filepath =
Dict.update
(fileWithin filepath)
(Maybe.map
(\entry -> { entry | files = Set.insert filepath entry.files })
>> Maybe.withDefault
{ files = Set.singleton filepath
, folders = Set.empty
}
>> Just
)
>> Dict.update
(folderWithin filepath)
(Maybe.map
(\entry -> { entry | folders = Set.insert (dropLast filepath) entry.folders })
>> Maybe.withDefault
{ folders = Set.singleton (dropLast filepath)
, files = Set.empty
}
>> (\entry -> { entry | folders = Set.filter ((/=) []) entry.folders })
>> Just
)
toDetails :
Dict String Items
-> List File.Details
toDetails dict =
Dict.toList dict
|> List.map
(\( moduleName, { files, folders } ) ->
{ moduleName = moduleName
, folders = Set.toList folders
, files = Set.toList files
}
)

View File

@ -1,30 +0,0 @@
port module Ports exposing
( createFiles
, error
)
import File exposing (File)
import Json.Encode as Json
port outgoing :
{ message : String
, data : Json.Value
}
-> Cmd msg
createFiles : List File -> Cmd msg
createFiles files =
outgoing
{ message = "createFiles"
, data = Json.list File.encode files
}
error : String -> Cmd msg
error reason =
outgoing
{ message = "error"
, data = Json.string reason
}

View File

@ -1,81 +0,0 @@
module Templates.Component exposing (contents)
contents : { modulePath : List String, ui : String } -> String
contents options =
"""
module Pages.{{moduleName}} exposing (Model, Msg, page)
import Spa.Page
import {{ui}} exposing (..)
import Generated{{moduleFolder}}.Params as Params
import Global
import Utils.Spa exposing (Page)
page : Page Params.{{fileName}} Model Msg model msg appMsg
page =
Spa.Page.component
{ title = always "{{moduleName}}"
, init = always init
, update = always update
, subscriptions = always subscriptions
, view = always view
}
-- INIT
type alias Model =
{}
init : Params.{{fileName}} -> ( Model, Cmd Msg, Cmd Global.Msg )
init _ =
( {}
, Cmd.none
, Cmd.none
)
-- UPDATE
type Msg
= Msg
update : Msg -> Model -> ( Model, Cmd Msg, Cmd Global.Msg )
update msg model =
( model
, Cmd.none
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view : Model -> {{ui}} Msg
view model =
text "{{moduleName}}"
"""
|> String.replace "{{moduleName}}" (String.join "." options.modulePath)
|> String.replace "{{fileName}}" (options.modulePath |> List.reverse |> List.head |> Maybe.withDefault "YellAtRyanOnTheInternet")
|> String.replace "{{moduleFolder}}" (options.modulePath |> List.reverse |> List.drop 1 |> List.reverse |> List.map (String.append ".") |> String.concat)
|> String.replace "{{ui}}" options.ui
|> String.trim

View File

@ -1,79 +0,0 @@
module Templates.Element exposing (contents)
contents : { modulePath : List String, ui : String } -> String
contents options =
"""
module Pages.{{moduleName}} exposing (Model, Msg, page)
import Spa.Page
import {{ui}} exposing (..)
import Generated{{moduleFolder}}.Params as Params
import Utils.Spa exposing (Page)
page : Page Params.{{fileName}} Model Msg model msg appMsg
page =
Spa.Page.element
{ title = always "{{moduleName}}"
, init = always init
, update = always update
, subscriptions = always subscriptions
, view = always view
}
-- INIT
type alias Model =
{}
init : Params.{{fileName}} -> ( Model, Cmd Msg )
init _ =
( {}
, Cmd.none
)
-- UPDATE
type Msg
= Msg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
( model
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view : Model -> {{ui}} Msg
view model =
text "{{moduleName}}"
"""
|> String.replace "{{moduleName}}" (String.join "." options.modulePath)
|> String.replace "{{fileName}}" (options.modulePath |> List.reverse |> List.head |> Maybe.withDefault "YellAtRyanOnTheInternet")
|> String.replace "{{moduleFolder}}" (options.modulePath |> List.reverse |> List.drop 1 |> List.reverse |> List.map (String.append ".") |> String.concat)
|> String.replace "{{ui}}" options.ui
|> String.trim

View File

@ -1,21 +0,0 @@
module Templates.Layout exposing (contents)
contents : { modulePath : List String, ui : String } -> String
contents options =
"""
module Layouts.{{moduleName}} exposing (view)
import {{ui}} exposing (..)
import Utils.Spa as Spa
view : Spa.LayoutContext msg -> {{ui}} msg
view { page } =
page
"""
|> String.replace "{{moduleName}}" (String.join "." options.modulePath)
|> String.replace "{{ui}}" options.ui
|> String.trim

View File

@ -1,64 +0,0 @@
module Templates.Sandbox exposing (contents)
contents : { modulePath : List String, ui : String } -> String
contents options =
"""
module Pages.{{moduleName}} exposing (Model, Msg, page)
import Spa.Page
import {{ui}} exposing (..)
import Generated{{moduleFolder}}.Params as Params
import Utils.Spa exposing (Page)
page : Page Params.{{fileName}} Model Msg model msg appMsg
page =
Spa.Page.sandbox
{ title = always "{{moduleName}}"
, init = always init
, update = always update
, view = always view
}
-- INIT
type alias Model =
{}
init : Params.{{fileName}} -> Model
init _ =
{}
-- UPDATE
type Msg
= Msg
update : Msg -> Model -> Model
update msg model =
model
-- VIEW
view : Model -> {{ui}} Msg
view model =
text "{{moduleName}}"
"""
|> String.replace "{{moduleName}}" (String.join "." options.modulePath)
|> String.replace "{{fileName}}" (options.modulePath |> List.reverse |> List.head |> Maybe.withDefault "YellAtRyanOnTheInternet")
|> String.replace "{{moduleFolder}}" (options.modulePath |> List.reverse |> List.drop 1 |> List.reverse |> List.map (String.append ".") |> String.concat)
|> String.replace "{{ui}}" options.ui
|> String.trim

View File

@ -1,44 +0,0 @@
module Templates.Static exposing (contents)
contents : { modulePath : List String, ui : String } -> String
contents options =
"""
module Pages.{{moduleName}} exposing (Model, Msg, page)
import Spa.Page
import {{ui}} exposing (..)
import Generated{{moduleFolder}}.Params as Params
import Utils.Spa exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Params.{{fileName}} Model Msg model msg appMsg
page =
Spa.Page.static
{ title = always "{{moduleName}}"
, view = always view
}
-- VIEW
view : {{ui}} Msg
view =
text "{{moduleName}}"
"""
|> String.replace "{{moduleName}}" (String.join "." options.modulePath)
|> String.replace "{{fileName}}" (options.modulePath |> List.reverse |> List.head |> Maybe.withDefault "YellAtRyanOnTheInternet")
|> String.replace "{{moduleFolder}}" (options.modulePath |> List.reverse |> List.drop 1 |> List.reverse |> List.map (String.append ".") |> String.concat)
|> String.replace "{{ui}}" options.ui
|> String.trim

View File

@ -1,62 +0,0 @@
module Utils exposing
( Items
, addInMissingFolders
, allSubsets
)
import Dict exposing (Dict)
import List.Extra
import Set exposing (Set)
type alias Filepath =
List String
type alias Items =
{ files : Set Filepath
, folders : Set Filepath
}
allSubsets : List (List comparable) -> List (List comparable)
allSubsets =
List.concatMap
(\list -> List.indexedMap (\i _ -> List.take (i + 1) list) list)
>> List.Extra.unique
addInMissingFolders : Dict String Items -> Dict String Items
addInMissingFolders dict =
let
keys : List String
keys =
Dict.keys dict
|> List.map (String.split ".")
|> allSubsets
|> List.map (String.join ".")
splitOnDot : String -> Filepath
splitOnDot str =
if String.isEmpty str then
[]
else
String.split "." str
oneLongerThan : Filepath -> Set Filepath
oneLongerThan filepath =
keys
|> List.map splitOnDot
|> List.filter (\dictFilepath -> List.length dictFilepath == List.length filepath + 1)
|> Set.fromList
in
keys
|> List.map
(\key ->
Dict.get key dict
|> Maybe.withDefault (Items Set.empty Set.empty)
|> (\items -> { items | folders = Set.union items.folders (oneLongerThan (splitOnDot key)) })
|> Tuple.pair key
)
|> Dict.fromList

View File

@ -1,138 +0,0 @@
#!/usr/bin/env node
const package = require('../package.json')
const path = require('path')
const cwd = process.cwd()
const { File, Elm, bold } = require('./utils.js')
const main = ([ command, ...args ] = []) =>
commands[command]
? commands[command](args)
: commands.help(args)
// elm-spa init
const init = (args) => {
const parseInitArgs = (args) => {
const flagPrefix = '--'
const isFlag = arg => arg.indexOf(flagPrefix) == 0
const flags =
args.filter(isFlag)
.reduce((obj, arg) => {
const [ key, value ] = arg.split('=')
obj[key.substring(flagPrefix.length)] = value
return obj
}, {})
const [ relative = '.' ] = args.filter(arg => !isFlag(arg))
return { ui: flags.ui || 'Element', relative }
}
const { ui, relative } = parseInitArgs(args)
return Promise.resolve(
(ui === 'Element')
? File.cp(path.join(__dirname, '..', 'initial-projects', 'elm-ui'), path.join(cwd, relative))
: File.cp(path.join(__dirname, '..', 'initial-projects', 'html'), path.join(cwd, relative))
)
.then(_ => console.info(`
${bold('elm-spa')} created a new project in:
${path.join(cwd, relative)}
run these commands to get started:
${bold('cd ' + path.join(cwd, relative))}
${bold('npm start')}
`))
.catch(console.error)
}
// elm-spa add
const add = args =>
Elm.friendlyAddMessages(args)
.then(_ => {
const [ pageType, moduleName, relative = '.' ] = args
const dir = path.join(cwd, relative)
return Elm.checkForElmSpaJson(dir)
.then(config =>
File.paths(path.join(dir, 'src', 'Layouts'))
.then(layoutPaths => Elm.run('add', { relative })({
pageType,
moduleName,
layoutPaths,
ui: config.ui
}))
.then(Elm.formatOutput)
)
.then(str => `\n${str}\n`)
})
.then(console.info)
.catch(console.error)
// elm-spa build
const build = ([ relative = '.' ]) => {
const dir = path.join(cwd, relative)
return Elm.checkForElmSpaJson(dir)
.then(json =>
File.paths(path.join(dir, 'src', 'Pages'))
.then(Elm.run('build', { relative }, json['elm-spa']))
.then(Elm.formatOutput)
)
.then(str => `\n${str}\n`)
.then(console.info)
.catch(console.error)
}
const version =
`${bold('elm-spa')} ${package.version}`
// elm-spa help
const help = () => console.info(`
${version}
usage: ${bold('elm-spa')} <command> [...]
commands:
${bold('init')} [options] <path> create a new project at <path>
options:
${bold('--ui=')}<module> the ui module your \`view\` uses
(default: Element)
examples:
${bold('elm-spa init your-project')}
${bold('elm-spa init --ui=Html your-project')}
${bold('build')} <path> generate pages and routes
examples:
${bold('elm-spa build .')}
${bold('add')} static <module> create a new static page
sandbox <module> create a new sandbox page
element <module> create a new element page
component <module> create a new component page
examples:
${bold('elm-spa add static AboutUs')}
${bold('elm-spa add element Settings.Index')}
${bold('help')} print this help screen
examples:
${bold('elm-spa help')}
${bold('elm-spa wat')}
`)
const commands = {
init,
add,
build,
help,
'-v': _ => console.info(version)
}
main(process.argv.slice(2))

View File

@ -1,233 +0,0 @@
const path = require('path')
const fs = require('fs')
const cwd = process.cwd()
const File = (_ => {
const mkdir = (filepath) =>
new Promise((resolve, reject) =>
fs.mkdir(filepath, { recursive: true },
(err) => err ? reject(err) : resolve(filepath)
)
)
const read = (filepath) =>
new Promise((resolve, reject) =>
fs.readFile(filepath, (err, data) =>
err ? reject(err) : resolve(data.toString('utf8'))
)
)
const cp = (src, dest) => {
const exists = fs.existsSync(src)
const stats = exists && fs.statSync(src)
if (stats && stats.isDirectory()) {
fs.mkdirSync(dest)
fs.readdirSync(src).forEach(child =>
cp(path.join(src, child), path.join(dest, child))
)
} else {
fs.copyFileSync(src, dest)
}
}
const deleteFolderRecursive = (filepath) => {
if (fs.existsSync(filepath)) {
fs.readdirSync(filepath).forEach(file => {
const current = path.join(filepath, file)
if (fs.lstatSync(current).isDirectory()) {
deleteFolderRecursive(current)
} else {
fs.unlinkSync(current)
}
})
fs.rmdirSync(filepath)
}
}
const rmdir = (folder) => new Promise((resolve, reject) => {
try {
deleteFolderRecursive(folder)
resolve()
} catch (err) {
reject(err)
}
})
const create = (filepath, contents) => {
const folderOf = (path_) =>
path_.split(path.sep).slice(0, -1).join(path.sep)
const write = (filepath, contents) =>
new Promise((resolve, reject) =>
fs.writeFile(filepath, contents, { encoding: 'utf8' }, (err) =>
err ? reject(err) : resolve(filepath)
)
)
return fs.existsSync(folderOf(filepath))
? write(filepath, contents)
: mkdir(folderOf(filepath)).then(_ => write(filepath, contents))
}
const paths = (filepath) => {
const ls = (filepath) =>
new Promise((resolve, reject) =>
fs.readdir(filepath, (err, files) =>
err ? reject(err) : resolve(files))
)
const reduce = (fn, init) => (items) =>
items.reduce(fn, init)
const concat = (a, b) =>
a.concat(b)
const all = (fn) => (items) =>
Promise.all(items.map(fn))
const isFile = (name) =>
name.indexOf('.') > -1
const toPath = (filepath) => (name) =>
isFile(name)
? Promise.resolve(
name.split('.')[1] == 'elm'
? [[ name.split('.')[0] ]]
: []
)
: paths(path.join(filepath, name))
.then(files => files.map(file => [ name ].concat(file)))
return ls(filepath)
.then(all(toPath(filepath)))
.then(reduce(concat, []))
}
return {
read,
paths,
mkdir,
rmdir,
cp,
create
}
})()
const Elm = (_ => {
const { Elm } = require('../dist/elm.compiled.js')
const handlers = {
error: (_, message) =>
Promise.reject(message),
createFiles: ({ relative }, files) =>
File.rmdir(path.join(cwd, relative, 'elm-stuff', '.elm-spa', 'Generated'))
.then(_ =>
Promise.all(
files
.map(item => ({
...item,
filepath: path.join(cwd, relative, ...item.filepath) + '.elm'
}))
.map(item => File.create(item.filepath, item.contents))
)
)
}
const run = (command, args) => (data) =>
new Promise((resolve, reject) => {
const app = Elm.Main.init({ flags: { command, data } })
app.ports.outgoing.subscribe(({ message, data }) =>
handlers[message]
? Promise.resolve(handlers[message](args, data))
.then(resolve)
.catch(reject)
: reject(`Didn't recognize message "${message}" yell at @ryannhg on the internet!\n`)
)
})
const checkForElmSpaJson = (paths) =>
new Promise((resolve, reject) =>
fs.readFile(path.join(paths, 'elm-spa.json'), (_, contents) =>
contents
? Promise.resolve(contents.toString())
.then(JSON.parse)
.then(resolve)
.catch(_ => `Couldn't understand the ${bold('elm-spa.json')} file at:\n${paths}`)
: reject(`Couldn't find an ${bold('elm-spa.json')} file at:\n${paths}`)
)
)
const alphabetically = (a, b) =>
(a < b) ? -1 : (a > b) ? 1 : 0
const formatOutput = files => [
bold('elm-spa') + ` created ${bold(files.length)} file${files.length === 1 ? '' : 's'}:`,
files.sort(alphabetically).map(file => ' ' + file).join('\n'),
].join('\n\n')
const friendlyAddMessages = (args = []) => {
const [ page, moduleName, relative = '.' ] = args
const expectedFiles = [
path.join(cwd, relative, 'elm-spa.json'),
path.join(cwd, relative, 'src', 'Layouts')
]
if (expectedFiles.some(file => !fs.existsSync(file))) {
return Promise.reject(`\n I don't see an elm-spa project here...\n\n Please run this command in the directory with your ${bold('elm-spa.json')}\n`)
}
const isValidPage = {
'static': true,
'sandbox': true,
'element': true,
'component': true
}
const isValidModuleName = (name = '') => {
const isAlphaOnly = word => word.match(/[A-Z|a-z]+/)[0] === word
const isCapitalized = word => word[0].toUpperCase() === word[0]
return name &&
name.length &&
name.split('.').every(word => isAlphaOnly(word) && isCapitalized(word))
}
const messages = {
invalidPage: ({ page, name }) => `
${bold(page)} is not a valid page.
Try one of these?
${bold(Object.keys(isValidPage).map(page => `elm-spa add ${page} ${name}`).join('\n '))}
`,
invalidModuleName: ({ page, name }) => `
${bold(name)} doesn't look like an Elm module.
Here are some examples of what I'm expecting:
${bold(`elm-spa add ${page} Example`)}
${bold(`elm-spa add ${page} Settings.User`)}
`
}
if (isValidPage[page] !== true) {
return Promise.reject(messages.invalidPage({
page,
name: isValidModuleName(moduleName) ? moduleName : 'Example'
}))
} else if (isValidModuleName(moduleName) === false) {
return Promise.reject(messages.invalidModuleName({ page, name: moduleName }))
} else {
return Promise.resolve(args)
}
}
return { run, checkForElmSpaJson, formatOutput, friendlyAddMessages }
})()
const bold = str => '\033[1m' + str + '\033[0m'
module.exports = {
Elm,
File,
bold
}

View File

@ -0,0 +1,233 @@
module Tests.Generators.Pages exposing (suite)
import Expect exposing (Expectation)
import Generators.Pages as Pages
import Path exposing (Path)
import Test exposing (..)
paths :
{ empty : List Path
, single : List Path
, multiple : List Path
}
paths =
{ empty = []
, single =
[ Path.fromFilepath "Top.elm"
]
, multiple =
[ Path.fromFilepath "Top.elm"
, Path.fromFilepath "About.elm"
, Path.fromFilepath "NotFound.elm"
, Path.fromFilepath "Posts/Top.elm"
, Path.fromFilepath "Posts/Dynamic.elm"
, Path.fromFilepath "Authors/Dynamic/Posts/Dynamic.elm"
]
}
suite : Test
suite =
describe "Generators.Pages"
[ describe "pagesImports"
[ test "returns empty string when no paths" <|
\_ ->
paths.empty
|> Pages.pagesImports
|> Expect.equal ""
, test "returns single import for single path" <|
\_ ->
paths.single
|> Pages.pagesImports
|> Expect.equal "import Pages.Top"
, test "returns multiple import for multiple path" <|
\_ ->
paths.multiple
|> Pages.pagesImports
|> Expect.equal (String.trim """
import Pages.Top
import Pages.About
import Pages.NotFound
import Pages.Posts.Top
import Pages.Posts.Dynamic
import Pages.Authors.Dynamic.Posts.Dynamic
""")
, describe "pagesCustomType"
[ test "works with single path" <|
\_ ->
paths.single
|> Pages.pagesCustomType "Model"
|> Expect.equal "type Model = Top_Model Pages.Top.Model"
, test "works with multiple paths" <|
\_ ->
paths.multiple
|> Pages.pagesCustomType "Model"
|> Expect.equal (String.trim """
type Model
= Top_Model Pages.Top.Model
| About_Model Pages.About.Model
| NotFound_Model Pages.NotFound.Model
| Posts_Top_Model Pages.Posts.Top.Model
| Posts_Dynamic_Model Pages.Posts.Dynamic.Model
| Authors_Dynamic_Posts_Dynamic_Model Pages.Authors.Dynamic.Posts.Dynamic.Model
""")
]
, describe "pagesUpgradedTypes"
[ test "works with single path" <|
\_ ->
paths.single
|> Pages.pagesUpgradedTypes
|> Expect.equal " { top : UpgradedPage Pages.Top.Flags Pages.Top.Model Pages.Top.Msg }"
, test "works with multiple paths" <|
\_ ->
paths.multiple
|> Pages.pagesUpgradedTypes
|> Expect.equal """ { top : UpgradedPage Pages.Top.Flags Pages.Top.Model Pages.Top.Msg
, about : UpgradedPage Pages.About.Flags Pages.About.Model Pages.About.Msg
, notFound : UpgradedPage Pages.NotFound.Flags Pages.NotFound.Model Pages.NotFound.Msg
, posts_top : UpgradedPage Pages.Posts.Top.Flags Pages.Posts.Top.Model Pages.Posts.Top.Msg
, posts_dynamic : UpgradedPage Pages.Posts.Dynamic.Flags Pages.Posts.Dynamic.Model Pages.Posts.Dynamic.Msg
, authors_dynamic_posts_dynamic : UpgradedPage Pages.Authors.Dynamic.Posts.Dynamic.Flags Pages.Authors.Dynamic.Posts.Dynamic.Model Pages.Authors.Dynamic.Posts.Dynamic.Msg
}"""
]
, describe "pagesUpgradedValues"
[ test "works with single path" <|
\_ ->
paths.single
|> Pages.pagesUpgradedValues
|> Expect.equal " { top = Pages.Top.page |> Spa.upgrade Top_Model Top_Msg }"
, test "works with multiple paths" <|
\_ ->
paths.multiple
|> Pages.pagesUpgradedValues
|> Expect.equal """ { top = Pages.Top.page |> Spa.upgrade Top_Model Top_Msg
, about = Pages.About.page |> Spa.upgrade About_Model About_Msg
, notFound = Pages.NotFound.page |> Spa.upgrade NotFound_Model NotFound_Msg
, posts_top = Pages.Posts.Top.page |> Spa.upgrade Posts_Top_Model Posts_Top_Msg
, posts_dynamic = Pages.Posts.Dynamic.page |> Spa.upgrade Posts_Dynamic_Model Posts_Dynamic_Msg
, authors_dynamic_posts_dynamic = Pages.Authors.Dynamic.Posts.Dynamic.page |> Spa.upgrade Authors_Dynamic_Posts_Dynamic_Model Authors_Dynamic_Posts_Dynamic_Msg
}"""
]
, describe "pagesInit"
[ test "works with single path" <|
\_ ->
paths.single
|> Pages.pagesInit
|> Expect.equal (String.trim """
init : Route -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
init route =
case route of
Route.Top ->
pages.top.init ()
""")
, test "works with multiple path" <|
\_ ->
paths.multiple
|> Pages.pagesInit
|> Expect.equal (String.trim """
init : Route -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
init route =
case route of
Route.Top ->
pages.top.init ()
Route.About ->
pages.about.init ()
Route.NotFound ->
pages.notFound.init ()
Route.Posts_Top ->
pages.posts_top.init ()
Route.Posts_Dynamic params ->
pages.posts_dynamic.init params
Route.Authors_Dynamic_Posts_Dynamic params ->
pages.authors_dynamic_posts_dynamic.init params
""")
]
, describe "pagesUpdate" <|
[ test "works with single path" <|
\_ ->
paths.single
|> Pages.pagesUpdate
|> Expect.equal (String.trim """
update : Msg -> Model -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
update bigMsg bigModel =
case ( bigMsg, bigModel ) of
( Top_Msg msg, Top_Model model ) ->
pages.top.update msg model
""")
, test "works with multiple paths" <|
\_ ->
paths.multiple
|> Pages.pagesUpdate
|> Expect.equal (String.trim """
update : Msg -> Model -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
update bigMsg bigModel =
case ( bigMsg, bigModel ) of
( Top_Msg msg, Top_Model model ) ->
pages.top.update msg model
( About_Msg msg, About_Model model ) ->
pages.about.update msg model
( NotFound_Msg msg, NotFound_Model model ) ->
pages.notFound.update msg model
( Posts_Top_Msg msg, Posts_Top_Model model ) ->
pages.posts_top.update msg model
( Posts_Dynamic_Msg msg, Posts_Dynamic_Model model ) ->
pages.posts_dynamic.update msg model
( Authors_Dynamic_Posts_Dynamic_Msg msg, Authors_Dynamic_Posts_Dynamic_Model model ) ->
pages.authors_dynamic_posts_dynamic.update msg model
_ ->
always ( bigModel, Cmd.none, Cmd.none )
""")
]
, describe "pagesBundle" <|
[ test "works with single path" <|
\_ ->
paths.single
|> Pages.pagesBundle
|> Expect.equal (String.trim """
bundle : Model -> Global.Model -> Spa.Bundle Msg
bundle bigModel =
case bigModel of
Top_Model model ->
pages.top.bundle model
""")
, test "works with multiple paths" <|
\_ ->
paths.multiple
|> Pages.pagesBundle
|> Expect.equal (String.trim """
bundle : Model -> Global.Model -> Spa.Bundle Msg
bundle bigModel =
case bigModel of
Top_Model model ->
pages.top.bundle model
About_Model model ->
pages.about.bundle model
NotFound_Model model ->
pages.notFound.bundle model
Posts_Top_Model model ->
pages.posts_top.bundle model
Posts_Dynamic_Model model ->
pages.posts_dynamic.bundle model
Authors_Dynamic_Posts_Dynamic_Model model ->
pages.authors_dynamic_posts_dynamic.bundle model
""")
]
]
]

View File

@ -0,0 +1,121 @@
module Tests.Generators.Route exposing (suite)
import Expect exposing (Expectation)
import Generators.Route as Route
import Path exposing (Path)
import Test exposing (..)
paths :
{ empty : List Path
, single : List Path
, multiple : List Path
}
paths =
{ empty = []
, single =
[ Path.fromFilepath "Top.elm"
]
, multiple =
[ Path.fromFilepath "Top.elm"
, Path.fromFilepath "About.elm"
, Path.fromFilepath "NotFound.elm"
, Path.fromFilepath "Posts/Top.elm"
, Path.fromFilepath "Posts/Dynamic.elm"
, Path.fromFilepath "Authors/Dynamic/Posts/Dynamic.elm"
]
}
suite : Test
suite =
describe "Generators.Route"
[ describe "routeCustomType"
[ test "returns empty for missing variants" <|
\_ ->
paths.empty
|> Route.routeCustomType
|> Expect.equal ""
, test "handles single path" <|
\_ ->
paths.single
|> Route.routeCustomType
|> Expect.equal "type Route = Top"
, test "handles multiple paths" <|
\_ ->
paths.multiple
|> Route.routeCustomType
|> Expect.equal (String.trim """
type Route
= Top
| About
| NotFound
| Posts_Top
| Posts_Dynamic { param1 : String }
| Authors_Dynamic_Posts_Dynamic { param1 : String, param2 : String }
""")
]
, describe "routeParsers"
[ test "handles empty path" <|
\_ ->
paths.empty
|> Route.routeParsers
|> Expect.equal " []"
, test "handles single path" <|
\_ ->
paths.single
|> Route.routeParsers
|> Expect.equal " [ Parser.map Top Parser.top ]"
, test "handles multiple paths" <|
\_ ->
paths.multiple
|> Route.routeParsers
|> Expect.equal """ [ Parser.map Top Parser.top
, Parser.map About (Parser.s "about")
, Parser.map NotFound (Parser.s "not-found")
, Parser.map Posts_Top (Parser.s "posts")
, (Parser.s "posts" </> Parser.string)
|> Parser.map (\\param1 -> { param1 = param1 })
|> Parser.map Posts_Dynamic
, (Parser.s "authors" </> Parser.string </> Parser.s "posts" </> Parser.string)
|> Parser.map (\\param1 param2 -> { param1 = param1, param2 = param2 })
|> Parser.map Authors_Dynamic_Posts_Dynamic
]"""
]
, describe "routeSegments"
[ test "handles empty path" <|
\_ ->
paths.empty
|> Route.routeSegments
|> Expect.equal ""
, test "handles single path" <|
\_ ->
paths.single
|> Route.routeSegments
|> Expect.equal """ case route of
Top ->
[]"""
, test "handles multiple paths" <|
\_ ->
paths.multiple
|> Route.routeSegments
|> Expect.equal """ case route of
Top ->
[]
About ->
[ "about" ]
NotFound ->
[ "not-found" ]
Posts_Top ->
[ "posts" ]
Posts_Dynamic { param1 } ->
[ "posts", param1 ]
Authors_Dynamic_Posts_Dynamic { param1, param2 } ->
[ "authors", param1, "posts", param2 ]"""
]
]

183
cli/tests/Tests/Path.elm Normal file
View File

@ -0,0 +1,183 @@
module Tests.Path exposing (suite)
import Expect exposing (Expectation)
import Path exposing (Path)
import Test exposing (..)
suite : Test
suite =
describe "Path"
[ describe "fromFilepath"
[ test "ignores first slash" <|
\_ ->
"/Top.elm"
|> Path.fromFilepath
|> Path.toList
|> Expect.equalLists [ "Top" ]
, test "works without folders" <|
\_ ->
"Top.elm"
|> Path.fromFilepath
|> Path.toList
|> Expect.equalLists [ "Top" ]
, test "works with a single folder" <|
\_ ->
"Posts/Top.elm"
|> Path.fromFilepath
|> Path.toList
|> Expect.equalLists [ "Posts", "Top" ]
, test "works with nested folders" <|
\_ ->
"Authors/Dynamic/Posts/Dynamic.elm"
|> Path.fromFilepath
|> Path.toList
|> Expect.equalLists [ "Authors", "Dynamic", "Posts", "Dynamic" ]
]
, describe "toModulePath"
[ test "works without folders" <|
\_ ->
"Top.elm"
|> Path.fromFilepath
|> Path.toModulePath
|> Expect.equal "Top"
, test "works with a single folder" <|
\_ ->
"Posts/Top.elm"
|> Path.fromFilepath
|> Path.toModulePath
|> Expect.equal "Posts.Top"
, test "works with nested folders" <|
\_ ->
"Authors/Dynamic/Posts/Dynamic.elm"
|> Path.fromFilepath
|> Path.toModulePath
|> Expect.equal "Authors.Dynamic.Posts.Dynamic"
]
, describe "toVariableName"
[ test "works without folders" <|
\_ ->
"Top.elm"
|> Path.fromFilepath
|> Path.toVariableName
|> Expect.equal "top"
, test "works correctly with capital letters" <|
\_ ->
"NotFound.elm"
|> Path.fromFilepath
|> Path.toVariableName
|> Expect.equal "notFound"
, test "works with a single folder" <|
\_ ->
"Posts/Top.elm"
|> Path.fromFilepath
|> Path.toVariableName
|> Expect.equal "posts_top"
, test "works with nested folders" <|
\_ ->
"Authors/Dynamic/Posts/Dynamic.elm"
|> Path.fromFilepath
|> Path.toVariableName
|> Expect.equal "authors_dynamic_posts_dynamic"
]
, describe "toTypeName"
[ test "works without folders" <|
\_ ->
"Top.elm"
|> Path.fromFilepath
|> Path.toTypeName
|> Expect.equal "Top"
, test "works with a single folder" <|
\_ ->
"Posts/Top.elm"
|> Path.fromFilepath
|> Path.toTypeName
|> Expect.equal "Posts_Top"
, test "works with nested folders" <|
\_ ->
"Authors/Dynamic/Posts/Dynamic.elm"
|> Path.fromFilepath
|> Path.toTypeName
|> Expect.equal "Authors_Dynamic_Posts_Dynamic"
]
, describe "optionalParams"
[ test "works without folders" <|
\_ ->
"Top.elm"
|> Path.fromFilepath
|> Path.optionalParams
|> Expect.equal ""
, test "works with a single folder" <|
\_ ->
"Posts/Dynamic.elm"
|> Path.fromFilepath
|> Path.optionalParams
|> Expect.equal " { param1 : String }"
, test "works with nested folders" <|
\_ ->
"Authors/Dynamic/Posts/Dynamic.elm"
|> Path.fromFilepath
|> Path.optionalParams
|> Expect.equal " { param1 : String, param2 : String }"
]
, describe "toParser"
[ test "works with top" <|
\_ ->
"Top.elm"
|> Path.fromFilepath
|> Path.toParser
|> Expect.equal "Parser.map Top Parser.top"
, test "works with single static path" <|
\_ ->
"About.elm"
|> Path.fromFilepath
|> Path.toParser
|> Expect.equal "Parser.map About (Parser.s \"about\")"
, test "works with multiple static paths" <|
\_ ->
"About/Team.elm"
|> Path.fromFilepath
|> Path.toParser
|> Expect.equal "Parser.map About_Team (Parser.s \"about\" </> Parser.s \"team\")"
, test "works with single dynamic path" <|
\_ ->
"Posts/Dynamic.elm"
|> Path.fromFilepath
|> Path.toParser
|> Expect.equal (String.trim """
(Parser.s "posts" </> Parser.string)
|> Parser.map (\\param1 -> { param1 = param1 })
|> Parser.map Posts_Dynamic
""")
, test "works with multiple dynamic paths" <|
\_ ->
"Authors/Dynamic/Posts/Dynamic.elm"
|> Path.fromFilepath
|> Path.toParser
|> Expect.equal (String.trim """
(Parser.s "authors" </> Parser.string </> Parser.s "posts" </> Parser.string)
|> Parser.map (\\param1 param2 -> { param1 = param1, param2 = param2 })
|> Parser.map Authors_Dynamic_Posts_Dynamic
""")
, describe "toFlags"
[ test "works with no dynamic params" <|
\_ ->
"Top.elm"
|> Path.fromFilepath
|> Path.toFlags
|> Expect.equal "()"
, test "works with one dynamic param" <|
\_ ->
"Posts/Dynamic.elm"
|> Path.fromFilepath
|> Path.toFlags
|> Expect.equal "{ param1 : String }"
, test "works with multiple dynamic params" <|
\_ ->
"Authors/Dynamic/Posts/Dynamic.elm"
|> Path.fromFilepath
|> Path.toFlags
|> Expect.equal "{ param1 : String, param2 : String }"
]
]
]

View File

@ -0,0 +1,274 @@
module Tests.Utils.Generate exposing (suite)
import Expect exposing (Expectation)
import Test exposing (..)
import Utils.Generate as Generate
suite : Test
suite =
describe "Utils.Generate"
[ describe "indent"
[ test "indents with four spaces" <|
\_ ->
"abc"
|> Generate.indent 1
|> Expect.equal " abc"
, test "supports indenting twice" <|
\_ ->
"abc"
|> Generate.indent 2
|> Expect.equal " abc"
, test "works with multiple lines" <|
\_ ->
"abc\ndef"
|> Generate.indent 1
|> Expect.equal " abc\n def"
]
, describe "customType"
[ test "returns empty string with no variants" <|
\_ ->
{ name = "Fruit"
, variants = []
}
|> Generate.customType
|> Expect.equal ""
, test "returns single line for single variant" <|
\_ ->
{ name = "Fruit"
, variants = [ "Apple" ]
}
|> Generate.customType
|> Expect.equal "type Fruit = Apple"
, test "returns multiple lines for multiple variants" <|
\_ ->
{ name = "Fruit"
, variants =
[ "Apple"
, "Banana"
, "Cherry"
]
}
|> Generate.customType
|> Expect.equal (String.trim """
type Fruit
= Apple
| Banana
| Cherry
""")
, describe "import_"
[ test "works with only a name" <|
\_ ->
{ name = "Url"
, alias = Nothing
, exposing_ = []
}
|> Generate.import_
|> Expect.equal "import Url"
, test "works with an alias" <|
\_ ->
{ name = "Data.User"
, alias = Just "User"
, exposing_ = []
}
|> Generate.import_
|> Expect.equal "import Data.User as User"
, test "works with a name and exposing" <|
\_ ->
{ name = "Url"
, alias = Nothing
, exposing_ = [ "Url" ]
}
|> Generate.import_
|> Expect.equal "import Url exposing (Url)"
, test "works with an alias and exposing" <|
\_ ->
{ name = "Data.User"
, alias = Just "User"
, exposing_ = [ "User" ]
}
|> Generate.import_
|> Expect.equal "import Data.User as User exposing (User)"
, test "works with an alias and mutiple exposing items" <|
\_ ->
{ name = "Css.Html"
, alias = Just "Html"
, exposing_ = [ "Html", "div" ]
}
|> Generate.import_
|> Expect.equal "import Css.Html as Html exposing (Html, div)"
]
, describe "recordType"
[ test "has empty record when given no properties" <|
\_ ->
[]
|> Generate.recordType
|> Expect.equal "{}"
, test "has single-line record when given one property" <|
\_ ->
[ ( "name", "String" ) ]
|> Generate.recordType
|> Expect.equal "{ name : String }"
, test "has multi-line record when given multiple property" <|
\_ ->
[ ( "name", "String" )
, ( "age", "Int" )
]
|> Generate.recordType
|> Expect.equal (String.trim """
{ name : String
, age : Int
}
""")
]
, describe "recordValue"
[ test "has empty record when given no properties" <|
\_ ->
[]
|> Generate.recordValue
|> Expect.equal "{}"
, test "has single-line record when given one property" <|
\_ ->
[ ( "name", "\"Ryan\"" ) ]
|> Generate.recordValue
|> Expect.equal "{ name = \"Ryan\" }"
, test "has multi-line record when given multiple property" <|
\_ ->
[ ( "name", "\"Ryan\"" )
, ( "age", "123" )
]
|> Generate.recordValue
|> Expect.equal (String.trim """
{ name = "Ryan"
, age = 123
}
""")
]
, describe "tuple"
[ test "has empty tuple when given no properties" <|
\_ ->
[]
|> Generate.tuple
|> Expect.equal "()"
, test "has single-line tuple when given one property" <|
\_ ->
[ "123" ]
|> Generate.tuple
|> Expect.equal "( 123 )"
, test "has multi-line tuple when given multiple property" <|
\_ ->
[ "123"
, "456"
]
|> Generate.tuple
|> Expect.equal (String.trim """
( 123
, 456
)
""")
]
, describe "list"
[ test "has empty list when given no properties" <|
\_ ->
[]
|> Generate.list
|> Expect.equal "[]"
, test "has single-line list when given one property" <|
\_ ->
[ "123" ]
|> Generate.list
|> Expect.equal "[ 123 ]"
, test "has multi-line list when given multiple property" <|
\_ ->
[ "123"
, "456"
]
|> Generate.list
|> Expect.equal (String.trim """
[ 123
, 456
]
""")
]
, describe "function"
[ test "returns blank string without annotation" <|
\_ ->
{ name = "name"
, annotation = []
, inputs = []
, body = "\"Ryan\""
}
|> Generate.function
|> Expect.equal ""
, test "works with no inputs" <|
\_ ->
{ name = "name"
, annotation = [ "String" ]
, inputs = []
, body = "\"Ryan\""
}
|> Generate.function
|> Expect.equal (String.trim """
name : String
name =
"Ryan"
""")
, test "works with one input" <|
\_ ->
{ name = "length"
, annotation = [ "String", "Int" ]
, inputs = [ "name" ]
, body = "String.length name"
}
|> Generate.function
|> Expect.equal (String.trim """
length : String -> Int
length name =
String.length name
""")
, test "works with multiple input" <|
\_ ->
{ name = "fullname"
, annotation = [ "String", "String", "String" ]
, inputs = [ "first", "last" ]
, body = "first ++ \" \" ++ last"
}
|> Generate.function
|> Expect.equal (String.trim """
fullname : String -> String -> String
fullname first last =
first ++ " " ++ last
""")
]
, describe "caseExpression"
[ test "returns empty string with missing conditionals" <|
\_ ->
{ variable = "route"
, cases = []
}
|> Generate.caseExpression
|> Expect.equal ""
, test "works with multiple conditions" <|
\_ ->
{ variable = "route"
, cases =
[ ( "Top", "\"/\"" )
, ( "About", "\"/about\"" )
, ( "NotFound", "\"/not-found\"" )
]
}
|> Generate.caseExpression
|> Expect.equal (String.trim """
case route of
Top ->
"/"
About ->
"/about"
NotFound ->
"/not-found"
""")
]
]
]

View File

@ -1,84 +0,0 @@
module UtilsTest exposing (expected, input, suite)
import Dict exposing (Dict)
import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer, int, list, string)
import Set exposing (Set)
import Test exposing (..)
import Utils exposing (Items)
suite : Test
suite =
describe "Utils"
[ describe "allSubsets"
[ test "works with authors posts example" <|
\_ ->
Utils.allSubsets
[ [ "Top" ]
, [ "NotFound" ]
, [ "Authors", "Dynamic", "Posts", "Dynamic" ]
]
|> Expect.equalLists
[ [ "Top" ]
, [ "NotFound" ]
, [ "Authors" ]
, [ "Authors", "Dynamic" ]
, [ "Authors", "Dynamic", "Posts" ]
, [ "Authors", "Dynamic", "Posts", "Dynamic" ]
]
]
, describe "addInMissingFolders"
[ test "works with authors post example" <|
\_ ->
Utils.addInMissingFolders input
|> Expect.equalDicts expected
]
]
input : Dict String Items
input =
Dict.fromList
[ ( ""
, { files = Set.fromList [ [ "NotFound" ], [ "Top" ] ]
, folders = Set.fromList []
}
)
, ( "Authors.Dynamic"
, { files = Set.fromList []
, folders = Set.fromList [ [ "Authors", "Dynamic", "Posts" ] ]
}
)
, ( "Authors.Dynamic.Posts"
, { files = Set.fromList [ [ "Authors", "Dynamic", "Posts", "Dynamic" ] ]
, folders = Set.fromList []
}
)
]
expected : Dict String Items
expected =
Dict.fromList
[ ( ""
, { files = Set.fromList [ [ "NotFound" ], [ "Top" ] ]
, folders = Set.fromList [ [ "Authors" ] ]
}
)
, ( "Authors"
, { files = Set.fromList []
, folders = Set.fromList [ [ "Authors", "Dynamic" ] ]
}
)
, ( "Authors.Dynamic"
, { files = Set.fromList []
, folders = Set.fromList [ [ "Authors", "Dynamic", "Posts" ] ]
}
)
, ( "Authors.Dynamic.Posts"
, { files = Set.fromList [ [ "Authors", "Dynamic", "Posts", "Dynamic" ] ]
, folders = Set.fromList []
}
)
]

1
docs.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,6 +0,0 @@
{
"checks" : {
"SingleFieldRecord": false,
"ImportAll": false
}
}

View File

@ -1,23 +1,18 @@
{
"type": "package",
"name": "ryannhg/elm-spa",
"summary": "single page apps made easy",
"summary": "a way to build single page apps with Elm",
"license": "BSD-3-Clause",
"version": "3.0.2",
"version": "4.0.0",
"exposed-modules": [
"Spa",
"Spa.Page",
"Spa.Types",
"Spa.Transition",
"Spa.Path"
"Spa"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {
"elm/browser": "1.0.0 <= v < 2.0.0",
"elm/core": "1.0.0 <= v < 2.0.0",
"elm/html": "1.0.0 <= v < 2.0.0",
"elm/url": "1.0.0 <= v < 2.0.0",
"mdgriffith/elm-ui": "1.1.5 <= v < 2.0.0"
"elm/url": "1.0.0 <= v < 2.0.0"
},
"test-dependencies": {}
}
}

View File

@ -2,25 +2,18 @@
"type": "application",
"source-directories": [
"src",
"elm-stuff/.elm-spa"
"../src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.4",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/parser": "1.1.0",
"elm/url": "1.0.0",
"elm-explorations/markdown": "1.0.0",
"mdgriffith/elm-ui": "1.1.5",
"ryannhg/elm-spa": "3.0.0"
"elm/url": "1.0.0"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2"
}

View File

@ -1,16 +1,16 @@
{
"name": "my-elm-spa-project",
"version": "1.0.0",
"name": "elm-spa-example",
"version": "4.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"ajv": {
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
"integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
"integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
"dev": true,
"requires": {
"fast-deep-equal": "^2.0.1",
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
@ -72,9 +72,9 @@
"dev": true
},
"aws4": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==",
"dev": true
},
"bcrypt-pbkdf": {
@ -161,19 +161,28 @@
},
"dependencies": {
"chokidar": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz",
"integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz",
"integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.1.1",
"fsevents": "~2.1.2",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.2.0"
"readdirp": "~3.3.0"
}
},
"readdirp": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz",
"integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==",
"dev": true,
"requires": {
"picomatch": "^2.0.7"
}
}
}
@ -343,15 +352,15 @@
}
},
"elm-hot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/elm-hot/-/elm-hot-1.1.1.tgz",
"integrity": "sha512-ZHjoHd2Ev6riNXNQirj3J+GKKXXwedAUikfFBYzlVL/+3CdGs96cpZ7nhAk4c5l//Qa9ymltrqX36mOlr0pPFA==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/elm-hot/-/elm-hot-1.1.4.tgz",
"integrity": "sha512-qPDP/o/Fkifriaxaf3E7hHFB5L6Ijihyg8is4A6xna6/h/zebUiNssbQrxywI2oxNUkr6W/leEu/WlIC1tmVnw==",
"dev": true
},
"elm-live": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/elm-live/-/elm-live-4.0.1.tgz",
"integrity": "sha512-IlonaC1pO/QoXlOrwwrJaxyvpJAT8QDSfzenkChbhU1PC1fJetkj2TwZfki+y1ZxpSMTnMSomMraOdWA6DO3VQ==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/elm-live/-/elm-live-4.0.2.tgz",
"integrity": "sha512-4I3UvJxF6MubC14VsgtV11B0zBxaaKtdKKsWquoaa5a3UHBIGW83qgTnt/NxOj4omOLfupaftmDaE4yRMTgTcw==",
"dev": true,
"requires": {
"chalk": "^1.1.1",
@ -359,7 +368,7 @@
"commander": "2.17.1",
"crocks": "0.12.1",
"cross-spawn": "5.0.1",
"elm-hot": "1.1.1",
"elm-hot": "1.1.4",
"finalhandler": "1.1.2",
"http-proxy": "1.17.0",
"internal-ip": "4.3.0",
@ -392,9 +401,9 @@
}
},
"es6-promisify": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.0.2.tgz",
"integrity": "sha512-eO6vFm0JvqGzjWIQA6QVKjxpmELfhWbDUWHm1rPfIbn55mhKPiAa5xpLmQWJrNa629ZIeQ8ZvMAi13kvrjK6Mg==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.1.0.tgz",
"integrity": "sha512-jCsk2fpfEFusVv1MDkF4Uf0hAzIKNDMgR6LyOIw6a3jwkN1sCgWzuwgnsHY9YSQ8n8P31HoncvE0LC44cpWTrw==",
"dev": true
},
"escape-html": {
@ -464,15 +473,15 @@
"dev": true
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
"integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
"dev": true
},
"fast-json-stable-stringify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true
},
"fill-range": {
@ -509,9 +518,9 @@
}
},
"follow-redirects": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.9.0.tgz",
"integrity": "sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.10.0.tgz",
"integrity": "sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ==",
"dev": true,
"requires": {
"debug": "^3.0.0"
@ -843,18 +852,18 @@
"dev": true
},
"mime-db": {
"version": "1.42.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz",
"integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==",
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
"dev": true
},
"mime-types": {
"version": "2.1.25",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz",
"integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==",
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"dev": true,
"requires": {
"mime-db": "1.42.0"
"mime-db": "1.43.0"
}
},
"ms": {
@ -930,9 +939,9 @@
"dev": true
},
"p-limit": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
"integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
"integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
@ -990,9 +999,9 @@
"dev": true
},
"picomatch": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.1.1.tgz",
"integrity": "sha512-OYMyqkKzK7blWO/+XZYP6w8hH0LDvkBvdvKukti+7kqYFCiEAk+gI3DWnryapc0Dau05ugGTy0foQ6mqn4AHYA==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"dev": true
},
"pseudomap": {
@ -1002,9 +1011,9 @@
"dev": true
},
"psl": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz",
"integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz",
"integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==",
"dev": true
},
"pump": {
@ -1036,18 +1045,18 @@
"dev": true
},
"readdirp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz",
"integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
"integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
"dev": true,
"requires": {
"picomatch": "^2.0.4"
"picomatch": "^2.2.1"
}
},
"request": {
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"dev": true,
"requires": {
"aws-sign2": "~0.7.0",
@ -1057,7 +1066,7 @@
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.0",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
@ -1067,7 +1076,7 @@
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.4.3",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
}
@ -1276,21 +1285,13 @@
"dev": true
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"dev": true,
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
},
"dependencies": {
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
}
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
},
"tunnel-agent": {
@ -1324,9 +1325,9 @@
}
},
"uuid": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
},
"verror": {
@ -1420,9 +1421,9 @@
"dev": true
},
"yargs": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz",
"integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==",
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
@ -1434,13 +1435,13 @@
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.1"
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",

23
example/package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "elm-spa-example",
"version": "4.0.0",
"description": "single page apps made easy",
"main": "index.js",
"scripts": {
"start": "npm install && npm run build && npm run dev",
"build": "npm run build:elm-spa && npm run build:elm",
"build:elm-spa": "elm-spa build .",
"build:elm": "elm make src/Main.elm --optimize --output public/dist/elm.js",
"dev": "npm run dev:elm-spa & npm run dev:elm",
"dev:elm-spa": "chokidar src/Pages -c \"npm run build:elm-spa\"",
"dev:elm": "elm-live src/Main.elm -u -d public -- --debug --output public/dist/elm.js"
},
"keywords": [],
"author": "Ryan Haskell-Glatz",
"license": "ISC",
"devDependencies": {
"chokidar-cli": "2.1.0",
"elm": "0.19.1-3",
"elm-live": "4.0.2"
}
}

16
example/public/index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Elm Application</title>
<link rel="stylesheet" href="https://not-much-css.netlify.com/not-much.css">
<link rel="stylesheet" href="/main.css">
</head>
<body>
<script src="/dist/elm.compiled.js"></script>
<script>
Elm.Main.init()
</script>
</body>
</html>

32
example/public/main.css Normal file
View File

@ -0,0 +1,32 @@
.fixed--full {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.absolute--full {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.absolute--center {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.bg--overlay {
background: rgba(0,0,0,0.25);
}
.min-width--480 {
min-width: 320px;
}
.bg--white {
background: white;
}

172
example/src/Components.elm Normal file
View File

@ -0,0 +1,172 @@
module Components exposing
( footer
, layout
, navbar
)
import Browser exposing (Document)
import Data.Modal as Modal exposing (Modal)
import Data.SignInForm exposing (SignInForm)
import Data.User as User exposing (User)
import Html exposing (..)
import Html.Attributes as Attr exposing (class, href)
import Html.Events as Events
import Generated.Route as Route
-- LAYOUT
layout :
{ page : Document msg
, global :
{ global
| modal : Maybe Modal
, user : Maybe User
}
, actions :
{ onSignOut : msg
, openSignInModal : msg
, closeModal : msg
, attemptSignIn : msg
, onSignInEmailInput : String -> msg
, onSignInPasswordInput : String -> msg
}
}
-> Document msg
layout { page, actions, global } =
{ title = page.title
, body =
[ div [ class "container pad--medium column spacing--large h--fill" ]
[ navbar { user = global.user, actions = actions }
, div [ class "column spacing--large", Attr.style "flex" "1 0 auto" ] page.body
, footer
, global.modal
|> Maybe.map (viewModal actions)
|> Maybe.withDefault (text "")
]
]
}
-- NAVBAR
navbar :
{ user : Maybe User
, actions : { actions | openSignInModal : msg, onSignOut : msg }
}
-> Html msg
navbar ({ actions } as options) =
header [ class "container" ]
[ div [ class "row spacing--between center-y" ]
[ a [ class "link font--h5 font--bold", href "/" ] [ text "home" ]
, div [ class "row spacing--medium center-y" ]
[ a [ class "link", href "/about" ] [ text "about" ]
, a [ class "link", href "/posts" ] [ text "posts" ]
, case options.user of
Just user ->
a [ class "link", href (Route.toHref Route.Profile) ] [ text "profile" ]
Nothing ->
button [ class "button", Events.onClick actions.openSignInModal ] [ text "sign in" ]
]
]
]
-- FOOTER
footer : Html msg
footer =
Html.footer [ class "container py--medium" ]
[ text "built with elm, 2020"
]
-- MODAL
viewModal :
{ actions
| closeModal : msg
, attemptSignIn : msg
, onSignInEmailInput : String -> msg
, onSignInPasswordInput : String -> msg
}
-> Modal
-> Html msg
viewModal actions modal_ =
case modal_ of
Modal.SignInModal { email, password } ->
modal
{ title = "Sign in"
, body =
form [ class "column spacing--medium", Events.onSubmit actions.attemptSignIn ]
[ emailField
{ label = "Email"
, value = email
, onInput = actions.onSignInEmailInput
}
, passwordField
{ label = "Password"
, value = password
, onInput = actions.onSignInPasswordInput
}
, button [ class "button" ] [ text "Sign in" ]
]
, actions = actions
}
modal :
{ title : String
, body : Html msg
, actions : { actions | closeModal : msg }
}
-> Html msg
modal ({ actions } as options) =
div [ class "fixed--full" ]
[ div [ class "absolute--full bg--overlay", Events.onClick actions.closeModal ] []
, div [ class "column spacing--large pad--large absolute--center min-width--480 bg--white" ]
[ div [ class "row spacing--between center-y" ]
[ h3 [ class "font--h3" ] [ text options.title ]
, button [ class "modal__close", Events.onClick actions.closeModal ] [ text "" ]
]
, options.body
]
]
-- FORMS
inputField :
String
-> { label : String, value : String, onInput : String -> msg }
-> Html msg
inputField type_ options =
label [ class "column spacing--small" ]
[ span [] [ text options.label ]
, input [ Attr.type_ type_, Attr.value options.value, Events.onInput options.onInput ] []
]
emailField :
{ label : String, value : String, onInput : String -> msg }
-> Html msg
emailField =
inputField "email"
passwordField :
{ label : String, value : String, onInput : String -> msg }
-> Html msg
passwordField =
inputField "password"

View File

@ -0,0 +1,28 @@
module Data.Modal exposing
( Modal(..)
, signInForm
, updateSignInForm
)
import Data.SignInForm exposing (SignInForm)
type Modal
= SignInModal SignInForm
signInForm : Modal -> Maybe SignInForm
signInForm modal =
case modal of
SignInModal form ->
Just form
updateSignInForm :
(SignInForm -> SignInForm)
-> Modal
-> Modal
updateSignInForm fn modal =
case modal of
SignInModal form ->
SignInModal (fn form)

View File

@ -0,0 +1,35 @@
module Data.SignInForm exposing
( Field(..)
, SignInForm
, empty
, updateEmail
, updatePassword
)
type alias SignInForm =
{ email : String
, password : String
}
type Field
= Email
| Password
empty : SignInForm
empty =
{ email = ""
, password = ""
}
updateEmail : String -> SignInForm -> SignInForm
updateEmail email form =
{ form | email = email }
updatePassword : String -> SignInForm -> SignInForm
updatePassword password form =
{ form | password = password }

41
example/src/Data/Tab.elm Normal file
View File

@ -0,0 +1,41 @@
module Data.Tab exposing
( Tab
, ourMission
, ourTeam
, ourValues
, toString
)
type Tab
= OurTeam
| OurValues
| OurMission
ourTeam : Tab
ourTeam =
OurTeam
ourValues : Tab
ourValues =
OurValues
ourMission : Tab
ourMission =
OurMission
toString : Tab -> String
toString tab =
case tab of
OurTeam ->
"Our Team"
OurValues ->
"Our Values"
OurMission ->
"Our Mission"

Some files were not shown because too many files have changed in this diff Show More