From 395800ace198f5217f7d92ef250adb0f7914329f Mon Sep 17 00:00:00 2001 From: Joel Chen Date: Wed, 10 Aug 2016 01:30:42 -0700 Subject: [PATCH] first commit --- .editorconfig | 11 + .gitattributes | 1 + .gitignore | 138 +++++++++ .travis.yml | 7 + LICENSE | 13 + README.md | 38 ++- generators/app/index.js | 292 ++++++++++++++++++ generators/app/templates/babelrc | 3 + generators/app/templates/client/app.jsx | 13 + .../app/templates/client/components/home.jsx | 9 + generators/app/templates/client/routes.jsx | 7 + generators/app/templates/config/default.json | 16 + .../app/templates/config/development.json | 13 + .../app/templates/config/production.json | 1 + generators/app/templates/gulpfile.js | 1 + generators/app/templates/package.json | 28 ++ generators/app/templates/server/index.js | 9 + generators/editorconfig/index.js | 22 ++ .../editorconfig/templates/editorconfig | 11 + generators/git/index.js | 83 +++++ generators/git/templates/gitattributes | 1 + generators/git/templates/gitignore | 157 ++++++++++ generators/readme/index.js | 79 +++++ generators/readme/templates/README.md | 37 +++ generators/webapp/index.js | 23 ++ .../server/plugins/webapp/index.html | 13 + .../templates/server/plugins/webapp/index.js | 187 +++++++++++ .../templates/server/views/index-view.jsx | 10 + gulpfile.js | 59 ++++ package.json | 63 ++++ test/app.js | 88 ++++++ 31 files changed, 1431 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 generators/app/index.js create mode 100644 generators/app/templates/babelrc create mode 100644 generators/app/templates/client/app.jsx create mode 100644 generators/app/templates/client/components/home.jsx create mode 100644 generators/app/templates/client/routes.jsx create mode 100644 generators/app/templates/config/default.json create mode 100644 generators/app/templates/config/development.json create mode 100644 generators/app/templates/config/production.json create mode 100644 generators/app/templates/gulpfile.js create mode 100644 generators/app/templates/package.json create mode 100644 generators/app/templates/server/index.js create mode 100644 generators/editorconfig/index.js create mode 100644 generators/editorconfig/templates/editorconfig create mode 100644 generators/git/index.js create mode 100644 generators/git/templates/gitattributes create mode 100644 generators/git/templates/gitignore create mode 100644 generators/readme/index.js create mode 100644 generators/readme/templates/README.md create mode 100644 generators/webapp/index.js create mode 100644 generators/webapp/templates/server/plugins/webapp/index.html create mode 100644 generators/webapp/templates/server/plugins/webapp/index.js create mode 100644 generators/webapp/templates/server/views/index-view.jsx create mode 100644 gulpfile.js create mode 100644 package.json create mode 100644 test/app.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..beffa30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4638240 --- /dev/null +++ b/.gitignore @@ -0,0 +1,138 @@ + +# Created by https://www.gitignore.io/api/gitbook,osx,webstorm,node + +### GitBook ### +# Node rules: +## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +## Dependency directory +## Commenting this out is preferred by some people, see +## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git +node_modules + +# Book build output +_book + +# eBook build output +*.epub +*.mobi +*.pdf + + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### WebStorm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries +# .idea/shelf + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +.tmp diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f2f84e8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - v6 + - v5 + - v4 + - '0.12' + - '0.10' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd35f4e --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2016 WalmartLabs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index b1b4f40..a2b239b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,36 @@ -# generator-electrode -Yeoman generator for Electrode App +# generator-electrode [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] +> Generate Electrode Isomorphic React App with NodeJS backend. + +## Installation + +First, install [Yeoman](http://yeoman.io) and generator-electrode using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). + +```bash +npm install -g yo +npm install -g generator-electrode +``` + +Then generate your new project: + +```bash +yo electrode +``` + +## Getting To Know Yeoman + + * Yeoman has a heart of gold. + * Yeoman is a person with feelings and opinions, but is very easy to work with. + * Yeoman can be too opinionated at times but is easily convinced not to be. + * Feel free to [learn more about Yeoman](http://yeoman.io/). + +## License + +Apache-2.0 © WalmartLabs + + +[npm-image]: https://badge.fury.io/js/generator-electrode.svg +[npm-url]: https://npmjs.org/package/generator-electrode +[travis-image]: https://travis-ci.org/electrode-io/generator-electrode.svg?branch=master +[travis-url]: https://travis-ci.org/electrode-io/generator-electrode +[daviddm-image]: https://david-dm.org/electrode-io/generator-electrode.svg?theme=shields.io +[daviddm-url]: https://david-dm.org/electrode-io/generator-electrode diff --git a/generators/app/index.js b/generators/app/index.js new file mode 100644 index 0000000..449062a --- /dev/null +++ b/generators/app/index.js @@ -0,0 +1,292 @@ +'use strict'; + +/* eslint-disable arrow-parens */ + +var generators = require('yeoman-generator'); +var chalk = require('chalk'); +var yosay = require('yosay'); +var path = require('path'); +var _ = require('lodash'); +var extend = _.merge; +var parseAuthor = require('parse-author'); +var githubUsername = require('github-username'); + +module.exports = generators.Base.extend({ + constructor: function () { + generators.Base.apply(this, arguments); + + this.option('generateInto', { + type: String, + required: false, + defaults: '', + desc: 'Relocate the location of the generated files.' + }); + + this.option('travis', { + type: Boolean, + required: false, + defaults: true, + desc: 'Include travis config' + }); + + this.option('license', { + type: Boolean, + required: false, + defaults: true, + desc: 'Include a license' + }); + + this.option('name', { + type: String, + required: false, + desc: 'Project name' + }); + + this.option('githubAccount', { + type: String, + required: false, + desc: 'GitHub username or organization' + }); + + this.option('projectRoot', { + type: String, + required: false, + defaults: 'lib', + desc: 'Relative path to the project code root' + }); + + this.option('readme', { + type: String, + required: false, + desc: 'Content to insert in the README.md file' + }); + }, + + initializing: function () { + this.pkg = this.fs.readJSON(this.destinationPath('package.json'), {}); + + if (this.pkg.keywords) { + this.pkg.keywords = this.pkg.keywords.filter((x) => x); + } + + // Pre set the default props from the information we have at this point + this.props = { + name: this.pkg.name, + description: this.pkg.description, + version: this.pkg.version, + homepage: this.pkg.homepage + }; + + if (_.isObject(this.pkg.author)) { + this.props.authorName = this.pkg.author.name; + this.props.authorEmail = this.pkg.author.email; + this.props.authorUrl = this.pkg.author.url; + } else if (_.isString(this.pkg.author)) { + var info = parseAuthor(this.pkg.author); + this.props.authorName = info.name; + this.props.authorEmail = info.email; + this.props.authorUrl = info.url; + } + }, + + prompting: { + greeting: function () { + this.log(yosay( + 'Welcome to the phenomenal ' + chalk.red('Electrode App') + ' generator!' + )); + }, + + askFor: function () { + if (this.pkg.name || this.options.name) { + this.props.name = this.pkg.name || _.kebabCase(this.options.name); + } + + var prompts = [ + { + name: 'name', + message: 'Application Name', + when: !this.props.name, + default: path.basename(process.cwd()) + }, + { + name: 'description', + message: 'Description', + when: !this.props.description + }, + { + name: 'homepage', + message: 'Project homepage url', + when: !this.props.homepage + }, + { + name: 'authorName', + message: 'Author\'s Name', + when: !this.props.authorName, + default: this.user.git.name(), + store: true + }, + { + name: 'authorEmail', + message: 'Author\'s Email', + when: !this.props.authorEmail, + default: this.user.git.email(), + store: true + }, + { + name: 'authorUrl', + message: 'Author\'s Homepage', + when: !this.props.authorUrl, + store: true + }, + { + name: 'keywords', + message: 'Package keywords (comma to split)', + when: _.isEmpty(this.pkg.keywords), + filter: function (words) { + return words.split(/\s*,\s*/g).filter((x) => x); + } + } + ]; + + return this.prompt(prompts).then((props) => { + this.props = extend(this.props, props); + }); + }, + + askForGithubAccount: function () { + if (this.options.githubAccount) { + this.props.githubAccount = this.options.githubAccount; + return; + } + var done = this.async(); + + githubUsername(this.props.authorEmail, (err, username) => { + if (err) { + username = username || ''; + } + this.prompt({ + name: 'githubAccount', + message: 'GitHub username or organization', + default: username + }).then((prompt) => { + this.props.githubAccount = prompt.githubAccount; + done(); + }); + }); + } + }, + + writing: function () { + // Re-read the content at this point because a composed generator might modify it. + var currentPkg = this.fs.readJSON(this.destinationPath('package.json'), {}); + var defaultPkg = require(this.templatePath('package.json')); + + ['name', 'version', 'description', 'homepage', 'main', 'license'].forEach((x) => { + currentPkg[x] = currentPkg[x] || undefined; + }); + + var updatePkg = _.defaultsDeep(currentPkg, { + name: _.kebabCase(this.props.name), + version: '0.0.0', + description: this.props.description, + homepage: this.props.homepage, + author: { + name: this.props.authorName, + email: this.props.authorEmail, + url: this.props.authorUrl + }, + files: [ + this.options.projectRoot + ], + main: path.join( + this.options.projectRoot, + 'index.js' + ).replace(/\\/g, '/'), + keywords: [] + }); + + var pkg = extend({}, defaultPkg, updatePkg); + + // Combine the keywords + if (this.props.keywords) { + pkg.keywords = _.uniq(this.props.keywords.concat(pkg.keywords)).filter((x) => x); + } + + // Let's extend package.json so we're not overwriting user previous fields + this.fs.writeJSON(this.destinationPath('package.json'), pkg); + + this.fs.copy( + this.templatePath('babelrc'), + this.destinationPath(this.options.generateInto, '.babelrc') + ); + + ['gulpfile.js', 'client', 'config', 'server', 'test'].forEach((f) => { + this.fs.copy( + this.templatePath(f), + this.destinationPath(this.options.generateInto, f) + ); + }); + }, + + default: function () { + if (this.options.travis) { + this.composeWith('travis', {}, { + local: require.resolve('generator-travis/generators/app') + }); + } + + this.composeWith('electrode:editorconfig', {}, { + local: require.resolve('../editorconfig') + }); + + this.composeWith('electrode:git', { + options: { + name: this.props.name, + githubAccount: this.props.githubAccount + } + }, { + local: require.resolve('../git') + }); + + if (this.options.license && !this.pkg.license) { + this.composeWith('license', { + options: { + name: this.props.authorName, + email: this.props.authorEmail, + website: this.props.authorUrl + } + }, { + local: require.resolve('generator-license/app') + }); + } + + if (!this.fs.exists(this.destinationPath('README.md'))) { + this.composeWith('electrode:readme', { + options: { + name: this.props.name, + description: this.props.description, + githubAccount: this.props.githubAccount, + authorName: this.props.authorName, + authorUrl: this.props.authorUrl, + content: this.options.readme + } + }, { + local: require.resolve('../readme') + }); + } + + if (!this.fs.exists(this.destinationPath('server/plugins/webapp'))) { + this.composeWith('electrode:webapp', { + options: { + } + }, { + local: require.resolve('../webapp') + }); + } + + }, + + install: function () { + this.installDependencies(); + } +}); diff --git a/generators/app/templates/babelrc b/generators/app/templates/babelrc new file mode 100644 index 0000000..e989ef9 --- /dev/null +++ b/generators/app/templates/babelrc @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/electrode-archetype-react-app/config/babel/.babelrc" +} diff --git a/generators/app/templates/client/app.jsx b/generators/app/templates/client/app.jsx new file mode 100644 index 0000000..992b6c6 --- /dev/null +++ b/generators/app/templates/client/app.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { routes } from "./routes"; +import { Router } from "react-router"; +import { Resolver } from "react-resolver"; +import { createHistory } from "history"; + +window.webappStart = () => { + Resolver.render( + () => {routes}, + document.querySelector(".js-content") + ); +}; + diff --git a/generators/app/templates/client/components/home.jsx b/generators/app/templates/client/components/home.jsx new file mode 100644 index 0000000..f7e0298 --- /dev/null +++ b/generators/app/templates/client/components/home.jsx @@ -0,0 +1,9 @@ +import React from "react"; + +export class Home extends React.Component { + render() { + return ( +

Hello React

+ ); + } +} diff --git a/generators/app/templates/client/routes.jsx b/generators/app/templates/client/routes.jsx new file mode 100644 index 0000000..568897a --- /dev/null +++ b/generators/app/templates/client/routes.jsx @@ -0,0 +1,7 @@ +import React from "react"; +import { Route, IndexRoute} from "react-router"; +import { Home } from "./components/home"; + +export const routes = ( + +); diff --git a/generators/app/templates/config/default.json b/generators/app/templates/config/default.json new file mode 100644 index 0000000..17ca742 --- /dev/null +++ b/generators/app/templates/config/default.json @@ -0,0 +1,16 @@ +{ + "plugins": { + "webapp": { + "module": "./server/plugins/webapp", + "options": { + "paths": { + "/{args*}": { + "content": { + "module": "./server/views/index-view" + } + } + } + } + } + } +} diff --git a/generators/app/templates/config/development.json b/generators/app/templates/config/development.json new file mode 100644 index 0000000..85e4e4c --- /dev/null +++ b/generators/app/templates/config/development.json @@ -0,0 +1,13 @@ +{ + "plugins": { + "inert": { + "enable": true + }, + "staticPaths": { + "enable": true, + "options": { + "pathPrefix": "dist" + } + } + } +} diff --git a/generators/app/templates/config/production.json b/generators/app/templates/config/production.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/generators/app/templates/config/production.json @@ -0,0 +1 @@ +{} diff --git a/generators/app/templates/gulpfile.js b/generators/app/templates/gulpfile.js new file mode 100644 index 0000000..a2ee8ce --- /dev/null +++ b/generators/app/templates/gulpfile.js @@ -0,0 +1 @@ +require("electrode-archetype-react-app")(); diff --git a/generators/app/templates/package.json b/generators/app/templates/package.json new file mode 100644 index 0000000..acab462 --- /dev/null +++ b/generators/app/templates/package.json @@ -0,0 +1,28 @@ +{ + "name": "application-name", + "version": "0.0.1", + "description": "Isomorphic React Application With NodeJS backend", + "homepage": "", + "author": {}, + "contributors": [], + "files": [], + "main": "server/index.js", + "keywords": [], + "repository": {}, + "license": "UNLICENSED", + "scripts": { + "test": "gulp test", + "coverage": "gulp check" + }, + "dependencies": { + "bluebird": "^2.10.2", + "electrode-router-resolver-engine": "^1.0.0", + "electrode-server": "^1.0.0", + "lodash": "^4.10.1" + }, + "devDependencies": { + "electrode-archetype-react-app": "^1.0.0", + "electrode-archetype-react-app-dev": "^1.0.0", + "gulp": "^3.9.1" + } +} diff --git a/generators/app/templates/server/index.js b/generators/app/templates/server/index.js new file mode 100644 index 0000000..5265f1d --- /dev/null +++ b/generators/app/templates/server/index.js @@ -0,0 +1,9 @@ +"use strict"; +process.on('SIGINT', function () { + process.exit(0); +}); +require("babel-register")({ + ignore: /node_modules\/(?!react\/)/ +}); +const config = require("electrode-confippet").config; +require("electrode-server")(config); diff --git a/generators/editorconfig/index.js b/generators/editorconfig/index.js new file mode 100644 index 0000000..0accc2b --- /dev/null +++ b/generators/editorconfig/index.js @@ -0,0 +1,22 @@ +'use strict'; +var generators = require('yeoman-generator'); + +module.exports = generators.Base.extend({ + constructor: function () { + generators.Base.apply(this, arguments); + + this.option('generateInto', { + type: String, + required: false, + defaults: '', + desc: 'Relocate the location of the generated files.' + }); + }, + + initializing: function () { + this.fs.copy( + this.templatePath('editorconfig'), + this.destinationPath(this.options.generateInto, '.editorconfig') + ); + } +}); diff --git a/generators/editorconfig/templates/editorconfig b/generators/editorconfig/templates/editorconfig new file mode 100644 index 0000000..beffa30 --- /dev/null +++ b/generators/editorconfig/templates/editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/generators/git/index.js b/generators/git/index.js new file mode 100644 index 0000000..aa11970 --- /dev/null +++ b/generators/git/index.js @@ -0,0 +1,83 @@ +'use strict'; +var generators = require('yeoman-generator'); +var originUrl = require('git-remote-origin-url'); + +module.exports = generators.Base.extend({ + constructor: function () { + generators.Base.apply(this, arguments); + + this.option('generateInto', { + type: String, + required: false, + defaults: '', + desc: 'Relocate the location of the generated files.' + }); + + this.option('name', { + type: String, + required: true, + desc: 'Module name' + }); + + this.option('github-account', { + type: String, + required: true, + desc: 'GitHub username or organization' + }); + }, + + initializing: function () { + this.fs.copy( + this.templatePath('gitattributes'), + this.destinationPath(this.options.generateInto, '.gitattributes') + ); + + this.fs.copy( + this.templatePath('gitignore'), + this.destinationPath(this.options.generateInto, '.gitignore') + ); + + return originUrl(this.destinationPath(this.options.generateInto)) + .then(function (url) { + this.originUrl = url; + }.bind(this), function () { + this.originUrl = ''; + }.bind(this)); + }, + + writing: function () { + this.pkg = this.fs.readJSON(this.destinationPath(this.options.generateInto, 'package.json'), {}); + + var repository = ''; + if (this.originUrl) { + repository = this.originUrl; + } else { + repository = this.options.githubAccount + '/' + this.options.name; + } + + this.pkg.repository = this.pkg.repository || {}; + if (!this.pkg.repository.url) { + this.pkg.repository.type = 'git'; + this.pkg.repository.url = repository; + } + + this.fs.writeJSON(this.destinationPath(this.options.generateInto, 'package.json'), this.pkg); + }, + + end: function () { + this.spawnCommandSync('git', ['init'], { + cwd: this.destinationPath(this.options.generateInto) + }); + + if (!this.originUrl) { + var repoSSH = this.pkg.repository; + var url = this.pkg.repository && this.pkg.repository.url; + if (url && url.indexOf('.git') === -1) { + repoSSH = 'git@github.com:' + this.pkg.repository + '.git'; + } + this.spawnCommandSync('git', ['remote', 'add', 'origin', repoSSH], { + cwd: this.destinationPath(this.options.generateInto) + }); + } + } +}); diff --git a/generators/git/templates/gitattributes b/generators/git/templates/gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/generators/git/templates/gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/generators/git/templates/gitignore b/generators/git/templates/gitignore new file mode 100644 index 0000000..ad8ad8c --- /dev/null +++ b/generators/git/templates/gitignore @@ -0,0 +1,157 @@ + + +# Created by https://www.gitignore.io/api/gitbook,node,webstorm,bower,osx + +### GitBook ### +# Node rules: +## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +## Dependency directory +## Commenting this out is preferred by some people, see +## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git +node_modules + +# Book build output +_book + +# eBook build output +*.epub +*.mobi +*.pdf + + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + + +### WebStorm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries +# .idea/shelf + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + + +### Bower ### +bower_components +.bower-cache +.bower-registry +.bower-tmp + + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# others +.hg +.project +.tmp + +# Build +dist +Procfile +config/assets.json +npm-shrinkwrap.json + +.isomorphic-loader-config.json diff --git a/generators/readme/index.js b/generators/readme/index.js new file mode 100644 index 0000000..7521b72 --- /dev/null +++ b/generators/readme/index.js @@ -0,0 +1,79 @@ +'use strict'; +var _ = require('lodash'); +var generators = require('yeoman-generator'); + +module.exports = generators.Base.extend({ + constructor: function () { + generators.Base.apply(this, arguments); + + this.option('generateInto', { + type: String, + required: false, + defaults: '', + desc: 'Relocate the location of the generated files.' + }); + + this.option('name', { + type: String, + required: true, + desc: 'Project name' + }); + + this.option('description', { + type: String, + required: true, + desc: 'Project description' + }); + + this.option('githubAccount', { + type: String, + required: true, + desc: 'User github account' + }); + + this.option('authorName', { + type: String, + required: true, + desc: 'Author name' + }); + + this.option('authorUrl', { + type: String, + required: true, + desc: 'Author url' + }); + + this.option('coveralls', { + type: Boolean, + required: true, + desc: 'Include coveralls badge' + }); + + this.option('content', { + type: String, + required: false, + desc: 'Readme content' + }); + }, + + writing: function () { + var pkg = this.fs.readJSON(this.destinationPath(this.options.generateInto, 'package.json'), {}); + this.fs.copyTpl( + this.templatePath('README.md'), + this.destinationPath(this.options.generateInto, 'README.md'), + { + projectName: this.options.name, + safeProjectName: _.camelCase(this.options.name), + description: this.options.description, + githubAccount: this.options.githubAccount, + author: { + name: this.options.authorName, + url: this.options.authorUrl + }, + license: pkg.license, + includeCoveralls: this.options.coveralls, + content: this.options.content + } + ); + } +}); diff --git a/generators/readme/templates/README.md b/generators/readme/templates/README.md new file mode 100644 index 0000000..8fd620d --- /dev/null +++ b/generators/readme/templates/README.md @@ -0,0 +1,37 @@ +# <%= projectName %> [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url]<% +if (includeCoveralls) { %> [![Coverage percentage][coveralls-image]][coveralls-url]<% } -%> + +> <%= description %> + +<% if (!content) { -%> +## Installation + +```sh +$ npm install --save <%= projectName %> +``` + +## Usage + +```js +var <%= safeProjectName %> = require('<%= projectName %>'); + +<%= safeProjectName %>('Rainbow'); +``` +<% } else { -%> +<%= content %> +<% } -%> +## License + +<%= license %> © [<%= author.name %>](<%= author.url %>) + + +[npm-image]: https://badge.fury.io/js/<%= projectName %>.svg +[npm-url]: https://npmjs.org/package/<%= projectName %> +[travis-image]: https://travis-ci.org/<%= githubAccount %>/<%= projectName %>.svg?branch=master +[travis-url]: https://travis-ci.org/<%= githubAccount %>/<%= projectName %> +[daviddm-image]: https://david-dm.org/<%= githubAccount %>/<%= projectName %>.svg?theme=shields.io +[daviddm-url]: https://david-dm.org/<%= githubAccount %>/<%= projectName %> +<% if (includeCoveralls) { -%> +[coveralls-image]: https://coveralls.io/repos/<%= githubAccount %>/<%= projectName %>/badge.svg +[coveralls-url]: https://coveralls.io/r/<%= githubAccount %>/<%= projectName %> +<% } -%> diff --git a/generators/webapp/index.js b/generators/webapp/index.js new file mode 100644 index 0000000..c966d8c --- /dev/null +++ b/generators/webapp/index.js @@ -0,0 +1,23 @@ +'use strict'; +var _ = require('lodash'); +var generators = require('yeoman-generator'); + +module.exports = generators.Base.extend({ + constructor: function () { + generators.Base.apply(this, arguments); + + this.option('generateInto', { + type: String, + required: false, + defaults: '', + desc: 'Relocate the location of the generated files.' + }); + }, + + writing: function () { + this.fs.copy( + this.templatePath('server'), + this.destinationPath(this.options.generateInto, 'server') + ); + } +}); diff --git a/generators/webapp/templates/server/plugins/webapp/index.html b/generators/webapp/templates/server/plugins/webapp/index.html new file mode 100644 index 0000000..a826c42 --- /dev/null +++ b/generators/webapp/templates/server/plugins/webapp/index.html @@ -0,0 +1,13 @@ + + + + + {{PAGE_TITLE}} + {{WEBAPP_BUNDLES}} + {{PREFETCH_BUNDLES}} + + +
{{SSR_CONTENT}}
+ + + diff --git a/generators/webapp/templates/server/plugins/webapp/index.js b/generators/webapp/templates/server/plugins/webapp/index.js new file mode 100644 index 0000000..96e235d --- /dev/null +++ b/generators/webapp/templates/server/plugins/webapp/index.js @@ -0,0 +1,187 @@ +"use strict"; + +const _ = require("lodash"); +const Promise = require("bluebird"); +const fs = require("fs"); +const Path = require("path"); +const assert = require("assert"); + +const HTTP_ERROR_500 = 500; +const HTTP_REDIRECT = 302; + +/** + * Load stats.json which is created during build. + * The file contains bundle files which are to be loaded on the client side. + * + * @param {string} statsFilePath - path of stats.json + * @returns {Promise.} an object containing an array of file names + */ +function loadAssetsFromStats(statsFilePath) { + return Promise.resolve(Path.resolve(statsFilePath)) + .then(require) + .then((stats) => { + const assets = {}; + _.each(stats.assetsByChunkName.main, (v) => { + if (v.endsWith(".js")) { + assets.js = v; + } else if (v.endsWith(".css")) { + assets.css = v; + } + }); + return assets; + }) + .catch(() => ({})); +} + +function makeRouteHandler(options, userContent) { + const CONTENT_MARKER = "{{SSR_CONTENT}}"; + const BUNDLE_MARKER = "{{WEBAPP_BUNDLES}}"; + const TITLE_MARKER = "{{PAGE_TITLE}}"; + const PREFETCH_MARKER = "{{PREFETCH_BUNDLES}}"; + const WEBPACK_DEV = options.webpackDev; + const RENDER_JS = options.renderJS; + const RENDER_SS = options.serverSideRendering; + const html = fs.readFileSync(Path.join(__dirname, "index.html")).toString(); + const assets = options.__internals.assets; + const devJSBundle = options.__internals.devJSBundle; + const devCSSBundle = options.__internals.devCSSBundle; + + /* Create a route handler */ + return (request, reply) => { + const mode = request.query.__mode || ""; + const renderJs = RENDER_JS && mode !== "nojs"; + const renderSs = RENDER_SS && mode !== "noss"; + + const bundleCss = () => { + return WEBPACK_DEV ? devCSSBundle : assets.css && `/js/${assets.css}` || ""; + }; + + const bundleJs = () => { + if (!renderJs) { + return ""; + } + return WEBPACK_DEV ? devJSBundle : assets.js && `/js/${assets.js}` || ""; + }; + + const callUserContent = (content) => { + const x = content(request); + return !x.catch ? x : x.catch((err) => { + return { + status: err.status || HTTP_ERROR_500, + html: err.toString() + }; + }); + }; + + const makeBundles = () => { + const css = bundleCss(); + const cssLink = css ? `` : ""; + const js = bundleJs(); + const jsLink = js ? `` : ""; + return `${cssLink}${jsLink}`; + }; + + const renderPage = (content) => { + return html.replace(/{{[A-Z_]*}}/g, (m) => { + switch (m) { + case CONTENT_MARKER: + return content.html || ""; + case TITLE_MARKER: + return options.pageTitle; + case BUNDLE_MARKER: + return makeBundles(); + case PREFETCH_MARKER: + return ""; + default: + return `Unknown marker ${m}`; + } + }); + }; + + const renderSSRContent = (content) => { + const p = _.isFunction(content) ? + callUserContent(content) : + Promise.resolve(_.isObject(content) ? content : {html: content}); + return p.then((c) => renderPage(c)); + }; + + const handleStatus = (data) => { + const status = data.status; + if (status === HTTP_REDIRECT) { + reply.redirect(data.path); + } else { + reply({message: "error"}).code(status); + } + }; + + const doRender = () => { + return renderSs ? renderSSRContent(userContent) : renderPage(""); + }; + + Promise.try(doRender) + .then((data) => { + return data.status ? handleStatus(data) : reply(data); + }) + .catch((err) => { + reply(err.message).code(err.status || HTTP_ERROR_500); + }); + }; +} + +const registerRoutes = (server, options, next) => { + + const pluginOptionsDefaults = { + pageTitle: "Untitled Electrode Web Application", + webpackDev: process.env.WEBPACK_DEV === "true", + renderJS: true, + serverSideRendering: true, + devServer: { + host: "127.0.0.1", + port: "2992" + }, + paths: {}, + stats: "dist/server/stats.json" + }; + + const resolveContent = (content) => { + if (!_.isString(content) && !_.isFunction(content) && content.module) { + const module = content.module.startsWith(".") ? Path.join(process.cwd(), content.module) : content.module; + return require(module); // eslint-disable-line + } + + return content; + }; + + const pluginOptions = _.defaultsDeep({}, pluginOptionsDefaults, options); + + return Promise.try(() => loadAssetsFromStats(pluginOptions.stats)) + .then((assets) => { + const devServer = pluginOptions.devServer; + pluginOptions.__internals = { + assets, + devJSBundle: `http://${devServer.host}:${devServer.port}/js/bundle.dev.js`, + devCSSBundle: `http://${devServer.host}:${devServer.port}/js/style.css` + }; + + _.each(options.paths, (v, path) => { + assert(v.content, `You must define content for the webapp plugin path ${path}`); + server.route({ + method: "GET", + path, + config: v.config || {}, + handler: makeRouteHandler(pluginOptions, resolveContent(v.content)) + }); + }); + next(); + }) + .catch(next); +}; + +registerRoutes.attributes = { + pkg: { + name: "webapp", + version: "1.0.0" + } +}; + +module.exports = registerRoutes; diff --git a/generators/webapp/templates/server/views/index-view.jsx b/generators/webapp/templates/server/views/index-view.jsx new file mode 100644 index 0000000..9752aef --- /dev/null +++ b/generators/webapp/templates/server/views/index-view.jsx @@ -0,0 +1,10 @@ +import RouterResolverEngine from "electrode-router-resolver-engine"; +import { routes } from "../../client/routes"; + +module.exports = (req) => { + if (!req.server.app.routesEngine) { + req.server.app.routesEngine = RouterResolverEngine(routes); + } + + return req.server.app.routesEngine(req); +}; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..d5f5e6b --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,59 @@ +'use strict'; +var path = require('path'); +var gulp = require('gulp'); +var eslint = require('gulp-eslint'); +var excludeGitignore = require('gulp-exclude-gitignore'); +var mocha = require('gulp-mocha'); +var istanbul = require('gulp-istanbul'); +var nsp = require('gulp-nsp'); +var plumber = require('gulp-plumber'); +var gulpIgnore = require('gulp-ignore'); + +function excludeTemplates() { + return gulpIgnore.exclude('**/templates/**'); +} + +gulp.task('static', function () { + return gulp.src('**/*.js') + .pipe(excludeGitignore()) + .pipe(excludeTemplates()) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +gulp.task('nsp', function (cb) { + nsp({package: path.resolve('package.json')}, cb); +}); + +gulp.task('pre-test', function () { + return gulp.src('generators/**/*.js') + .pipe(excludeGitignore()) + .pipe(excludeTemplates()) + .pipe(istanbul({ + includeUntested: true + })) + .pipe(istanbul.hookRequire()); +}); + +gulp.task('test', ['pre-test'], function (cb) { + var mochaErr; + + gulp.src('test/**/*.js') + .pipe(plumber()) + .pipe(mocha({reporter: 'spec'})) + .on('error', function (err) { + mochaErr = err; + }) + .pipe(istanbul.writeReports()) + .on('end', function () { + cb(mochaErr); + }); +}); + +gulp.task('watch', function () { + gulp.watch(['generators/**/*.js', 'test/**'], ['test']); +}); + +gulp.task('prepublish', ['nsp']); +gulp.task('default', ['static', 'test']); diff --git a/package.json b/package.json new file mode 100644 index 0000000..f0e6afa --- /dev/null +++ b/package.json @@ -0,0 +1,63 @@ +{ + "name": "generator-electrode", + "version": "0.1.0", + "description": "Generate Electrode React App", + "homepage": "https://github.com/electrode-io", + "author": { + "name": "Joel Chen", + "email": "xchen@walmartlabs.com", + "url": "https://github.com/jchip" + }, + "files": [ + "generators" + ], + "main": "generators/index.js", + "keywords": [ + "Electrode", + "React", + "yeoman-generator" + ], + "dependencies": { + "chalk": "^1.0.0", + "generator-license": "^4.0.0", + "generator-travis": "^1.3.0", + "git-remote-origin-url": "^2.0.0", + "github-username": "^2.1.0", + "parse-author": "^1.0.0", + "yeoman-generator": "^0.23.4", + "yosay": "^1.0.0" + }, + "devDependencies": { + "eslint": "^3.1.1", + "eslint-config-xo-space": "^0.14.0", + "gulp": "^3.9.0", + "gulp-eslint": "^2.0.0", + "gulp-exclude-gitignore": "^1.0.0", + "gulp-ignore": "^2.0.1", + "gulp-istanbul": "^1.0.0", + "gulp-line-ending-corrector": "^1.0.1", + "gulp-mocha": "^2.0.0", + "gulp-nsp": "^2.1.0", + "gulp-plumber": "^1.0.0", + "mocha": "^3.0.2", + "mockery": "^1.7.0", + "pinkie-promise": "^2.0.1", + "yeoman-assert": "^2.2.1", + "yeoman-test": "^1.4.0" + }, + "eslintConfig": { + "extends": "xo-space", + "env": { + "mocha": true + } + }, + "repository": { + "type": "git", + "url": "https://github.com/electrode-io/generator-electrode" + }, + "scripts": { + "prepublish": "gulp prepublish", + "test": "gulp" + }, + "license": "Apache-2.0" +} diff --git a/test/app.js b/test/app.js new file mode 100644 index 0000000..1d22025 --- /dev/null +++ b/test/app.js @@ -0,0 +1,88 @@ +'use strict'; +var mockery = require('mockery'); +var path = require('path'); +var assert = require('yeoman-assert'); +var helpers = require('yeoman-test'); +var Promise = require('pinkie-promise'); + +describe('electrode:app', function () { + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false + }); + + mockery.registerMock('npm-name', function () { + return Promise.resolve(true); + }); + + mockery.registerMock('github-username', function (name, cb) { + cb(null, 'unicornUser'); + }); + + mockery.registerMock( + require.resolve('generator-license/app'), + helpers.createDummyGenerator() + ); + }); + + after(function () { + mockery.disable(); + }); + + describe('running on new project', function () { + before(function () { + this.answers = { + name: 'generator-electrode', + description: 'Electrode app generator', + homepage: 'http://electrode.io', + githubAccount: 'electrode-io', + authorName: 'Electrode', + authorEmail: 'hi@electrode.io', + authorUrl: 'http://electrode.io', + keywords: ['foo', 'bar'] + }; + return helpers.run(path.join(__dirname, '../generators/app')) + .withPrompts(this.answers) + .toPromise(); + }); + + it('creates files', function () { + assert.file([ + '.travis.yml', + '.editorconfig', + '.gitignore', + '.gitattributes', + 'README.md' + ]); + }); + + it('creates package.json', function () { + assert.file('package.json'); + assert.jsonFileContent('package.json', { + name: 'generator-electrode', + version: '0.0.0', + description: this.answers.description, + homepage: this.answers.homepage, + repository: {url: 'electrode-io/generator-electrode'}, + author: { + name: this.answers.authorName, + email: this.answers.authorEmail, + url: this.answers.authorUrl + }, + files: [], + keywords: this.answers.keywords, + main: 'lib/index.js' + }); + }); + + it('creates and fill contents in README.md', function () { + assert.file('README.md'); + assert.fileContent('README.md', 'var generatorElectrode = require(\'generator-electrode\');'); + assert.fileContent('README.md', '> Electrode app generator'); + assert.fileContent('README.md', '$ npm install --save generator-electrode'); + assert.fileContent('README.md', '© [Electrode](http://electrode.io)'); + assert.fileContent('README.md', '[travis-image]: https://travis-ci.org/electrode-io/generator-electrode.svg?branch=master'); + }); + }); +});