├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── generators ├── app │ ├── index.js │ └── templates │ │ ├── _package.json │ │ ├── editorconfig │ │ ├── gitignore │ │ ├── license │ │ ├── readme.md │ │ ├── scripts │ │ ├── build-prod.sh │ │ └── dev-server.sh │ │ ├── src │ │ ├── components │ │ │ ├── site-footer.js │ │ │ ├── site-header.js │ │ │ ├── todo-filter.js │ │ │ ├── todo-form.js │ │ │ ├── todo-item.js │ │ │ └── todo-list.js │ │ ├── index.js │ │ ├── models │ │ │ └── todos.js │ │ ├── routes.js │ │ ├── styles │ │ │ └── main.css │ │ └── views │ │ │ ├── about.js │ │ │ ├── home.js │ │ │ └── todos.js │ │ └── static │ │ └── index.html ├── component │ ├── index.js │ └── templates │ │ └── _component.js ├── element │ ├── index.js │ └── templates │ │ └── _element.js ├── generate.js ├── model │ ├── index.js │ └── templates │ │ └── _model.js └── view │ ├── index.js │ └── templates │ └── _view.js ├── license ├── package.json ├── readme.md └── tests └── generators ├── app.js ├── component.js ├── element.js ├── modal.js └── view.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | dist 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | Contact GitHub API Training Shop Blog About 41 | 42 | # Mac OSX 43 | *.DS_Store 44 | .AppleDouble 45 | .LSOverride 46 | 47 | # Icon must end with two \r 48 | Icon 49 | 50 | 51 | # Thumbnails 52 | ._* 53 | 54 | # Files that might appear in the root of a volume 55 | .DocumentRevisions-V100 56 | .fseventsd 57 | .Spotlight-V100 58 | .TemporaryItems 59 | .Trashes 60 | .VolumeIcon.icns 61 | .com.apple.timemachine.donotpresent 62 | 63 | # Directories potentially created on remote AFP share 64 | .AppleDB 65 | .AppleDesktop 66 | Network Trash Folder 67 | Temporary Items 68 | .apdisk 69 | 70 | 71 | # Linux 72 | 73 | # temporary files which can be created if a process still has a handle open of a deleted file 74 | .fuse_hidden* 75 | 76 | # KDE directory preferences 77 | .directory 78 | 79 | # Linux trash folder which might appear on any partition or disk 80 | .Trash-* 81 | 82 | # Windows 83 | 84 | # Windows image file caches 85 | Thumbs.db 86 | ehthumbs.db 87 | 88 | # Folder config file 89 | Desktop.ini 90 | 91 | # Recycle Bin used on file shares 92 | $RECYCLE.BIN/ 93 | 94 | # Windows Installer files 95 | *.cab 96 | *.msi 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | # VS Code 104 | .vscode 105 | 106 | #IntelliJ 107 | 108 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 109 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 110 | 111 | # User-specific stuff: 112 | .idea/workspace.xml 113 | .idea/tasks.xml 114 | .idea/dictionaries 115 | .idea/vcs.xml 116 | .idea/jsLibraryMappings.xml 117 | 118 | # Sensitive or high-churn files: 119 | .idea/dataSources.ids 120 | .idea/dataSources.xml 121 | .idea/dataSources.local.xml 122 | .idea/sqlDataSources.xml 123 | .idea/dynamic.xml 124 | .idea/uiDesigner.xml 125 | 126 | # Gradle: 127 | .idea/gradle.xml 128 | .idea/libraries 129 | 130 | # Mongo Explorer plugin: 131 | .idea/mongoSettings.xml 132 | 133 | ## File-based project format: 134 | *.iws 135 | 136 | ## Plugin-specific files: 137 | 138 | # IntelliJ 139 | /out/ 140 | 141 | # mpeltonen/sbt-idea plugin 142 | .idea_modules/ 143 | 144 | # JIRA plugin 145 | atlassian-ide-plugin.xml 146 | 147 | # Crashlytics plugin (for Android Studio and IntelliJ) 148 | com_crashlytics_export_strings.xml 149 | crashlytics.properties 150 | crashlytics-build.properties 151 | fabric.properties 152 | node_modules 153 | temp 154 | npm-debug.log 155 | .vscode 156 | .idea 157 | .history 158 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /generators/app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const yeoman = require('yeoman-generator') 3 | const kebab = require('lodash.kebabcase') 4 | 5 | module.exports = yeoman.Base.extend({ 6 | prompting: function () { 7 | return this.prompt([{ 8 | name: 'projectName', 9 | message: 'What do you want to name your project?', 10 | default: this.appname.replace(/\s/g, '-'), 11 | filter: x => kebab(x) 12 | }, { 13 | name: 'projectDescription', 14 | message: 'A brief description of your app?', 15 | default: '' 16 | }, { 17 | name: 'githubUsername', 18 | message: 'What is your GitHub username?', 19 | store: true, 20 | validate: x => x.length > 0 ? true : 'You have to provide a username' 21 | }]).then(props => 22 | this.config.set({ 23 | template: Object.assign({}, props, { 24 | name: this.user.git.name(), 25 | email: this.user.git.email() 26 | }) 27 | }) 28 | ) 29 | }, 30 | writing: function () { 31 | const mv = (from, to) => { 32 | this.fs.move(this.destinationPath(from), this.destinationPath(to)) 33 | } 34 | 35 | this.fs.copyTpl([ 36 | `${this.templatePath()}/**` 37 | ], this.destinationPath(), this.config.get('template')) 38 | mv('editorconfig', '.editorconfig') 39 | mv('gitignore', '.gitignore') 40 | mv('_package.json', 'package.json') 41 | }, 42 | install: function () { 43 | this.spawnCommandSync('git', ['init']) 44 | this.installDependencies({ bower: false }) 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /generators/app/templates/_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= projectName %>", 3 | "version": "0.0.0", 4 | "description": "<%= projectDescription %>", 5 | "license": "MIT", 6 | "repository": "<%= githubUsername %>/<%= projectName %>", 7 | "scripts": { 8 | "start": "npm run dev:server", 9 | "predeploy": "npm test && npm run build:prod", 10 | "deploy": "surge dist || npm i surge && surge dist", 11 | "dev:server": "scripts/dev-server.sh", 12 | "build:prod": "scripts/build-prod.sh", 13 | "lint": "standard --verbose | snazzy", 14 | "test": "npm run lint" 15 | }, 16 | "dependencies": { 17 | "choo": "^3.2.0" 18 | }, 19 | "devDependencies": { 20 | "browserify": "^13.0.1", 21 | "budo": "8.3.0", 22 | "choo-log": "^1.4.0", 23 | "es2040": "1.2.2", 24 | "envify": "^3.4.1", 25 | "normalize.css": "^4.2.0", 26 | "sheetify": "^5.0.3", 27 | "standard": "^7.1.2", 28 | "snazzy": "^4.0.0", 29 | "uglifyify": "^3.0.2", 30 | "unassertify": "^2.0.3", 31 | "yo-yoify": "^3.3.0" 32 | }, 33 | "standard": { 34 | "ignore": [ 35 | "scripts" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /generators/app/templates/editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /generators/app/templates/gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | dist 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | Contact GitHub API Training Shop Blog About 41 | 42 | # Mac OSX 43 | *.DS_Store 44 | .AppleDouble 45 | .LSOverride 46 | 47 | # Icon must end with two \r 48 | Icon 49 | 50 | 51 | # Thumbnails 52 | ._* 53 | 54 | # Files that might appear in the root of a volume 55 | .DocumentRevisions-V100 56 | .fseventsd 57 | .Spotlight-V100 58 | .TemporaryItems 59 | .Trashes 60 | .VolumeIcon.icns 61 | .com.apple.timemachine.donotpresent 62 | 63 | # Directories potentially created on remote AFP share 64 | .AppleDB 65 | .AppleDesktop 66 | Network Trash Folder 67 | Temporary Items 68 | .apdisk 69 | 70 | 71 | # Linux 72 | 73 | # temporary files which can be created if a process still has a handle open of a deleted file 74 | .fuse_hidden* 75 | 76 | # KDE directory preferences 77 | .directory 78 | 79 | # Linux trash folder which might appear on any partition or disk 80 | .Trash-* 81 | 82 | # Windows 83 | 84 | # Windows image file caches 85 | Thumbs.db 86 | ehthumbs.db 87 | 88 | # Folder config file 89 | Desktop.ini 90 | 91 | # Recycle Bin used on file shares 92 | $RECYCLE.BIN/ 93 | 94 | # Windows Installer files 95 | *.cab 96 | *.msi 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | # VS Code 104 | .vscode 105 | 106 | #IntelliJ 107 | 108 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 109 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 110 | 111 | # User-specific stuff: 112 | .idea/workspace.xml 113 | .idea/tasks.xml 114 | .idea/dictionaries 115 | .idea/vcs.xml 116 | .idea/jsLibraryMappings.xml 117 | 118 | # Sensitive or high-churn files: 119 | .idea/dataSources.ids 120 | .idea/dataSources.xml 121 | .idea/dataSources.local.xml 122 | .idea/sqlDataSources.xml 123 | .idea/dynamic.xml 124 | .idea/uiDesigner.xml 125 | 126 | # Gradle: 127 | .idea/gradle.xml 128 | .idea/libraries 129 | 130 | # Mongo Explorer plugin: 131 | .idea/mongoSettings.xml 132 | 133 | ## File-based project format: 134 | *.iws 135 | 136 | ## Plugin-specific files: 137 | 138 | # IntelliJ 139 | /out/ 140 | 141 | # mpeltonen/sbt-idea plugin 142 | .idea_modules/ 143 | 144 | # JIRA plugin 145 | atlassian-ide-plugin.xml 146 | 147 | # Crashlytics plugin (for Android Studio and IntelliJ) 148 | com_crashlytics_export_strings.xml 149 | crashlytics.properties 150 | crashlytics-build.properties 151 | fabric.properties -------------------------------------------------------------------------------- /generators/app/templates/license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) <%= name %> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /generators/app/templates/readme.md: -------------------------------------------------------------------------------- 1 | # <%= projectName %> 2 | *<%= projectDescription %>* 3 | 4 | ## Contributing 5 | -------------------------------------------------------------------------------- /generators/app/templates/scripts/build-prod.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Clean distribution directory. 4 | rm -rf dist && mkdir dist && mkdir dist/js 5 | # Copy static files to distribution. 6 | cp -r static/* dist 7 | 8 | # Duplicate index.html as 200.html for Surge pushState routing. 9 | cp static/index.html dist/200.html 10 | 11 | # Bundle the main js file. 12 | 13 | # add -d switch for sourcemapping and debugging production. 14 | NODE_ENV=production browserify -e src/index.js -o dist/js/main.js \ 15 | -t envify \ 16 | -t sheetify/transform \ 17 | -g yo-yoify \ 18 | -g unassertify \ 19 | -g es2040 \ 20 | -g uglifyify | uglifyjs 21 | 22 | echo 'Built dist directory' 23 | -------------------------------------------------------------------------------- /generators/app/templates/scripts/dev-server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | NODE_ENV=development budo src/index.js:js/main.js --live \ 4 | --open \ 5 | --host localhost \ 6 | --dir static \ 7 | --pushstate \ 8 | --title <%= projectName %> \ 9 | --port 3000 \ 10 | -- -t sheetify/transform -g es2040 11 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/site-footer.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | 3 | module.exports = (state, prev, send) => html` 4 | 32 | ` 33 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/site-header.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | 3 | module.exports = (state, prev, send) => html` 4 | 17 | ` 18 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/todo-filter.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | const links = [ 3 | { 4 | label: 'All', 5 | action: 'SHOW_ALL' 6 | }, 7 | { 8 | label: 'Active', 9 | action: 'SHOW_ACTIVE' 10 | }, 11 | { 12 | label: 'Completed', 13 | action: 'SHOW_COMPLETED' 14 | } 15 | ] 16 | 17 | module.exports = (state, prev, send) => html` 18 |
19 | Show: 20 | ${links.map(link => { 21 | if (link.action === state.todoDemo.visibility) { 22 | return html`${link.label}` 23 | } 24 | return html` 25 | 26 | { 27 | e.preventDefault() 28 | send('todoDemo:visibilityFilter', { visibility: link.action }) 29 | }}>${link.label} 30 | 31 | ` 32 | })} 33 |
` 34 | 35 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/todo-form.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | 3 | module.exports = (state, prev, send) => html` 4 |
{ 5 | e.preventDefault() 6 | const input = document.getElementById('addTodo') 7 | 8 | if (!input.value.trim()) { 9 | return 10 | } 11 | send('todoDemo:add', {text: input.value}) 12 | input.value = '' 13 | }}> 14 | 15 | 16 |
17 | ` 18 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/todo-item.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | 3 | module.exports = ({ done, text }, onClick) => html` 4 |
  • 5 | ${text} 6 |
  • 7 | ` 8 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/todo-list.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | const todo = require('./todo-item') 3 | 4 | module.exports = (todos = [], send) => html` 5 | 15 | ` 16 | -------------------------------------------------------------------------------- /generators/app/templates/src/index.js: -------------------------------------------------------------------------------- 1 | const sf = require('sheetify') 2 | const choo = require('choo') 3 | 4 | sf('normalize.css', { global: true }) 5 | sf('./styles/main.css', { global: true }) 6 | 7 | const app = choo() 8 | if (process.env.NODE_ENV !== 'production') { 9 | const log = require('choo-log') 10 | app.use(log()) 11 | } 12 | 13 | app.model(require('./models/todos')) 14 | app.router(require('./routes')) 15 | 16 | const tree = app.start() 17 | 18 | document.body.appendChild(tree) 19 | -------------------------------------------------------------------------------- /generators/app/templates/src/models/todos.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | namespace: 'todoDemo', 3 | state: { 4 | todos: [{ 5 | id: 1, 6 | text: 'Learn Choo', 7 | done: false 8 | }], 9 | visibility: 'SHOW_ALL' 10 | }, 11 | subscriptions: [], 12 | reducers: { 13 | add: (action, state) => ({ 14 | todos: state.todos.concat({ 15 | id: state.todos.length + 1, 16 | text: action.text, 17 | done: false 18 | }) 19 | }), 20 | toggle: (action, state) => ({ 21 | todos: state.todos.map(todo => 22 | todo.id === action.id ? Object.assign({}, todo, { done: !todo.done }) : todo 23 | ) 24 | }), 25 | visibilityFilter: ({visibility}) => ({ 26 | visibility 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /generators/app/templates/src/routes.js: -------------------------------------------------------------------------------- 1 | module.exports = (route) => [ 2 | route('/todos', require('./views/todos')), 3 | route('/about', require('./views/about')), 4 | route('/', require('./views/home')) 5 | ] 6 | -------------------------------------------------------------------------------- /generators/app/templates/src/styles/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | color: #393939; 3 | } 4 | 5 | a { 6 | color: #b30816; 7 | text-decoration: none; 8 | transition: color 0.3s; 9 | } 10 | 11 | a:hover { 12 | text-decoration: underline; 13 | color: #8a3139; 14 | } 15 | 16 | code { 17 | background: #eee; 18 | color: #8a3139; 19 | padding-left: 2px; 20 | padding-right: 2px; 21 | } 22 | 23 | body { 24 | background: #eee; 25 | } 26 | 27 | .container { 28 | margin: 0 auto; 29 | padding: 0; 30 | max-width: 960px; 31 | width: 80%; 32 | } 33 | 34 | .card { 35 | box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12); 36 | } 37 | 38 | .well { 39 | padding: 0.5em; 40 | border: thin solid #bbb; 41 | } 42 | .site-header { 43 | clear: both; 44 | display: block; 45 | margin-bottom: 2em; 46 | height: 50px; 47 | } 48 | .site-header nav { 49 | float: right; 50 | } 51 | 52 | .site-header .container { 53 | clear:both; 54 | vertical-align: center; 55 | overflow: hidden; 56 | line-height: 50px; 57 | max-height: 50px; 58 | padding: 0; 59 | } 60 | 61 | .site-header nav a { 62 | display: inline-block; 63 | min-width: 50px; 64 | text-align: center; 65 | font-size: 13px; 66 | line-height: 50px; 67 | margin-left: 1em; 68 | text-transform: uppercase; 69 | } 70 | 71 | .site-header .brand { 72 | display: inline-block; 73 | color: #666; 74 | } 75 | 76 | .site-header .brand * { 77 | line-height: 50px; 78 | font-size: 22px; 79 | float: left; 80 | margin: 0 0.6em 0 0; 81 | color: inherit; 82 | } 83 | 84 | .site-header .brand:after { 85 | clear: both; 86 | } 87 | 88 | .color-white { 89 | background:white; 90 | } 91 | 92 | .color-white.darken-1 { 93 | background: #eee; 94 | } 95 | 96 | .color-white.darken-2 { 97 | background: #ddd; 98 | } 99 | 100 | .center-text { 101 | text-align: center; 102 | } 103 | 104 | .row { 105 | margin-right: -25px; 106 | margin-left: -25px; 107 | display: block; 108 | overflow: hidden; 109 | } 110 | 111 | .col-half, 112 | .col-two-thirds, 113 | .col-one-third { 114 | float: left; 115 | position: relative; 116 | min-height: 1px; 117 | margin: 0; 118 | } 119 | 120 | .col-half { 121 | width: 50%; 122 | } 123 | .col-one-third { 124 | width: 33%; 125 | } 126 | .col-two-thirds { 127 | width: 66%; 128 | } 129 | 130 | .panel { 131 | margin: 1em; 132 | padding: 1em; 133 | } 134 | 135 | .content { 136 | margin-top: 1em; 137 | } 138 | 139 | p { 140 | line-height: 1.2; 141 | } 142 | 143 | .footer-meta { 144 | overflow: hidden; 145 | height: 50px; 146 | line-height: 50px; 147 | color: rgba(255,255,255,0.8); 148 | background-color: #222; 149 | } 150 | 151 | .site-footer { 152 | margin-top: 20px; 153 | padding: 10px 0 0 0; 154 | background-color: #444; 155 | color: #fff; 156 | } 157 | 158 | .site-footer h5 { 159 | font-size: 18px; 160 | } 161 | 162 | .site-footer a { 163 | color: #eee; 164 | } 165 | 166 | .pull-right { 167 | float: right!important; 168 | } 169 | 170 | .pull-left { 171 | float: left!important; 172 | } 173 | -------------------------------------------------------------------------------- /generators/app/templates/src/views/about.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | const header = require('../components/site-header') 3 | const footer = require('../components/site-footer') 4 | 5 | module.exports = (state, prev, send) => html` 6 |
    7 | ${header()} 8 | 9 |
    10 |

    About

    11 | 12 |

    This is a simple about page to show that the routing works! :-)

    13 |

    The source code for this page exists at /src/views/about.js

    14 |

    Happy Coding!

    15 |

    🚂🚋🚋🚋

    16 |
    17 |
    18 | ${footer()} 19 |
    20 | ` 21 | -------------------------------------------------------------------------------- /generators/app/templates/src/views/home.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | const header = require('../components/site-header') 3 | const footer = require('../components/site-footer') 4 | 5 | const links = [{ 6 | label: 'Generator Docs', 7 | href: 'https://github.com/trainyard/generator-choo', 8 | info: 'How to use this generator' 9 | }, { 10 | label: 'Choo Documentation', 11 | href: 'https://github.com/yoshuawuyts/choo', 12 | info: 'The choo repo has some great documentation' 13 | }, { 14 | label: 'Awesome Choo', 15 | href: 'https://github.com/YerkoPalma/awesome-choo', 16 | info: 'A list of awesome things for choo' 17 | }] 18 | module.exports = (state, prev, send) => html` 19 |
    20 | ${header()} 21 |
    22 |

    Congrats!

    23 |
    24 |

    You did it!! Now to make something amazing

    25 |

    🚂🚋🚋🚋

    26 |
    27 |
    28 |
    29 |
    30 |
    31 |

    Some Tips:

    32 |

    33 |

      34 |
    1. You can see how this demo works by viewing the src folder.
    2. 35 |
    3. You can see how state management works by viewing the todos section of this site.
    4. 36 |
    5. You can remove the demo at any time using npm run remove-demo command.
    6. 37 |
    38 |

    39 |
    40 |
    41 |
    42 |
    43 |

    More Choo!

    44 |

    Check out these helpful links

    45 |

    46 |

      47 | ${links.map(({label, href, info}) => html` 48 |
    • 49 | 50 | ${label} 51 | - ${info} 52 |
    • `)} 53 |
    54 |

    55 |
    56 |
    57 |
    58 |
    59 | ${footer()} 60 |
    61 | ` 62 | -------------------------------------------------------------------------------- /generators/app/templates/src/views/todos.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | const header = require('../components/site-header') 3 | const footer = require('../components/site-footer') 4 | const todoList = require('../components/todo-list') 5 | const todoFilter = require('../components/todo-filter') 6 | const todoForm = require('../components/todo-form') 7 | 8 | const getVisibleTodos = (todos, filter) => { 9 | switch (filter) { 10 | case 'SHOW_ALL': 11 | return todos 12 | case 'SHOW_COMPLETED': 13 | return todos.filter(t => t.done) 14 | case 'SHOW_ACTIVE': 15 | return todos.filter(t => !t.done) 16 | } 17 | } 18 | 19 | module.exports = (state, prev, send) => html` 20 |
    21 | ${header()} 22 |
    23 |
    24 |

    Todos

    25 |

    Based on the Redux Example Implementation

    26 |
    27 |
    28 | ${todoForm(state, prev, send)} 29 | ${todoList(getVisibleTodos(state.todoDemo.todos, state.todoDemo.visibility), send)} 30 | ${todoFilter(state, prev, send)} 31 |
    32 |
    33 | ${footer()} 34 |
    35 | ` 36 | -------------------------------------------------------------------------------- /generators/app/templates/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= projectName %> 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /generators/component/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('yeoman-generator').Base.extend({ 2 | do: require('../generate')({ 3 | category: 'components', 4 | templateFileName: '_component.js' 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /generators/component/templates/_component.js: -------------------------------------------------------------------------------- 1 | /* Component: <%= name %> */ 2 | 3 | const html = require('choo/html') 4 | 5 | module.exports = (state, prev, send) => html` 6 |
    Your component here
    7 | ` 8 | -------------------------------------------------------------------------------- /generators/element/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('yeoman-generator').Base.extend({ 2 | do: require('../generate')({ 3 | category: 'elements', 4 | templateFileName: '_element.js' 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /generators/element/templates/_element.js: -------------------------------------------------------------------------------- 1 | /* Element: <%= name %> */ 2 | 3 | const html = require('choo/html') 4 | 5 | module.exports = (props) => html` 6 |
    Your element here
    7 | ` 8 | -------------------------------------------------------------------------------- /generators/generate.js: -------------------------------------------------------------------------------- 1 | const { resolve: resolvePath } = require('path') 2 | const kebab = require('lodash.kebabcase') 3 | 4 | module.exports = ({category, templateFileName}) => function () { 5 | const agent = this 6 | if (!agent.args[0]) { 7 | console.log('Missing paramater') 8 | process.exit(0) 9 | } 10 | agent.projectPath = agent.destinationRoot() || agent.config.get('projectPath') || '' 11 | agent.targetName = agent.args[0] 12 | agent.fileName = `${kebab(agent.targetName)}.js` 13 | agent.targetPath = resolvePath(agent.projectPath, 'src', category, agent.fileName) 14 | agent.fs.copyTpl( 15 | agent.templatePath(templateFileName), 16 | agent.destinationPath(agent.targetPath), 17 | { name: agent.targetName } 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /generators/model/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('yeoman-generator').Base.extend({ 2 | do: require('../generate')({ 3 | category: 'models', 4 | templateFileName: '_model.js' 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /generators/model/templates/_model.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /* namespace the model so that it cannot access any properties and handlers in other models */ 3 | namespace: '<%= name %>', 4 | state: { 5 | /* initial values of state inside the model */ 6 | // counter: 1 7 | }, 8 | reducers: { 9 | /* synchronous operations that modify state. Triggered by actions. Signature of (data, state). */ 10 | // add: (action, state) => ({ counter: state.counter + 1}) 11 | }, 12 | effects: { 13 | // asynchronous operations that don't modify state directly. 14 | // Triggered by actions, can call actions. Signature of (data, state, send, done) 15 | }, 16 | subscriptions: { 17 | // asynchronous read-only operations that don't modify state directly. 18 | // Can call actions. Signature of (send, done). 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /generators/view/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const esprima = require('esprima') 3 | const escodegen = require('escodegen') 4 | const fs = require('fs') 5 | 6 | module.exports = require('yeoman-generator').Base.extend({ 7 | scaffold: require('../generate')({ 8 | category: 'views', 9 | templateFileName: '_view.js' 10 | }), 11 | // Inject the view into the router 12 | end: function () { 13 | // adds a new route to the routes array, which looks like: 14 | // route('/myNewView', require('./views/myNewView')) 15 | const addRoute = `[route('/${this.targetName}', require('./views/${this.targetName}'))]` 16 | const injection = esprima.parse(addRoute).body[0].expression.elements[0] 17 | // if we cannot write to the routes for any reason we should fail silently, because 18 | // the view has been succesfully made. 19 | try { 20 | const routesPath = this.projectPath + '/src/routes.js' 21 | const routes = esprima.parse(require(routesPath)) 22 | const injectionTarget = routes.body[0].expression.body.elements 23 | 24 | // duplicates will be ignored 25 | if (injectionTarget.find(expr => expr.arguments[0].value === injection.arguments[0].value)) { 26 | this.log.identical('src/routes.js') 27 | process.exit(0) 28 | } 29 | injectionTarget.push(injection) 30 | const esCodeOptions = { 31 | format: { 32 | indent: { style: ' ' }, 33 | semicolons: false 34 | } 35 | } 36 | const data = 'module.exports = ' + escodegen.generate(routes, esCodeOptions) + '\n' 37 | 38 | // Purposely using node fs to avoid write conflict issue. 39 | fs.writeFile(routesPath, data, (err) => { 40 | if (err) { 41 | this.log.skip('src/routes.js') 42 | } 43 | this.log.ok('src/routes.js updated') 44 | }) 45 | } catch (e) { 46 | this.log.skip('src/routes.js') 47 | if (process.env.NODE_ENV === 'test') { 48 | throw e 49 | } 50 | process.exit(0) 51 | } 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /generators/view/templates/_view.js: -------------------------------------------------------------------------------- 1 | /* <%= name %> */ 2 | 3 | const html = require('choo/html') 4 | 5 | module.exports = (state, prev, send) => html` 6 |
    7 |

    <%= name %>

    8 |
    9 | ` 10 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Juan Soto (juansoto.me) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-choo", 3 | "version": "1.0.6", 4 | "description": "Scaffold a choo app", 5 | "license": "MIT", 6 | "repository": "trainyard/generator-choo", 7 | "contributors": [ 8 | { 9 | "name": "Juan Soto", 10 | "email": "juan@juansoto.me", 11 | "url": "http://juansoto.me" 12 | }, 13 | { 14 | "name": "Matt McFarland", 15 | "email": "contact@mattmcfarland.com", 16 | "url": "https://github.com/MattMcFarland" 17 | }, 18 | { 19 | "name": "Hemanth.HM", 20 | "email": "hemanth.hm@gmail.com", 21 | "url": "https://h3manth.com" 22 | }, 23 | { 24 | "name": "Ilya Radchenko", 25 | "email": "ilya@burstcreations.com", 26 | "url": "http://burstcreations.com/" 27 | }, 28 | { 29 | "name": "Ronn Ross", 30 | "email": "ronn.ross@gmail.com", 31 | "url": "https://github.com/ronnross" 32 | }, 33 | { 34 | "name": "Yerko Palma", 35 | "email": "yerko.palma@usach.cl", 36 | "url": "https://github.com/YerkoPalma" 37 | } 38 | ], 39 | "engines": { 40 | "node": ">=6.0.0" 41 | }, 42 | "scripts": { 43 | "prepublish": "npm test", 44 | "deploy": "surge -p dist || npm i surge -D && npm run deploy", 45 | "lint": "standard --verbose | snazzy", 46 | "clean": "rm -rf temp", 47 | "pretest": "npm run clean && npm run lint", 48 | "test": "NODE_ENV=test tape --require ./tests/**/*.js | tap-summary" 49 | }, 50 | "files": [ 51 | "generators", 52 | "license", 53 | "readme.md" 54 | ], 55 | "keywords": [ 56 | "yeoman-generator", 57 | "yeoman", 58 | "boilerplate", 59 | "template", 60 | "scaffold", 61 | "choo" 62 | ], 63 | "dependencies": { 64 | "escodegen": "^1.8.0", 65 | "esprima": "^2.7.2", 66 | "lodash.kebabcase": "^4.0.1", 67 | "yeoman-generator": "^0.24.1" 68 | }, 69 | "devDependencies": { 70 | "deep-equal": "^1.0.1", 71 | "eslint": "^3.1.1", 72 | "fs.extra": "^1.3.2", 73 | "snazzy": "^4.0.0", 74 | "standard": "^7.1.2", 75 | "tap-summary": "^3.0.1", 76 | "tape": "^4.6.0", 77 | "yeoman-assert": "^2.2.1", 78 | "yeoman-test": "^1.4.0" 79 | }, 80 | "standard": { 81 | "ignore": [ 82 | "scripts", 83 | "test.js" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # generator-choo 2 | [![npm](https://img.shields.io/npm/v/generator-choo.svg?maxAge=2592000)](https://www.npmjs.com/package/generator-choo) 3 | [![Build Status](https://travis-ci.org/trainyard/generator-choo.svg?branch=master)](https://travis-ci.org/trainyard/generator-choo) 4 | [![Coverage Status](https://coveralls.io/repos/github/trainyard/generator-choo/badge.svg?branch=master)](https://coveralls.io/github/trainyard/generator-choo?branch=master) 5 | ![MIT Licensed](https://img.shields.io/npm/l/generator-choo.svg) 6 | 7 | A [Yeoman](http://yeoman.io/) generator for [Choo](https://github.com/yoshuawuyts/choo). 8 | 9 | ### Installation 10 | 11 | Make sure you have [Yeoman](http://yeoman.io/) installed globally, then install the generator. 12 | 13 | ``` 14 | npm install -g yo 15 | npm install -g generator-choo 16 | ``` 17 | 18 | Awesome, you are now ready to use it! 19 | 20 | ### Usage 21 | 22 | To use, make a new directory, hop into it and then run `yo choo` 23 | 24 | ``` 25 | mkdir my-choo-app && cd my-choo-app && yo choo 26 | ``` 27 | 28 | Follow the prompts to victory!!! Once the process is complete your app should be ready, you can use the following commands: 29 | 30 | ### NPM Scripts 31 | 32 | - `npm start` -- Starts up a dev server with live reloading 33 | - `npm run build:prod` -- Builds a production ready index.html and bundle.js that you can serve remotely. 34 | - `npm run lint` -- Lints your code. 35 | - `npm run deploy` -- Deploy 36 | 37 | ### Scaffolding 38 | 39 | - `yo choo` -- Create a new choo app 40 | - `yo choo:view` -- Create a new view that is also wired up to the router 41 | - `yo choo:model` -- Create a new choo model. 42 | - `yo choo:component` -- Create a new choo component 43 | - `yo choo:element` -- Create a new choo element 44 | 45 | ### Architecture Generated 46 | Here's the architecture of the generated app. 47 | 48 | ``` 49 | dist/ <- Production ready, and auto-generated when you run build:prod 50 | src/ <- Source directory that is built 51 | components <- stateless (mostly) ui components that may be complex 52 | elements <- stateless elements (like components) but smaller 53 | models <- choo models 54 | styles <- css files 55 | views <- whole views or pages 56 | index.js <- main app/entry file 57 | routes.js <- router uses this, routes auto-added with yo choo:view 58 | static/ <- available files that will be in dev server and production server 59 | package.json <- npm package 60 | readme.md <- your readme file 61 | .gitignore <- gitignore 62 | ``` 63 | 64 | ### Similar Projects 65 | 66 | - [generator-choo-webpack](https://github.com/danneu/generator-choo-webpack) - the minimal choo + webpack yeoman project generator 67 | 68 | 69 | ### FYI 70 | 71 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](http://standardjs.com) 72 | 73 | -------------------------------------------------------------------------------- /tests/generators/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | 5 | const { resolve: resolvePath } = require('path') 6 | const { run } = require('yeoman-test') 7 | const { file: filesExist, fileContent } = require('yeoman-assert') 8 | 9 | test('choo:app', tap => run(resolvePath(__dirname, '../../generators/app')) 10 | .withPrompts({ projectName: 'test', githubUsername: 'test' }) 11 | .on('end', () => { 12 | tap.doesNotThrow(() => { 13 | filesExist([ 14 | '.editorconfig', 15 | '.git', 16 | '.gitignore', 17 | 'license', 18 | 'package.json', 19 | 'readme.md', 20 | 'scripts/build-prod.sh', 21 | 'scripts/dev-server.sh', 22 | 'src/index.js', 23 | 'src/routes.js', 24 | 'src/components/site-footer.js', 25 | 'src/components/site-header.js', 26 | 'src/components/todo-filter.js', 27 | 'src/components/todo-form.js', 28 | 'src/components/todo-item.js', 29 | 'src/components/todo-list.js', 30 | 'src/models/todos.js', 31 | 'src/styles/main.css', 32 | 'src/views/about.js', 33 | 'src/views/home.js', 34 | 'src/views/todos.js', 35 | 'static/index.html' 36 | ]) 37 | }, 'expected files should exist.') 38 | tap.doesNotThrow(() => 39 | fileContent('src/components/site-header.js', '

    test

    '), 'Expected file to contain name in heading') 40 | 41 | tap.end() 42 | }) 43 | ) 44 | -------------------------------------------------------------------------------- /tests/generators/component.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | 5 | const { resolve: resolvePath } = require('path') 6 | const { run } = require('yeoman-test') 7 | const { file: fileExists, fileContent } = require('yeoman-assert') 8 | 9 | test('choo:component', tap => run(resolvePath(__dirname, '../../generators/component')) 10 | .withArguments(['component-test']) 11 | .on('end', () => { 12 | const expectedPath = 'src/components/component-test.js' 13 | 14 | tap.doesNotThrow(() => 15 | fileExists(expectedPath), `Expected ${expectedPath} to exist.`) 16 | tap.doesNotThrow(() => 17 | fileContent(expectedPath, '/* Component: component-test */'), 'Expected file to contain name in heading') 18 | tap.end() 19 | }) 20 | ) 21 | -------------------------------------------------------------------------------- /tests/generators/element.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | 5 | const { resolve: resolvePath } = require('path') 6 | const { run } = require('yeoman-test') 7 | const { file: fileExists, fileContent } = require('yeoman-assert') 8 | 9 | test('choo:element', tap => run(resolvePath(__dirname, '../../generators/element')) 10 | .withArguments(['element-test']) 11 | .on('end', () => { 12 | const expectedPath = 'src/elements/element-test.js' 13 | 14 | tap.doesNotThrow(() => 15 | fileExists(expectedPath), `Expected ${expectedPath} to exist.`) 16 | tap.doesNotThrow(() => 17 | fileContent(expectedPath, '/* Element: element-test */'), 'Expected file to contain name in heading') 18 | tap.end() 19 | }) 20 | ) 21 | -------------------------------------------------------------------------------- /tests/generators/modal.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | 5 | const { resolve: resolvePath } = require('path') 6 | const { run } = require('yeoman-test') 7 | const { file: fileExists, fileContent } = require('yeoman-assert') 8 | 9 | test('choo:model', tap => run(resolvePath(__dirname, '../../generators/model')) 10 | .withArguments(['model-test']) 11 | .on('end', () => { 12 | const expectedPath = 'src/models/model-test.js' 13 | 14 | tap.doesNotThrow(() => 15 | fileExists(expectedPath), `Expected ${expectedPath} to exist.`) 16 | tap.doesNotThrow(() => 17 | fileContent(expectedPath, "namespace: 'model-test'"), 'Expected file to contain namespace') 18 | tap.end() 19 | }) 20 | ) 21 | -------------------------------------------------------------------------------- /tests/generators/view.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | 5 | const { copyRecursive } = require('fs.extra') 6 | const { resolve: resolvePath } = require('path') 7 | const { run } = require('yeoman-test') 8 | const { file: fileExists, fileContent } = require('yeoman-assert') 9 | 10 | test('choo:view', tap => run(resolvePath(__dirname, '../../generators/view')) 11 | .inTmpDir(function (dir) { 12 | const copyFrom = resolvePath(__dirname, '../../generators/app/templates') 13 | const copyTo = resolvePath(dir) 14 | 15 | copyRecursive(copyFrom, copyTo, this.async()) 16 | }) 17 | .withArguments(['view-test']) 18 | .on('error', msg => { 19 | console.log('fail', msg) 20 | tap.fail(msg) 21 | tap.end() 22 | }) 23 | .on('end', () => { 24 | const expectedPath = 'src/views/view-test.js' 25 | 26 | tap.doesNotThrow(() => 27 | fileExists(expectedPath), `Expected ${expectedPath} to exist.`) 28 | tap.doesNotThrow(() => 29 | fileContent(expectedPath, '

    view-test

    '), 'Expected file to contain name in heading') 30 | const injection = "route('/view-test', require('./views/view-test'))" 31 | 32 | tap.doesNotThrow(() => 33 | fileContent('src/routes.js', ''), `Expected routes.js to contain ${injection}`) 34 | tap.end() 35 | }) 36 | ) 37 | --------------------------------------------------------------------------------