diff --git a/app/index.js b/app/index.js index 431f6ea31..2f274a3b9 100644 --- a/app/index.js +++ b/app/index.js @@ -47,6 +47,21 @@ var Generator = module.exports = function Generator(args, options) { this.env.options.coffee = this.options.coffee; } + if (typeof this.env.options.typescript === 'undefined') { + this.option('typescript', { + desc: 'Generate TypeScript instead of JavaScript' + }); + + // attempt to detect if user is using TS or not + // if cml arg provided, use that; else look for the existence of ts + if (!this.options.typescript && + this.expandFiles(path.join(this.appPath, '/scripts/**/*.ts'), {}).length > 0) { + this.options.typescript = true; + } + + this.env.options.typescript = this.options.typescript; + } + if (typeof this.env.options.minsafe === 'undefined') { this.option('minsafe', { desc: 'Generate AngularJS minification safe code' @@ -206,13 +221,15 @@ Generator.prototype.askForModules = function askForModules() { if (this.cookiesModule) { angMods.push("'ngCookies'"); + this.env.options.ngCookies = true; } - if (this.resourceModule) { angMods.push("'ngResource'"); + this.env.options.ngResource = true; } if (this.sanitizeModule) { angMods.push("'ngSanitize'"); + this.env.options.ngSanitize = true; } if (this.routeModule) { angMods.push("'ngRoute'"); @@ -263,6 +280,7 @@ Generator.prototype.createIndexHtml = function createIndexHtml() { Generator.prototype.packageFiles = function () { this.coffee = this.env.options.coffee; + this.typescript = this.env.options.typescript; this.template('../../templates/common/_bower.json', 'bower.json'); this.template('../../templates/common/_package.json', 'package.json'); this.template('../../templates/common/Gruntfile.js', 'Gruntfile.js'); diff --git a/main/index.js b/main/index.js index 000bcd62c..022308617 100644 --- a/main/index.js +++ b/main/index.js @@ -13,6 +13,9 @@ util.inherits(Generator, ScriptBase); Generator.prototype.createAppFile = function createAppFile() { this.angularModules = this.env.options.angularDeps; + this.ngCookies = this.env.options.ngCookies; + this.ngResource = this.env.options.ngResource; + this.ngSanitize = this.env.options.ngSanitize; this.ngRoute = this.env.options.ngRoute; this.appTemplate('app', 'scripts/app'); }; diff --git a/readme.md b/readme.md index 8cf58f6c5..3310e1290 100644 --- a/readme.md +++ b/readme.md @@ -173,8 +173,8 @@ angular.module('myMod').config(function ($provide) { ## Options In general, these options can be applied to any generator, though they only affect generators that produce scripts. -### CoffeeScript -For generators that output scripts, the `--coffee` option will output CoffeeScript instead of JavaScript. +### CoffeeScript and TypeScript +For generators that output scripts, the `--coffee` option will output CoffeeScript instead of JavaScript, and `--typescript` will out put TypeScript instead of JavaScript. For example: ```bash @@ -187,9 +187,42 @@ angular.module('myMod') .controller 'UserCtrl', ($scope) -> ``` -A project can mix CoffeScript and JavaScript files. +For example: +```bash +yo angular:controller user --typescript +``` + +Produces `app/scripts/controller/user.ts`: +```typescript +/// + +'use strict'; + +module demoApp { + export interface IUserScope extends ng.IScope { + awesomeThings: any[]; + } + + export class UserCtrl { + + constructor (private $scope:IUserScope) { + $scope.awesomeThings = [ + 'HTML5 Boilerplate', + 'AngularJS', + 'Karma' + ]; + } + } +} + +angular.module('demoApp') + .controller('UserCtrl', demoApp.UserCtrl); +``` + + +A project can mix TypeScript, CoffeScript, and JavaScript files. -To output JavaScript files, even if CoffeeScript files exist (the default is to output CoffeeScript files if the generator finds any in the project), use `--coffee=false`. +To output JavaScript files, even if CoffeeScript (or TypeScript) files exist (the default is to output CoffeeScript files if the generator finds any in the project), use `--coffee=false` and/or `--typescript=false`. ### Minification Safe diff --git a/route/index.js b/route/index.js index f30d60939..0fd7439a8 100644 --- a/route/index.js +++ b/route/index.js @@ -15,10 +15,12 @@ util.inherits(Generator, ScriptBase); Generator.prototype.rewriteAppJs = function () { var coffee = this.env.options.coffee; + var typescript = this.env.options.typescript; + var config = { file: path.join( this.env.options.appPath, - 'scripts/app.' + (coffee ? 'coffee' : 'js') + 'scripts/app.' + (coffee ? 'coffee' : typescript ? 'ts': 'js') ), needle: '.otherwise', splicable: [ diff --git a/script-base.js b/script-base.js index 32bd81756..7c1d3dbcc 100644 --- a/script-base.js +++ b/script-base.js @@ -32,6 +32,20 @@ var Generator = module.exports = function Generator() { this.env.options.testPath = this.env.options.testPath || 'test/spec'; } + this.env.options.typescript = this.options.typescript; + if (typeof this.env.options.typescript === 'undefined') { + this.option('typescript'); + + // attempt to detect if user is using TS or not + // if cml arg provided, use that; else look for the existence of ts + if (!this.options.typescript && + this.expandFiles(path.join(this.env.options.appPath, '/scripts/**/*.ts'), {}).length > 0) { + this.options.typescript = true; + } + + this.env.options.typescript = this.options.typescript; + } + this.env.options.coffee = this.options.coffee; if (typeof this.env.options.coffee === 'undefined') { this.option('coffee'); @@ -59,6 +73,11 @@ var Generator = module.exports = function Generator() { this.scriptSuffix = '.coffee'; } + if (this.env.options.typescript) { + sourceRoot = '/templates/typescript'; + this.scriptSuffix = '.ts'; + } + if (this.env.options.minsafe) { sourceRoot += '-min'; } diff --git a/templates/common/Gruntfile.js b/templates/common/Gruntfile.js index d1e7b8b9a..ec9a2b963 100644 --- a/templates/common/Gruntfile.js +++ b/templates/common/Gruntfile.js @@ -34,6 +34,14 @@ module.exports = function (grunt) { coffeeTest: { files: ['test/spec/{,*/}*.{coffee,litcoffee,coffee.md}'], tasks: ['newer:coffee:test', 'karma'] + },<% } else if (typescript) { %> + typescript: { + files: ['<%%= yeoman.app %>/scripts/{,*/}*.ts'], + tasks: ['typescript:base'] + }, + typescriptTest: { + files: ['test/spec/{,*/}*.ts'], + tasks: ['typescript:test', 'karma'] },<% } else { %> js: { files: ['<%%= yeoman.app %>/scripts/{,*/}*.js'], @@ -63,7 +71,7 @@ module.exports = function (grunt) { }, files: [ '<%%= yeoman.app %>/{,*/}*.html', - '.tmp/styles/{,*/}*.css',<% if (coffee) { %> + '.tmp/styles/{,*/}*.css',<% if (coffee || typescript) { %> '.tmp/scripts/{,*/}*.js',<% } %> '<%%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' ] @@ -111,9 +119,9 @@ module.exports = function (grunt) { reporter: require('jshint-stylish') }, all: [ - 'Gruntfile.js'<% if (!coffee) { %>, + 'Gruntfile.js'<% if (!coffee && !typescript) { %>, '<%%= yeoman.app %>/scripts/{,*/}*.js'<% } %> - ]<% if (!coffee) { %>, + ]<% if (!coffee && !typescript) { %>, test: { options: { jshintrc: 'test/.jshintrc' @@ -160,6 +168,33 @@ module.exports = function (grunt) { } }, +<% if (typescript) { %> + // Compiles TypeScript to JavaScript + typescript: { + base: { + src: ['<%%= yeoman.app %>/scripts/{,*/}*.ts'], + dest: '.tmp/scripts', + options: { + module: 'amd', //or commonjs + target: 'es5', //or es3 + 'base_path': '<%%= yeoman.app %>/scripts', //quoting base_path to get around jshint warning. + sourcemap: true, + declaration: true + } + }, + test: { + src: ['test/spec/{,*/}*.ts', 'test/e2e/{,*/}*.ts'], + dest: '.tmp/spec', + options: { + module: 'amd', //or commonjs + target: 'es5', //or es3 + sourcemap: true, + declaration: true + } + } + },<% } %> + + <% if (coffee) { %> // Compiles CoffeeScript to JavaScript coffee: { @@ -343,17 +378,20 @@ module.exports = function (grunt) { // Run some tasks in parallel to speed up the build process concurrent: { server: [<% if (coffee) { %> - 'coffee:dist',<% } %><% if (compass) { %> + 'coffee:dist',<% } %><% if (typescript) { %> + 'typescript:base',<% } %><% if (compass) { %> 'compass:server'<% } else { %> 'copy:styles'<% } %> ], test: [<% if (coffee) { %> - 'coffee',<% } %><% if (compass) { %> + 'coffee',<% } %><% if (typescript) { %> + 'typescript',<% } %><% if (compass) { %> 'compass'<% } else { %> 'copy:styles'<% } %> ], dist: [<% if (coffee) { %> - 'coffee',<% } %><% if (compass) { %> + 'coffee',<% } %><% if (typescript) { %> + 'typescript',<% } %><% if (compass) { %> 'compass:dist',<% } else { %> 'copy:styles',<% } %> 'imagemin', diff --git a/templates/common/_bower.json b/templates/common/_bower.json index da12c309c..7d6a990a9 100644 --- a/templates/common/_bower.json +++ b/templates/common/_bower.json @@ -15,6 +15,8 @@ }, "devDependencies": { "angular-mocks": "1.2.6", - "angular-scenario": "1.2.6" + "angular-scenario": "1.2.6"<% if (typescript) { %>, + "dt-jasmine": "~2.0.0", + "dt-angular": "https://github.com/jedmao/dt-angular/archive/v1.2.0.tar.gz"<% } %> } } diff --git a/templates/common/_package.json b/templates/common/_package.json index 4a756a6a3..c140667d2 100644 --- a/templates/common/_package.json +++ b/templates/common/_package.json @@ -27,7 +27,8 @@ "grunt-usemin": "~2.0.0", "jshint-stylish": "~0.1.3", "load-grunt-tasks": "~0.2.0", - "time-grunt": "~0.2.1" + "time-grunt": "~0.2.1", + "grunt-typescript": "~0.2.7" }, "engines": { "node": ">=0.10.0" diff --git a/templates/common/root/app/.buildignore b/templates/common/root/app/.buildignore index fc98b8eb5..548f58c3c 100644 --- a/templates/common/root/app/.buildignore +++ b/templates/common/root/app/.buildignore @@ -1 +1,2 @@ -*.coffee \ No newline at end of file +*.coffee +*.ts \ No newline at end of file diff --git a/templates/typescript/app.ts b/templates/typescript/app.ts new file mode 100644 index 000000000..4d525dd58 --- /dev/null +++ b/templates/typescript/app.ts @@ -0,0 +1,19 @@ +/// <% if (ngCookies) { %> +/// <% } %><% if (ngResource) { %> +/// <% } %><% if (ngSanitize) { %> +/// <% } %><% if (ngRoute) { %> +/// <% } %> + +'use strict'; + +angular.module('<%= scriptAppName %>', [<%= angularModules %>])<% if (ngRoute) { %> + .config(($routeProvider:ng.route.IRouteProvider) => { + $routeProvider + .when('/', { + templateUrl: 'views/main.html', + controller: 'MainCtrl' + }) + .otherwise({ + redirectTo: '/' + }); + })<% } %>; diff --git a/templates/typescript/controller.ts b/templates/typescript/controller.ts new file mode 100644 index 000000000..7a91b6894 --- /dev/null +++ b/templates/typescript/controller.ts @@ -0,0 +1,23 @@ +/// + +'use strict'; + +module <%= scriptAppName %> { + export interface I<%= classedName %>Scope extends ng.IScope { + awesomeThings: any[]; + } + + export class <%= classedName %>Ctrl { + + constructor (private $scope: I<%= classedName %>Scope) { + $scope.awesomeThings = [ + 'HTML5 Boilerplate', + 'AngularJS', + 'Karma' + ]; + } + } +} + +angular.module('<%= scriptAppName %>') + .controller('<%= classedName %>Ctrl', <%= scriptAppName %>.<%= classedName %>Ctrl); diff --git a/templates/typescript/decorator.ts b/templates/typescript/decorator.ts new file mode 100644 index 000000000..8272e1d5a --- /dev/null +++ b/templates/typescript/decorator.ts @@ -0,0 +1,18 @@ +/// + +'use strict'; + +module <%= scriptAppName %> { + export function <%= cameledName %>DecoratorProvider($provide: ng.auto.IProvideService): void { + //decorate <%= cameledName %> + $provide.decorator('<%= cameledName %>', <%= cameledName %>Decorator); + } + + export function <%= cameledName %>Decorator($delegate: any) { + // decorate the $delegate + return $delegate; + } +} + +angular.module('<%= scriptAppName %>') + .config(<%= scriptAppName %>.<%= cameledName %>DecoratorProvider); diff --git a/templates/typescript/directive.ts b/templates/typescript/directive.ts new file mode 100644 index 000000000..9c0940391 --- /dev/null +++ b/templates/typescript/directive.ts @@ -0,0 +1,22 @@ +/// + +'use strict'; + +module <%= scriptAppName %> { + + export class <%= classedName %> implements ng.IDirective { + template = '
'; + restrict = 'E'; + link = (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes): void => { + element.text('this is the <%= cameledName %> directive'); + } + } + + export function <%= cameledName %>Factory() { + return new <%= scriptAppName %>.<%= classedName %>(); + } + +} + +angular.module('<%= scriptAppName %>') + .directive('<%= cameledName %>', <%= scriptAppName %>.<%= cameledName %>Factory); diff --git a/templates/typescript/filter.ts b/templates/typescript/filter.ts new file mode 100644 index 000000000..07708ed42 --- /dev/null +++ b/templates/typescript/filter.ts @@ -0,0 +1,18 @@ +/// + +'use strict'; + +module <%= scriptAppName %> { + export function <%= cameledName %>FilterFactory(): Function { + return <%= cameledName %>Filter; + } + + function <%= cameledName %>Filter(input, param) { + //usage {{"text" | <%= cameledName %>: "suffix"}} + //returns '<%= cameledName %> filter: text suffix' + return '<%= cameledName %> filter: ' + input + (param ? ' ' + param: ''); + } +} + +angular.module('<%= scriptAppName %>') + .filter('<%= cameledName %>', <%= scriptAppName %>.<%= cameledName %>FilterFactory); \ No newline at end of file diff --git a/templates/typescript/service/constant.ts b/templates/typescript/service/constant.ts new file mode 100644 index 000000000..30cbacd48 --- /dev/null +++ b/templates/typescript/service/constant.ts @@ -0,0 +1,6 @@ +/// + +'use strict'; + +angular.module('<%= scriptAppName %>') + .constant('<%= cameledName %>', 42); diff --git a/templates/typescript/service/factory.ts b/templates/typescript/service/factory.ts new file mode 100644 index 000000000..bc25a3cb8 --- /dev/null +++ b/templates/typescript/service/factory.ts @@ -0,0 +1,22 @@ +/// + +'use strict'; + +module <%= scriptAppName %> { + export function <%= cameledName %>Factory() { + return new <%= classedName %>(42); + } + + export class <%= classedName %> { + + constructor (private meaningOfLife) { + } + + someMethod() { + return this.meaningOfLife; + } + } +} + +angular.module('<%= scriptAppName %>') + .factory('<%= cameledName %>', <%= scriptAppName %>.<%= cameledName %>Factory); diff --git a/templates/typescript/service/provider.ts b/templates/typescript/service/provider.ts new file mode 100644 index 000000000..0ec9dddad --- /dev/null +++ b/templates/typescript/service/provider.ts @@ -0,0 +1,24 @@ +/// + +'use strict'; + +module <%= scriptAppName %> { + + var salutation: string; + + export class Greeter { + greet = () => salutation; + } + + export class <%= classedName %>Provider { + $get = () => new Greeter(); + + // Public API for configuration + setSalutation = (s: string) => salutation = s; + } + +} + + +angular.module('<%= scriptAppName %>') + .provider('<%= cameledName %>', <%= scriptAppName %>.<%= classedName %>Provider); diff --git a/templates/typescript/service/service.ts b/templates/typescript/service/service.ts new file mode 100644 index 000000000..2e4d750b1 --- /dev/null +++ b/templates/typescript/service/service.ts @@ -0,0 +1,16 @@ +/// + +'use strict'; + +module <%= scriptAppName %> { + export class <%= classedName %> { + awesomeThings:any[] = [ + 'HTML5 Boilerplate', + 'AngularJS', + 'Karma' + ]; + } +} + +angular.module('<%= scriptAppName %>') + .service('<%= cameledName %>', <%= scriptAppName %>.<%= classedName %>); diff --git a/templates/typescript/service/value.ts b/templates/typescript/service/value.ts new file mode 100644 index 000000000..8809e9db6 --- /dev/null +++ b/templates/typescript/service/value.ts @@ -0,0 +1,6 @@ +/// + +'use strict'; + +angular.module('<%= scriptAppName %>') + .value('<%= cameledName %>', 42); diff --git a/templates/typescript/spec/controller.ts b/templates/typescript/spec/controller.ts new file mode 100644 index 000000000..d711c49fe --- /dev/null +++ b/templates/typescript/spec/controller.ts @@ -0,0 +1,26 @@ +/// +/// +/// + +'use strict'; + +describe('Controller: <%= classedName %>Ctrl', () => { + + // load the controller's module + beforeEach(module('<%= scriptAppName %>')); + + var <%= classedName %>Ctrl: <%= scriptAppName %>.<%= classedName %>Ctrl, + scope: <%= scriptAppName %>.I<%= classedName %>Scope; + + // Initialize the controller and a mock scope + beforeEach(inject(($controller: ng.IControllerService, $rootScope: ng.IRootScopeService) => { + scope = $rootScope.$new(); + <%= classedName %>Ctrl = $controller('<%= classedName %>Ctrl', { + $scope: scope + }); + })); + + it('should attach a list of awesomeThings to the scope', () => { + expect(scope.awesomeThings.length).toBe(3); + }); +}); diff --git a/templates/typescript/spec/directive.ts b/templates/typescript/spec/directive.ts new file mode 100644 index 000000000..cdab0afd4 --- /dev/null +++ b/templates/typescript/spec/directive.ts @@ -0,0 +1,24 @@ +/// +/// +/// + +'use strict'; + +describe('Directive: <%= cameledName %>', () => { + + // load the directive's module + beforeEach(module('<%= scriptAppName %>')); + + var element: JQuery, + scope: ng.IScope; + + beforeEach(inject(($rootScope: ng.IRootScopeService) => { + scope = $rootScope.$new(); + })); + + it('should make hidden element visible', inject(($compile: ng.ICompileService) => { + element = angular.element('<<%= _.dasherize(name) %>>>'); + element = $compile(element)(scope); + expect(element.text()).toBe('this is the <%= cameledName %> directive'); + })); +}); diff --git a/templates/typescript/spec/filter.ts b/templates/typescript/spec/filter.ts new file mode 100644 index 000000000..aa92bbaed --- /dev/null +++ b/templates/typescript/spec/filter.ts @@ -0,0 +1,23 @@ +/// +/// +/// + +'use strict'; + +describe('Filter: <%= cameledName %>', () => { + + // load the filter's module + beforeEach(module('<%= scriptAppName %>')); + + // initialize a new instance of the filter before each test + var <%= cameledName %>; + beforeEach(inject($filter => { + <%= cameledName %> = $filter('<%= cameledName %>'); + })); + + it('should return the input prefixed with "<%= cameledName %> filter:"', () => { + var text = 'angularjs'; + expect(<%= cameledName %>(text)).toBe('<%= cameledName %> filter: ' + text); + }); + +}); diff --git a/templates/typescript/spec/service.ts b/templates/typescript/spec/service.ts new file mode 100644 index 000000000..3828ef469 --- /dev/null +++ b/templates/typescript/spec/service.ts @@ -0,0 +1,22 @@ +/// +/// +/// + +'use strict'; + +describe('Service: <%= cameledName %>', () => { + + // load the service's module + beforeEach(module('<%= scriptAppName %>')); + + // instantiate service + var <%= cameledName %>; + beforeEach(inject(_<%= cameledName %>_ => { + <%= cameledName %> = _<%= cameledName %>_; + })); + + it('should do something', () => { + expect(!!<%= cameledName %>).toBe(true); + }); + +}); diff --git a/test/test-file-creation.js b/test/test-file-creation.js index 7f97be91b..4dafc4a64 100644 --- a/test/test-file-creation.js +++ b/test/test-file-creation.js @@ -104,6 +104,37 @@ describe('Angular generator', function () { }); }); + it('creates typescript files', function (done) { + var expected = ['app/.htaccess', + 'app/404.html', + 'app/favicon.ico', + 'app/robots.txt', + 'app/styles/main.scss', + 'app/views/main.html', + ['.bowerrc', /"directory": "app\/bower_components"/], + 'Gruntfile.js', + 'package.json', + ['bower.json', /"name":\s+"temp"/], + 'app/scripts/app.ts', + 'app/index.html', + 'app/scripts/controllers/main.ts', + 'test/spec/controllers/main.ts' + ]; + helpers.mockPrompt(angular, { + compass: true, + bootstrap: true, + compassBootstrap: true, + modules: [] + }); + + angular.env.options.typescript = true; + angular.run([], function () { + helpers.assertFiles(expected); + done(); + }); + }); + + /** * Generic test function that can be used to cover the scenarios where a generator is creating both a source file * and a test file. The function will run the respective generator, and then check for the existence of the two @@ -209,9 +240,9 @@ describe('Angular generator', function () { }); angular.run([], function (){ angularView.run([], function () { - helpers.assertFiles([ + helpers.assertFile( ['app/views/foo.html'] - ]); + ); done(); }); }); @@ -230,9 +261,9 @@ describe('Angular generator', function () { }); angular.run([], function (){ angularView.run([], function () { - helpers.assertFiles([ + helpers.assertFile( ['app/views/foo/bar.html'] - ]); + ); done(); }); });