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) %>><%= _.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();
});
});