mirror of
https://github.com/ryan-haskell/elm-spa.git
synced 2024-11-22 11:31:58 +03:00
version 4 bb
This commit is contained in:
parent
b0af558f38
commit
4d015a2faf
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
.DS_Store
|
||||
./dist
|
||||
elm-stuff
|
||||
node_modules
|
||||
node_modules
|
||||
dist
|
||||
Generated
|
30
LICENSE
30
LICENSE
@ -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.
|
28
README.md
28
README.md
@ -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
3
cli/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
dist
|
||||
elm-stuff
|
||||
node_modules
|
@ -1,5 +1,5 @@
|
||||
elm-stuff
|
||||
src/elm
|
||||
tests
|
||||
/src
|
||||
/tests
|
||||
/elm.json
|
||||
!.gitignore
|
@ -1,3 +0,0 @@
|
||||
# elm-spa/cli
|
||||
> the thing that types the stuff
|
||||
|
@ -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
184
cli/index.js
Executable 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)])
|
@ -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
|
||||
```
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"ui": "Element"
|
||||
}
|
@ -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": {}
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
# sends all routes to /index.html
|
||||
# (so you can handle 404s there!)
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
@ -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"
|
||||
}
|
||||
}
|
@ -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>
|
@ -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)
|
||||
}
|
@ -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%;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# src/Components
|
||||
> views shared across the site
|
@ -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
|
@ -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
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# src/Layouts
|
||||
> where all your pages go
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
@ -1,3 +0,0 @@
|
||||
# src/Pages
|
||||
> where all your pages go
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
@ -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
|
||||
}
|
@ -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 = []
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# src/Utils
|
||||
> a place for helper functions
|
@ -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)
|
4
cli/initial-projects/html/.gitignore
vendored
4
cli/initial-projects/html/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
.DS_Store
|
||||
dist
|
||||
elm-stuff
|
||||
node_modules
|
@ -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
|
||||
```
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"ui": "Html"
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
# sends all routes to /index.html
|
||||
# (so you can handle 404s there!)
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
1451
cli/initial-projects/html/package-lock.json
generated
1451
cli/initial-projects/html/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
@ -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>
|
@ -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)
|
||||
}
|
@ -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%;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# src/Components
|
||||
> views shared across the site
|
@ -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
|
@ -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 ]
|
@ -1,2 +0,0 @@
|
||||
# src/Layouts
|
||||
> where all your pages go
|
@ -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
|
||||
}
|
@ -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?" ]
|
||||
]
|
@ -1,3 +0,0 @@
|
||||
# src/Pages
|
||||
> where all your pages go
|
||||
|
@ -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"
|
||||
]
|
||||
]
|
@ -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
|
||||
}
|
@ -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 = []
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# src/Utils
|
||||
> a place for helper functions
|
@ -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
1106
cli/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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": {}
|
||||
}
|
||||
|
@ -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": {
|
26
cli/projects/new/package.json
Normal file
26
cli/projects/new/package.json
Normal 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"
|
||||
}
|
||||
}
|
15
cli/projects/new/public/index.html
Normal file
15
cli/projects/new/public/index.html
Normal 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>
|
36
cli/projects/new/src/Components.elm
Normal file
36
cli/projects/new/src/Components.elm
Normal 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 ❤" ]
|
98
cli/projects/new/src/Global.elm
Normal file
98
cli/projects/new/src/Global.elm
Normal 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)
|
141
cli/projects/new/src/Main.elm
Normal file
141
cli/projects/new/src/Main.elm
Normal 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
|
32
cli/projects/new/src/Pages/Docs.elm
Normal file
32
cli/projects/new/src/Pages/Docs.elm
Normal 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" ]
|
||||
}
|
32
cli/projects/new/src/Pages/NotFound.elm
Normal file
32
cli/projects/new/src/Pages/NotFound.elm
Normal 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" ]
|
||||
}
|
32
cli/projects/new/src/Pages/Top.elm
Normal file
32
cli/projects/new/src/Pages/Top.elm
Normal 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
64
cli/src/Add/Component.elm
Normal 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
64
cli/src/Add/Element.elm
Normal 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
58
cli/src/Add/Sandbox.elm
Normal 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
44
cli/src/Add/Static.elm
Normal 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
|
299
cli/src/Generators/Pages.elm
Normal file
299
cli/src/Generators/Pages.elm
Normal 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
|
111
cli/src/Generators/Route.elm
Normal file
111
cli/src/Generators/Route.elm
Normal 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
33
cli/src/Main.elm
Normal 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
266
cli/src/Path.elm
Normal 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
72
cli/src/Ports.elm
Normal 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
256
cli/src/Utils/Generate.elm
Normal 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 ", "
|
||||
)
|
||||
++ " }"
|
@ -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
|
1133
cli/src/elm/File.elm
1133
cli/src/elm/File.elm
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
||||
)
|
@ -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
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
138
cli/src/index.js
138
cli/src/index.js
@ -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))
|
233
cli/src/utils.js
233
cli/src/utils.js
@ -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
|
||||
}
|
233
cli/tests/Tests/Generators/Pages.elm
Normal file
233
cli/tests/Tests/Generators/Pages.elm
Normal 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
|
||||
""")
|
||||
]
|
||||
]
|
||||
]
|
121
cli/tests/Tests/Generators/Route.elm
Normal file
121
cli/tests/Tests/Generators/Route.elm
Normal 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
183
cli/tests/Tests/Path.elm
Normal 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 }"
|
||||
]
|
||||
]
|
||||
]
|
274
cli/tests/Tests/Utils/Generate.elm
Normal file
274
cli/tests/Tests/Utils/Generate.elm
Normal 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"
|
||||
""")
|
||||
]
|
||||
]
|
||||
]
|
@ -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,6 +0,0 @@
|
||||
{
|
||||
"checks" : {
|
||||
"SingleFieldRecord": false,
|
||||
"ImportAll": false
|
||||
}
|
||||
}
|
15
elm.json
15
elm.json
@ -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": {}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
@ -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
23
example/package.json
Normal 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
16
example/public/index.html
Normal 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
32
example/public/main.css
Normal 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
172
example/src/Components.elm
Normal 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"
|
28
example/src/Data/Modal.elm
Normal file
28
example/src/Data/Modal.elm
Normal 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)
|
35
example/src/Data/SignInForm.elm
Normal file
35
example/src/Data/SignInForm.elm
Normal 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
41
example/src/Data/Tab.elm
Normal 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
Loading…
Reference in New Issue
Block a user