├── 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 |
--------------------------------------------------------------------------------