diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..649296c --- /dev/null +++ b/.jscsrc @@ -0,0 +1,81 @@ +{ + "disallowKeywords": ["with"], + "disallowKeywordsOnNewLine": ["else"], + "disallowMixedSpacesAndTabs": true, + "disallowNewlineBeforeBlockStatements": true, + "disallowQuotedKeysInObjects": true, + "disallowTrailingWhitespace": true, + + "requireCapitalizedConstructors": true, + "requireCurlyBraces": [ + "if", + "else", + "for", + "while", + "do", + "try", + "catch", + "function", + "switch" + ], + "requireMultipleVarDecl": true, + "requirePaddingNewLineAfterVariableDeclaration": true, + "requirePaddingNewLinesAfterBlocks": true, + "requirePaddingNewlinesBeforeKeywords": [ + "if", + "for", + "while", + "do", + "switch", + "return", + "try", + "function" + ], + "requireSpaceBeforeKeywords": [ + "else", + "catch", + "while" + ], + "requireSpaceAfterKeywords": [ + "if", + "else", + "for", + "while", + "do", + "switch", + "case", + "return", + "try", + "catch", + "typeof", + "function" + ], + "requireSpaceAfterLineComment": true, + "requireSpaceAfterBinaryOperators": true, + "requireSpaceBeforeBinaryOperators": true, + "requireOperatorBeforeLineBreak": [ + "?", + "=", + "+", + "-", + "/", + "*", + "==", + "===", + "!=", + "!==", + ">", + ">=", + "<", + "<=" + ], + + "requireSpaceBeforeBlockStatements": true, + "requireSpaceBeforeObjectValues": true, + "requireSpacesInFunction": { + "beforeOpeningRoundBrace": true, + "beforeOpeningCurlyBrace": true + }, + "validateIndentation": 4, + "validateLineBreaks": "LF" +} diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..3b40835 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,56 @@ +{ + "bitwise" : false, + "curly" : true, + "eqeqeq" : true, + "es3" : false, + "forin" : true, + "iterator" : true, + "latedef" : "nofunc", + "maxerr" : 100, + "noarg" : true, + "nonew" : true, + "regexp" : true, + "smarttabs" : true, + "shadow" : true, + "singleGroups" : true, + "strict" : false, + "undef" : true, + "unused" : true, + "varstmt" : false, + + "asi" : false, + "boss" : false, + "debug" : false, + "eqnull" : false, + "esnext" : true, + "evil" : false, + "expr" : false, + "funcscope" : false, + "globalstrict" : false, + "lastsemic" : false, + "loopfunc" : false, + "onecase" : false, + "plusplus" : false, + "proto" : false, + "scripturl" : false, + "supernew" : false, + "validthis" : false, + + "laxbreak" : true, + "laxcomma" : true, + "sub" : true, + "camelcase" : false, + "indent" : false, + "immed" : false, + "maxlen" : false, + "multistr" : false, + "newcap" : false, + "noempty" : false, + "quotmark" : false, + + "browser" : true, + "devel" : false, + "jasmine" : false, + "jquery" : false, + "node" : true +} diff --git a/README.md b/README.md index 13ebd74..3757aa2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,18 @@ # angular-bro Angular app scaffolding and build chain + +## Generators +``` +yo angular-bro +yo angular-bro:controller name +yo angular-bro:directive name +yo angular-bro:factory name +yo angular-bro:mock path/to/url +yo angular-bro:module name +yo angular-bro:provider name +yo angular-bro:proxy path/to/url http://domain.com/path/to/proxy +yo angular-bro:server +yo angular-bro:service name +yo angular-bro:state name +yo angular-bro:template name +``` diff --git a/generators/app/index.js b/generators/app/index.js new file mode 100644 index 0000000..8b9a316 --- /dev/null +++ b/generators/app/index.js @@ -0,0 +1,146 @@ +var generators = require('yeoman-generator'), + _ = require('underscore.string'); + +module.exports = generators.Base.extend({ + constructor: function () { + generators.Base.apply(this, arguments); + + this.appName = _.camelize(this.appname); + this.hyphenName = _.dasherize(this.appName); + this.friendlyAppName = _.titleize(_.humanize(this.appName)); + }, + + ask: function () { + var done = this.async(), + prompts = [{ + type: 'input', + name: 'appName', + message: 'name', + default: this.hyphenName + }]; + + this.prompt(prompts, function (props) { + this.appName = props.appName; + + done(); + }.bind(this)); + }, + + writing: { + root: function () { + this.fs.copy( + this.templatePath('.jscsrc'), + this.destinationPath('.jscsrc') + ); + + this.fs.copyTpl( + this.templatePath('bower.json'), + this.destinationPath('bower.json'), + this + ); + + this.fs.copyTpl( + this.templatePath('Brocfile.js'), + this.destinationPath('Brocfile.js'), + this + ); + + this.fs.copy( + this.templatePath('circle.yml'), + this.destinationPath('circle.yml') + ); + + this.fs.copy( + this.templatePath('Gruntfile.js'), + this.destinationPath('Gruntfile.js') + ); + + this.fs.copyTpl( + this.templatePath('index.html'), + this.destinationPath('index.html'), + this + ); + + this.fs.copy( + this.templatePath('karma.conf.js'), + this.destinationPath('karma.conf.js') + ); + + this.fs.copyTpl( + this.templatePath('package.json'), + this.destinationPath('package.json'), + this + ); + }, + + app: function () { + this.fs.copyTpl( + this.templatePath('app/app.js'), + this.destinationPath('app/app.js'), + this + ); + }, + + assets: function () { + this.fs.copy( + this.templatePath('assets/.gitkeep'), + this.destinationPath('assets/.gitkeep') + ); + }, + + config: function () { + this.fs.copy( + this.templatePath('config/environment.js'), + this.destinationPath('config/environment.js') + ); + }, + + styles: function () { + this.fs.copy( + this.templatePath('styles/app.less'), + this.destinationPath('styles/app.less') + ); + + this.fs.copy( + this.templatePath('styles/_colors.less'), + this.destinationPath('styles/_colors.less') + ); + + this.fs.copy( + this.templatePath('styles/_mixins.less'), + this.destinationPath('styles/_mixins.less') + ); + + this.fs.copy( + this.templatePath('styles/_variables.less'), + this.destinationPath('styles/_variables.less') + ); + }, + + tests: function () { + this.fs.copy( + this.templatePath('tests/e2e/.gitkeep'), + this.destinationPath('tests/e2e/.gitkeep') + ); + + this.fs.copy( + this.templatePath('tests/helpers/afterAll.js'), + this.destinationPath('tests/helpers/afterAll.js') + ); + + this.fs.copy( + this.templatePath('tests/helpers/beforeAll.js'), + this.destinationPath('tests/helpers/beforeAll.js') + ); + + this.fs.copy( + this.templatePath('tests/unit/.gitkeep'), + this.destinationPath('tests/unit/.gitkeep') + ); + } + }, + + end: function () { + this.installDependencies(); + } +}); diff --git a/generators/app/templates/.jscsrc b/generators/app/templates/.jscsrc new file mode 100644 index 0000000..89faa8b --- /dev/null +++ b/generators/app/templates/.jscsrc @@ -0,0 +1,17 @@ +{ + "disallowMixedSpacesAndTabs": true, + "disallowTrailingWhitespace": true, + "validateIndentation": 4, + "validateLineBreaks": "LF", + "requireCurlyBraces": [ + "if", + "else", + "for", + "while", + "do", + "try", + "catch", + "function", + "switch" + ] +} diff --git a/generators/app/templates/Brocfile.js b/generators/app/templates/Brocfile.js new file mode 100644 index 0000000..b289101 --- /dev/null +++ b/generators/app/templates/Brocfile.js @@ -0,0 +1,8 @@ +var AngularApp = require('angular-bro-app'), + app = new AngularApp(); + +// Use `app.importScript` or `app.importStyle` to add additional +// libraries to the generated output files. + + +module.exports = app.toTree(); diff --git a/generators/app/templates/Gruntfile.js b/generators/app/templates/Gruntfile.js new file mode 100644 index 0000000..9eb5073 --- /dev/null +++ b/generators/app/templates/Gruntfile.js @@ -0,0 +1,102 @@ +module.exports = function (grunt) { + "use strict"; + + var environment = grunt.option('environment') || 'development'; + + switch (environment) { + case 'production': + break; + case 'development': + break; + default: + console.log('Could not map environment `' + environment + '`'); + console.log('Defaulting to `development`'); + environment = 'development'; + break; + } + process.env.ANGULAR_ENV = environment; + + grunt.task.loadNpmTasks('grunt-contrib-clean'); + grunt.task.loadNpmTasks('grunt-broccoli'); + grunt.task.loadNpmTasks('grunt-bump'); + grunt.task.loadNpmTasks('grunt-exec'); + grunt.task.loadNpmTasks('grunt-express-server'); + + grunt.initConfig({ + config: { + destDir: 'dist/' + }, + clean: { + options : { force: true }, + dist : [ '<%= config.destDir %>' ], + tmp : [ 'tmp' ] + }, + broccoli: { + dist: { + dest: '<%= config.destDir %>', + } + }, + bump: { + options: { + files : [ 'bower.json', 'package.json' ], + commitFiles : [ 'bower.json', 'package.json' ], + commitMessage : 'Bumped version to: %VERSION%', + createTag : false, + push : false, + + } + }, + exec: { + updateWebdriver: { + cmd: 'npm run update-webdriver' + }, + broccoliTest: { + cmd: 'npm test' + } + }, + express: { + options: { + port : 4200, + script : 'node_modules/angular-bro-app/lib/server.js' + }, + e2e: { + options: { + background: true + } + }, + server: { + options: { + background: false, + } + } + } + }); + + grunt.registerTask('default', 'Builds the angular app.', 'build'); + + grunt.registerTask('build', 'Builds the angular app', 'broccoli:dist:build'); + + grunt.registerTask('serve', 'Builds and launches a server for the app.', 'server'); + grunt.registerTask('server', 'Builds and launches a server for the app.', function () { + process.env.ANGULAR_SERVER = true; + process.env.ANGULAR_DEST_DIR = grunt.config('config').destDir; + + grunt.task.run([ + 'clean', + 'express:server' + ]); + }); + + grunt.registerTask('test', 'Builds and runs unit tests.', function () { + var broccoliType = 'build'; + + process.env.ANGULAR_TEST = true; + + if (!!grunt.option('server')) { + broccoliType = 'server'; + process.env.ANGULAR_SERVER = true; + } + + grunt.task.run(broccoliType); + }); +}; diff --git a/generators/app/templates/app/app.js b/generators/app/templates/app/app.js new file mode 100644 index 0000000..144fac3 --- /dev/null +++ b/generators/app/templates/app/app.js @@ -0,0 +1,20 @@ +import angular from 'angular'; +import config from 'config/environment'; +import 'app/scripts/templates'; + +var app = angular.module('<%= appName %>', [ + 'templates-app' +]).config(function ($locationProvider) { + "ngInject"; + "use strict"; + + $locationProvider.html5Mode(config.html5Mode); +}); + +angular.element(document).ready(function () { + angular.bootstrap(document, [ + app.name + ], { + strictDi: true + }); +}); diff --git a/generators/app/templates/assets/.gitkeep b/generators/app/templates/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/generators/app/templates/bower.json b/generators/app/templates/bower.json new file mode 100644 index 0000000..b5acd1b --- /dev/null +++ b/generators/app/templates/bower.json @@ -0,0 +1,25 @@ +{ + "name": "<%= hyphenName %>", + "version": "0.0.0", + "description": "", + "main": "app/app.js", + "authors": [], + "license": "MIT", + "moduleType": [], + "homepage": "", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "angular": "1.5.0", + "angular-shim": "2.0.0", + "loader.js": "4.0.1" + }, + "devDependencies": { + "angular-mocks": "1.5.0" + } +} diff --git a/generators/app/templates/circle.yml b/generators/app/templates/circle.yml new file mode 100644 index 0000000..f9473bb --- /dev/null +++ b/generators/app/templates/circle.yml @@ -0,0 +1,11 @@ +machine: + node: + version: 5.1.0 + +dependencies: + override: + - npm install + +test: + override: + - npm test diff --git a/generators/app/templates/config/environment.js b/generators/app/templates/config/environment.js new file mode 100644 index 0000000..68a06b0 --- /dev/null +++ b/generators/app/templates/config/environment.js @@ -0,0 +1,24 @@ +module.exports = function (environment) { + 'use strict'; + + var ENV = { + environment: environment, + html5Mode: true, + routes: {} + }; + + if (environment === 'development') { + + } + + if (environment === 'test') { + // Karma prefers this + ENV.html5Mode = false; + } + + if (environment === 'production') { + + } + + return ENV; +}; diff --git a/generators/app/templates/index.html b/generators/app/templates/index.html new file mode 100644 index 0000000..69cb922 --- /dev/null +++ b/generators/app/templates/index.html @@ -0,0 +1,20 @@ + + + + <%= friendlyAppName %> + + + + + + + + + + + + + + + + diff --git a/generators/app/templates/karma.conf.js b/generators/app/templates/karma.conf.js new file mode 100644 index 0000000..43724fc --- /dev/null +++ b/generators/app/templates/karma.conf.js @@ -0,0 +1,20 @@ +module.exports = function (config) { + 'use strict'; + /** + * WARNING: No not set the following properties. They will be + * overwritten in the execution of the karma tests: + * - files + * - exclude + * - autoWatch + * - singleRun + */ + var configuration = { + browsers : [ 'PhantomJS' ], + frameworks : [ 'jasmine' ], + reporters : [ 'dots', 'coverage'], + preprocessors : { 'app/**/*.js': 'coverage' }, + coverageReporter : { type: 'text' } + }; + + config.set(configuration); +}; diff --git a/generators/app/templates/package.json b/generators/app/templates/package.json new file mode 100644 index 0000000..68c869b --- /dev/null +++ b/generators/app/templates/package.json @@ -0,0 +1,21 @@ +{ + "name": "<%= hyphenName %>", + "version": "0.0.0", + "description": "", + "main": "app/app.js", + "scripts": { + "pretest": "rm -rf .tmp", + "test": "ANGULAR_TEST=true broccoli build .tmp" + }, + "author": "", + "license": "MIT", + "dependencies": { + "angular-bro-app": "0.4.1", + "grunt": "0.4.5", + "grunt-broccoli": "0.4.1", + "grunt-bump": "0.7.0", + "grunt-contrib-clean": "1.0.0", + "grunt-exec": "0.4.6", + "grunt-express-server": "0.5.2" + } +} diff --git a/generators/app/templates/styles/_colors.less b/generators/app/templates/styles/_colors.less new file mode 100644 index 0000000..e69de29 diff --git a/generators/app/templates/styles/_mixins.less b/generators/app/templates/styles/_mixins.less new file mode 100644 index 0000000..e69de29 diff --git a/generators/app/templates/styles/_variables.less b/generators/app/templates/styles/_variables.less new file mode 100644 index 0000000..e69de29 diff --git a/generators/app/templates/styles/app.less b/generators/app/templates/styles/app.less new file mode 100644 index 0000000..e883d40 --- /dev/null +++ b/generators/app/templates/styles/app.less @@ -0,0 +1,7 @@ +@import '_variables.less'; +@import '_colors.less'; +@import '_mixins.less'; + +html { + font-size: 62.5%; +} diff --git a/generators/app/templates/tests/e2e/.gitkeep b/generators/app/templates/tests/e2e/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/generators/app/templates/tests/helpers/afterAll.js b/generators/app/templates/tests/helpers/afterAll.js new file mode 100644 index 0000000..3c69cc8 --- /dev/null +++ b/generators/app/templates/tests/helpers/afterAll.js @@ -0,0 +1,10 @@ +afterAll(function () { + "use strict"; + + /** + * Testing Teardown happens here + * + * Usually this would be used to remove anything persistent + * that was added during testing. + */ +}); diff --git a/generators/app/templates/tests/helpers/beforeAll.js b/generators/app/templates/tests/helpers/beforeAll.js new file mode 100644 index 0000000..8676ed3 --- /dev/null +++ b/generators/app/templates/tests/helpers/beforeAll.js @@ -0,0 +1,12 @@ +beforeAll(function () { + "use strict"; + + /** + * Testing setup happens here. + * + * Usually you will put anything that needs to be shared between tests, + * such as registering helpers with angular, or mock servers. These might + * require cleanup after all testing has completed by using the `afterAll` + * helper. + */ +}); diff --git a/generators/app/templates/tests/unit/.gitkeep b/generators/app/templates/tests/unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/generators/controller/index.js b/generators/controller/index.js new file mode 100644 index 0000000..8bd84ce --- /dev/null +++ b/generators/controller/index.js @@ -0,0 +1,27 @@ +var baseComponent = require('../../lib/base-component'), + generators = require('yeoman-generator'), + extend = require('extend'), + generator; + +generator = extend(true, {}, baseComponent, { + componentType: 'Controller', + fromDirective: false, + fromState: false, + + writing: { + module: function () { + this.composeWith('angular-bro:module', { + args: [this.componentPath], + options: { + fromController: true, + fromState: this.fromState, + fromDirective: this.fromDirective + } + }, { + link: 'weak' + }); + } + } +}); + +module.exports = generators.Base.extend(generator); diff --git a/generators/controller/templates/controller.js b/generators/controller/templates/controller.js new file mode 100644 index 0000000..0c3ed37 --- /dev/null +++ b/generators/controller/templates/controller.js @@ -0,0 +1,7 @@ +function Controller () { + 'use strict'; + 'ngInject'; + +} + +exports.controller = Controller; diff --git a/generators/controller/templates/controller.spec.js b/generators/controller/templates/controller.spec.js new file mode 100644 index 0000000..b919e4b --- /dev/null +++ b/generators/controller/templates/controller.spec.js @@ -0,0 +1,21 @@ +import 'app/<%= componentPath %>/module'; + +describe('<%= classComponentName %> <%= componentType %>', function () { + 'use strict'; + + var $controller; + + beforeEach(module('<%= componentName %>')); + + beforeEach(inject(function (_$controller_) { + // The injector unwraps the underscores (_) from around the parameter names when matching + $controller = _$controller_; + })); + + it('exists', inject(function ($controller) { + var controller = $controller('<%= classComponentName %>Ctrl', { + }); + + expect(controller).toBeDefined(); + })); +}); diff --git a/generators/directive/index.js b/generators/directive/index.js new file mode 100644 index 0000000..685cbba --- /dev/null +++ b/generators/directive/index.js @@ -0,0 +1,116 @@ +var generators = require('yeoman-generator'), + parseComponentName = require('../../lib/parse-component-name'), + _ = require('underscore.string'); + +module.exports = generators.Base.extend({ + componentType: 'Directive', + + constructor: function (componentPath, options) { + generators.Base.apply(this, arguments); + + this.argument('componentPath', { + type : String, + desc : this.componentType + ' path and name (e.g. `user` or a nested `account/user`)', + required : false + }); + + if (this.componentPath !== undefined) { + this.componentPath = _.dasherize(this.componentPath); + + this.componentName = parseComponentName.call(this, this.componentPath); + + this.hyphenComponentName = _.dasherize(this.componentName); + + this.classComponentName = _.classify(this.componentName); + this.componentName = _.camelize(this.componentName); + } + }, + + ask: function () { + var prompts = [{ + type : 'input', + name : 'componentPath', + message : this.componentType + ' name', + validate : function (componentPath) { + if (!componentPath) { + return this.componentType + ' name is required'; + } + + return true; + }.bind(this) + }], + done; + + if (this.componentName !== undefined) { + return; + } + + done = this.async(); + this.prompt(prompts, function (props) { + this.componentPath = props.componentPath; + this.componentPath = _.dasherize(this.componentPath); + + this.componentName = parseComponentName.call(this, this.componentPath); + + this.hyphenComponentName = _.dasherize(this.componentName); + + this.classComponentName = _.classify(this.componentName); + this.componentName = _.camelize(this.componentName); + + done(); + }.bind(this)); + }, + + writing: { + component: function () { + var componentLowerCase = this.componentType.toLowerCase(); + + this.fs.copyTpl( + this.templatePath(componentLowerCase + '.js'), + this.destinationPath('app/' + this.componentPath + '/' + componentLowerCase + '.js'), + this + ); + }, + + componentTest: function () { + var componentLowerCase = this.componentType.toLowerCase(); + + if (this.generateTest === false) { + return; + } + + this.fs.copyTpl( + this.templatePath(componentLowerCase + '.spec.js'), + this.destinationPath('tests/unit/' + this.componentPath + '/' + componentLowerCase + '.spec.js'), + this + ); + }, + + // module: function () { + // this.composeWith('angular-bro:module', { + // args: [this.componentPath] + // }, { + // link: 'weak' + // }); + // }, + + controller: function () { + this.composeWith('angular-bro:controller', { + args: [this.componentPath], + options: { + fromDirective: true + } + }, { + link: 'weak' + }); + }, + + template: function () { + this.composeWith('angular-bro:template', { + args: [this.componentPath] + }, { + link: 'weak' + }); + } + } +}); diff --git a/generators/directive/templates/directive.js b/generators/directive/templates/directive.js new file mode 100644 index 0000000..04ddedc --- /dev/null +++ b/generators/directive/templates/directive.js @@ -0,0 +1,16 @@ +import { controller } from 'app/<%= componentPath %>/controller'; + +function Directive () { + 'use strict'; + 'ngInject'; + + return { + + restrict: 'AECM', + templateUrl: '<%= componentPath %>/template.html', + controller: controller + + }; +} + +exports.directive = Directive; diff --git a/generators/directive/templates/directive.spec.js b/generators/directive/templates/directive.spec.js new file mode 100644 index 0000000..eaa1018 --- /dev/null +++ b/generators/directive/templates/directive.spec.js @@ -0,0 +1,37 @@ +import 'app/scripts/templates'; +import 'app/<%= componentPath %>/module'; + +describe('<%= componentName %> <%= componentType %>', function () { + 'use strict'; + + var $compile, $rootScope; + + beforeEach(module('templates-app')); + beforeEach(module('<%= componentName %>')); + + beforeEach(inject(function (_$compile_, _$rootScope_) { + // The injector unwraps the underscores (_) from around the parameter names when matching + $compile = _$compile_; + $rootScope = _$rootScope_.$new(); + })); + + it('exists', inject(function () { + var element = $compile("<<%= hyphenComponentName %>>>")($rootScope); + + // fire all the watches, so the scope expressions will be evaluated + $rootScope.$digest(); + + // Check that the compiled element contains the templated content + expect(element.html()).not.toContain('<<%= hyphenComponentName %>>>'); + })); + + it('replaces html', inject(function () { + var element = $compile("<<%= hyphenComponentName %>>>")($rootScope); + + // fire all the watches, so the scope expressions will be evaluated + $rootScope.$digest(); + + expect(element.find('div')).toBeDefined(); + expect(element.find('div').text()).toEqual('<%= componentName %>'); + })); +}); diff --git a/generators/factory/index.js b/generators/factory/index.js new file mode 100644 index 0000000..25840dd --- /dev/null +++ b/generators/factory/index.js @@ -0,0 +1,23 @@ +var baseComponent = require('../../lib/base-component'), + generators = require('yeoman-generator'), + extend = require('extend'), + generator; + +generator = extend(true, {}, baseComponent, { + componentType: 'Factory', + + writing: { + module: function () { + this.composeWith('angular-bro:module', { + args: [this.componentPath], + options: { + fromFactory: true, + } + }, { + link: 'weak' + }); + } + } +}); + +module.exports = generators.Base.extend(generator); diff --git a/generators/factory/templates/factory.js b/generators/factory/templates/factory.js new file mode 100644 index 0000000..1bb6d8c --- /dev/null +++ b/generators/factory/templates/factory.js @@ -0,0 +1,8 @@ +function Factory () { + 'use strict'; + 'ngInject'; + + return {}; +} + +exports.factory = Factory; diff --git a/generators/factory/templates/factory.spec.js b/generators/factory/templates/factory.spec.js new file mode 100644 index 0000000..f1d3b9e --- /dev/null +++ b/generators/factory/templates/factory.spec.js @@ -0,0 +1,18 @@ +import 'app/<%= componentPath %>/module'; + +describe('<%= classComponentName %> <%= componentType %>', function () { + 'use strict'; + + var $factory; + + beforeEach(module('<%= componentName %>')); + + beforeEach(inject(function (_<%= componentName %>_) { + // The injector unwraps the underscores (_) from around the parameter names when matching + $factory = _<%= componentName %>_; + })); + + it('exists', inject(function () { + expect($factory).toBeDefined(); + })); +}); diff --git a/generators/mock/index.js b/generators/mock/index.js new file mode 100644 index 0000000..2578e5c --- /dev/null +++ b/generators/mock/index.js @@ -0,0 +1,20 @@ +var baseComponent = require('../../lib/base-component'), + generators = require('yeoman-generator'), + extend = require('extend'), + generator; + +generator = extend(true, {}, baseComponent, { + componentType: 'Mock', + + writing: { + component: function () { + this.fs.copyTpl( + this.templatePath('mock.js'), + this.destinationPath('server/mocks/' + this.componentPath + '.js'), + this + ); + } + } +}); + +module.exports = generators.Base.extend(generator); diff --git a/generators/mock/templates/mock.js b/generators/mock/templates/mock.js new file mode 100644 index 0000000..9dd6e54 --- /dev/null +++ b/generators/mock/templates/mock.js @@ -0,0 +1,46 @@ +module.exports = function (app) { + var express = require('express'), + router = express.Router(); + + router.get('/', function (req, res) { + res.send({ + '<%= componentName %>': [] + }); + }); + + router.post('/', function (req, res) { + res.status(201).end(); + }); + + router.get('/:id', function (req, res) { + res.send({ + '<%= componentName %>': { + id: req.params.id + } + }); + }); + + router.put('/:id', function (req, res) { + res.send({ + '<%= componentName %>': { + id: req.params.id + } + }); + }); + + router.delete('/:id', function (req, res) { + res.status(204).end(); + }); + + // The POST and PUT call will not contain a request body + // because the body-parser is not included by default. + // To use req.body, run: + + // npm install --save-dev body-parser + + // After installing, you need to `use` the body-parser for + // this mock uncommenting the following line: + // + // app.use('/api/<%= componentName %>', require('body-parser').json()); + app.use('/api/<%= componentName %>', router); +}; diff --git a/generators/module/index.js b/generators/module/index.js new file mode 100644 index 0000000..8af036f --- /dev/null +++ b/generators/module/index.js @@ -0,0 +1,14 @@ +var baseComponent = require('../../lib/base-component'), + generators = require('yeoman-generator'), + extend = require('extend'), + generator; + +generator = extend(true, {}, baseComponent, { + componentType: 'Module', + generateTest: false, + fromState: false, + fromDirective: false, + fromController: false +}); + +module.exports = generators.Base.extend(generator); diff --git a/generators/module/templates/module.js b/generators/module/templates/module.js new file mode 100644 index 0000000..2c5b749 --- /dev/null +++ b/generators/module/templates/module.js @@ -0,0 +1,40 @@ +import angular from 'angular'; +<% if (fromState === true) { -%> +import { state } from 'app/<%= componentPath %>/state'; +<% } -%> +<% if (fromDirective === true) { -%> +import { directive } from 'app/<%= componentPath %>/directive'; +<% } -%> +<% if (fromController === true) { -%> +import { controller } from 'app/<%= componentPath %>/controller'; +<% } -%> +<% if (fromService === true) { -%> +import { service } from 'app/<%= componentPath %>/service'; +<% } -%> +<% if (fromFactory === true) { -%> +import { factory } from 'app/<%= componentPath %>/factory'; +<% } -%> +<% if (fromProvider === true) { -%> +import { provider } from 'app/<%= componentPath %>/provider'; +<% } -%> + +export default angular.module('<%= componentName %>', []) +<% if (fromState === true) { -%> + .config(state) +<% } -%> +<% if (fromDirective === true) { -%> + .directive('<%= componentName %>', directive) +<% } -%> +<% if (fromController === true) { -%> + .controller('<%= classComponentName %>Ctrl', controller) +<% } -%> +<% if (fromService === true) { -%> + .service('<%= classComponentName %>', service) +<% } -%> +<% if (fromFactory === true) { -%> + .factory('<%= componentName %>', factory) +<% } -%> +<% if (fromProvider === true) { -%> + .provider('<%= componentName %>', provider) +<% } -%> +; // Ends the module declaration diff --git a/generators/provider/index.js b/generators/provider/index.js new file mode 100644 index 0000000..50413c5 --- /dev/null +++ b/generators/provider/index.js @@ -0,0 +1,23 @@ +var baseComponent = require('../../lib/base-component'), + generators = require('yeoman-generator'), + extend = require('extend'), + generator; + +generator = extend(true, {}, baseComponent, { + componentType: 'Provider', + + writing: { + module: function () { + this.composeWith('angular-bro:module', { + args: [this.componentPath], + options: { + fromProvider: true, + } + }, { + link: 'weak' + }); + } + } +}); + +module.exports = generators.Base.extend(generator); diff --git a/generators/provider/templates/provider.js b/generators/provider/templates/provider.js new file mode 100644 index 0000000..03bb0af --- /dev/null +++ b/generators/provider/templates/provider.js @@ -0,0 +1,10 @@ +function Provider () { + 'use strict'; + 'ngInject'; + + this.$get = function () { + + }; +} + +exports.provider = Provider; diff --git a/generators/provider/templates/provider.spec.js b/generators/provider/templates/provider.spec.js new file mode 100644 index 0000000..1179951 --- /dev/null +++ b/generators/provider/templates/provider.spec.js @@ -0,0 +1,19 @@ +import 'app/<%= componentPath %>/module'; + +describe('<%= classComponentName %> <%= componentType %>', function () { + 'use strict'; + + var $provider; + + beforeEach(module('<%= componentName %>')); + + beforeEach(inject(function (_<%= componentName %>_) { + // The injector unwraps the underscores (_) from around the parameter names when matching + $provider = _<%= componentName %>_; + console.log($provider); + })); + + it('exists', inject(function () { + expect($provider).toBeDefined(); + })); +}); diff --git a/generators/proxy/index.js b/generators/proxy/index.js new file mode 100644 index 0000000..3b420b7 --- /dev/null +++ b/generators/proxy/index.js @@ -0,0 +1,65 @@ +var generators = require('yeoman-generator'), + parseComponentName = require('../../lib/parse-component-name'); + +module.exports = generators.Base.extend({ + componentType: 'Proxy', + + constructor: function () { + generators.Base.apply(this, arguments); + + this.argument('proxyPath', { + type : String, + desc : this.componentType + ' relative path and name (e.g. `/user` or a nested `/account/user`)', + required : false + }); + + this.argument('proxyTarget', { + type : String, + desc : this.componentType + ' target path and name (e.g. `http://localhost/api/v1/user/`)', + required : false + }); + + if (this.componentPath !== undefined) { + this.componentName = parseComponentName.call(this, this.componentPath); + } + }, + + ask: function () { + var prompts = [{ + type : 'input', + name : 'componentPath', + message : this.componentType + ' name', + validate : function (componentPath) { + if (!componentPath) { + return this.componentType + ' name is required'; + } + + return true; + }.bind(this) + }], + done; + + if (this.componentName !== undefined) { + return; + } + + done = this.async(); + this.prompt(prompts, function (props) { + this.componentPath = props.componentPath; + this.componentName = parseComponentName.call(this, props.componentPath); + done(); + }.bind(this)); + }, + + writing: { + component: function () { + var componentLowerCase = this.componentType.toLowerCase(); + + this.fs.copyTpl( + this.templatePath(componentLowerCase + '.js'), + this.destinationPath('app/' + this.componentPath + '/' + componentLowerCase + '.js'), + this + ); + } + } +}); diff --git a/generators/proxy/templates/proxy.js b/generators/proxy/templates/proxy.js new file mode 100644 index 0000000..df254b5 --- /dev/null +++ b/generators/proxy/templates/proxy.js @@ -0,0 +1,17 @@ +var proxyPath = '<%= componentPath %>'; + +module.exports = function (app) { + // For options, see: + // https://github.com/nodejitsu/node-http-proxy + var proxy = require('http-proxy').createProxyServer({}); + + proxy.on('error', function (err, req) { + console.error(err, req.url); + }); + + app.use(proxyPath, function (req, res/*, next*/) { + // include root path in proxied request + req.url = proxyPath + '/' + req.url; + proxy.web(req, res, { target: '<%= proxyTarget %>' }); + }); +}; diff --git a/generators/server/index.js b/generators/server/index.js new file mode 100644 index 0000000..a656159 --- /dev/null +++ b/generators/server/index.js @@ -0,0 +1,30 @@ +var generators = require('yeoman-generator'); + +module.exports = generators.Base.extend({ + constructor: function () { + generators.Base.apply(this, arguments); + }, + + writing: { + server: function () { + this.fs.copy( + this.templatePath('index.js'), + this.destinationPath('server/index.js') + ); + + this.fs.copy( + this.templatePath('.gitkeep'), + this.destinationPath('server/mocks/.gitkeep') + ); + + this.fs.copy( + this.templatePath('.gitkeep'), + this.destinationPath('server/proxies/.gitkeep') + ); + } + }, + + end: function () { + this.npmInstall(['morgan@1.7.0'], { saveDev: true }); + } +}); diff --git a/generators/server/templates/.gitkeep b/generators/server/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/generators/server/templates/index.js b/generators/server/templates/index.js new file mode 100644 index 0000000..487f52e --- /dev/null +++ b/generators/server/templates/index.js @@ -0,0 +1,22 @@ +// To use it create some files under `routes/` +// e.g. `server/routes/users.js` +// +// module.exports = function(app) { +// app.get('/users', function(req, res) { +// res.send('hello'); +// }); +// }; + +module.exports = function (app, config) { + var globSync = require('glob').sync, + mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require), + proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require), + morgan = require('morgan'); + + app.use(morgan('dev')); + + // Enable mock data + mocks.forEach(function (route) { route(app); }); + // Enable proxies + proxies.forEach(function (route) { route(app); }); +}; diff --git a/generators/service/index.js b/generators/service/index.js new file mode 100644 index 0000000..4dfbd12 --- /dev/null +++ b/generators/service/index.js @@ -0,0 +1,23 @@ +var baseComponent = require('../../lib/base-component'), + generators = require('yeoman-generator'), + extend = require('extend'), + generator; + +generator = extend(true, {}, baseComponent, { + componentType: 'Service', + + writing: { + module: function () { + this.composeWith('angular-bro:module', { + args: [this.componentPath], + options: { + fromService: true, + } + }, { + link: 'weak' + }); + } + } +}); + +module.exports = generators.Base.extend(generator); diff --git a/generators/service/templates/service.js b/generators/service/templates/service.js new file mode 100644 index 0000000..ee385ce --- /dev/null +++ b/generators/service/templates/service.js @@ -0,0 +1,7 @@ +function Service () { + 'use strict'; + 'ngInject'; + +} + +exports.service = Service; diff --git a/generators/service/templates/service.spec.js b/generators/service/templates/service.spec.js new file mode 100644 index 0000000..3ef74d3 --- /dev/null +++ b/generators/service/templates/service.spec.js @@ -0,0 +1,18 @@ +import 'app/<%= componentPath %>/module'; + +describe('<%= classComponentName %> <%= componentType %>', function () { + 'use strict'; + + var $service; + + beforeEach(module('<%= componentName %>')); + + beforeEach(inject(function (_<%= classComponentName %>_) { + // The injector unwraps the underscores (_) from around the parameter names when matching + $service = _<%= classComponentName %>_; + })); + + it('exists', inject(function () { + expect($service).toBeDefined(); + })); +}); diff --git a/generators/state/index.js b/generators/state/index.js new file mode 100644 index 0000000..a050863 --- /dev/null +++ b/generators/state/index.js @@ -0,0 +1,96 @@ +var generators = require('yeoman-generator'), + parseComponentName = require('../../lib/parse-component-name'), + _ = require('underscore.string'); + +module.exports = generators.Base.extend({ + componentType: 'State', + + constructor: function () { + generators.Base.apply(this, arguments); + + this.argument('componentPath', { + type : String, + desc : this.componentType + ' relative path and name (e.g. `user` or a nested `account/user`)', + required : false + }); + + if (this.componentPath !== undefined) { + this.componentPath = _.dasherize(this.componentPath); + + this.componentName = parseComponentName.call(this, this.componentPath); + this.classComponentName = _.classify(this.componentName); + this.componentName = _.camelize(this.componentName); + } + }, + + ask: function () { + var prompts = [{ + type : 'input', + name : 'componentPath', + message : this.componentType + ' name', + validate : function (componentPath) { + if (!componentPath) { + return this.componentType + ' name is required'; + } + + return true; + }.bind(this) + }], + done; + + if (this.componentName !== undefined) { + return; + } + + done = this.async(); + this.prompt(prompts, function (props) { + this.componentPath = props.componentPath; + this.componentPath = _.dasherize(this.componentPath); + + this.componentName = parseComponentName.call(this, this.componentPath); + this.classComponentName = _.classify(this.componentName); + this.componentName = _.camelize(this.componentName); + + done(); + }.bind(this)); + }, + + writing: { + component: function () { + var componentLowerCase = this.componentType.toLowerCase(); + + this.fs.copyTpl( + this.templatePath(componentLowerCase + '.js'), + this.destinationPath('app/' + this.componentPath + '/' + componentLowerCase + '.js'), + this + ); + }, + + // module: function () { + // this.composeWith('angular-bro:module', { + // args: [this.componentPath] + // }, { + // link: 'weak' + // }); + // }, + + controller: function () { + this.composeWith('angular-bro:controller', { + args: [this.componentPath], + options: { + fromState: true + } + }, { + link: 'weak' + }); + }, + + template: function () { + this.composeWith('angular-bro:template', { + args: [this.componentPath] + }, { + link: 'weak' + }); + } + } +}); diff --git a/generators/state/templates/state.js b/generators/state/templates/state.js new file mode 100644 index 0000000..5c06ee4 --- /dev/null +++ b/generators/state/templates/state.js @@ -0,0 +1,14 @@ +import { controller } from 'app/<%= componentPath %>/controller'; + +function State ($stateProvider) { + 'use strict'; + 'ngInject'; + + $stateProvider.state('<%= componentPath %>', { + url: '/<%= componentPath %>', + templateUrl: '<%= componentPath %>/template.html', + controller: controller + }); +} + +exports.state = State; diff --git a/generators/state/templates/state.spec.js b/generators/state/templates/state.spec.js new file mode 100644 index 0000000..e69de29 diff --git a/generators/template/index.js b/generators/template/index.js new file mode 100644 index 0000000..fcfdab9 --- /dev/null +++ b/generators/template/index.js @@ -0,0 +1,22 @@ +var baseComponent = require('../../lib/base-component'), + generators = require('yeoman-generator'), + extend = require('extend'), + generator; + +generator = extend(true, {}, baseComponent, { + componentType: 'Template', + generateTest: false, + writing: { + component: function () { + var componentLowerCase = this.componentType.toLowerCase(); + + this.fs.copyTpl( + this.templatePath(componentLowerCase + '.html'), + this.destinationPath('app/' + this.componentPath + '/' + componentLowerCase + '.html'), + this + ); + } + } +}); + +module.exports = generators.Base.extend(generator); diff --git a/generators/template/templates/template.html b/generators/template/templates/template.html new file mode 100644 index 0000000..5c18d8b --- /dev/null +++ b/generators/template/templates/template.html @@ -0,0 +1 @@ +
<%= componentName %>
diff --git a/lib/base-component.js b/lib/base-component.js new file mode 100644 index 0000000..284a832 --- /dev/null +++ b/lib/base-component.js @@ -0,0 +1,102 @@ +var generators = require('yeoman-generator'), + parseComponentName = require('./parse-component-name'), + _ = require('underscore.string'); + +module.exports = { + generateTest: true, + fromState: false, + fromDirective: false, + fromController: false, + fromService: false, + fromFactory: false, + fromProvider: false, + + constructor: function (componentPath, options) { + generators.Base.apply(this, arguments); + + this.fromState = options.fromState; + this.fromDirective = options.fromDirective; + this.fromController = options.fromController; + this.fromService = options.fromService; + this.fromFactory = options.fromFactory; + this.fromProvider = options.fromProvider; + + this.argument('componentPath', { + type : String, + desc : this.componentType + ' path and name (e.g. `user` or a nested `account/user`)', + required : false + }); + + if (this.componentPath !== undefined) { + this.componentPath = _.dasherize(this.componentPath); + + this.componentName = parseComponentName.call(this, this.componentPath); + + this.hyphenComponentName = _.dasherize(this.componentName); + + this.classComponentName = _.classify(this.componentName); + this.componentName = _.camelize(this.componentName); + } + }, + + ask: function () { + var prompts = [{ + type : 'input', + name : 'componentPath', + message : this.componentType + ' name', + validate : function (componentPath) { + if (!componentPath) { + return this.componentType + ' name is required'; + } + + return true; + }.bind(this) + }], + done; + + if (this.componentName !== undefined) { + return; + } + + done = this.async(); + this.prompt(prompts, function (props) { + this.componentPath = props.componentPath; + this.componentPath = _.dasherize(this.componentPath); + + this.componentName = parseComponentName.call(this, this.componentPath); + + this.hyphenComponentName = _.dasherize(this.componentName); + + this.classComponentName = _.classify(this.componentName); + this.componentName = _.camelize(this.componentName); + + done(); + }.bind(this)); + }, + + writing: { + component: function () { + var componentLowerCase = this.componentType.toLowerCase(); + + this.fs.copyTpl( + this.templatePath(componentLowerCase + '.js'), + this.destinationPath('app/' + this.componentPath + '/' + componentLowerCase + '.js'), + this + ); + }, + + componentTest: function () { + var componentLowerCase = this.componentType.toLowerCase(); + + if (this.generateTest === false) { + return; + } + + this.fs.copyTpl( + this.templatePath(componentLowerCase + '.spec.js'), + this.destinationPath('tests/unit/' + this.componentPath + '/' + componentLowerCase + '.spec.js'), + this + ); + } + } +}; diff --git a/lib/parse-component-name.js b/lib/parse-component-name.js new file mode 100644 index 0000000..1c7a88b --- /dev/null +++ b/lib/parse-component-name.js @@ -0,0 +1,16 @@ +/** + * To be called in context of a generator instance. + * + * @param {String} componentPath path to parse name from + * + * @returns {String} component name + */ +module.exports = function (componentPath) { + var pieces = componentPath.split('/'); + + if (!componentPath || typeof componentPath !== 'string') { + this.env.error('Must provide the name of the ' + this.componentType.toLowerCase() + ' to be created'); + } + + return pieces.pop(); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..4c9bca0 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "generator-angular-bro", + "version": "0.1.0", + "description": "Angular app scaffolding and build chain", + "main": "generators/app/index.js", + "files": [ + "generators/app", + "generators/controller", + "generators/directive", + "generators/factory", + "generators/mock", + "generators/module", + "generators/provider", + "generators/proxy", + "generators/server", + "generators/service", + "generators/state", + "generators/template" + ], + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/adambullmer/generator-angular-bro.git" + }, + "keywords": [ + "yeoman-generator", + "angular", + "broccoli", + "es6" + ], + "author": "Adam Bullmer ", + "license": "MIT", + "bugs": { + "url": "https://github.com/adambullmer/generator-angular-bro/issues" + }, + "homepage": "https://github.com/adambullmer/generator-angular-bro#readme", + "dependencies": { + "extend": "^3.0.0", + "generator-node": "^1.10.0", + "underscore.string": "^3.3.4", + "yeoman-generator": "^0.22.5" + } +}