├── generators └── app │ ├── templates │ ├── parcel │ │ ├── style.css │ │ ├── gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── app.js │ │ └── README.md │ └── elm │ │ ├── element │ │ ├── tests │ │ │ └── Example.elm │ │ ├── _elm.json │ │ └── src │ │ │ └── Main.elm │ │ ├── sandbox │ │ ├── tests │ │ │ └── Example.elm │ │ ├── _elm.json │ │ └── src │ │ │ └── Main.elm │ │ ├── application │ │ ├── tests │ │ │ └── Example.elm │ │ ├── _elm.json │ │ └── src │ │ │ └── Main.elm │ │ └── document │ │ ├── tests │ │ └── Example.elm │ │ ├── _elm.json │ │ └── src │ │ └── Main.elm │ └── index.js ├── .gitignore ├── cli ├── elm-app-gen.js ├── elm-app-gen-create.js ├── quickstart-options.js ├── elm-app-gen-quickstart.js └── create-options.js ├── LICENSE ├── package.json ├── lib ├── prompt-builder.js └── options-parser.js └── README.md /generators/app/templates/parcel/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "sans-serif"; 3 | } 4 | -------------------------------------------------------------------------------- /generators/app/templates/parcel/gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | elm-stuff/ 3 | dist/ 4 | .cache/ 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | generators/app/package-lock.json 3 | generators/app/templates/elm-stuff/ 4 | -------------------------------------------------------------------------------- /generators/app/templates/parcel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= name %> 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /generators/app/templates/elm/element/tests/Example.elm: -------------------------------------------------------------------------------- 1 | module Example exposing (..) 2 | 3 | import Expect exposing (Expectation) 4 | import Fuzz exposing (Fuzzer, int, list, string) 5 | import Test exposing (..) 6 | 7 | 8 | suite : Test 9 | suite = 10 | todo "Implement our first test. See https://package.elm-lang.org/packages/elm-explorations/test/latest for how to do this!" 11 | -------------------------------------------------------------------------------- /generators/app/templates/elm/sandbox/tests/Example.elm: -------------------------------------------------------------------------------- 1 | module Example exposing (..) 2 | 3 | import Expect exposing (Expectation) 4 | import Fuzz exposing (Fuzzer, int, list, string) 5 | import Test exposing (..) 6 | 7 | 8 | suite : Test 9 | suite = 10 | todo "Implement our first test. See https://package.elm-lang.org/packages/elm-explorations/test/latest for how to do this!" 11 | -------------------------------------------------------------------------------- /generators/app/templates/elm/application/tests/Example.elm: -------------------------------------------------------------------------------- 1 | module Example exposing (..) 2 | 3 | import Expect exposing (Expectation) 4 | import Fuzz exposing (Fuzzer, int, list, string) 5 | import Test exposing (..) 6 | 7 | 8 | suite : Test 9 | suite = 10 | todo "Implement our first test. See https://package.elm-lang.org/packages/elm-explorations/test/latest for how to do this!" 11 | -------------------------------------------------------------------------------- /generators/app/templates/elm/document/tests/Example.elm: -------------------------------------------------------------------------------- 1 | module Example exposing (..) 2 | 3 | import Expect exposing (Expectation) 4 | import Fuzz exposing (Fuzzer, int, list, string) 5 | import Test exposing (..) 6 | 7 | 8 | suite : Test 9 | suite = 10 | todo "Implement our first test. See https://package.elm-lang.org/packages/elm-explorations/test/latest for how to do this!" 11 | -------------------------------------------------------------------------------- /cli/elm-app-gen.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Imports 4 | const commander = require("commander"); 5 | const version = require("../package.json").version; 6 | 7 | // CLI Option parsing 8 | commander 9 | .version(version) 10 | .command("create ", "Creates a new application called ", { isDefault: true }) 11 | .command("quickstart ", "Like 'create', but with no prompts") 12 | .parse(process.argv); 13 | -------------------------------------------------------------------------------- /cli/elm-app-gen-create.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | const yeoman = require("yeoman-environment"); 3 | const appGen = require.resolve("../generators/app"); 4 | const optionParser = require("../lib/options-parser.js"); 5 | const cliOpts = require("./create-options.js"); 6 | 7 | let options = optionParser.parse(cliOpts); 8 | 9 | const yoEnv = yeoman.createEnv(); 10 | yoEnv.register(appGen, "elm:app"); 11 | yoEnv.run("elm:app", { 12 | cliOpts: options 13 | }); 14 | -------------------------------------------------------------------------------- /cli/quickstart-options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arg: { 3 | name: "name", 4 | description: "The name of the application", 5 | prompt: "What is the name of your application?", 6 | ifNotSet: "I need a name to create your application." 7 | }, 8 | options: [], 9 | helpMessage: ` 10 | The quickstart command is used to create a new Elm project. It only requires that the 11 | user supply a name for the project and uses defaults for all other options. 12 | ` 13 | }; 14 | -------------------------------------------------------------------------------- /cli/elm-app-gen-quickstart.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | const yeoman = require("yeoman-environment"); 3 | const appGen = require.resolve("../generators/app"); 4 | const optionParser = require("../lib/options-parser.js"); 5 | const cliOpts = require("./quickstart-options.js"); 6 | 7 | let options = optionParser.parse(cliOpts); 8 | options.installer = "npm"; 9 | options.prompt = false; 10 | options.start = true; 11 | options.type = "document"; 12 | 13 | const yoEnv = yeoman.createEnv(); 14 | yoEnv.register(appGen, "elm:app"); 15 | yoEnv.run("elm:app", { 16 | cliOpts: options 17 | }); 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Matthew Cheely 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /generators/app/templates/elm/sandbox/_elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= name %>", 3 | "type": "application", 4 | "source-directories": ["src"], 5 | "elm-version": "0.19.1", 6 | "dependencies": { 7 | "direct": { 8 | "elm/browser": "1.0.2", 9 | "elm/core": "1.0.4", 10 | "elm/html": "1.0.0", 11 | "elm/url": "1.0.0" 12 | }, 13 | "indirect": { 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/virtual-dom": "1.0.2" 17 | } 18 | }, 19 | "test-dependencies": { 20 | "direct": { 21 | "elm-explorations/test": "1.2.1" 22 | }, 23 | "indirect": { 24 | "elm/random": "1.0.0" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /generators/app/templates/elm/application/_elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= name %>", 3 | "type": "application", 4 | "source-directories": ["src"], 5 | "elm-version": "0.19.1", 6 | "dependencies": { 7 | "direct": { 8 | "elm/browser": "1.0.2", 9 | "elm/core": "1.0.4", 10 | "elm/html": "1.0.0", 11 | "elm/url": "1.0.0" 12 | }, 13 | "indirect": { 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/virtual-dom": "1.0.2" 17 | } 18 | }, 19 | "test-dependencies": { 20 | "direct": { 21 | "elm-explorations/test": "1.2.1" 22 | }, 23 | "indirect": { 24 | "elm/random": "1.0.0" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /generators/app/templates/elm/document/_elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= name %>", 3 | "type": "application", 4 | "source-directories": ["src"], 5 | "elm-version": "0.19.1", 6 | "dependencies": { 7 | "direct": { 8 | "elm/browser": "1.0.2", 9 | "elm/core": "1.0.4", 10 | "elm/html": "1.0.0", 11 | "elm/http": "2.0.0", 12 | "elm/json": "1.1.2" 13 | }, 14 | "indirect": { 15 | "elm/bytes": "1.0.5", 16 | "elm/file": "1.0.1", 17 | "elm/time": "1.0.0", 18 | "elm/url": "1.0.0", 19 | "elm/virtual-dom": "1.0.2" 20 | } 21 | }, 22 | "test-dependencies": { 23 | "direct": { 24 | "elm-explorations/test": "1.2.1" 25 | }, 26 | "indirect": { 27 | "elm/random": "1.0.0" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /generators/app/templates/parcel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= name %>", 3 | "version": "1.0.0", 4 | <% if (locals.description) { %> 5 | "description": "<%= description %>", 6 | <% } %> 7 | "scripts": { 8 | "start": "parcel src/index.html", 9 | "build": "rm -r dist && parcel build src/index.html --public-url ./", 10 | "test": "elm-test", 11 | "autotest": "elm-test --watch" 12 | }, 13 | <% if (locals.author) { %> 14 | "author": "<%= author %>", 15 | <% } %> 16 | <% if (locals.license) { %> 17 | "license": "<%= license %>", 18 | <% } %> 19 | "dependencies": {}, 20 | "devDependencies": { 21 | "elm": "^0.19.1-3", 22 | "elm-debug-transformer": "^1.0.0", 23 | "elm-hot": "^1.1.0", 24 | "elm-test": "^0.19.1", 25 | "node-elm-compiler": "^5.0.0", 26 | "parcel-bundler": "^1.12.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /generators/app/templates/elm/sandbox/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser 4 | import Html exposing (Html, button, div, text) 5 | import Html.Events exposing (onClick) 6 | 7 | 8 | main = 9 | Browser.sandbox { init = init, update = update, view = view } 10 | 11 | 12 | -- MODEL 13 | 14 | type alias Model = Int 15 | 16 | init : Model 17 | init = 18 | 0 19 | 20 | 21 | -- UPDATE 22 | 23 | type Msg = Increment | Decrement 24 | 25 | update : Msg -> Model -> Model 26 | update msg model = 27 | case msg of 28 | Increment -> 29 | model + 1 30 | 31 | Decrement -> 32 | model - 1 33 | 34 | 35 | -- VIEW 36 | 37 | view : Model -> Html Msg 38 | view model = 39 | div [] 40 | [ button [ onClick Decrement ] [ text "-" ] 41 | , div [] [ text (String.fromInt model) ] 42 | , button [ onClick Increment ] [ text "+" ] 43 | ] 44 | -------------------------------------------------------------------------------- /generators/app/templates/elm/element/_elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= name %>", 3 | "type": "application", 4 | "source-directories": [ 5 | "src" 6 | ], 7 | "elm-version": "0.19.1", 8 | "dependencies": { 9 | "direct": { 10 | "elm/browser": "1.0.2", 11 | "elm/core": "1.0.4", 12 | "elm/html": "1.0.0", 13 | "elm/http": "2.0.0", 14 | "elm/json": "1.1.3" 15 | }, 16 | "indirect": { 17 | "elm/bytes": "1.0.8", 18 | "elm/file": "1.0.4", 19 | "elm/time": "1.0.0", 20 | "elm/url": "1.0.0", 21 | "elm/virtual-dom": "1.0.2" 22 | } 23 | }, 24 | "test-dependencies": { 25 | "direct": { 26 | "elm-explorations/test": "1.2.1" 27 | }, 28 | "indirect": { 29 | "elm/random": "1.0.0" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-app-gen", 3 | "version": "1.3.0", 4 | "description": "A CLI for generating Elm projects that build with Parcel", 5 | "keywords": [ 6 | "elm", 7 | "parcel" 8 | ], 9 | "repository": "github:MattCheely/elm-app-gen", 10 | "contributors": [ 11 | "Matthew Cheely", 12 | "Théophile Kalumbu" 13 | ], 14 | "license": "Apache-2.0", 15 | "bin": { 16 | "elm-app-gen": "./cli/elm-app-gen.js" 17 | }, 18 | "files": [ 19 | "cli", 20 | "generators", 21 | "lib" 22 | ], 23 | "scripts": { 24 | "template-lint": "ejslint generators/app/templates/**/*", 25 | "test": "echo \"Error: no test specified\" && exit 1", 26 | "prepublishOnly": "git clean -xdf" 27 | }, 28 | "dependencies": { 29 | "commander": "^3.0.0", 30 | "cross-spawn": "^6.0.5", 31 | "parcel-bundler": "^1.12.3", 32 | "yeoman-environment": "^2.4.0", 33 | "yeoman-generator": "^3.1.1" 34 | }, 35 | "devDependencies": { 36 | "ejs-lint": "^0.3.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /generators/app/templates/parcel/app.js: -------------------------------------------------------------------------------- 1 | import { Elm } from "../Main.elm"; 2 | 3 | if (process.env.NODE_ENV === 'development') { 4 | // Only runs in development and will be stripped from production build. 5 | // See https://parceljs.org/production.html 6 | 7 | const ElmDebugger = require("elm-debug-transformer"); 8 | function hasFormatterSupport() { 9 | const originalFormatters = window.devtoolsFormatters; 10 | let supported = false; 11 | 12 | window.devtoolsFormatters = [ 13 | { 14 | header: function(obj, config) { 15 | supported = true; 16 | return null; 17 | }, 18 | hasBody: function(obj) {}, 19 | body: function(obj, config) {}, 20 | }, 21 | ]; 22 | console.log('elm-debug-transformer: checking for formatter support.', {}); 23 | window.devtoolsFormatters = originalFormatters; 24 | return supported; 25 | } 26 | 27 | if (hasFormatterSupport()) { 28 | ElmDebugger.register(); 29 | } else { 30 | ElmDebugger.register({simple_mode: true}); 31 | } 32 | } 33 | 34 | Elm.Main.init({ 35 | node: document.getElementById("app") 36 | }); 37 | -------------------------------------------------------------------------------- /lib/prompt-builder.js: -------------------------------------------------------------------------------- 1 | function buildPrompts(commandOpts, providedOpts) { 2 | let prompts = []; 3 | 4 | if (commandOpts.arg) { 5 | addPrompt(providedOpts, prompts, commandOpts.arg); 6 | } 7 | 8 | commandOpts.options.forEach(optionInfo => { 9 | addPrompt(providedOpts, prompts, optionInfo); 10 | }); 11 | return prompts; 12 | } 13 | 14 | function addPrompt(providedOpts, prompts, optionInfo) { 15 | if (!providedOpts[optionInfo.name]) { 16 | prompts.push(buildPrompt(optionInfo)); 17 | } 18 | } 19 | 20 | function buildPrompt(opt) { 21 | let prompt = { 22 | name: opt.name, 23 | message: opt.prompt 24 | }; 25 | 26 | if (opt.choices) { 27 | prompt.type = "list"; 28 | prompt.choices = opt.choices; 29 | } else if (opt.confirm) { 30 | prompt.type = "confirm"; 31 | if (opt.default === false) { 32 | prompt.default = false; 33 | } else { 34 | prompt.default = true; 35 | } 36 | } else { 37 | prompt.type = "input"; 38 | } 39 | 40 | if (opt.ifNotSet) { 41 | prompt.validate = function(answer) { 42 | if (answer.length > 0) { 43 | return true; 44 | } else { 45 | return opt.ifNotSet; 46 | } 47 | }; 48 | } 49 | 50 | return prompt; 51 | } 52 | 53 | module.exports = { 54 | build: buildPrompts 55 | }; 56 | -------------------------------------------------------------------------------- /generators/app/templates/elm/element/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser 4 | import Html exposing (Html, text, pre) 5 | import Http 6 | 7 | 8 | 9 | -- MAIN 10 | 11 | 12 | main = 13 | Browser.element 14 | { init = init 15 | , update = update 16 | , subscriptions = subscriptions 17 | , view = view 18 | } 19 | 20 | 21 | 22 | -- MODEL 23 | 24 | 25 | type Model 26 | = Failure 27 | | Loading 28 | | Success String 29 | 30 | 31 | init : () -> (Model, Cmd Msg) 32 | init _ = 33 | ( Loading 34 | , Http.get 35 | { url = "https://elm-lang.org/assets/public-opinion.txt" 36 | , expect = Http.expectString GotText 37 | } 38 | ) 39 | 40 | 41 | 42 | -- UPDATE 43 | 44 | 45 | type Msg 46 | = GotText (Result Http.Error String) 47 | 48 | 49 | update : Msg -> Model -> (Model, Cmd Msg) 50 | update msg model = 51 | case msg of 52 | GotText result -> 53 | case result of 54 | Ok fullText -> 55 | (Success fullText, Cmd.none) 56 | 57 | Err _ -> 58 | (Failure, Cmd.none) 59 | 60 | 61 | 62 | -- SUBSCRIPTIONS 63 | 64 | 65 | subscriptions : Model -> Sub Msg 66 | subscriptions model = 67 | Sub.none 68 | 69 | 70 | 71 | -- VIEW 72 | 73 | 74 | view : Model -> Html Msg 75 | view model = 76 | case model of 77 | Failure -> 78 | text "I was unable to load your book." 79 | 80 | Loading -> 81 | text "Loading..." 82 | 83 | Success fullText -> 84 | pre [] [ text fullText ] 85 | -------------------------------------------------------------------------------- /cli/create-options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arg: { 3 | name: "name", 4 | description: "The name of the application", 5 | prompt: "What is the name of your application?", 6 | ifNotSet: "I need a name to create your application." 7 | }, 8 | options: [ 9 | { 10 | name: "type", 11 | type: "list", 12 | choices: ["sandbox", "element", "document", "application"], 13 | description: "The type of the application", 14 | prompt: `What type of application would you like to create? 15 | If you don't know what this means, see 16 | https://package.elm-lang.org/packages/elm/browser/latest/ 17 | for an explanation`, 18 | default: "document" 19 | }, 20 | { 21 | name: "description", 22 | description: "A description of the application", 23 | prompt: "Please provide a brief description of your application:" 24 | }, 25 | { 26 | name: "author", 27 | description: "The author of the application", 28 | prompt: "Who is the author of this project?" 29 | }, 30 | { 31 | name: "license", 32 | description: "The SPDX license identifier for the project", 33 | prompt: "What license (SPDX identifier) would you like to use?" 34 | }, 35 | { 36 | name: "installer", 37 | description: "The tool to use for dependency installation", 38 | prompt: "What would you like to use to install dependencies?", 39 | choices: ["npm", "yarn"], 40 | ifNotSet: 41 | "I need to know what install tool you want to use for build dependencies." 42 | }, 43 | { 44 | name: "start", 45 | description: "If set, immediately start the application in dev mode", 46 | prompt: "Should I start the development server?", 47 | confirm: true, 48 | default: false 49 | } 50 | ], 51 | helpMessage: ` 52 | The create command is used to create a new Elm project. Any options 53 | not specified on the command line will be requested via interactive prompts, 54 | unless --no-prompt is used. When called with --no-prompt, --name and --installer 55 | are required. 56 | ` 57 | }; 58 | -------------------------------------------------------------------------------- /generators/app/templates/elm/application/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser 4 | import Browser.Navigation as Nav 5 | import Html exposing (..) 6 | import Html.Attributes exposing (..) 7 | import Url 8 | 9 | 10 | 11 | -- MAIN 12 | 13 | 14 | main : Program () Model Msg 15 | main = 16 | Browser.application 17 | { init = init 18 | , view = view 19 | , update = update 20 | , subscriptions = subscriptions 21 | , onUrlChange = UrlChanged 22 | , onUrlRequest = LinkClicked 23 | } 24 | 25 | 26 | 27 | -- MODEL 28 | 29 | 30 | type alias Model = 31 | { key : Nav.Key 32 | , url : Url.Url 33 | } 34 | 35 | 36 | init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg ) 37 | init flags url key = 38 | ( Model key url, Cmd.none ) 39 | 40 | 41 | 42 | -- UPDATE 43 | 44 | 45 | type Msg 46 | = LinkClicked Browser.UrlRequest 47 | | UrlChanged Url.Url 48 | 49 | 50 | update : Msg -> Model -> ( Model, Cmd Msg ) 51 | update msg model = 52 | case msg of 53 | LinkClicked urlRequest -> 54 | case urlRequest of 55 | Browser.Internal url -> 56 | ( model, Nav.pushUrl model.key (Url.toString url) ) 57 | 58 | Browser.External href -> 59 | ( model, Nav.load href ) 60 | 61 | UrlChanged url -> 62 | ( { model | url = url } 63 | , Cmd.none 64 | ) 65 | 66 | 67 | 68 | -- SUBSCRIPTIONS 69 | 70 | 71 | subscriptions : Model -> Sub Msg 72 | subscriptions _ = 73 | Sub.none 74 | 75 | 76 | 77 | -- VIEW 78 | 79 | 80 | view : Model -> Browser.Document Msg 81 | view model = 82 | { title = "URL Interceptor" 83 | , body = 84 | [ text "The current URL is: " 85 | , b [] [ text (Url.toString model.url) ] 86 | , ul [] 87 | [ viewLink "/home" 88 | , viewLink "/profile" 89 | , viewLink "/reviews/the-century-of-the-self" 90 | , viewLink "/reviews/public-opinion" 91 | , viewLink "/reviews/shah-of-shahs" 92 | ] 93 | ] 94 | } 95 | 96 | 97 | viewLink : String -> Html msg 98 | viewLink path = 99 | li [] [ a [ href path ] [ text path ] ] 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elm App Generator 2 | 3 | Generate an Elm app, with only the parts that you need, and no hidden 4 | configuration. 5 | 6 | ## Getting Started 7 | 8 | ### Installation 9 | 10 | #### NPM 11 | 12 | ``` 13 | $ npm install --global elm-app-gen 14 | ``` 15 | 16 | #### Yarn 17 | 18 | ``` 19 | $ yarn global add elm-app-gen 20 | ``` 21 | 22 | ### Create your project 23 | 24 | In the parent directory of your (soon-to-be) project: 25 | 26 | ``` 27 | $ elm-app-gen yourProjectName 28 | ``` 29 | 30 | You'll be prompted to provide some information about your project, such as a 31 | license, description, and they type of Elm program to generate. When you're done, 32 | the new app is created in a directory based on the name you provided. It will 33 | contain a README with instructions on how to start a live server and perform 34 | other development tasks. 35 | 36 | ## QuickStart 37 | 38 | If you want to start coding as quickly as possible, you can run 39 | 40 | ``` 41 | $ elm-app-gen quickstart yourProjectName 42 | ``` 43 | 44 | This will create a an application with default settings and immediately 45 | start an application server. 46 | 47 | ## What's included in a new project? 48 | 49 | Elm App Generator creates a project for you that includes: 50 | 51 | - [Elm](https://elm-lang.org) 52 | - [Elm Test](https://package.elm-lang.org/packages/elm-exploration/test/latest) 53 | - [Parcel](https://parceljs.org) 54 | - [`elm-debug-transformer`](https://github.com/kraklin/elm-debug-transformer) 55 | 56 | The list of initial dependencies is intentionally small to keep your app simple 57 | until it needs more features and tools. 58 | 59 | ## Project Goals 60 | 61 | ### Simple 62 | 63 | Elm App Generator creates apps that only contain the tools you need to start working 64 | on your project. It won't make assumptions about what you're trying to do, 65 | other than building an app with Elm. Where multiple tools are available for a 66 | particular task, Elm App Generator opts for the simpler choice. 67 | 68 | ### Friendly 69 | 70 | Elm App Generator always tries to provide useful context when asking users to make 71 | decisions. When the user needs to take additional steps, it will describe them when 72 | it runs, and include them in the documentation for the generated application. 73 | Generated apps contain links to documentation for the libraries and tools in use. 74 | 75 | ### Explicit 76 | 77 | There's no hidden configuration in the generated application. Some other tools 78 | hide a lot of configuration behind the scenes, which can be overwhelming when 79 | it's finally exposed. Elm App Generator exposes all of you project to you up front, so 80 | nothing is a mystery. 81 | -------------------------------------------------------------------------------- /lib/options-parser.js: -------------------------------------------------------------------------------- 1 | const commander = require("commander"); 2 | 3 | const errors = []; 4 | 5 | function parseOptions(cliOpts) { 6 | addCliOptions(cliOpts); 7 | 8 | commander.parse(process.argv); 9 | 10 | let parsedOpts = { 11 | prompt: getCliOption("prompt") 12 | }; 13 | 14 | if (cliOpts.arg && commander.args[0]) { 15 | parsedOpts[cliOpts.arg.name] = commander.args[0]; 16 | } 17 | 18 | cliOpts.options.forEach(opt => { 19 | parsedOpts[opt.name] = getCliOption(opt.name); 20 | }); 21 | 22 | if (!parsedOpts.prompt) { 23 | validateRequiredCliOptions(cliOpts, parsedOpts); 24 | } 25 | 26 | handleErrors(); 27 | 28 | return parsedOpts; 29 | } 30 | 31 | function addCliOptions(cliOpts) { 32 | if (cliOpts.arg) { 33 | commander.usage(`[options] <${cliOpts.arg.name}>`); 34 | } 35 | 36 | cliOpts.options.forEach(addCliOption); 37 | commander.option("--no-prompt", "Don't prompt for unknown options"); 38 | 39 | if (cliOpts.helpMessage) { 40 | commander.on("--help", function() { 41 | console.log(cliOpts.helpMessage); 42 | }); 43 | } 44 | } 45 | 46 | function addCliOption(opt) { 47 | if (opt.choices) { 48 | commander.option( 49 | `--${opt.name} <${opt.name}>`, 50 | `${opt.description} (${opt.choices.join("/")})`, 51 | choiceValidator(opt.name, opt.choices) 52 | ); 53 | } else if (opt.confirm) { 54 | commander.option(`--${opt.name}`, opt.description); 55 | } else { 56 | commander.option(`--${opt.name} <${opt.name}>`, opt.description); 57 | } 58 | } 59 | 60 | function choiceValidator(name, choices) { 61 | return input => { 62 | let normalized = input.toLowerCase(); 63 | if (choices.indexOf(normalized) >= 0) { 64 | return normalized; 65 | } else { 66 | errors.push(`The --${name} option must be one of ${choices.join("/")}`); 67 | } 68 | }; 69 | } 70 | 71 | function getCliOption(name) { 72 | let maybeOption = commander[name]; 73 | if (maybeOption && typeof maybeOption != "function") { 74 | return maybeOption; 75 | } 76 | } 77 | 78 | function validateRequiredCliOptions(cliOpts, parsedOpts) { 79 | if (cliOpts.arg.ifNotSet && !parsedOpts[cliOpts.arg.name]) { 80 | errors.push(cliOpts.arg.ifNotSet); 81 | } 82 | 83 | cliOpts.options.forEach(opt => { 84 | if (opt.ifNotSet && !parsedOpts[opt.name]) { 85 | errors.push(opt.ifNotSet); 86 | } 87 | }); 88 | } 89 | 90 | function handleErrors() { 91 | if (errors.length > 0) { 92 | console.error(` 93 | There were some problems with your selected options: 94 | ${errors.join("\n ")} 95 | `); 96 | 97 | process.exit(1); 98 | } 99 | } 100 | 101 | module.exports = { 102 | parse: parseOptions 103 | }; 104 | -------------------------------------------------------------------------------- /generators/app/templates/elm/document/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import Http 8 | import Json.Decode exposing (Decoder, field, string) 9 | 10 | 11 | 12 | -- MAIN 13 | 14 | 15 | main : Program () Model Msg 16 | main = 17 | Browser.document 18 | { init = init 19 | , update = update 20 | , subscriptions = subscriptions 21 | , view = viewDocument 22 | } 23 | 24 | 25 | 26 | -- MODEL 27 | 28 | 29 | type Model 30 | = Failure 31 | | Loading 32 | | Success String 33 | 34 | 35 | init : () -> ( Model, Cmd Msg ) 36 | init _ = 37 | ( Loading, getRandomCatGif ) 38 | 39 | 40 | 41 | -- UPDATE 42 | 43 | 44 | type Msg 45 | = MorePlease 46 | | GotGif (Result Http.Error String) 47 | 48 | 49 | update : Msg -> Model -> ( Model, Cmd Msg ) 50 | update msg model = 51 | case msg of 52 | MorePlease -> 53 | ( Loading, getRandomCatGif ) 54 | 55 | GotGif result -> 56 | case result of 57 | Ok url -> 58 | ( Success url, Cmd.none ) 59 | 60 | Err _ -> 61 | ( Failure, Cmd.none ) 62 | 63 | 64 | 65 | -- SUBSCRIPTIONS 66 | 67 | 68 | subscriptions : Model -> Sub Msg 69 | subscriptions model = 70 | Sub.none 71 | 72 | 73 | 74 | -- VIEW 75 | 76 | 77 | viewDocument : Model -> Browser.Document Msg 78 | viewDocument model = 79 | { title = "Some cats", body = [ view model ] } 80 | 81 | 82 | view : Model -> Html Msg 83 | view model = 84 | div [] 85 | [ h2 [] [ text "Random Cats" ] 86 | , viewGif model 87 | ] 88 | 89 | 90 | viewGif : Model -> Html Msg 91 | viewGif model = 92 | case model of 93 | Failure -> 94 | div [] 95 | [ text "I could not load a random cat for some reason. " 96 | , button [ onClick MorePlease ] [ text "Try Again!" ] 97 | ] 98 | 99 | Loading -> 100 | text "Loading..." 101 | 102 | Success url -> 103 | div [] 104 | [ button [ onClick MorePlease, style "display" "block" ] [ text "More Please!" ] 105 | , img [ src url ] [] 106 | ] 107 | 108 | 109 | 110 | -- HTTP 111 | 112 | 113 | getRandomCatGif : Cmd Msg 114 | getRandomCatGif = 115 | Http.get 116 | { url = "https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=cat" 117 | , expect = Http.expectJson GotGif gifDecoder 118 | } 119 | 120 | 121 | gifDecoder : Decoder String 122 | gifDecoder = 123 | field "data" (field "image_url" string) 124 | -------------------------------------------------------------------------------- /generators/app/templates/parcel/README.md: -------------------------------------------------------------------------------- 1 | # <%= name %> 2 | 3 | <% if (locals.description) { -%> 4 | <%= description %> 5 | <% } -%> 6 | 7 | ## Getting Started 8 | 9 | ### Install Dependencies 10 | 11 | `<%= installer %> install` 12 | 13 | ### Running Locally 14 | 15 | `<%= installer %> start` 16 | 17 | Will compile your app and serve it from http://localhost:1234/ 18 | Changes to your source code will trigger a hot-reload in the browser, which 19 | will also show compiler errors on build failures. 20 | 21 | ### Running Tests 22 | 23 | `<%= installer %> test` 24 | 25 | or 26 | 27 | `<%= installer %> run autotest` 28 | 29 | To re-run tests when files change. 30 | 31 | ### Production build 32 | 33 | `<%= installer %> run build` 34 | 35 | Will generate a production-ready build of your app in the `dist` folder. 36 | 37 | ### Elm Commands 38 | 39 | Elm binaries can be found in `node_modules/.bin`. They can be run from within 40 | your project via <% if (installer == 'npm') { %> `npx` 41 | <% } else if (installer == 'yarn') { %> `yarn run` <% } %> 42 | 43 | To install new Elm packages, run: 44 | 45 | <% if (installer == 'npm') { -%> 46 | `npx elm install ` 47 | <% } else if (installer == 'yarn') { -%> 48 | `yarn run elm install ` 49 | <% } -%> 50 | 51 | ## Libraries & Tools 52 | 53 | These are the main libraries and tools used to build <%= name %>. If you're not 54 | sure how something works, getting more familiar with these might help. 55 | 56 | ### [Elm](https://elm-lang.org) 57 | 58 | Elm is a delightful language for creating reliable webapps. It guarantees no 59 | runtime exceptions, and provides excellent performance. If you're not familiar 60 | with it, [the official guide](https://guide.elm-lang.org) is a great place to get 61 | started, and the folks on [Slack](https://elmlang.herokuapp.com) and 62 | [Discourse](https://discourse.elm-lang.org) are friendly and helpful if you get 63 | stuck. 64 | 65 | ### [Elm Test](https://package.elm-lang.org/packages/elm-exploration/test/latest) 66 | 67 | This is the standard testing library for Elm. In addition to being useful for 68 | traditional fixed-input unit tests, it also supports property-based testing 69 | where random data is used to validate behavior over a large input space. It's 70 | really useful! 71 | 72 | ### [Parcel](https://parceljs.org) 73 | 74 | Parcel build and bundles the application's assets into individual HTML, CSS, and 75 | JavaScript files. It also runs the live-server used during development. 76 | 77 | ### [`elm-debug-transform`](https://github.com/kraklin/elm-debug-transformer) 78 | 79 | This is a simple tool for improving the output of `Debug.log` statements. 80 | It applies some nice formatting for elm data structures. When you do a 81 | `parcel build` to produce your prod bundle, this won't be wired in. 82 | Read more in this discourse post: https://discourse.elm-lang.org/t/nicer-debug-log-console-output/3780. 83 | -------------------------------------------------------------------------------- /generators/app/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const Generator = require("yeoman-generator"); 4 | const promptBuilder = require("../../lib/prompt-builder.js"); 5 | const createOpts = require("../../cli/create-options.js"); 6 | const spawn = require("cross-spawn"); 7 | 8 | module.exports = class extends Generator { 9 | async prompting() { 10 | this.answers = this.options.cliOpts; 11 | 12 | if (this.options.cliOpts.prompt) { 13 | let prompts = promptBuilder.build(createOpts, this.options.cliOpts); 14 | let responses = await this.prompt(prompts); 15 | Object.assign(this.answers, responses); 16 | } 17 | 18 | // Make destinationRoot() the directory with the name of the app, 19 | // not the directory where the command was run 20 | const appPath = path.join(this.destinationRoot(), this.answers.name); 21 | this.destinationRoot(appPath); 22 | } 23 | 24 | writing() { 25 | const templateDirectory = `elm/${this.answers.type}`; 26 | const destDir = this.answers.name; 27 | 28 | const props = this.answers; 29 | 30 | //--- ELM 31 | this.fs.copyTpl( 32 | this.templatePath(`${templateDirectory}/_elm.json`), 33 | this.destinationPath(`elm.json`), 34 | props 35 | ); 36 | 37 | this.fs.copy( 38 | this.templatePath(`${templateDirectory}/src`), 39 | this.destinationPath(`src`) 40 | ); 41 | 42 | this.fs.copy( 43 | this.templatePath(`${templateDirectory}/tests`), 44 | this.destinationPath(`tests`) 45 | ); 46 | 47 | //--- PARCEL 48 | this.fs.copyTpl( 49 | this.templatePath("parcel/style.css"), 50 | this.destinationPath(`src/css/style.css`), 51 | props 52 | ); 53 | 54 | this.fs.copyTpl( 55 | this.templatePath("parcel/README.md"), 56 | this.destinationPath(`README.md`), 57 | props 58 | ); 59 | 60 | this.fs.copyTpl( 61 | this.templatePath("parcel/index.html"), 62 | this.destinationPath(`src/index.html`), 63 | props 64 | ); 65 | 66 | this.fs.copyTpl( 67 | this.templatePath("parcel/app.js"), 68 | this.destinationPath(`src/js/app.js`), 69 | props 70 | ); 71 | 72 | this.fs.copyTpl( 73 | this.templatePath("parcel/package.json"), 74 | this.destinationPath(`package.json`), 75 | props 76 | ); 77 | 78 | this.fs.copyTpl( 79 | this.templatePath("parcel/gitignore"), 80 | this.destinationPath(`.gitignore`), 81 | props 82 | ); 83 | } 84 | 85 | async install() { 86 | if (this.answers.start) { 87 | await installAndRun(this.destinationRoot(), this.answers); 88 | } else { 89 | const installer = this.answers.installer; 90 | 91 | await this.installDependencies({ 92 | bower: false, 93 | npm: installer === "npm", 94 | yarn: installer === "yarn" 95 | }); 96 | } 97 | } 98 | 99 | end() { 100 | // Skip this part if the server is running. 101 | if (!this.answers.start) { 102 | sayFinished(this.destinationRoot()); 103 | } 104 | } 105 | }; 106 | 107 | async function installAndRun(appPath, options) { 108 | const installer = options.installer; 109 | 110 | const parcelPath = require.resolve(".bin/parcel"); 111 | const indexPath = path.join(appPath, "src/index.html"); 112 | 113 | let installComplete = false; 114 | 115 | say(`\nStarting development server...`); 116 | const parcelProc = spawn(parcelPath, [indexPath], { 117 | cwd: appPath, 118 | stdio: "inherit" 119 | }); 120 | 121 | const installProc = spawn(installer, ["install"], { 122 | cwd: appPath, 123 | stdio: "ignore" 124 | }); 125 | 126 | installProc.on("exit", () => { 127 | installComplete = true; 128 | }); 129 | 130 | process.once("SIGINT", () => { 131 | if (!installComplete) { 132 | say(` 133 | 134 | Your project is ready to go, but I wasn't able to finish installing dependencies 135 | in the background while you were working. You'll need to run '${installer} install' 136 | yourself to complete the process. The generated README.md in ${appPath} 137 | contains instructions for running the live server, tests, etc. 138 | Have fun! 139 | `); 140 | } else { 141 | sayFinished(appPath); 142 | } 143 | }); 144 | } 145 | 146 | function say(str) { 147 | console.log(str); 148 | } 149 | 150 | function sayFinished(appPath) { 151 | say(` 152 | You're all set. The generated README.md in ${appPath} contains 153 | instructions for running the live server, tests, etc. 154 | Have fun!`); 155 | } 156 | --------------------------------------------------------------------------------