+
+ {{#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
+ # '