diff --git a/README.md b/README.md index 2d5deaef..8392cca8 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ yo bat:demo **Note:** that the latter _will_ result in a few clashes with some of the files produced from the earlier `yo bat` run. These are however, caveat codor, harmless. -## Grunt tasks +### Grunt tasks After you're all set up, you'll want to build your project; this is where [Grunt](http://gruntjs.com) comes in: @@ -169,9 +169,10 @@ Grunt can do more than just that; here's a recap of common grunt idioms: command | description :-- |:-- -`grunt [default]` | does a for-production, non-debugging, all-parts, tested, minified build plus artifacts; +`grunt [default]` | shortcut for `grunt dist` unless the `GRUNT_TASKS` environment variable specifies a space separated list of alternative tasks to run instead; +`grunt dist` | does a for-production, non-debugging, all-parts, tested, minified build plus artifacts; `grunt debug` | does a for-testing, debugging, all-parts except documentation, tested, as-is build; -`grunt dev` | does a for-local, debugging, all-parts except documentation, as-is build;
_(**Note that this variant doesn't exit**. Instead it'll keep a close watch on filesystem changes, selectively re-triggering part builds as needed)_ +`grunt dev` | does a for-local, debugging, all-parts except documentation, as-is build;
_**(Note that this variant doesn't exit**. Instead it'll keep a close watch on filesystem changes, selectively re-triggering part builds as needed)_ `grunt doc` | will build just the code documentation; `grunt lint` | will just lint your code; `grunt test` | will run your test suite; @@ -179,11 +180,11 @@ command | description `grunt --help` | will show you all of the above and the kitchen sink; -## Unit tests +### Unit tests BAT comes with support for unit testing using [Karma](http://karma-runner.github.io/1.0/), [Jasmine](http://jasmine.github.io/2.4/introduction.html) and [PhantomJS](http://phantomjs.org/). -Unit testing is an integrated build step in both `default` and `debug` build runs, but can also be run independently as: +Unit testing is an integrated build step in both `dist` and `debug` build runs, but can also be run independently as: ```shell grunt test @@ -222,15 +223,19 @@ Clone this repository somewhere, switch to it, then: ```bash $ git config commit.template ./.gitmessage +$ git checkout master +$ git checkout develop $ git flow init -d $ npm install ``` This will: - * Setup [a helpful reminder](.gitmessage) of how to make [a good commit message](#commit-message-format-discipline). If you adhere to this, then a detailed, - meaningful [CHANGELOG](CHANGELOG.md) can be constructed automatically. - * Setup the git flow [branching model](#branching-model) and checkout the `develop` branch. - * Install all required dependencies. + + * Set up [a helpful reminder](.gitmessage) of how to make [a good commit message](#commit-message-format-discipline). If you adhere to this, then a + detailed, meaningful [CHANGELOG](CHANGELOG.md) can be constructed automatically; + * Ensure you have local `master` and `develop` branches tracking their respective remote counterparts; + * Set up the git flow [branching model](#branching-model) with default branch names; + * Install all required dependencies; ### Commit @@ -256,40 +261,45 @@ $ git config commit.template ./.gitmessage ### Release -* Determine what your next [semver](https://docs.npmjs.com/getting-started/semantic-versioning#semver-for-publishers) `` should be: - ```bash - $ version="" - ``` + * Determine what your next [semver](https://docs.npmjs.com/getting-started/semantic-versioning#semver-for-publishers) `` should be: + + ```bash + $ version="" + ``` + + * Create and checkout a `release/v` branch off of `develop`: + + ```bash + $ git flow release start "v${version}" + ``` + + * Bump the package's `.version`, update the [CHANGELOG](./CHANGELOG.md), commit these, and tag the commit as `v`: + + ```bash + $ npm run release + ``` -* Create and checkout a `release/v` branch off of `develop`: - ```bash - $ git flow release start "v${version}" - ``` + * If all is well this new `version` **should** be identical to your intended ``: -* Bump the package's `.version`, update the [CHANGELOG](./CHANGELOG.md), commit these, and tag the commit as `v`: - ```bash - $ npm run release - ``` + ```bash + $ jq ".version == \"${version}\"" package.json + ``` -* If all is well this new `version` **should** be identical to your intended ``: - ```bash - $ jq ".version == \"${version}\"" package.json - ``` + *If this is not the case*, then either your assumptions about what changed are wrong, or (at least) one of your commits did not adhere to the + [Commit Message Format Discipline](#commit-message-format-discipline); **Abort the release, and sort it out first.** - *If this is not the case*, then either your assumptions about what changed are wrong, or (at least) one of your commits did not adhere to the - [Commit Message Format Discipline](#commit-message-format-discipline); **Abort the release, and sort it out first.** + * Merge `release/v` back into both `develop` and `master`, checkout `develop` and delete `release/v`: -* Merge `release/v` back into both `develop` and `master`, checkout `develop` and delete `release/v`: - ```bash - $ git flow release finish -n "v${version}" - ``` + ```bash + $ git flow release finish -n "v${version}" + ``` - Note that contrary to vanilla `git flow`, the merge commit into `master` will *not* have been tagged (that's what the - [`-n`](https://github.com/nvie/gitflow/wiki/Command-Line-Arguments#git-flow-release-finish--fsumpkn-version) was for). This is done because `npm run release` - has already tagged its own commit. + Note that contrary to vanilla `git flow`, the merge commit into `master` will *not* have been tagged (that's what the + [`-n`](https://github.com/nvie/gitflow/wiki/Command-Line-Arguments#git-flow-release-finish--fsumpkn-version) was for). This is done because + `npm run release` has already tagged its own commit. - I believe that in practice, this won't make a difference for the use of `git flow`; and ensuring it's done the other way round instead would render the use - of `conventional-changelog` impossible. + I believe that in practice, this won't make a difference for the use of `git flow`; and ensuring it's done the other way round instead would render the use + of `conventional-changelog` impossible. ### Publish diff --git a/generators/api/index.js b/generators/api/index.js new file mode 100644 index 00000000..ad6b162a --- /dev/null +++ b/generators/api/index.js @@ -0,0 +1,177 @@ +'use strict'; + +// +// Yeoman bat:api sub-generator. +// + +var generators = require( 'yeoman-generator' ) +, yosay = require( 'yosay' ) +, youtil = require( './../../lib/youtil.js' ) +, chalk = require( 'chalk' ) +, _ = require( 'lodash' ) +; + +var ApiGenerator = generators.Base.extend( + { + constructor: function () + { + generators.Base.apply( this, arguments ); + + this.description = this._description( 'API-instance' ); + + this.argument( + 'apiName' + , { + type: String + , required: false + , desc: 'The name of the API-instance to create.' + } + ); + + // Also add 'apiName' as a - hidden - option, defaulting to the positional argument's value. + // This way `_promptsPruneByOptions()` can filter away prompting for the API name too. + // + this.option( + 'apiName' + , { + type: String + , desc: 'The name of the API-instance to create.' + , default: this.apiName + , hide: true + } + ); + + // Normal options. + // + this.option( + 'description' + , { + type: String + , desc: 'The purpose of this API.' + } + ); + + this.option( + 'url' + , { + type: String + , desc: 'The base URL for this API.' + } + ); + + } + + , initializing: function () + { + this._assertBatApp(); + + // Container for template expansion data. + // + this.templateData = {}; + } + + , prompting: + { + askSomeQuestions: function () + { + // Ask only those question that have not yet been provided with answers via the command line. + // + var prompts = this._promptsPruneByOptions( + [ + { + type: 'input' + , name: 'apiName' + , message: 'What is the name of this API-instance you so desire?' + , default: youtil.definedToString( this.options.apiName ) + , validate: youtil.isIdentifier + , filter: function ( value ) + { + return _.lowerFirst( _.trim( value ).replace( /api$/i, '' )); + } + } + , { + type: 'input' + , name: 'description' + , message: 'What is the purpose (description) of this API?' + , default: function ( answers ) + { + return ( + youtil.definedToString( this.options.description ) + || ( + 'A collection of services\' endpoints available on the ' + + ( answers.apiName || this.templateData.apiName ) + + ' API.' + ) + ); + }.bind( this ) + , validate: youtil.isNonBlank + , filter: youtil.sentencify + } + , { + type: 'input' + , name: 'url' + , message: 'What is the base URL for this API? ' + chalk.gray( ' - please enter as code:' ) + , default: youtil.definedToString( this.options.url ) + , validate: youtil.isCoffeeScript + } + ] + ) + ; + + if ( prompts.length ) + { + // Have Yeoman greet the user. + // + this.log( yosay( 'So you want a BAT API-instance?' ) ); + + return ( + this + .prompt( prompts ) + .then( function ( answers ) { _.extend( this.templateData, answers ); }.bind( this ) ) + ); + } + } + } + + , configuring: function () + { + var data = this.templateData + , apiName = data.apiName + ; + + _.extend( + data + , { + className: _.upperFirst( apiName ) + 'Api' + , fileBase: _.kebabCase( _.deburr( apiName )) + + , userName: this.user.git.name() + + } + ); + } + + , writing: + { + createApi: function () + { + var data = this.templateData + , templates = + { + 'api.coffee': [ 'src/apis/' + data.fileBase + '.coffee' ] + } + ; + + this._templatesProcess( templates ); + } + } + } +); + +_.extend( + ApiGenerator.prototype +, require( './../../lib/generator.js' ) +, require( './../../lib/sub-generator.js' ) +); + +module.exports = ApiGenerator; diff --git a/generators/api/templates/api.coffee b/generators/api/templates/api.coffee new file mode 100644 index 00000000..1c1c20ea --- /dev/null +++ b/generators/api/templates/api.coffee @@ -0,0 +1,54 @@ +( ( factory ) -> + if typeof exports is 'object' + module.exports = factory( + + require( './../collections/api-services.coffee' ) + ) + else if typeof define is 'function' and define.amd + define( [ + + './../collections/api-services.coffee' + ], factory ) + return +)(( + + ApiServicesCollection +) -> + + ###* + # @author <%- userName %> + # @module App + # @submodule Apis + ### + + 'use strict' + + + ###*<% if ( description ) { %> + # <%- description %> + #<% } %> + # @class <%- className %> + # @static + ### + + new ApiServicesCollection( + + [ + ] + + , + ###* + # The `<%- className %>`'s base url. + # + # @property url + # @type String + # @final + # + # @default <%- url %> + ### + + url: <%- url %> + + ) + +) diff --git a/generators/app/index.js b/generators/app/index.js index abe877d0..c5a17a30 100644 --- a/generators/app/index.js +++ b/generators/app/index.js @@ -14,10 +14,7 @@ var generators = require( 'yeoman-generator' ) , _ = require( 'lodash' ) ; -var clean = require( 'underscore.string/clean' ) -, dasherize = require( 'underscore.string/dasherize' ) -, trim = require( 'underscore.string/trim' ) -; +var clean = require( 'underscore.string/clean' ); // Use a different delimiter when our template itself is meant to be a template or template-like. // @@ -33,12 +30,14 @@ var AppGenerator = generators.Base.extend( { generators.Base.apply( this, arguments ); + this.description = this._description( 'project and barebones app' ); + this.argument( 'packageName' , { type: String , required: false - , desc: 'The name of the webapp to create.' + , desc: 'The name of the app to create.' } ); @@ -49,7 +48,7 @@ var AppGenerator = generators.Base.extend( 'packageName' , { type: String - , desc: 'The name of the webapp to create.' + , desc: 'The name of the app to create.' , default: this.packageName , hide: true } @@ -61,7 +60,7 @@ var AppGenerator = generators.Base.extend( 'description' , { type: String - , desc: 'The purpose of the webapp to create.' + , desc: 'The purpose of this app.' } ); @@ -69,7 +68,7 @@ var AppGenerator = generators.Base.extend( 'authorName' , { type: String - , desc: 'The name of the main author creating the webapp.' + , desc: 'The name of the main author creating this app.' } ); @@ -77,7 +76,7 @@ var AppGenerator = generators.Base.extend( 'authorEmail' , { type: String - , desc: 'The email address of the main author creating the webapp.' + , desc: 'The email address of this author.' } ); @@ -85,7 +84,7 @@ var AppGenerator = generators.Base.extend( 'authorUrl' , { type: String - , desc: 'A website url identifying the main author creating the webapp.' + , desc: 'An optional website URL identifying this author.' } ); @@ -93,7 +92,7 @@ var AppGenerator = generators.Base.extend( 'copyrightOwner' , { type: String - , desc: 'The full name of the webapp\'s copyright owner.' + , desc: 'The full name of the copyright owner of this app.' } ); @@ -101,7 +100,7 @@ var AppGenerator = generators.Base.extend( 'i18n' , { type: Boolean - , desc: 'Specify whether internationalisation support is needed.' + , desc: 'Whether this app should support internationalisation' } ); @@ -109,7 +108,7 @@ var AppGenerator = generators.Base.extend( 'i18nLocaleDefault' , { type: Boolean - , desc: 'Specify the default locale.' + , desc: 'The default locale for this app.' , alias: 'locale' } ); @@ -118,7 +117,7 @@ var AppGenerator = generators.Base.extend( 'ie8' , { type: Boolean - , desc: 'Specify whether internet explorer version 8 support is needed.' + , desc: 'Whether this app should still support IE8.' } ); @@ -126,18 +125,11 @@ var AppGenerator = generators.Base.extend( 'demo' , { type: Boolean - , desc: 'Specify whether the demonstration app should also be included.' + , desc: 'Whether the demonstration app should also be included.' } ); } - , description: - chalk.bold( - 'This is the ' + chalk.cyan( 'project and barebones app' ) - + ' generator for BAT, the Backbone Application Template' - + ' created by ' + chalk.blue( 'marv' ) + chalk.red( 'iq' ) + '.' - ) - , initializing: function () { // Load the BAT generator's 'package.json'. @@ -162,18 +154,18 @@ var AppGenerator = generators.Base.extend( { type: 'input' , name: 'packageName' - , message: 'What is the name of this webapp you so desire?' + , message: 'What is the name of this app you so desire?' , default: ( youtil.definedToString( this.options.packageName ) - || trim( dasherize( youtil.definedToString( this.appname )), '-' ) + || _.kebabCase( youtil.definedToString( this.appname )) ) , validate: youtil.isNpmName } , { type: 'input' , name: 'description' - , message: 'What is the purpose (description) of this webapp?' + , message: 'What is the purpose (description) of this app?' , default: youtil.definedToString( this.options.description ) , validate: youtil.isNonBlank , filter: youtil.sentencify @@ -181,7 +173,7 @@ var AppGenerator = generators.Base.extend( , { type: 'input' , name: 'authorName' - , message: 'What is the main author\'s name?' + , message: 'What is the name of the main author creating this app?' , default: ( youtil.definedToString( this.options.auhorName ) || youtil.definedToString( this.user.git.name() )) , validate: youtil.isNonBlank , filter: clean @@ -189,7 +181,7 @@ var AppGenerator = generators.Base.extend( , { type: 'input' , name: 'authorEmail' - , message: 'What is the main author\'s email address?' + , message: 'What is the email address of this author?' , default: ( youtil.definedToString( this.options.auhorEmail ) || youtil.definedToString( this.user.git.email() )) , validate: youtil.isNonBlank , filter: _.trim @@ -197,18 +189,22 @@ var AppGenerator = generators.Base.extend( , { type: 'input' , name: 'authorUrl' - , message: 'If any, what is the main author\'s website url?' + , message: 'If any, what is the website URL identifying this author?' , default: ( youtil.definedToString( this.options.auhorUrl ) || '' ) , filter: _.trim } , { type: 'input' , name: 'copyrightOwner' - , message: 'What is the full name of the copyright owner of this webapp?' + , message: 'What is the full name of the copyright owner of this app?' , default: function ( answers ) { - return answers.authorName; - } + return ( + youtil.definedToString( this.options.copyrightOwner ) + || answers.authorName + || this.templateData.authorName + ); + }.bind( this ) , validate: youtil.isNonBlank , filter: _.trim , store: true @@ -216,13 +212,23 @@ var AppGenerator = generators.Base.extend( , { type: 'confirm' , name: 'i18n' - , message: 'Do you need internationalisation support?' + , message: 'Should this app support internationalisation?' , default: false } , { type: 'input' , name: 'i18nLocaleDefault' - , message: 'What should the default locale be? (Please use a valid [BCP 47 language tag](https://tools.ietf.org/html/bcp47#section-2))' + , message: ( 'What is the default locale for this app?' + + chalk.gray( + ' - please use a valid ' + + chalk.cyan( '[' ) + + 'BCP 47 language tag' + + chalk.cyan( '](' ) + + chalk.blue( 'https://tools.ietf.org/html/bcp47#section-2' ) + + chalk.cyan( ')' ) + + '.' + ) + ) , default: ( youtil.definedToString( this.options.i18nLocaleDefault ) || 'en-US' ) , validate: tags.check , filter: function ( value ) @@ -237,13 +243,22 @@ var AppGenerator = generators.Base.extend( , { type: 'confirm' , name: 'jqueryCdn' - , message: 'Would you like your app to load jQuery from a CDN (googleapis.com) instead of bundling it?' + , message: ( + 'Should this app load jQuery from a CDN ' + + chalk.gray( '(googleapis.com)' ) + + ' instead of bundling it?' + ) , default: false } , { type: 'confirm' , name: 'jqueryExpose' - , message: 'Do you still need to expose that jQuery on the global scope? (Perhaps because some CDN loaded code expects it to be there)' + , message: ( + 'Should this app expose ' + + chalk.yellow( 'jQuery' ) + + ' on the global scope?' + + chalk.gray( ' - Perhaps because some CDN loaded code expects it to be there.' ) + ) , default: false , when: function( answers ) { @@ -253,13 +268,21 @@ var AppGenerator = generators.Base.extend( , { type: 'confirm' , name: 'ie8' - , message: 'Do you need IE8 and lower support? (affects the jQuery version and shims HTML5 and media query support)' + , message: ( + 'Should this app still support IE8?' + + chalk.gray( ' - affects the jQuery version and shims HTML5 and media query support.' ) + ) , default: false } , { type: 'confirm' , name: 'demo' - , message: 'Would you like the demo app now? (If not, you can always get it later through `yo bat:demo`)' + , message: ( + 'Would you like the demo app now?' + + chalk.gray( ' - if not, you can always get it later through `' ) + + chalk.yellow( 'yo bat:demo ' ) + + chalk.gray( '`.' ) + ) , default: false } ] @@ -300,6 +323,7 @@ var AppGenerator = generators.Base.extend( data.copyrightYear = new Date().getFullYear(); data.packageDescription = data.description; + data.backbone = ( this.config.get( 'backbone' ) || { className: 'Backbone', modulePath: 'backbone' } ); // // Save a '.yo-rc.json' config file. @@ -316,6 +340,8 @@ var AppGenerator = generators.Base.extend( , i18n: data.i18n , jqueryCdn: data.jqueryCdn , jqueryExpose: data.jqueryExpose + + , backbone: data.backbone } ); @@ -337,6 +363,7 @@ var AppGenerator = generators.Base.extend( // App source: 'src' + , 'src/apis' // Backbone: @@ -372,6 +399,15 @@ var AppGenerator = generators.Base.extend( , 'test/unit/spec/models' , 'test/unit/spec/views' + , 'test-report' + + // Utils + + , 'utils' + , 'utils/hbs' + , 'utils/hbs/helpers' + , 'utils/hbs/partials' + // Third-party, external libraries: , 'vendor' @@ -417,11 +453,22 @@ var AppGenerator = generators.Base.extend( // App source: + , 'src/apis/default.coffee' + , [ 'src/apis/env.coffee' ] , [ 'src/collections/api-services.coffee' ] , 'src/models/api-service.coffee' , 'src/models/build-brief.coffee' , [ 'src/models/settings-environment.coffee' ] + // App source for debug builds + + , 'src/views/debug.environment-ribbon.coffee' + , 'src/views/debug.environment-ribbon.hbs' + + // Utils + + , 'src/utils/hbs/helpers/moment.coffee' + // Style and Compass: , 'src/sass/settings/_compass.sass' @@ -432,10 +479,18 @@ var AppGenerator = generators.Base.extend( , 'src/sass/_settings.sass' , 'src/sass/_views.sass' , [ 'src/sass/app.sass' ] - , 'src/sass/debug.sass' , 'src/style/images/debug/internals.jpg' , 'src/style/images/sprites/check-green.png' + // Style for debug builds + + , 'src/sass/debug.sass' + , 'src/sass/views/_debug.environment-ribbon.sass' + + // Utils + + , 'src/utils/hbs/helpers/moment.coffee' + // Target environment settings: , 'settings/@README.md' @@ -447,6 +502,7 @@ var AppGenerator = generators.Base.extend( // Testing: , [ 'test/unit/init.coffee' ] + , 'test/unit/spec/models/settings-environment.spec.coffee' , 'test/unit/spec/trivial.spec.coffee' ] @@ -492,6 +548,13 @@ var AppGenerator = generators.Base.extend( } this._templatesProcess( templates ); + + // + // Symlink assets for Testing: + // + + this._symLink( 'settings/testing.json', 'test/unit/asset/settings.json' ); + } , setupDemo: function () @@ -568,7 +631,14 @@ var AppGenerator = generators.Base.extend( , end: { - intro: function () + // Get rid of any extraneous packages; perhaps left over from previous generator attemtps. + // + prune: function () + { + this.spawnCommand( 'npm', [ 'prune' ] ); + } + + , intro: function () { /* jshint laxbreak: true */ @@ -667,7 +737,7 @@ var AppGenerator = generators.Base.extend( this.spawnCommand( 'command', [ 'compass', '-q', '-v' ], { stdio: [ 'ignore', 'pipe', 'ignore' ] } ) .on( 'exit', function ( exit ) { - version = version.trim(); + version = _.trim( version ); if ( exit || !( semver.satisfies( version, '>=' + minver )) ) { error( exit ); } done(); @@ -694,6 +764,11 @@ var AppGenerator = generators.Base.extend( + '\n' + chalk.bold( ' * ' + chalk.yellow( 'grunt ' + chalk.cyan( '[' ) + 'default' + chalk.cyan( ']' ) + ' ' )) + + '- is a shortcut for ' + chalk.bold.yellow( 'grunt dist' ) + ' unless the ' + chalk.bold.yellow( 'GRUNT_TASKS' ) + ' environment ' + + 'variable specifies a space separated list of alternative tasks to run instead;\n' + + + '\n' + + chalk.bold( ' * ' + chalk.yellow( 'grunt dist ' )) + '- does a for-production, non-debugging, all-parts, tested, minified build plus artifacts;\n' + chalk.bold( ' * ' + chalk.yellow( 'grunt debug ' )) diff --git a/generators/app/templates/@.gitignore b/generators/app/templates/@.gitignore index aed25519..e843df0c 100644 --- a/generators/app/templates/@.gitignore +++ b/generators/app/templates/@.gitignore @@ -51,6 +51,8 @@ tmp ## ## * All files in the 'dist' build and distribution dir, but not the dir itself. ## * The Netbeans project dir. +## * All test runner reports. /dist/ /nbproject +/test-report diff --git a/generators/app/templates/@Gruntfile.coffee b/generators/app/templates/@Gruntfile.coffee index faf0a1e7..b36e0183 100644 --- a/generators/app/templates/@Gruntfile.coffee +++ b/generators/app/templates/@Gruntfile.coffee @@ -17,8 +17,9 @@ ## * Source directory for per target-environment settings ## * settings/ ## -## * Tests directory: +## * Tests and reports directories: ## * test/ +## * test-report/ ## ## * The build's distribution artifacts ## @@ -120,7 +121,10 @@ ## ## Finally, this is how the main grunt commandline tasks are mapped to all of the above: ## -## * grunt [default] - does a for-production, non-debugging, all-parts, tested, minified build plus artifacts; +## * grunt [default] - shortcut for `grunt dist` unless the `GRUNT_TASKS` environment variable specifies a space separated list of alternative tasks to +## run instead; +## +## * grunt dist - does a for-production, non-debugging, all-parts, tested, minified build plus artifacts; ## * grunt debug - does a for-testing, debugging, all-parts except documentation, tested, as-is build; ## * grunt dev - does a for-local, debugging, all-parts except documentation, as-is build; ## (Note that this variant doesn't exit. Instead, it'll keep a close watch on @@ -130,11 +134,13 @@ ## The `--target` command line option sets the build target environment. ## So, for an for-acceptance, non-debugging, all-parts, tested, minified build, do: ## -## * grunt --target acceptance +## * grunt --target=acceptance ## ## ==== ## +'use strict' + child_process = require( 'child_process' ) path = require( 'path' ) _ = require( 'underscore' ) @@ -167,6 +173,10 @@ module.exports = ( grunt ) -> ## Filesystem: ## + ## Included for configurations that need an absolute path. + ## + base: '<%= process.cwd() %>/' + source: 'src/' dist: 'dist/' assembly: @@ -174,7 +184,9 @@ module.exports = ( grunt ) -> doc: '<%= build.dist %>doc/' settings: 'settings/' - test: 'test/' + test: + src: 'test/' + report: 'test-report/' artifactBase: '<%= build.dist %><%= npm.pkg.name %>-<%= npm.pkg.version %>' @@ -213,7 +225,7 @@ module.exports = ( grunt ) -> doc: ## NOTE: Directories to include and to exclude cannot be expressed in a single expression. ## - src: '<%= build.source %>' + src: [ '<%= build.source %>', 'vendor' ] srcExclude: [] ## NOTE: `tgt` - must - be a directory. @@ -458,7 +470,7 @@ module.exports = ( grunt ) -> test: files: [ - src: '<%= build.test %>**/*.coffee' + src: '<%= build.test.src %>**/*.coffee' ] @@ -697,7 +709,7 @@ module.exports = ( grunt ) -> ## https://karma-runner.github.io/1.0/config/configuration-file.html ## options: - basePath: '<%= build.test %>' + basePath: '<%= build.test.src %>' ## https://karma-runner.github.io/1.0/config/browsers.html ## @@ -854,7 +866,9 @@ module.exports = ( grunt ) -> served: true ] - proxies: {} + proxies: + '/settings.json': + '/base/unit/asset/settings.json' unit_dev: @@ -881,11 +895,13 @@ module.exports = ( grunt ) -> options: data: () -> - file = grunt.config( 'build.part.brief.tgt' ) + file = grunt.config( 'build.part.brief.tgt' ) ## Don't let grunt handle the exception if this fails. ## - try brief = grunt.file.readJSON( file ) + brief = do () -> + ### jshint unused: false ### + try grunt.file.readJSON( file ) catch dummy grunt.fail.fatal( "Unable to read the build brief (\"#{file}\"). Wasn't it created?" ) unless brief?.timestamp @@ -1232,6 +1248,15 @@ module.exports = ( grunt ) -> grunt.registerTask( 'default' + 'Shortcut for `grunt dist` unless the `GRUNT_TASKS` environment variable specifies a space separated list of alternative tasks to run instead.' + () -> + tasks = process.env.GRUNT_TASKS?.split( /\s/ ) + + grunt.task.run( if tasks?.length then tasks else 'dist' ) + ) + + grunt.registerTask( + 'dist' [ 'clean:dist' diff --git a/generators/app/templates/@README.md b/generators/app/templates/@README.md index 0e44f9e8..57786452 100644 --- a/generators/app/templates/@README.md +++ b/generators/app/templates/@README.md @@ -7,18 +7,20 @@ ### Prerequisites -* [npm and node](https://nodejs.org/en/download/) -* [git flow](https://github.com/nvie/gitflow/wiki/Installation) -* [jq](https://stedolan.github.io/jq/download/) -* [grunt](http://gruntjs.com/getting-started#installing-the-cli) - ```bash - $ [sudo ]npm install -g grunt-cli - ``` + * [npm and node](https://nodejs.org/en/download/) + * [git flow](https://github.com/nvie/gitflow/wiki/Installation) + * [jq](https://stedolan.github.io/jq/download/) + * [grunt](http://gruntjs.com/getting-started#installing-the-cli) -* [compass v1.0.0 or greater](http://thesassway.com/beginner/getting-started-with-sass-and-compass#install-sass-and-compass) - ```bash - $ [sudo ]gem install compass - ``` + ```bash + $ [sudo ]npm install -g grunt-cli + ``` + + * [compass v1.0.0 or greater](http://thesassway.com/beginner/getting-started-with-sass-and-compass#install-sass-and-compass) + + ```bash + $ [sudo ]gem install compass + ``` ### Setup @@ -27,37 +29,45 @@ Clone this repository somewhere, switch to it, then: ```bash $ git config commit.template ./.gitmessage -# ... initialize your branching model tools here, if need be ... ex: git flow init -d +# ... Set up any local tracking branches in addition to the default one. Depends on the branching model used, if any; +# ... Initialize your branching model tools, if need be ... ex: `git flow init -d`; $ npm install ``` This will: - * Setup [a helpful reminder](.gitmessage) of how to make [a good commit message](#commit-message-format-discipline). If you adhere to this, then a detailed, - meaningful [CHANGELOG](CHANGELOG.md) can be constructed automatically. - * Setup the git flow [branching model](#branching-model) and checkout the `develop` branch. - * Install all required dependencies. - * The latter command will also invoke `grunt` (no args) for you, creating a production build in `./dist` (plus artifacts). + + * Set up [a helpful reminder](.gitmessage) of how to make [a good commit message](#commit-message-format-discipline). If you adhere to this, then a + detailed, meaningful [CHANGELOG](CHANGELOG.md) can be constructed automatically; + * _\[... Ensure you have local ... and ... branches tracking their respective remote counterparts;\]_ + * _\[... Set up the ... [branching model](#branching-model);\]_ + * Install all required dependencies; + * The latter command will also invoke `grunt` (no args) for you, creating a production build in `./dist` (plus artifacts); ### Build Most of the time you will want to do a + ```bash grunt dev ``` + ... for creating a watched development build, or simply + ```bash grunt ``` + ... for a production-ready build. If you would like something different, here's a recap of most common grunt idioms: command | description :-- |:-- -`grunt [default]` | does a for-production, non-debugging, all-parts, tested, minified build plus artifacts; +`grunt [default]` | shortcut for `grunt dist` unless the `GRUNT_TASKS` environment variable specifies a space separated list of alternative tasks to run instead; +`grunt dist` | does a for-production, non-debugging, all-parts, tested, minified build plus artifacts; `grunt debug` | does a for-testing, debugging, all-parts except documentation, tested, as-is build; -`grunt dev` | does a for-local, debugging, all-parts except documentation, as-is build;
_(**Note that this variant doesn't exit**. Instead it'll keep a close watch on filesystem changes, selectively re-triggering part builds as needed)_ +`grunt dev` | does a for-local, debugging, all-parts except documentation, as-is build;
_**(Note that this variant doesn't exit**. Instead it'll keep a close watch on filesystem changes, selectively re-triggering part builds as needed)_ `grunt doc` | will build just the code documentation; `grunt lint` | will just lint your code; `grunt test` | will run your test suite; @@ -67,7 +77,7 @@ command | description ### Test -Unit testing is an integrated build step in both `default` and `debug` build runs, but can also be run independently as: +Unit testing is an integrated build step in both `dist` and `debug` build runs, but can also be run independently as: ```shell grunt test @@ -90,7 +100,9 @@ The latter invocation, while it is kept running, also offers the opportunity to #### Branching Model -_\[Here, you might want to say something about the branching model you intend to use. Examples are [git flow](https://github.com/nvie/gitflow#readme), [GitHub flow](https://help.github.com/articles/what-is-a-good-git-workflow/) and [GitLab flow](http://docs.gitlab.com/ee/workflow/gitlab_flow.html). Should you want to change this, then do not forget to adjust the [**setup** section](#setup) accordingly.) +_\[Here, you might want to say something about the branching model you intend to use. Examples are [git flow](https://github.com/nvie/gitflow#readme), +[GitHub flow](https://help.github.com/articles/what-is-a-good-git-workflow/) and [GitLab flow](http://docs.gitlab.com/ee/workflow/gitlab_flow.html). Should you +want to change this, then do not forget to adjust the [**setup** section](#setup) accordingly.\]_ #### Commit Message Format Discipline @@ -109,30 +121,40 @@ $ git config commit.template ./.gitmessage ### Release -_\[Here, you might want to say something about your release- and versioning strategy. Likely, this is related to what you chose for a branching model. At the very least it should include:]_ +_\[Here, you might want to say something about your release- and versioning strategy. Likely, this is related to what you chose for a branching model. At the +very least it should include:\]_ -* Determine what your next [semver](https://docs.npmjs.com/getting-started/semantic-versioning#semver-for-publishers) `` should be: - ```bash - $ version="" - ``` + * Determine what your next [semver](https://docs.npmjs.com/getting-started/semantic-versioning#semver-for-publishers) `` should be: -* Bump the package's `.version`, update the [CHANGELOG](./CHANGELOG.md), commit these, and tag the commit as `v`: - ```bash - $ npm run release - ``` + ```bash + $ version="" + ``` -* If all is well this new `version` **should** be identical to your intended ``: - ```bash - $ jq ".version == \"${version}\"" package.json - ``` + * Bump the package's `.version`, update the [CHANGELOG](./CHANGELOG.md), commit these, and tag the commit as `v`: - *If this is not the case*, then either your assumptions about what changed are wrong, or (at least) one of your commits did not adhere to the - [Commit Message Format Discipline](#commit-message-format-discipline); **Abort the release, and sort it out first.** + ```bash + $ npm run release + ``` + + * If all is well this new `version` **should** be identical to your intended ``: + + ```bash + $ jq ".version == \"${version}\"" package.json + ``` + + *If this is not the case*, then either your assumptions about what changed are wrong, or (at least) one of your commits did not adhere to the + [Commit Message Format Discipline](#commit-message-format-discipline); **Abort the release, and sort it out first.** ### Publish -_\[Ultimately, you may also want to include instructions on how to publish and deploy a production release of *<%- packageName %>*. This text is just a +_\[Ultimately, you may also want to include instructions on how to publish a production release of *<%- packageName %>*. This text is just a +placeholder.\]_ + + +### Deploy + +_\[Ultimately, you may also want to include instructions on how to deploy a production release of *<%- packageName %>*. This text is just a placeholder.\]_ diff --git a/generators/app/templates/@package.json b/generators/app/templates/@package.json index 8f985b74..51fed0e3 100644 --- a/generators/app/templates/@package.json +++ b/generators/app/templates/@package.json @@ -4,7 +4,6 @@ "name": "<%- authorName %>"<% if ( authorUrl ) { %>, "url": "<%- authorUrl %>"<% } %> }, - "bugs": {}, "browser": {<% if ( jqueryExpose ) { %> "jquery-for-cdns-shim": "./vendor/jquery-for-cdns-shim.coffee" <% } %>}, @@ -26,11 +25,16 @@ ] } <% } %>}, + "bugs": {}, + "config": { + "dist": "dist/" + }, "dependencies": {}, "description": "<%- packageDescription %>", "devDependencies": {}, "files": [ "AUTHORS", + "CHANGELOG.md", "LICENSE", "README.md", "dist" @@ -43,7 +47,11 @@ "private": true, "repository": {}, "scripts": { - "prepublish": "command -v grunt > /dev/null || { echo >&2 'It appears that \"grunt\" is not installed. Consider running \"[sudo ]npm install -g grunt-cli\" first.'; exit ; } && grunt", + "build": "command -v grunt > /dev/null || { echo >&2 'It appears that \"grunt\" is not installed. Consider running \"[sudo ]npm install -g grunt-cli\" first.'; exit ; } && grunt --no-color ${npm_config_debug+debug} ${npm_config_target+\"--target=${npm_config_target}\"}", + "dist": "true", + "prebuild": "rm -rf \"${npm_package_config_dist}\"", + "predist": "npm run build", + "prepublish": "npm run dist", "release": "standard-version", "test": "command -v grunt > /dev/null || { echo >&2 'It appears that \"grunt\" is not installed. Consider running \"[sudo ]npm install -g grunt-cli\" first.'; exit ; } && grunt test" }, diff --git a/generators/app/templates/settings/@README.md b/generators/app/templates/settings/@README.md index b88373ac..221d5d0e 100644 --- a/generators/app/templates/settings/@README.md +++ b/generators/app/templates/settings/@README.md @@ -2,7 +2,7 @@ This directory contains one settings JSON file per each build distribution's target-environment: - * local (aka developement) + * local (aka development) * testing` * acceptance * production @@ -12,24 +12,24 @@ Settings to be determined in such a file include: setting | explanation :--- | :--- `environment` | The target-environments name (production, acceptance, testing, staging, local, etc). Should really be identical to the settings file's name (excluding the `.json` extension). - `apiBaseUrl` | The base URL of the API to use. + `apiBaseUrl` | The base URL of the *default* API to use. `locales` | A list of available locales. Feel free to add further files to this directory as you see fit; for instance `.js` for your personal local development configuration. Just keep in mind that the [`Gruntfile.coffee`](../Gruntfile.coffee) is tailored towards using the defaults outlined above, and that you would need to supply a -`--target ` argument to the `grunt` command to override that default. +`--target=` argument to the `grunt` command to override that default. -So, to target a development build distribution to use settings different from `local.json`, f.i. `.js`, use +So, to target a development build distribution to use settings different from `local.json`, f.i. `.json`, use ```sh -grunt --target dev` +grunt dev --target=` ``` To target a production build distribution to use the `acceptance.json` settings instead of `production.json`, use: ```sh -grunt --target acceptance` +grunt --target=acceptance` ``` #### Example settings file @@ -47,5 +47,5 @@ grunt --target acceptance` } ``` -For more detailed information on builds, distributions and target-environments, see the introductory comments included within the +For more detailed information on builds, distributions and target-environments, see the introductory comments included within the [`Gruntfile.coffee`](../Grunfile.coffee) diff --git a/generators/app/templates/src/apis/default.coffee b/generators/app/templates/src/apis/default.coffee new file mode 100644 index 00000000..2678438d --- /dev/null +++ b/generators/app/templates/src/apis/default.coffee @@ -0,0 +1,59 @@ +( ( factory ) -> + if typeof exports is 'object' + module.exports = factory( + require( 'madlib-settings' ) + + require( './../collections/api-services.coffee' ) + ) + else if typeof define is 'function' and define.amd + define( [ + 'madlib-settings' + + './../collections/api-services.coffee' + ], factory ) + return +)(( + settings + + ApiServicesCollection +) -> + + ###* + # @author David Bouman + # @module App + # @submodule Apis + ### + + 'use strict' + + + ###* + # A collection of services' endpoints available on the app's default API. + # + # @class DefaultApi + # @static + ### + + new ApiServicesCollection( + + [ + ] + + , + ###* + # The `DefaultApi`'s base url. + # + # Defined through the {{#crossLink 'Settings/environment.apiBaseUrl:property'}}environment.apiBaseUrl setting{{/crossLink}}. + # + # @property url + # @type String + # @final + # + # @default `settings.get( 'environment.apiBaseUrl' )` + ### + + url: settings.get( 'environment.apiBaseUrl' ) + + ) + +) diff --git a/generators/app/templates/src/apis/env.coffee b/generators/app/templates/src/apis/env.coffee new file mode 100644 index 00000000..066fb779 --- /dev/null +++ b/generators/app/templates/src/apis/env.coffee @@ -0,0 +1,107 @@ +( ( factory ) -> + if typeof exports is 'object' + module.exports = factory( + require( 'madlib-settings' ) + + require( './../collections/api-services.coffee' ) + ) + else if typeof define is 'function' and define.amd + define( [ + 'madlib-settings' + + './../collections/api-services.coffee' + ], factory ) + return +)(( + settings + + ApiServicesCollection +) -> + + ###* + # @author David Bouman + # @module App + # @submodule Apis + ### + + 'use strict' + + + ###* + # A collection of services' endpoints available on the app's target-environment's API. + # + # @class EnvApi + # @static + ### + + new ApiServicesCollection( + + [ + ###* + # Service API endpoint for retrieving the app's current build's {{#crossLink 'BuildBrief'}}briefing data{{/crossLink}}. + # + # This data includes: + # + # * `buildNumber` + # * `buildId` + # * `revision` + # * `grunted` + # * `environment` + # * `debugging` + # * `name` + # * `version` + # * `timestamp` + # + # @attribute buildBrief + # @type ApiServiceModel + # @final + # + # @default '/build.json' + ### + + id: 'buildBrief' + urlPath: 'build.json' + + , + ###* + # Service API endpoint for retrieving the app's {{#crossLink 'SettingsEnvironment'}}target-environment settings{{/crossLink}}. + # + # These settings include: + # + # * `apiBaseUrl` + # * `environment`<% if ( i18n ) { %> + # * `locales`<% } %> + # + # Once retrieved these can be referenced through the {{#crossLink 'Settings/environment:property'}}the `environment` setting{{/crossLink}}. + # + # @attribute settingsEnvironment + # @type ApiServiceModel + # @final + # + # @default '/settings.json' + ### + + id: 'settingsEnvironment' + urlPath: 'settings.json' + + , + ] + + , + ###* + # The `EnvApi`'s base url. + # + # Defined through the {{#crossLink 'Settings/appBaseUrl:property'}}appBaseUrl setting{{/crossLink}}. + # + # @property url + # @type String + # @final + # + # @default `settings.get( 'appBaseUrl' )` + ### + + url: settings.get( 'appBaseUrl' ) + + ) + +) diff --git a/generators/app/templates/src/app.coffee b/generators/app/templates/src/app.coffee index c7dcf36b..bad7c37c 100644 --- a/generators/app/templates/src/app.coffee +++ b/generators/app/templates/src/app.coffee @@ -21,6 +21,12 @@ ## That is why the `@module App` declaration is repeated at the bottom; to indicate this intent to Yuidoc. ## +###* +# The app's APIs. +# +# @submodule Apis +### + ###* # The app's backbone collections. # @@ -75,13 +81,13 @@ npm = require( './../package.json' ) Q = require( 'q' ) -## Comment out or remove this for your developement convenience. +## Comment out or remove this for your development convenience. ## ## https://github.com/kriskowal/q/wiki/API-Reference#qstopunhandledrejectiontracking ## Q.stopUnhandledRejectionTracking() -## Set this to true for your developement convenience. +## Set this to true for your development convenience. ## ## https://github.com/kriskowal/q/wiki/API-Reference#qlongstacksupport ## @@ -148,20 +154,11 @@ Handlebars = require( 'hbsfy/runtime' ) ## Register Handlebars helpers: ## -do () -> - - ### jshint forin: false ### +require( './utils/hbs/helpers/moment.coffee' ) - Handlebars.registerHelper( name, helper ) for name, helper of { - - ## - ## This would be a good place to register any Handlebars helpers: - ## - ## foo: ( value ) -> "bar: #{ value }" - ## etc: ... - ## - - }<% if ( jqueryExpose ) { %> +## Register Handlebars partials: +## +#require( './utils/hbs/partials/...' )<% if ( jqueryExpose ) { %> ## ============================================================================ @@ -187,7 +184,7 @@ require( 'jquery-for-cdns-shim' )<% } %> # # These are exposed through the `madlib-settings` singleton object. Simply `require(...)` it wherever you have a need for them. # -# @class Settings +# @class Settings # @static ### @@ -211,14 +208,14 @@ settings = require( 'madlib-settings' ) # # Often the `document` and this app will share the same base url, but not necessarily so. # -# @property appBaseUrl +# @attribute appBaseUrl # @type String # @final ### ## Leverage `document.currentScript` or a fallback (for IE <=11). ## -appBaseUrl = ( document.currentScript ? Array::slice.call( document.scripts, -1 )[0] ).src.match( /^.*\// )[0] +appBaseUrl = ( document.currentScript ? Array::slice.call( document.scripts, -1 )[0] ).src.match( /^(.*)\// )[1] settings.init( 'appBaseUrl', appBaseUrl ) @@ -228,7 +225,7 @@ settings.init( 'appBaseUrl', appBaseUrl ) # # Often the `document` and this app will share the same root element, but not necessarily so. # -# @property $appRoot +# @attribute $appRoot # @type jQuery # @final ### @@ -250,8 +247,9 @@ localeManager = require( 'madlib-locale' ) # # https://tools.ietf.org/html/bcp47#section-2 # -# @property locale +# @attribute locale # @type String +# # @default '<%= i18nLocaleDefault %>' ### @@ -265,16 +263,6 @@ $appRoot.attr( 'lang', locale = $( 'html' ).attr( 'lang' ) ? '<%= i18nLocaleDefa settings.init( 'locale', locale )<% } %> -## ============================================================================ -## -## [API] -## - -## `require()` the API services here to ensure their endpoints have been defined on the madlib-settings object before they are used anywhere else. -## -services = require( './collections/api-services.coffee' ) - - ## ============================================================================ ## ## [App] @@ -284,8 +272,6 @@ services = require( './collections/api-services.coffee' ) ## settingsEnv = require( './models/settings-environment.coffee' ) -router = require( './router.coffee' ) - initialized = Q.all( [ ## Wait until the DOM is ready. @@ -298,7 +284,7 @@ initialized = Q.all( ## At this point we cannot know for sure that this locale really is available. ## Once the `settingsEnv` has been initialized we -will- know this, but we're not going to wait for that; instead, just assume it'll be there. ## - localeManager.initialize( Handlebars, locale, "#{ appBaseUrl }i18n" )<% } %> + localeManager.initialize( Handlebars, locale, "#{ appBaseUrl }/i18n" )<% } %> ## Wait until the target-environment settings have been loaded.<% if ( i18n ) { %> ## This should list the available locales.<% } %> @@ -311,6 +297,10 @@ initialized.done( () -> + ## Load router only now, so the environment settings are known to have been loaded, and so the `DefaultApi` is ready to be used. + ## + router = require( './router.coffee' ) + router.startApp() return diff --git a/generators/app/templates/src/collections/api-services.coffee b/generators/app/templates/src/collections/api-services.coffee index fc58eb83..4718a284 100644 --- a/generators/app/templates/src/collections/api-services.coffee +++ b/generators/app/templates/src/collections/api-services.coffee @@ -2,23 +2,20 @@ if typeof exports is 'object' module.exports = factory( require( 'backbone' ) - require( './../models/api-service.coffee' ) - require( 'madlib-settings' ) + require( './../models/api-service.coffee' ) ) else if typeof define is 'function' and define.amd define( [ 'backbone' - './../models/api-service.coffee' - 'madlib-settings' + './../models/api-service.coffee' ], factory ) return )(( Backbone - ApiServiceModel - settings + ApiServiceModel ) -> ###* @@ -31,22 +28,21 @@ ###* - # A collection of services available on the API. + # A collection of services available on an API. # # @class ApiServicesCollection # @extends Backbone.Collection - # @static + # @constructor ### class ApiServicesCollection extends Backbone.Collection ###* - # The collection's `{{#crossLink "ApiServiceModel"}}{{/crossLink}}`. + # The collection's `{{#crossLink 'ApiServiceModel'}}{{/crossLink}}`. # # @property model # @type Backbone.Model # @protected - # @static # @final # # @default ApiServiceModel @@ -55,99 +51,33 @@ model: ApiServiceModel + ###* + # The API's base url. + # + # This property will be initialized from the `options.url` constructor argument. + # + # @property url + # @type String + ### - ###* - # The app's globally sharable configuration settings. - # - # These are exposed through the `madlib-settings` singleton object. Simply `require(...)` it wherever you have a need for them. - # - # @class Settings - # @static - ### - - appBaseUrl = settings.get( 'appBaseUrl' ) - - apiServices = - new ApiServicesCollection( - - [ - ###* - # Absolute url path for retrieving the app's current build's {{#crossLink "BuildBrief"}}briefing data{{/crossLink}}. - # - # This data includes: - # - # * `buildNumber` - # * `buildId` - # * `revision` - # * `grunted` - # * `environment` - # * `debugging` - # * `name` - # * `version` - # * `timestamp` - # - # @property services.buildBrief - # @type String - # @final - # - # @default '/build.json' - ### - - id: 'buildBrief' - url: "#{appBaseUrl}build.json" - - , - ###* - # Absolute url path for retrieving the app's {{#crossLink "SettingsEnvironment"}}target-environment settings{{/crossLink}}. - # - # These settings include: - # - # * `apiBaseUrl` - # * `environment`<% if ( i18n ) { %> - # * `locales`<% } %> - # - # Once retrieved these can be referenced through the {{#crossLink "Settings/environment:property"}}the `environment` setting{{/crossLink}}. - # - # @property services.settingsEnvironment - # @type String - # @final - # - # @default '/settings.json' - ### - - id: 'settingsEnvironment' - url: "#{appBaseUrl}settings.json" - - , - - ## - ## NOTE: - ## - ## Before using any of the services below, the target-environment settings need to have been retrieved first in order to have an API base url - ## to base these values off of. - ## - - ] - ) - - - ###* - # The services available on the API. - # - # @property services - # @type Object - ### + url: undefined - settings.init( 'services', apiServices.reduce( ( ( memo, service ) -> memo[ service.id ] = service.get( 'url' ); return memo ), {} ) ) + ###* + # Initialize the `@url` property from `options`. + # + # @method initialize + # @protected + # + # @param {Array} [models] An initial array of models for the collection. + # @param {Object} [options] + # @param {String} [options.url] The base url for the API. + ### - ###* - # @class ApiServicesCollection - ### + initialize: ( models, options ) -> + @url = options?.url - ## Export singleton. - ## - return apiServices + return ) diff --git a/generators/app/templates/src/index.template.html b/generators/app/templates/src/index.template.html index fb3c018e..512d3155 100644 --- a/generators/app/templates/src/index.template.html +++ b/generators/app/templates/src/index.template.html @@ -3,7 +3,7 @@ var buildRun = 'build-run=' + encodeURIComponent( buildRun ); %> - lang="<@= i18nLocaleDefault @>"<@ } @>> + lang="<@= i18nLocaleDefault @>"<@ } @>> <% // @@ -58,7 +58,7 @@ link.pathname = '/livereload.js'; livereload = document.createElement( 'script' ); - livereload.src = link.href + livereload.src = link.href; document.head.appendChild( livereload ); diff --git a/generators/app/templates/src/models/api-service.coffee b/generators/app/templates/src/models/api-service.coffee index 2085382e..5fcb66f3 100644 --- a/generators/app/templates/src/models/api-service.coffee +++ b/generators/app/templates/src/models/api-service.coffee @@ -22,7 +22,7 @@ ###* - # Model for the `{{#crossLink "ApiServicesCollection"}}{{/crossLink}}`. + # Model for the `{{#crossLink 'ApiServicesCollection'}}{{/crossLink}}`. # # @class ApiServiceModel # @extends Backbone.Model @@ -36,7 +36,6 @@ # # @property schema # @type Array[String] - # @static # @final ### @@ -48,16 +47,48 @@ ### ###* - # A url base path for accessing this API service. + # A url path relative to the API's base `url` for accessing this service's API endpoint. # - # @attribute url + # @attribute urlPath # @type String ### schema: [ 'id' - 'url' + 'urlPath' ] + + ###* + # This method caters for `Backbone.Model.url()` to not break on `ApiServiceModel` values. It is, in short, a hack. + # + # Not many libraries do ensure to stringify values before invoking `String` prototype methods on them and `Backbone.Model.url()` is no exception. + # + # See: + # [`String.prototype.replace`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#wiki-document-head) + # for a complete description of this method's signature. + # + # @method replace + ### + + replace: () -> + + return @toString().replace( arguments... ) + + + ###* + # Creates a complete service API endpoint url from the API's base `url` and the model's `urlPath`. + # + # Most time you won't need to call this method explicitly; simply provide this model wherever you have a need for this value. + # + # @method toString + # + # @return {String} The complete url of the service's API endpoint. + ### + + toString: () -> + + return "#{ @collection.url }/#{ @attributes.urlPath }" + ) diff --git a/generators/app/templates/src/models/build-brief.coffee b/generators/app/templates/src/models/build-brief.coffee index 1f0666c7..484570e2 100644 --- a/generators/app/templates/src/models/build-brief.coffee +++ b/generators/app/templates/src/models/build-brief.coffee @@ -2,23 +2,26 @@ if typeof exports is 'object' module.exports = factory( require( 'backbone' ) - require( 'madlib-settings' ) require( 'moment' ) require( 'q' ) + + require( './../apis/env.coffee' ) ) else if typeof define is 'function' and define.amd define( [ 'backbone' - 'madlib-settings' 'moment' 'q' + + './../apis/env.coffee' ], factory ) return )(( Backbone - settings moment Q + + api ) -> ###* @@ -50,7 +53,6 @@ # # @property schema # @type Array[String] - # @static # @final ### @@ -106,7 +108,7 @@ ### ###* - # Flag for signalling wether this was debugging build. + # Flag for signalling whether this was debugging build. # # @attribute debugging # @type Boolean @@ -150,15 +152,16 @@ ###* + # Service API endpoint; defined in the {{#crossLink 'EnvApi/buildBrief:attribute'}}EnvApi{{/crossLink}}. + # # @property url - # @type String - # @static + # @type ApiServiceModel # @final # - # @default 'build.json' + # @default '/build.json' ### - url: settings.get( 'services.buildBrief' ) + url: api.get( 'buildBrief' ) ###* diff --git a/generators/app/templates/src/models/settings-environment.coffee b/generators/app/templates/src/models/settings-environment.coffee index ca9647a0..bc9d8224 100644 --- a/generators/app/templates/src/models/settings-environment.coffee +++ b/generators/app/templates/src/models/settings-environment.coffee @@ -4,18 +4,24 @@ require( 'backbone' ) require( 'madlib-settings' ) require( 'q' ) + + require( './../apis/env.coffee' ) ) else if typeof define is 'function' and define.amd define( [ 'backbone' 'madlib-settings' 'q' + + './../apis/env.coffee' ], factory ) return )(( Backbone settings Q + + api ) -> ###* @@ -47,7 +53,6 @@ # # @property schema # @type Array[String] - # @static # @final ### @@ -99,15 +104,16 @@ ###* + # Service API endpoint; defined in the {{#crossLink 'EnvApi/settingsEnvironment:attribute'}}EnvApi{{/crossLink}}. + # # @property url - # @type String - # @static + # @type ApiServiceModel # @final # - # @default '/settings.json' + # @default '/settings.json' ### - url: settings.get( 'services.settingsEnvironment' ) + url: api.get( 'settingsEnvironment' ) ###* @@ -136,20 +142,18 @@ ###* # The target-environment's settings. # - # @property environment - # @type Object - # + # @attribute environment # @for Settings + # @type Object ### ###* # The base URL to use for consuming API services. All `services` URL settings are assumed to be relative to this one. # - # @property environment.apiBaseUrl + # @attribute environment.apiBaseUrl + # @for Settings # @type String # @final - # - # @for Settings ### ###* @@ -160,11 +164,10 @@ # * `'acceptance'` # * `'production'` # - # @property environment.environment + # @attribute environment.environment + # @for Settings # @type String # @final - # - # @for Settings ###<% if ( i18n ) { %> ###* @@ -183,11 +186,10 @@ # # See also: [IETF language tags](https://en.wikipedia.org/wiki/IETF_language_tag#Syntax_of_language_tags). # - # @property environment.locales + # @attribute environment.locales + # @for Settings # @type {Object} # @final - # - # @for Settings ###<% } %> settings.init( 'environment', @attributes ) diff --git a/generators/app/templates/src/router.coffee b/generators/app/templates/src/router.coffee index 7883c149..33368aed 100644 --- a/generators/app/templates/src/router.coffee +++ b/generators/app/templates/src/router.coffee @@ -2,7 +2,6 @@ if typeof exports is 'object' module.exports = factory( require( 'backbone' ) - require( 'jquery' )<% if ( i18n ) { %> require( 'madlib-locale' )<% } %> require( 'madlib-settings' ) @@ -13,7 +12,6 @@ else if typeof define is 'function' and define.amd define( [ 'backbone' - 'jquery'<% if ( i18n ) { %> 'madlib-locale'<% } %> 'madlib-settings' @@ -24,7 +22,6 @@ return )(( Backbone - $<% if ( i18n ) { %> localeManager<% } %> settings @@ -41,6 +38,7 @@ 'use strict' + ###* # The app's main router. # @@ -111,7 +109,6 @@ # # @property routes # @type Object - # @static # @final ### @@ -143,7 +140,6 @@ # # @property viewMap # @type Object - # @static # @final ### @@ -160,7 +156,6 @@ # # @property homeUrl # @type String - # @static # @final # # @default '/' diff --git a/generators/app/templates/src/sass/debug.sass b/generators/app/templates/src/sass/debug.sass index 100958f0..30549cd1 100644 --- a/generators/app/templates/src/sass/debug.sass +++ b/generators/app/templates/src/sass/debug.sass @@ -7,3 +7,6 @@ html // background: image-url('debug/internals.jpg') center / cover no-repeat fixed border-box #C00 box-shadow: inset 0px 10vh 30vmin 15vmin rgba(16,0,0,0.9) + + +@import "views/_debug.environment-ribbon" diff --git a/generators/app/templates/src/sass/views/_debug.environment-ribbon.sass b/generators/app/templates/src/sass/views/_debug.environment-ribbon.sass new file mode 100644 index 00000000..758b2846 --- /dev/null +++ b/generators/app/templates/src/sass/views/_debug.environment-ribbon.sass @@ -0,0 +1,67 @@ +$default_textColor: #6a6340 +$default_gradientHigh: #BFDC7A +$default_gradientLow: #8EBF45 + +$acceptance_textColor: #801111 +$acceptance_gradientHigh: #d64b4b +$acceptance_gradientLow: #ab2c2c + +$testing_textColor: #807712 +$testing_gradientHigh: #d4c94c +$testing_gradientLow: #aba02c + +=ribbon-colors( $textColor, $gradientHigh, $gradientLow ) + + background-color: $gradientHigh + background-image: -webkit-gradient(linear, left top, left bottom, from($gradientHigh), to($gradientLow)) + background-image: -webkit-linear-gradient(top, $gradientHigh, $gradientLow) + background-image: -moz-linear-gradient(top, $gradientHigh, $gradientLow) + background-image: -ms-linear-gradient(top, $gradientHigh, $gradientLow) + background-image: -o-linear-gradient(top, $gradientHigh, $gradientLow) + + color: $textColor + +.environment-ribbon-view + + position: fixed + z-index: 1001 + + bottom: 25px + right: -100px + width: 300px + + transform: rotate(-45deg) + + .ribbon + padding: 2px 0 + font: bold 15px Sans-Serif + + text-align: center + text-shadow: rgba(214,92,92,0.5) 0px 1px 0px + + text-shadow: rgba(255,255,255,0.5) 0px 1px 0px + + box-shadow: 0px 0px 3px rgba(0,0,0,0.3) + + transition: opacity 0.2s + + &:hover + opacity: 0 + + .stitches-top + margin-bottom: 2px + border-top: 1px dashed rgba(0, 0, 0, 0.2) + box-shadow: 0px 0px 2px rgba(255, 255, 255, 0.3) + + .stitches-bottom + margin-top: 2px + border-top: 1px dashed rgba(0, 0, 0, 0.2) + box-shadow: 0px 0px 2px rgba(255, 255, 255, 0.3) + + +ribbon-colors( $default_textColor, $default_gradientHigh, $default_gradientLow ) + + &.env-acceptance + +ribbon-colors( $acceptance_textColor, $acceptance_gradientHigh, $acceptance_gradientLow ) + + &.env-testing + +ribbon-colors( $testing_textColor, $testing_gradientHigh, $testing_gradientLow ) diff --git a/generators/app/templates/src/utils/hbs/helpers/moment.coffee b/generators/app/templates/src/utils/hbs/helpers/moment.coffee new file mode 100644 index 00000000..fd0435df --- /dev/null +++ b/generators/app/templates/src/utils/hbs/helpers/moment.coffee @@ -0,0 +1,48 @@ +( ( factory ) -> + if typeof exports is 'object' + module.exports = factory( + require( 'hbsfy/runtime' ) + require( 'moment' ) + ) + else if typeof define is 'function' and define.amd + define( [ + 'hbsfy/runtime' + 'moment' + ], factory ) + return +)(( + Handlebars + moment +) -> + + ###* + # @author David Bouman + # @module App + ### + + 'use strict' + + + ###* + # Format any value into a date and/or time string representation using [`moment.format()`](http://momentjs.com/docs/#/displaying/format/). + # + # @method moment + # @for Handlebars.Helpers + # + # @param {String} any The value to format. + # @param {String} [format] The (optional) `moment.format()` format string argument. + # @param {String} [pattern] An optional [parsing format](http://momentjs.com/docs/#/parsing/string-format/) you may + # need to direct coercion of the `any` value into a `moment` object first. Useful if the + # `any` value isn't already a `moment` object. + # + # @return {String} The `any` value formatted into the desired date and/or time respresentation string. + ### + + Handlebars.registerHelper( 'moment', ( any, format, pattern, options ) -> + + return ( if options then moment( any, pattern ) else moment( any )).format( format ) + ) + + return + +) diff --git a/generators/app/templates/src/views/debug.environment-ribbon.coffee b/generators/app/templates/src/views/debug.environment-ribbon.coffee new file mode 100644 index 00000000..c0f7af36 --- /dev/null +++ b/generators/app/templates/src/views/debug.environment-ribbon.coffee @@ -0,0 +1,191 @@ +( ( factory ) -> + if typeof exports is 'object' + module.exports = factory( + require( 'backbone' ) + require( 'jquery' ) + require( 'q' ) + + require( './../models/build-brief.coffee' ) + require( './../models/settings-environment.coffee' ) + + require( './../utils/hbs/helpers/moment.coffee' ) + + require( './debug.environment-ribbon.hbs' ) + ) + else if typeof define is 'function' and define.amd + define( [ + 'backbone' + 'jquery' + 'q' + + './../models/build-brief.coffee' + './../models/settings-environment.coffee' + + './../utils/hbs/helpers/moment.coffee' + + './debug.environment-ribbon.hbs' + ], factory ) + return +)(( + Backbone + $ + Q + + buildBrief + settingsEnv + + hbsHelperMoment + + template +) -> + + ###* + # @author David Bouman + # @module App + # @submodule Views + ### + + 'use strict' + + + ###* + # Visually decorate site with a ribbon revealing the target environment and latest build info. + # + # * Fades to transparent on mouse hover. + # * Removes itself from the DOM when clicked. + # + # @class EnvironmentRibbonView + # @extends Backbone.View + # @constructor + ### + + class EnvironmentRibbonView extends Backbone.View + + ###* + # CSS class(es) to set on this view's root DOM element. + # + # @property className + # @type String + # @final + # + # @default 'environment-ribbon-view' + ### + + className: 'environment-ribbon-view' + + + ###* + # The compiled handlebars template expander function. + # + # @property template + # @type Function + # @protected + # @final + ### + + template: template + + + ###* + # Delegated DOM event handler definition. + # + # @property events + # @type Object + # @final + ### + + events: + 'click': '_onPokeIntent' + + + ###* + # Attach view to the DOM and `@render()` as soon as the data becomes available. + # + # @method initialize + # @protected + ### + + initialize: () -> + + Q.all( + [ + ## Wait until the DOM is ready. + ## + new Q.Promise( ( resolve ) -> $( resolve ); return; ) + + , + ## Wait until the target-environment settings have been loaded. + ## + settingsEnv.initialized + + , + ## Wait until the buid's briefing data have been loaded. + ## + buildBrief.initialized + + , + ] + + ).done( () => + + @render().$el.appendTo( $( 'body' )) + + return + ) + + return + + + ###* + # @method render + # + # @chainable + ### + + render: () -> + + ## Expand the handlebars template into this view's container element. + ## + @$el.html( @template( @renderData() ) ) + + ## This method is chainable. + ## + return @ + + + ###* + # Collect and return all data needed to expand the handlebars `@template` with. + # + # @method renderData + # @protected + # + # @return {Object} + ### + + renderData: () -> + + settings: settingsEnv.attributes + buildBrief: buildBrief.attributes + + + ###* + # Respond to the user interacting with the ribbon, removing it from the DOM. + # + # @method _onPokeIntent + # @protected + ### + + _onPokeIntent: () -> + + ## Remove ourselves from the DOM + ## + @remove() + + return + + + ## Export singleton. + ## + return new EnvironmentRibbonView() + +) diff --git a/generators/app/templates/src/views/debug.environment-ribbon.hbs b/generators/app/templates/src/views/debug.environment-ribbon.hbs new file mode 100644 index 00000000..e0199031 --- /dev/null +++ b/generators/app/templates/src/views/debug.environment-ribbon.hbs @@ -0,0 +1,7 @@ +
+
+ {{#if buildBrief.buildNumber}}{{buildBrief.buildNumber}} / {{/if}} {{moment buildBrief.timestamp "MMM DD HH:mm"}} +
+ {{settings.environment}} +
+
diff --git a/generators/app/templates/src/views/index.coffee b/generators/app/templates/src/views/index.coffee index 0c68d897..89caa2fc 100644 --- a/generators/app/templates/src/views/index.coffee +++ b/generators/app/templates/src/views/index.coffee @@ -1,17 +1,20 @@ ( ( factory ) -> if typeof exports is 'object' module.exports = factory( - require( 'backbone' ) + require( '<%- backbone.modulePath %>' ) + require( './index.hbs' ) ) else if typeof define is 'function' and define.amd define( [ - 'backbone' + '<%- backbone.modulePath %>' + './index.hbs' ], factory ) return )(( - Backbone + <%- backbone.className %> + template ) -> @@ -23,22 +26,22 @@ 'use strict' + ###* # Default index view of BAT # # @class IndexView - # @extends Backbone.View + # @extends <%- backbone.className %>.View # @constructor ### - class IndexView extends Backbone.View + class IndexView extends <%- backbone.className %>.View ###* # Expose this view's name to the router. # # @property viewName # @type String - # @static # @final # # @default 'index' @@ -52,7 +55,6 @@ # # @property className # @type String - # @static # @final # # @default 'index-view' @@ -67,7 +69,6 @@ # @property template # @type Function # @protected - # @static # @final ### diff --git a/generators/app/templates/test/unit/asset/settings.json b/generators/app/templates/test/unit/asset/settings.json new file mode 120000 index 00000000..24455ce2 --- /dev/null +++ b/generators/app/templates/test/unit/asset/settings.json @@ -0,0 +1 @@ +../../../settings/testing.json \ No newline at end of file diff --git a/generators/app/templates/test/unit/init.coffee b/generators/app/templates/test/unit/init.coffee index bcde3368..c5b0e8fd 100644 --- a/generators/app/templates/test/unit/init.coffee +++ b/generators/app/templates/test/unit/init.coffee @@ -48,21 +48,11 @@ settings = require( 'madlib-settings' ) # # Often the `document` and this app will share the same base url, but not necessarily so. # -# @property appBaseUrl +# @attribute appBaseUrl # @type String # @final ### -appBaseUrl = '/' +appBaseUrl = '' settings.init( 'appBaseUrl', appBaseUrl ) - - -## ============================================================================ -## -## [API] -## - -## `require()` the API services here to ensure their endpoints have been defined on the madlib-settings object before they are used anywhere else. -## -services = require( './../../src/collections/api-services.coffee' ) diff --git a/generators/app/templates/test/unit/spec/models/settings-environment.spec.coffee b/generators/app/templates/test/unit/spec/models/settings-environment.spec.coffee new file mode 100644 index 00000000..7752b33b --- /dev/null +++ b/generators/app/templates/test/unit/spec/models/settings-environment.spec.coffee @@ -0,0 +1,87 @@ +'use strict' + +Q = require( 'q' ) +settings = require( 'madlib-settings' ) +settingsJSON = require( './../../asset/settings.json' ) +settingsEnv = require( './../../../../src/models/settings-environment.coffee' ) + +## `settings-environment` is exporting a singleton object. +## But in order to unit-test properly, once is simply not enough. We want to start from the class; hence: +## +SettingsEnvironmentModel = Object.getPrototypeOf( settingsEnv ).constructor + +describe( 'Target-environment settings', () -> + + beforeEach( () -> + + settings.unset( 'environment' ) + ) + + it( 'should reject its `initialized` Promise property when loading fails.', ( done ) -> + + ## Stub `@fetch` call into failure. + ## + spyOn( SettingsEnvironmentModel::, 'fetch' ).and.returnValue( new Q.reject( 'because we can' )) + + settingsEnv = new SettingsEnvironmentModel() + + failureSpy = jasmine.createSpy( 'rejected' ) + + settingsEnv.initialized.fail( failureSpy ).fin( () -> + + expect( failureSpy ).toHaveBeenCalled() + + done() + + return + ) + + return + ) + + it( 'should resolve its `initialized` Promise property when loading succeeds.', ( done ) -> + + ## Stub `@fetch` call into succcess. + ## + spyOn( SettingsEnvironmentModel::, 'fetch' ).and.returnValue( new Q( settingsJSON )) + + settingsEnv = new SettingsEnvironmentModel() + + successSpy = jasmine.createSpy( 'resolved' ) + + settingsEnv.initialized.then( successSpy ).fin( () -> + + expect( successSpy ).toHaveBeenCalled() + + done() + + return + ) + + return + ) + + it( 'should initialize an `environment` section within the application\'s settings.', ( done ) -> + + ## Stub `@fetch` call to succcess. + ## + spyOn( SettingsEnvironmentModel::, 'fetch' ).and.returnValue( new Q( settingsJSON )) + + settingsEnv = new SettingsEnvironmentModel() + + settingsEnv.initialized.done( + () -> + actual = settings.get( 'environment' ) + + expect( actual ).toBe( settingsEnv.attributes ) + + done() + + return + ) + + return + ) + + return +) diff --git a/generators/collection/index.js b/generators/collection/index.js index 3139b974..849ac95e 100644 --- a/generators/collection/index.js +++ b/generators/collection/index.js @@ -7,18 +7,17 @@ var generators = require( 'yeoman-generator' ) , yosay = require( 'yosay' ) , youtil = require( './../../lib/youtil.js' ) -, chalk = require( 'chalk' ) , _ = require( 'lodash' ) ; -var decapitalize = require( 'underscore.string/decapitalize' ); - var CollectionGenerator = generators.Base.extend( { constructor: function () { generators.Base.apply( this, arguments ); + this.description = this._description( 'backbone collection' ); + this.argument( 'collectionName' , { @@ -47,7 +46,7 @@ var CollectionGenerator = generators.Base.extend( 'description' , { type: String - , desc: 'The purpose of the collection to create.' + , desc: 'The purpose of this collection.' } ); @@ -55,7 +54,7 @@ var CollectionGenerator = generators.Base.extend( 'singleton' , { type: Boolean - , desc: 'Specify whether this collection should be a singleton (instance).' + , desc: 'Whether this collection should be a singleton (instance).' } ); @@ -63,7 +62,7 @@ var CollectionGenerator = generators.Base.extend( 'modelName' , { type: String - , desc: 'The model name for the collection to create.' + , desc: 'The model name for this collection.' } ); @@ -71,18 +70,11 @@ var CollectionGenerator = generators.Base.extend( 'createModel' , { type: Boolean - , desc: 'Specify whether to create the collection\'s model too.' + , desc: 'Whether to create this model too.' } ); } - , description: - chalk.bold( - 'This is the ' + chalk.cyan( 'backbone collection' ) - + ' generator for BAT, the Backbone Application Template' - + ' created by ' + chalk.blue( 'marv' ) + chalk.red( 'iq' ) + '.' - ) - , initializing: function () { this._assertBatApp(); @@ -108,7 +100,7 @@ var CollectionGenerator = generators.Base.extend( , validate: youtil.isIdentifier , filter: function ( value ) { - return decapitalize( _.trim( value ).replace( /collection$/i, '' )); + return _.lowerFirst( _.trim( value ).replace( /collection$/i, '' )); } } , { @@ -122,7 +114,7 @@ var CollectionGenerator = generators.Base.extend( , { type: 'confirm' , name: 'singleton' - , message: 'Should this collection be a singleton?' + , message: 'Should this collection be a singleton (instance)?' , default: false , validate: _.isBoolean } @@ -135,20 +127,20 @@ var CollectionGenerator = generators.Base.extend( return ( youtil.definedToString( this.options.modelName ) || answers.collectionName - || youtil.definedToString( this.options.collectionName ) + || this.templateData.collectionName ); }.bind( this ) , validate: youtil.isIdentifier , filter: function ( value ) { - return decapitalize( _.trim( value ).replace( /model$/i, '' )); + return _.lowerFirst( _.trim( value ).replace( /model$/i, '' )); } } , { type: 'confirm' , name: 'createModel' - , message: 'Should i create this model now as well?' + , message: 'Should I create this model now as well?' , default: true , validate: _.isBoolean } @@ -181,13 +173,15 @@ var CollectionGenerator = generators.Base.extend( _.extend( data , { - className: _.capitalize( collectionName ) + 'Collection' - , fileBase: _.kebabCase( _.deburr( collectionName )) + className: _.upperFirst( collectionName ) + 'Collection' + , fileBase: _.kebabCase( _.deburr( collectionName )) + + , modelClassName: _.upperFirst( modelName ) + 'Model' + , modelFileName: _.kebabCase( _.deburr( modelName )) + '.coffee' - , modelClassName: _.capitalize( modelName ) + 'Model' - , modelFileName: _.kebabCase( _.deburr( modelName )) + '.coffee' + , userName: this.user.git.name() - , userName: this.user.git.name() + , backbone: ( this.config.get( 'backbone' ) || { className: 'Backbone', modulePath: 'backbone' } ) } ); } @@ -216,7 +210,7 @@ var CollectionGenerator = generators.Base.extend( , options: { - description: 'Model for the `{{#crossLink "' + data.className + '"}}{{/crossLink}}`.' + description: 'Model for the `{{#crossLink \'' + data.className + '\'}}{{/crossLink}}`.' , singleton: false } } diff --git a/generators/collection/templates/collection.coffee b/generators/collection/templates/collection.coffee index 9bd5ff72..bd1d38ff 100644 --- a/generators/collection/templates/collection.coffee +++ b/generators/collection/templates/collection.coffee @@ -1,17 +1,20 @@ ( ( factory ) -> if typeof exports is 'object' module.exports = factory( - require( 'backbone' ) + require( '<%- backbone.modulePath %>' ) + require( './../models/<%- modelFileName %>' ) ) else if typeof define is 'function' and define.amd define( [ - 'backbone' + '<%- backbone.modulePath %>' + './../models/<%- modelFileName %>' ], factory ) return )(( - Backbone + <%- backbone.className %> + <%- modelClassName %> ) -> @@ -28,20 +31,19 @@ # <%- description %> #<% } %> # @class <%- className %> - # @extends Backbone.Collection<% if ( singleton ) { %> + # @extends <%- backbone.className %>.Collection<% if ( singleton ) { %> # @static<% } else { %> # @constructor<% } %> ### - class <%- className %> extends Backbone.Collection + class <%- className %> extends <%- backbone.className %>.Collection ###* - # The collection's `{{#crossLink "<%- modelClassName %>"}}{{/crossLink}}`. + # The collection's `{{#crossLink '<%- modelClassName %>'}}{{/crossLink}}`. # # @property model - # @type Backbone.Model + # @type <%- backbone.className %>.Model # @protected - # @static # @final # # @default <%- modelClassName %> diff --git a/generators/demo/index.js b/generators/demo/index.js index 8a54f48d..95185e6f 100644 --- a/generators/demo/index.js +++ b/generators/demo/index.js @@ -24,6 +24,8 @@ var DemoGenerator = generators.Base.extend( { this._assertBatApp(); + this.description = this._description( 'demo app' ); + var npm = this.fs.readJSON( this.destinationPath( 'package.json' )); // Container for template expansion data. @@ -36,13 +38,6 @@ var DemoGenerator = generators.Base.extend( ; } - , description: - chalk.bold( - 'This is the ' + chalk.cyan( 'demo app' ) - + ' generator for BAT, the Backbone Application Template' - + ' created by ' + chalk.blue( 'marv' ) + chalk.red( 'iq' ) + '.' - ) - , configuring: function() { var config = this.config diff --git a/generators/demo/templates/src/models/example.coffee b/generators/demo/templates/src/models/example.coffee index 368a4b1c..bc3a700c 100644 --- a/generators/demo/templates/src/models/example.coffee +++ b/generators/demo/templates/src/models/example.coffee @@ -36,7 +36,6 @@ # @property defaults # @type Object # @protected - # @static # @final ### diff --git a/generators/demo/templates/src/router.coffee b/generators/demo/templates/src/router.coffee index ea93c3ba..7568f0ae 100644 --- a/generators/demo/templates/src/router.coffee +++ b/generators/demo/templates/src/router.coffee @@ -119,7 +119,6 @@ # # @property routes # @type Object - # @static # @final ### @@ -154,7 +153,6 @@ # # @property viewMap # @type Object - # @static # @final ### @@ -171,7 +169,6 @@ # # @property homeUrl # @type String - # @static # @final # # @default '/index' diff --git a/generators/demo/templates/src/views/buildscript.coffee b/generators/demo/templates/src/views/buildscript.coffee index 579cbaba..882ea6e6 100644 --- a/generators/demo/templates/src/views/buildscript.coffee +++ b/generators/demo/templates/src/views/buildscript.coffee @@ -38,7 +38,6 @@ # # @property viewName # @type String - # @static # @final # # @default 'buildscript' @@ -52,7 +51,6 @@ # # @property className # @type String - # @static # @final # # @default 'buildscript-view' @@ -67,7 +65,6 @@ # @property template # @type Function # @protected - # @static # @final ### diff --git a/generators/demo/templates/src/views/documentation.coffee b/generators/demo/templates/src/views/documentation.coffee index 6f55915b..5c524ad3 100644 --- a/generators/demo/templates/src/views/documentation.coffee +++ b/generators/demo/templates/src/views/documentation.coffee @@ -36,7 +36,6 @@ # # @property viewName # @type String - # @static # @final # # @default 'documentation' @@ -50,7 +49,6 @@ # # @property className # @type String - # @static # @final # # @default 'documentation-view' @@ -65,7 +63,6 @@ # @property template # @type Function # @protected - # @static # @final ### diff --git a/generators/demo/templates/src/views/i18n.coffee b/generators/demo/templates/src/views/i18n.coffee index f0fa5165..ecf2b4d5 100644 --- a/generators/demo/templates/src/views/i18n.coffee +++ b/generators/demo/templates/src/views/i18n.coffee @@ -41,7 +41,6 @@ # # @property viewName # @type String - # @static # @final # # @default 'i18n' @@ -55,7 +54,6 @@ # # @property className # @type String - # @static # @final # # @default 'i18n-view' @@ -70,7 +68,6 @@ # @property template # @type Function # @protected - # @static # @final ### @@ -83,7 +80,6 @@ # @property events # @type Object # @protected - # @static # @final ### diff --git a/generators/demo/templates/src/views/index.coffee b/generators/demo/templates/src/views/index.coffee index 9a4393bc..717f832e 100644 --- a/generators/demo/templates/src/views/index.coffee +++ b/generators/demo/templates/src/views/index.coffee @@ -38,7 +38,6 @@ # # @property viewName # @type String - # @static # @final # # @default 'index' @@ -52,7 +51,6 @@ # # @property className # @type String - # @static # @final # # @default 'index-view' @@ -67,7 +65,6 @@ # @property template # @type Function # @protected - # @static # @final ### diff --git a/generators/demo/templates/src/views/navigation.coffee b/generators/demo/templates/src/views/navigation.coffee index cd5a9f49..993431f6 100644 --- a/generators/demo/templates/src/views/navigation.coffee +++ b/generators/demo/templates/src/views/navigation.coffee @@ -38,7 +38,6 @@ # # @property className # @type String - # @static # @final # # @default 'navigation-view' @@ -53,7 +52,6 @@ # @property template # @type Function # @protected - # @static # @final ### diff --git a/generators/model/index.js b/generators/model/index.js index 7716f777..91ea83ae 100644 --- a/generators/model/index.js +++ b/generators/model/index.js @@ -8,17 +8,19 @@ var generators = require( 'yeoman-generator' ) , yosay = require( 'yosay' ) , youtil = require( './../../lib/youtil.js' ) , chalk = require( 'chalk' ) +, glob = require( 'glob' ) +, url = require( 'url' ) // https://nodejs.org/api/url.html , _ = require( 'lodash' ) ; -var decapitalize = require( 'underscore.string/decapitalize' ); - var ModelGenerator = generators.Base.extend( { constructor: function () { generators.Base.apply( this, arguments ); + this.description = this._description( 'backbone model' ); + this.argument( 'modelName' , { @@ -47,7 +49,23 @@ var ModelGenerator = generators.Base.extend( 'description' , { type: String - , desc: 'The purpose of the model to create.' + , desc: 'The purpose of this model.' + } + ); + + this.option( + 'api' + , { + type: String + , desc: 'The name of the API this model should connect to.' + } + ); + + this.option( + 'service' + , { + type: String + , desc: 'The service API endpoint URL this model should connect to (relative to the API\'s base).' } ); @@ -55,25 +73,49 @@ var ModelGenerator = generators.Base.extend( 'singleton' , { type: Boolean - , desc: 'Specify whether this model should be a singleton (instance).' + , desc: 'Whether this model should be a singleton (instance).' } ); } - , description: - chalk.bold( - 'This is the ' + chalk.cyan( 'backbone model' ) - + ' generator for BAT, the Backbone Application Template' - + ' created by ' + chalk.blue( 'marv' ) + chalk.red( 'iq' ) + '.' - ) - , initializing: function () { this._assertBatApp(); + // Find available APIs: + // + var apis = this.apis + = {} + , base = this.destinationPath( 'src/apis' ) + ; + + glob.sync( '**/*.coffee', { cwd: base } ).forEach( + + function ( path ) + { + var pathAbs = base + '/' + path; + var match = this.fs.read( pathAbs ).match( /@class\s+(\S+)/ ); + + if ( !( match ) ) { return; } + + var className = match[ 1 ] + , name = _.lowerFirst( className.replace( /Api$/, '' )) + ; + + apis[ name ] = + { + className: className + , pathAbs: pathAbs + , path: path + } + ; + + }.bind( this ) + ); + // Container for template expansion data. // - this.templateData = {}; + this.templateData = {}; } , prompting: @@ -92,7 +134,7 @@ var ModelGenerator = generators.Base.extend( , validate: youtil.isIdentifier , filter: function ( value ) { - return decapitalize( _.trim( value ).replace( /model$/i, '' )); + return _.lowerFirst( _.trim( value ).replace( /model$/i, '' )); } } , { @@ -103,10 +145,54 @@ var ModelGenerator = generators.Base.extend( , validate: youtil.isNonBlank , filter: youtil.sentencify } + , { + type: 'list' + , name: 'api' + , message: 'Should this model connect to an API?' + , choices: [ '- none -' ].concat( _.keys( this.apis )) + , default: youtil.definedToString( this.options.api ) + , validate: function ( value ) + { + return value in this.apis; + }.bind( this ) + , filter: function ( value ) { + return this.apis[ value ]; + }.bind( this ) + , when: !( _.isEmpty( this.apis )) + } + , { + type: 'input' + , name: 'service' + , message: ( + 'To which service API endpoint should this model connect?' + + chalk.gray ( ' - please enter a URL relative to the API\'s base.' ) + ) + , default: function ( answers ) + { + return ( + youtil.definedToString( this.options.service ) + || _.kebabCase( _.deburr( + answers.modelName + || this.templateData.modelName + )) + ); + }.bind( this ) + , validate: function( value ) { + return value === url.parse( value ).path + } + , filter: function ( value ) + { + return value.replace( /^\/+/, '' ); + } + , when: function ( answers ) + { + return answers.api || this.templateData.api; + }.bind( this ) + } , { type: 'confirm' , name: 'singleton' - , message: 'Should this model be a singleton?' + , message: 'Should this model be a singleton (instance)?' , default: false , validate: _.isBoolean } @@ -131,17 +217,19 @@ var ModelGenerator = generators.Base.extend( , configuring: function () { - var data = this.templateData - , modelName = data.modelName + var data = this.templateData + , modelName = data.modelName ; _.extend( data , { - className: _.capitalize( modelName ) + 'Model' - , fileBase: _.kebabCase( _.deburr( modelName )) + className: _.upperFirst( modelName ) + 'Model' + , fileBase: _.kebabCase( _.deburr( modelName )) + + , userName: this.user.git.name() - , userName: this.user.git.name() + , backbone: ( this.config.get( 'backbone' ) || { className: 'Backbone', modulePath: 'backbone' } ) } ); } @@ -153,13 +241,122 @@ var ModelGenerator = generators.Base.extend( var data = this.templateData , templates = { - 'model.coffee': [ 'src/models/' + data.fileBase + '.coffee' ] + 'model.coffee': [ 'src/models/' + data.fileBase + '.coffee' ] } ; this._templatesProcess( templates ); } } + + , install: { + + updateApi: function () { + + var data = this.templateData + , api = data.api + , modelName = data.modelName + ; + + if ( !( api )) { return; } + + // + // Insert the expanded fragment template into the api collection definition. + // Look for a place to insert, preferably at an alfanumerically ordered position. + // Do nothing if an service API endpoint defintion for this model seems to exist already. + // + + var fs = this.fs + , collection = fs.read( api.pathAbs ) + , matcherDec = /^([ \t]*).*?\bnew\s+ApiServicesCollection\(\s*?(^[ \t]*)?\[[ \t]*(\n)?/m + // 1------1 2-------2 3--3 + , match = collection.match( matcherDec ) + ; + + if ( !( match ) ) + { + this.log( + 'It appears that "' + api.pathAbs + '" does not contains an `ApiServicesCollection`\n' + + 'Leaving it untouched.' + ); + + return; + } + + var level = ' ' + , indent = (( match[ 2 ] != null ) ? match[ 2 ] : ( match[ 1 ] + level )) + , insertAt = match.index + match[ 0 ].length + , padPre = match[ 3 ] ? '' : '\n' + , padPost = match[ 3 ] ? '' : indent + , matcherDef = /^(([ \t]*)([ \t]+))###\*[\s\S]*?^\1###[\s\S]*?^\1id:\s*'([^\]]*?)'[^\]]*?^\2(?:,[ \t]*(\n)?|(?=(\])))/mg + // ^12======23======31 ^\1 ^\1 4-------4 ^\2 5--5 6--6 + ; + + // Start looking for definitions directly after API declaration opening. + // + matcherDef.lastIndex = insertAt; + + // Find a place to insert + // + while ( (( match = matcherDef.exec( collection ) )) ) + { + level = match[ 3 ]; + + if ( modelName > match[ 4 ] ) + { + // Possibly insert after this match. + // + indent = match[ 2 ]; + insertAt = match.index + match[ 0 ].length; + padPre = match[ 5 ] ? '' : match[ 6 ] ? ',\n' : '\n'; + padPost = match[ 5 ] ? '' : indent; + continue; + } + + if ( modelName < match[ 4 ] ) + { + // Insert before this match. + // + insertAt = match.index; + padPre = ''; + padPost = ''; + break; + } + + this.log( + 'It appears that "' + api.pathAbs + '" already contains a service API endpoint definition for "' + data.className + '".\n' + + 'Leaving it untouched.' + ); + + return; + } + + // Avoid conflict warning. + // + this.conflicter.force = true; + + // Expand fragment template and read it back. + // + var fragmentPath = 'src/apis/api-service-literal-fragment.coffee' + , fragmentDst = this.destinationPath( fragmentPath ) + ; + + this._templatesProcess( [ [ fragmentPath ] ] ); + + var fragment = fs.read( fragmentDst ); + + fs.write( + api.pathAbs + , collection.slice( 0, insertAt ) + + padPre + + fragment.replace( /^ /mg, level ).replace( /^(?=.*?\S)/mg, indent ) + + padPost + + collection.slice( insertAt ) + ); + + fs.delete( fragmentDst ); + } + } } ); diff --git a/generators/model/templates/model.coffee b/generators/model/templates/model.coffee index 6bba3930..f5b036f0 100644 --- a/generators/model/templates/model.coffee +++ b/generators/model/templates/model.coffee @@ -1,15 +1,21 @@ ( ( factory ) -> if typeof exports is 'object' module.exports = factory( - require( 'backbone' ) + require( '<%- backbone.modulePath %>' )<% if ( api ) { %> + + require( './../apis/<%- api.path %>' )<% } %> ) else if typeof define is 'function' and define.amd define( [ - 'backbone' + '<%- backbone.modulePath %>'<% if ( api ) { %> + + './../apis/<%- api.path %>'<% } %> ], factory ) return )(( - Backbone + <%- backbone.className %><% if ( api ) { %> + + api<% } %> ) -> ###* @@ -25,19 +31,18 @@ # <%- description %> #<% } %> # @class <%- className %> - # @extends Backbone.Model<% if ( singleton ) { %> + # @extends <%- backbone.className %>.Model<% if ( singleton ) { %> # @static<% } else { %> # @constructor<% } %> ### - class <%- className %> extends Backbone.Model + class <%- className %> extends <%- backbone.className %>.Model ###* # List of [valid attribute names](#attrs). # # @property schema # @type Array[String] - # @static # @final ### @@ -51,7 +56,33 @@ schema: [ 'id' - ]<% if ( singleton ) { %> + ] + + + ###* + # Default attribute values. + # + # @property defaults + # @type Object + # @final + ### + + defaults: {}<% if ( api ) { %> + + + ###* + # Service API endpoint; defined in the {{#crossLink '<%- api.className %>/<%- modelName %>:attribute'}}<%- api.className %>{{/crossLink}}. + #<% if ( singleton ) { %> + # @property url<% } else { %> + # @property urlRoot<% } %> + # @type ApiServiceModel + # @final + # + # @default '<<%- api.className %>.url>/<%- service %>' + ### +<% if ( singleton ) { %> + url: api.get( '<%- modelName %>' )<% } else { %> + urlRoot: api.get( '<%- modelName %>' )<% } %><% } %><% if ( singleton ) { %> ## Export singleton. diff --git a/generators/model/templates/src/apis/api-service-literal-fragment.coffee b/generators/model/templates/src/apis/api-service-literal-fragment.coffee new file mode 100644 index 00000000..fdaa61d0 --- /dev/null +++ b/generators/model/templates/src/apis/api-service-literal-fragment.coffee @@ -0,0 +1,14 @@ + ###* + # Service API endpoint for accessing <% if ( singleton ) { %>the <% } %>{{#crossLink '<%- className %>'}}<%- modelName %> resource<% if ( ! singleton ) { %>s<% } %>{{/crossLink}}. + # + # @attribute <%- modelName %> + # @type ApiServiceModel + # @final + # + # @default '<<%- api.className %>.url>/<%- service %>' + ### + + id: '<%- modelName %>' + urlPath: '<%- service %>' + +, diff --git a/generators/view/index.js b/generators/view/index.js index 72cbec4d..befa0511 100644 --- a/generators/view/index.js +++ b/generators/view/index.js @@ -7,18 +7,17 @@ var generators = require( 'yeoman-generator' ) , yosay = require( 'yosay' ) , youtil = require( './../../lib/youtil.js' ) -, chalk = require( 'chalk' ) , _ = require( 'lodash' ) ; -var decapitalize = require( 'underscore.string/decapitalize' ); - var ViewGenerator = generators.Base.extend( { constructor: function () { generators.Base.apply( this, arguments ); + this.description = this._description( 'backbone view' ); + this.argument( 'viewName' , { @@ -47,7 +46,7 @@ var ViewGenerator = generators.Base.extend( 'description' , { type: String - , desc: 'The purpose of the view to create.' + , desc: 'The purpose of this view.' } ); @@ -55,18 +54,11 @@ var ViewGenerator = generators.Base.extend( 'sass' , { type: Boolean - , desc: 'Specify whether this view should have a SASS file of its own.' + , desc: 'Whether this view should have a SASS file of its own.' } ); } - , description: - chalk.bold( - 'This is the ' + chalk.cyan( 'backbone view' ) - + ' generator for BAT, the Backbone Application Template' - + ' created by ' + chalk.blue( 'marv' ) + chalk.red( 'iq' ) + '.' - ) - , initializing: function () { this._assertBatApp(); @@ -92,7 +84,7 @@ var ViewGenerator = generators.Base.extend( , validate: youtil.isIdentifier , filter: function ( value ) { - return decapitalize( _.trim( value ).replace( /view$/i, '' )); + return _.lowerFirst( _.trim( value ).replace( /view$/i, '' )); } } , { @@ -105,7 +97,7 @@ var ViewGenerator = generators.Base.extend( , { type: 'confirm' , name: 'sass' - , message: 'Would you like a SASS file for this view?' + , message: 'Should this view have a a SASS file of its own?' , default: true , validate: _.isBoolean } @@ -137,11 +129,13 @@ var ViewGenerator = generators.Base.extend( _.extend( data , { - className: _.capitalize( viewName ) + 'View' - , cssClassName: _.kebabCase( viewName ) + '-view' - , fileBase: _.kebabCase( _.deburr( viewName )) + className: _.upperFirst( viewName ) + 'View' + , cssClassName: _.kebabCase( viewName ) + '-view' + , fileBase: _.kebabCase( _.deburr( viewName )) - , userName: this.user.git.name() + , userName: this.user.git.name() + + , backbone: ( this.config.get( 'backbone' ) || { className: 'Backbone', modulePath: 'backbone' } ) } ); } @@ -187,10 +181,27 @@ var ViewGenerator = generators.Base.extend( , statement = '@import "views/_' + data.fileBase + '"' ; + // Look for a place to insert, preferably at an alfanumerically ordered position. // Do nothing if an `@import` for this sass file seems to exist already. // - if ( views.indexOf( statement ) !== -1 ) + var insertAt, indent, match, matcher = /^([ \t]*)(@import.*)/mg; + + while ( (( match = matcher.exec( views ) )) ) { + if ( statement > match[ 2 ] ) + { + // Use indent of the (possibly) preceding line. + // + indent = match[ 1 ]; + continue; + } + + if ( statement < match[ 2 ] ) + { + insertAt = match.index; + break; + } + this.log( 'It appears that "' + viewsPath + '" already contains an `@import` for "' + data.fileBase + '.sass".\n' + 'Leaving it untouched.' @@ -199,28 +210,26 @@ var ViewGenerator = generators.Base.extend( return; } - // Avoid the conflict warning and use force for the write + // Avoid the conflict warning and use force for the write // this.conflicter.force = true; - // Look for a place to insert, preferably at an alfanumerically ordered position. - // - var insertAt, match, matcher = /^@import.*/mg; - - while ( (( match = matcher.exec( views ) )) ) + if ( indent == null ) { - if ( statement < match[ 0 ] ) { insertAt = match.index; } + // First @import; use indent of the following line, if any. + // + indent = match ? match[ 1 ] : ''; } if ( insertAt == null ) { - var pad = (( views.length && views.slice( -1 ) !== '\n' ) ? '\n' : '' ); - - fs.write( viewsPath, views + pad + statement + '\n' ); + // Append at end of file; Take care of possibly missing trailing newline. + // + fs.write( viewsPath, views + (( views.length && views.slice( -1 ) !== '\n' ) ? '\n' : '' ) + indent + statement + '\n' ); } else { - fs.write( viewsPath, views.slice( 0, insertAt ) + statement + '\n' + views.slice( insertAt ) ); + fs.write( viewsPath, views.slice( 0, insertAt ) + indent + statement + '\n' + views.slice( insertAt ) ); } } } diff --git a/generators/view/templates/view.coffee b/generators/view/templates/view.coffee index 55511bfa..92399c0c 100644 --- a/generators/view/templates/view.coffee +++ b/generators/view/templates/view.coffee @@ -1,17 +1,20 @@ ( ( factory ) -> if typeof exports is 'object' module.exports = factory( - require( 'backbone' ) + require( '<%- backbone.modulePath %>' ) + require( './<%- fileBase %>.hbs' ) ) else if typeof define is 'function' and define.amd define( [ - 'backbone' + '<%- backbone.modulePath %>' + './<%- fileBase %>.hbs' ], factory ) return )(( - Backbone + <%- backbone.className %> + template ) -> @@ -28,18 +31,17 @@ # <%- description %> #<% } %> # @class <%- className %> - # @extends Backbone.View + # @extends <%- backbone.className %>.View # @constructor ### - class <%- className %> extends Backbone.View + class <%- className %> extends <%- backbone.className %>.View ###* # Expose this view's name to the router. # # @property viewName # @type String - # @static # @final # # @default '<%- viewName %>' @@ -53,7 +55,6 @@ # # @property className # @type String - # @static # @final # # @default '<%- cssClassName %>' @@ -68,13 +69,30 @@ # @property template # @type Function # @protected - # @static # @final ### template: template + ###* + # Delegated DOM event handler definition. + # + # Format: + # + # ```coffee + # '[ ]': '' || + # ... + # ``` + # + # @property events + # @type Object + # @final + ### + + events: undefined + + ###* # @method initialize # @protected diff --git a/lib/generator.js b/lib/generator.js index ff51d6c0..8bfa896d 100644 --- a/lib/generator.js +++ b/lib/generator.js @@ -4,10 +4,34 @@ 'use strict'; -var _ = require( 'lodash' ); +var _ = require( 'lodash' ) +, chalk = require( 'chalk' ) +, fsCore = require( 'fs' ) +, path = require( 'path' ) +; module.exports = { + /** + * Churn out a commonly styled generator description. + * + * @method: _description + * + * @param {String} what String injected into the common descriptor template. + * + * @return {String} The expanded description. + */ + + _description: function ( what ) + { + return chalk.bold( + 'This is the ' + chalk.cyan( what ) + + ' generator for BAT, the Backbone Application Template' + + ' created by ' + chalk.blue( 'marv' ) + chalk.red( 'iq' ) + '.' + ); + } + + , /** * Takes an array of prompt definitions, as would be passed to `this.prompt()`, filters away those for which valid values have been supplied via the * identically named command line options, fills the `this.templateData` object with those values and returns the pruned list. @@ -124,5 +148,45 @@ module.exports = method.apply( fs, args ); } } + + , + /** + * Make a symbolic link within `#destinationRoot()`. + * + * @method _symLink + * + * @param {String} src The path relative to `#destinationRoot()` that should become the target of the symbolic link. + * @param {String} dst The path relative to `#destinationRoot()` that should become the symbolic link. + * + */ + + _symLink: function ( src, dst ) + { + src = this.destinationPath( src ); + dst = this.destinationPath( dst ); + + this.conflicter.checkForCollision( + + dst + , this.read( src ) + , function ( err, status ) { + + if ( 'identical' === status ) return; + + // The symlink may exist, but may link to a nonexistent file in which case `status` will be `'create'`, so try unlinking it anyway. + // + try + { + fsCore.unlinkSync( dst ); + } + catch ( error ) + { + if ( 'ENOENT' !== error.code ) throw error; + } + + fsCore.symlinkSync( path.relative( path.dirname( dst ), src ), dst ); + } + ); + } } ; diff --git a/lib/youtil.js b/lib/youtil.js index 55323415..96e84e75 100644 --- a/lib/youtil.js +++ b/lib/youtil.js @@ -1,7 +1,7 @@ 'use strict'; -var capitalize = require( 'underscore.string/capitalize' ) -, clean = require( 'underscore.string/clean' ) +var _ = require( 'lodash' ) +, clean = require( 'underscore.string/clean' ) ; module.exports = @@ -11,6 +11,20 @@ module.exports = return (( value == null ) ? value : '' + value ); } + , isCoffeeScript: function ( value ) + { + try + { + require( 'coffee-script' ).compile( value ); + } + catch ( e ) + { + return false; + } + + return true; + } + , isIdentifier: function ( value ) { return /^[$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*$/.test(( '' + value ).trim() ); @@ -31,7 +45,7 @@ module.exports = , sentencify: function ( value ) { - return capitalize( clean( value )).replace( /([^!?.,:;])$/, '$1.' ); + return _.upperFirst( clean( value )).replace( /([^!?.,:;])$/, '$1.' ); } } ; diff --git a/package.json b/package.json index 780ee4c0..8bcbb5ce 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ }, "dependencies": { "chalk": "^1.1.3", + "coffee-script": "^1.11.0", + "glob": "^7.0.5", "language-tags": "^1.0.5", "lodash": "^4.14.1", "mkdirp": "^0.5.1", @@ -23,6 +25,7 @@ }, "files": [ "AUTHORS", + "CHANGELOG.md", "LICENSE", "README.md", "generators"