├── .gitignore ├── .sass-lint.yml ├── LICENSE ├── Makefile ├── README.md ├── karma.conf.js ├── karma.entry.ts ├── package.json ├── src ├── app │ ├── _variables.scss │ ├── app.component.ts │ ├── app.html │ ├── app.module.ts │ ├── app.routing.ts │ ├── app.scss │ └── article │ │ ├── article-list.component.ts │ │ ├── article-list.html │ │ ├── article-list.scss │ │ ├── article.component.ts │ │ ├── article.html │ │ ├── article.model.ts │ │ ├── article.scss │ │ └── article.service.ts ├── index.ejs ├── main.ts ├── polyfills.ts └── vendor.ts ├── tests ├── activated-route-stub.ts └── app │ └── article │ ├── article-list.component.spec.ts │ ├── article.component.spec.ts │ └── article.service.spec.ts ├── tsconfig.json ├── tsconfig.test.json ├── tslint.json ├── typedoc.json ├── webpack.config.common.js ├── webpack.config.dev.js ├── webpack.config.js ├── webpack.config.prod.js └── webpack.config.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | # Build result 42 | dist 43 | 44 | # Documentation 45 | docs 46 | 47 | # TypeScript definitions 48 | typings 49 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | # Linter Options 2 | options: 3 | # Don't merge default rules 4 | merge-default-rules: false 5 | # File Options 6 | files: 7 | include: 'src/**/*.s+(a|c)ss' 8 | # Rule Configuration 9 | rules: 10 | extends-before-mixins: 2 11 | extends-before-declarations: 2 12 | placeholder-in-extend: 2 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Valentin Nazarov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | verify: 2 | @echo "Verify that installation, build, tests and docs generation succeed" 3 | rm -rf node_modules/ 4 | npm install 5 | npm run build 6 | npm run test 7 | npm run docs 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # An Angular 2 + Webpack boilerplate with examples 2 | 3 | Angular 2 application with some examples (currently only HTTP service and component). Please, feel free to create issues and PRs. 4 | 5 | Templates and stylesheets are embedded into JS bundle with help of [angular2-template-loader](https://github.com/TheLarkInn/angular2-template-loader). SASS/SCSS is used for styling. 6 | 7 | `index.html` is generated using [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin). 8 | 9 | Hot module replacement is not provided here. Possibly I'll add it later. Same goes for service workers. 10 | 11 | ### Installation 12 | 13 | Clone repository and run `npm install`. 14 | 15 | Run `npm start` and point your browser to `http://localhost:9045/`. 16 | 17 | ### Testing 18 | 19 | Use `npm test` to run test suite. [Karma](https://github.com/karma-runner/karma) is used as test runner, [Jasmine](https://github.com/jasmine/jasmine) - as testing framework. Code coverage reports are generated using [istanbul](https://github.com/gotwarlost/istanbul) and [remap-istanbul](https://github.com/SitePen/remap-istanbul) (this provides possibility to map back to TypeScript). 20 | 21 | Use `npm lint` to check TypeScript files (via [tslint](https://github.com/palantir/tslint)) and SASS stylesheets (via [sass-lint](https://github.com/sasstools/sass-lint)). [Codelyzer](https://github.com/mgechev/codelyzer) helps to keep code close to [Angular 2 Style Guide](https://angular.io/styleguide). 22 | 23 | You won't find end-to-end tests in this project (usually people use Protractor for this purpose). I believe that E2E/QA testing should be written in a separate project/repository, especially if you are working on projects with microservice architecture. 24 | 25 | ### Building bundle(s) 26 | 27 | Use `npm run build` and you will get JS bundles, their maps and `index.html` in `dist` directory. 28 | 29 | To build for production, use `NODE_ENV=production npm run build` (webpack configuration is chosen according to this environment variable). 30 | 31 | ### Documentation 32 | 33 | Run `npm run docs` to generate documentation for TypeScript ([typedoc](https://github.com/TypeStrong/typedoc) is used for that) and SASS stylesheets (done with [kss-node](https://github.com/kss-node/kss-node)). 34 | 35 | ### Dev notes 36 | 37 | - Upgrade to `istanbul-instrumenter-loader@1.2.0` breaks code coverage 38 | 39 | - Upgrade to `remap-istanbul@0.8.*` leads to error in their binary (`SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`). 40 | 41 | ## TODOs 42 | 43 | - Examples 44 | - Typeahead with throttle/distinct 45 | - Pipe 46 | - AuthGuard in router 47 | - Forms 48 | - Input and Output 49 | - rxjs-based WebSocket 50 | 51 | - Comments on tests 52 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Please see Karma config file reference for better understanding: 3 | * http://karma-runner.github.io/latest/config/configuration-file.html 4 | */ 5 | module.exports = function(config) { 6 | config.set({ 7 | /** 8 | * This path will be used for resolving. 9 | */ 10 | basePath: '', 11 | 12 | /** 13 | * List of test frameworks we will use. Most of them are provided by separate packages (adapters). 14 | * You can find them on npmjs.org: https://npmjs.org/browse/keyword/karma-adapter 15 | */ 16 | frameworks: ['jasmine', 'source-map-support'], 17 | 18 | /** 19 | * Entry point / test environment builder is also written in TypeScript. 20 | */ 21 | files: ['./karma.entry.ts'], 22 | 23 | /** 24 | * Transform files before loading them. 25 | */ 26 | preprocessors: { 27 | './karma.entry.ts': ['webpack'] 28 | }, 29 | 30 | webpack: require('./webpack.config.test'), 31 | 32 | /** 33 | * Make dev server silent. 34 | */ 35 | webpackServer: { noInfo: true }, 36 | 37 | /** 38 | * A lot of plugins are available for test results reporting. 39 | * You can find them here: https://npmjs.org/browse/keyword/karma-reporter 40 | */ 41 | reporters: ['mocha', 'coverage'], 42 | 43 | /** 44 | * This JSON file is "intermediate", in post-test script we use remap-istanbul to map back to TypeScript 45 | * and then generate coverage report. 46 | */ 47 | coverageReporter: { 48 | dir: 'coverage', 49 | reporters: [ 50 | { 51 | type: 'json', 52 | subdir: '.', 53 | file: 'coverage.json' 54 | } 55 | ] 56 | }, 57 | 58 | port: 9876, 59 | colors: true, 60 | logLevel: config.LOG_INFO, 61 | 62 | /** 63 | * Only Phantom is used in this example. 64 | * You can find more browser launchers here: https://npmjs.org/browse/keyword/karma-launcher 65 | */ 66 | browsers: ['PhantomJS'], 67 | 68 | /** 69 | * This is CI mode: run once and exit. 70 | * TODO: Non-CI mode 71 | */ 72 | autoWatch: true, 73 | singleRun: true 74 | }) 75 | }; 76 | -------------------------------------------------------------------------------- /karma.entry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TODO: Comments 3 | * 4 | * Order matters here for zone.js modules 5 | */ 6 | import 'core-js/es6'; 7 | import 'core-js/es7/reflect'; 8 | import 'reflect-metadata'; 9 | import 'zone.js/dist/zone'; 10 | import 'zone.js/dist/long-stack-trace-zone'; 11 | import 'zone.js/dist/proxy'; 12 | import 'zone.js/dist/sync-test'; 13 | import 'zone.js/dist/jasmine-patch'; 14 | import 'zone.js/dist/async-test'; 15 | import 'zone.js/dist/fake-async-test'; 16 | 17 | import 'ts-helpers'; 18 | 19 | import { TestBed } from '@angular/core/testing'; 20 | import * as browser from '@angular/platform-browser-dynamic/testing'; 21 | 22 | TestBed.initTestEnvironment( 23 | browser.BrowserDynamicTestingModule, 24 | browser.platformBrowserDynamicTesting() 25 | ); 26 | 27 | let testContext = (<{ context?: Function }>require).context('./tests', true, /\.spec\.ts/); 28 | testContext.keys().forEach(testContext); 29 | 30 | let coverageContext = (<{ context?: Function }>require).context('./src/app', true, /\.ts/); 31 | coverageContext.keys().forEach(coverageContext); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-webpack-boilerplate", 3 | "version": "1.0.0", 4 | "description": "A boilerplate for Angular 2 and Webpack", 5 | "keywords": [ 6 | "angular2", 7 | "webpack", 8 | "typescript", 9 | "rxjs", 10 | "boilerplate", 11 | "starter" 12 | ], 13 | "author": "Valentin Nazarov ", 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/kozlice/angular2-webpack-boilerplate.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/kozlice/angular2-webpack-boilerplate/issues" 21 | }, 22 | "homepage": "https://github.com/kozlice/angular2-webpack-boilerplate", 23 | "directories": { 24 | "test": "tests" 25 | }, 26 | "scripts": { 27 | "lint": "npm run lint:ts && npm run lint:sass", 28 | "lint:ts": "tslint **/*.ts --exclude node_modules", 29 | "lint:sass": "sass-lint", 30 | "pretest": "rimraf ./coverage && npm run lint", 31 | "test": "karma start", 32 | "posttest": "npm run coverage", 33 | "coverage": "npm run coverage:remap && npm run coverage:report", 34 | "coverage:remap": "remap-istanbul -i coverage/coverage.json -o coverage/coverage.json -t json -e node_modules,tests,karma.entry.ts", 35 | "coverage:report": "istanbul report", 36 | "prebuild": "rimraf ./dist", 37 | "build": "webpack", 38 | "postinstall": "", 39 | "predocs": "rimraf ./docs", 40 | "docs": "npm run docs:typedoc && npm run docs:kss", 41 | "docs:typedoc": "typedoc --options ./typedoc.json ./src/app", 42 | "docs:kss": "kss --source ./src --destination ./docs/styleguide", 43 | "start": "webpack-dev-server --inline --progress --profile --colors --watch --display-error-details --display-cached --content-base ./dist" 44 | }, 45 | "devDependencies": { 46 | "@angular/common": "2.4.4", 47 | "@angular/compiler": "2.4.4", 48 | "@angular/core": "2.4.4", 49 | "@angular/forms": "2.4.4", 50 | "@angular/http": "2.4.4", 51 | "@angular/platform-browser": "2.4.4", 52 | "@angular/platform-browser-dynamic": "2.4.4", 53 | "@angular/router": "3.4.4", 54 | "@types/core-js": "0.9.35", 55 | "@types/jasmine": "2.5.41", 56 | "@types/node": "7.0.4", 57 | "angular-in-memory-web-api": "0.2.4", 58 | "angular2-template-loader": "0.6.0", 59 | "codelyzer": "2.0.0-beta.4", 60 | "core-js": "2.4.1", 61 | "es6-shim": "0.35.3", 62 | "flexboxgrid-sass": "8.0.5", 63 | "html-loader": "0.4.4", 64 | "html-webpack-plugin": "2.26.0", 65 | "istanbul": "0.4.5", 66 | "istanbul-instrumenter-loader": "0.2.0", 67 | "jasmine-core": "2.5.2", 68 | "json-loader": "0.5.4", 69 | "karma": "1.4.0", 70 | "karma-coverage": "1.1.1", 71 | "karma-jasmine": "1.1.0", 72 | "karma-mocha-reporter": "2.2.2", 73 | "karma-phantomjs-launcher": "1.0.2", 74 | "karma-source-map-support": "1.2.0", 75 | "karma-sourcemap-loader": "0.3.7", 76 | "karma-webpack": "1.8.1", 77 | "kss": "3.0.0-beta.17", 78 | "node-sass": "4.2.0", 79 | "normalize-scss": "6.0.0", 80 | "phantomjs-prebuilt": "2.1.14", 81 | "raw-loader": "0.5.1", 82 | "reflect-metadata": "0.1.9", 83 | "remap-istanbul": "0.7.0", 84 | "rimraf": "2.5.4", 85 | "rxjs": "5.0.3", 86 | "sass-lint": "1.10.2", 87 | "sass-loader": "4.1.1", 88 | "skeleton-scss": "2.0.4", 89 | "source-map-loader": "0.1.6", 90 | "ts-helpers": "1.1.2", 91 | "ts-loader": "1.3.3", 92 | "tslint": "4.3.1", 93 | "typedoc": "0.5.5", 94 | "typescript": "2.1.5", 95 | "webpack": "1.14.0", 96 | "webpack-dev-server": "1.16.2", 97 | "webpack-md5-hash": "0.0.5", 98 | "zone.js": "0.7.6" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/app/_variables.scss: -------------------------------------------------------------------------------- 1 | $heading-color: #333; 2 | $heading-highlight-color: #fa2800; 3 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | 3 | /** 4 | * Styles required here are common for all components (SASS/SCSS versions of normalize.css and flexboxgrid), 5 | * so encapsulation is not used. Other components have their styles scoped with `ViewEncapsulation.Emulated`. 6 | */ 7 | @Component({ 8 | selector: 'da-app', 9 | templateUrl: './app.html', 10 | styleUrls: ['./app.scss'], 11 | encapsulation: ViewEncapsulation.None, 12 | providers: [] 13 | }) 14 | export class AppComponent {} 15 | -------------------------------------------------------------------------------- /src/app/app.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Example Angular 2 application

5 |
6 |
7 | 8 | 13 | 14 |
15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { HttpModule, JsonpModule } from '@angular/http'; 5 | import { FormsModule } from '@angular/forms'; 6 | 7 | import { routing, appRoutingProviders } from './app.routing'; 8 | import { AppComponent } from './app.component'; 9 | import { ArticleComponent } from './article/article.component'; 10 | import { ArticleListComponent } from './article/article-list.component'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | BrowserModule, 15 | FormsModule, 16 | HttpModule, 17 | JsonpModule, 18 | routing 19 | ], 20 | declarations: [ 21 | AppComponent, 22 | ArticleListComponent, 23 | ArticleComponent 24 | ], 25 | providers: [appRoutingProviders], 26 | bootstrap: [AppComponent] 27 | }) 28 | export class AppModule {} 29 | -------------------------------------------------------------------------------- /src/app/app.routing.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { ArticleComponent } from './article/article.component'; 5 | import { ArticleListComponent } from './article/article-list.component'; 6 | 7 | const appRoutes: Routes = [ 8 | { path: 'articles/:id', component: ArticleComponent }, 9 | { path: '', component: ArticleListComponent }, 10 | ]; 11 | 12 | export const appRoutingProviders: any[] = []; 13 | 14 | export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes); 15 | -------------------------------------------------------------------------------- /src/app/app.scss: -------------------------------------------------------------------------------- 1 | @import "~normalize-scss/sass/normalize"; 2 | @import "~flexboxgrid-sass"; 3 | -------------------------------------------------------------------------------- /src/app/article/article-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation, OnInit, OnDestroy } from '@angular/core'; 2 | 3 | import { ArticleService } from './article.service'; 4 | import { Article } from './article.model'; 5 | import { Response } from '@angular/http'; 6 | 7 | /** 8 | * A simple component, which fetches articles list from HTTP API and displays them. 9 | */ 10 | @Component({ 11 | selector: 'da-article-list', 12 | templateUrl: './article-list.html', 13 | styleUrls: ['./article-list.scss'], 14 | encapsulation: ViewEncapsulation.Emulated, 15 | providers: [ArticleService] 16 | }) 17 | export class ArticleListComponent implements OnInit, OnDestroy { 18 | private articles: Article[]; 19 | private error: Response; 20 | private isLoading: boolean = true; 21 | 22 | constructor(private articleService: ArticleService) {} 23 | 24 | ngOnInit(): void { 25 | this.articleService.getAll().subscribe( 26 | (data) => this.articles = data, 27 | (error) => this.error = error, 28 | () => this.isLoading = false 29 | ); 30 | } 31 | 32 | ngOnDestroy(): void {} 33 | } 34 | -------------------------------------------------------------------------------- /src/app/article/article-list.html: -------------------------------------------------------------------------------- 1 |
2 | Loading… 3 |
4 | 5 |
6 |

{{ article.title }}

7 |
-------------------------------------------------------------------------------- /src/app/article/article-list.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kozlice/angular2-webpack-boilerplate/49490ba63b0e970e6eb74876175032316184cecf/src/app/article/article-list.scss -------------------------------------------------------------------------------- /src/app/article/article.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation, OnInit, OnDestroy } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | import { ArticleService } from './article.service'; 5 | import { Article } from './article.model'; 6 | import { Response } from '@angular/http'; 7 | 8 | /** 9 | * A simple component, which fetches article from HTTP API and displays it. 10 | */ 11 | @Component({ 12 | selector: 'da-article', 13 | templateUrl: './article.html', 14 | styleUrls: ['./article.scss'], 15 | encapsulation: ViewEncapsulation.Emulated, 16 | providers: [ArticleService] 17 | }) 18 | export class ArticleComponent implements OnInit, OnDestroy { 19 | private article: Article; 20 | private error: Response; 21 | private isLoading: boolean = true; 22 | 23 | constructor( 24 | private route: ActivatedRoute, 25 | private articleService: ArticleService 26 | ) { 27 | 28 | } 29 | 30 | /** 31 | * TODO: Note about non-observable param 32 | */ 33 | ngOnInit(): void { 34 | let id = +this.route.snapshot.params['id']; 35 | this.articleService.get(id).subscribe( 36 | (data) => this.article = data, 37 | (error) => this.error = error, 38 | () => this.isLoading = false 39 | ); 40 | } 41 | 42 | ngOnDestroy(): void {} 43 | } 44 | -------------------------------------------------------------------------------- /src/app/article/article.html: -------------------------------------------------------------------------------- 1 |
2 | Loading… 3 |
4 | 5 |
6 | Something went wrong! 7 |
8 | 9 |
10 |
11 |

{{ article?.title }}

12 |
13 |
14 | {{ article?.body }} 15 |
16 |
17 | 18 |
19 |
-------------------------------------------------------------------------------- /src/app/article/article.model.ts: -------------------------------------------------------------------------------- 1 | export class Article { 2 | id: number; 3 | title: string; 4 | body: string; 5 | userId: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/article/article.scss: -------------------------------------------------------------------------------- 1 | @import "../variables"; 2 | 3 | // An article detail view 4 | // 5 | // header - Primary info 6 | // header h2 - Title 7 | // header h2:hover - Title is highlighted when hovered 8 | // 9 | // Styleguide base 10 | .article { 11 | header { 12 | h2 { 13 | color: $heading-color; 14 | 15 | &:hover { 16 | color: $heading-highlight-color; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/article/article.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, Response } from '@angular/http'; 3 | import { Observable } from 'rxjs'; 4 | import 'rxjs/add/operator/map'; 5 | 6 | import { Article } from './article.model'; 7 | 8 | const API_ENDPOINT = 'https://jsonplaceholder.typicode.com/posts'; 9 | 10 | /** 11 | * A simple service which fetches data from HTTP API. 12 | * 13 | * TODO: Move API endpoint to app config 14 | */ 15 | @Injectable() 16 | export class ArticleService { 17 | constructor(private http: Http) { 18 | 19 | } 20 | 21 | public get(id: number): Observable
{ 22 | return this.http 23 | .get(API_ENDPOINT + '/' + id) 24 | .map(this.extractData); 25 | } 26 | 27 | public getAll(): Observable { 28 | return this.http 29 | .get(API_ENDPOINT) 30 | .map(this.extractData); 31 | } 32 | 33 | private extractData(res: Response) { 34 | let body = res.json(); 35 | return body || { }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= webpackConfig.metadata.title %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Loading… 14 | <% for (var chunk in htmlWebpackPlugin.files.chunks) { %><% } %> 15 | 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | 6 | if (['prod', 'production'].indexOf(process.env.ENV) != -1) { 7 | enableProdMode(); 8 | } 9 | 10 | const platform = platformBrowserDynamic(); 11 | platform.bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use CoreJS polyfills. 3 | */ 4 | import 'core-js/es6'; 5 | import 'core-js/es7/reflect'; 6 | 7 | /** 8 | * Quote from docs: "A Zone is an execution context that persists across async tasks. You can think of it as 9 | * thread-local storage for JavaScript VMs." Angular 2 uses this library. 10 | * See details here: https://github.com/angular/zone.js/ 11 | */ 12 | import 'zone.js/dist/zone'; 13 | 14 | /** 15 | * This package helps to avoid generation of helper code in each file (e.g. for decorators). 16 | * See details here: https://github.com/ngParty/ts-helpers 17 | */ 18 | import 'ts-helpers'; 19 | 20 | if (['prod', 'production'].indexOf(process.env.ENV) == -1) { 21 | /** 22 | * You can use `import` only in a namespace or module in TypeScript, so use `require` here. 23 | */ 24 | Error['stackTraceLimit'] = Infinity; 25 | require('zone.js/dist/long-stack-trace-zone'); 26 | } 27 | -------------------------------------------------------------------------------- /src/vendor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Angular 2 and RxJS. 3 | */ 4 | import '@angular/platform-browser'; 5 | import '@angular/platform-browser-dynamic'; 6 | import '@angular/core'; 7 | import '@angular/common'; 8 | import '@angular/router'; 9 | import '@angular/http'; 10 | import '@angular/forms'; 11 | 12 | import 'rxjs'; 13 | -------------------------------------------------------------------------------- /tests/activated-route-stub.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | /** 5 | * This stub was borrowed from official tutorial. 6 | */ 7 | @Injectable() 8 | export class ActivatedRouteStub { 9 | private subject = new BehaviorSubject(this.testParams); 10 | params = this.subject.asObservable(); 11 | 12 | private _testParams: {}; 13 | get testParams() { return this._testParams; } 14 | set testParams(params: {}) { 15 | this._testParams = params; 16 | this.subject.next(params); 17 | } 18 | 19 | get snapshot() { 20 | return { params: this.testParams }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/app/article/article-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, fakeAsync, tick } from '@angular/core/testing'; 2 | import { Route } from '@angular/router'; 3 | import { HttpModule } from '@angular/http'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { Observable } from 'rxjs'; 6 | 7 | import { Article } from '../../../src/app/article/article.model'; 8 | import { ArticleService } from '../../../src/app/article/article.service'; 9 | import { ArticleComponent } from '../../../src/app/article/article.component'; 10 | import { ArticleListComponent } from '../../../src/app/article/article-list.component'; 11 | 12 | const exampleArticleList: Article[] = [ 13 | { 14 | id: 1, 15 | title: 'Hello, world', 16 | body: 'Lorem ipsum dolor sit amet.', 17 | userId: 1 18 | }, 19 | { 20 | id: 2, 21 | title: 'Good bye, world', 22 | body: 'Lorem ipsum dolor sit amet.', 23 | userId: 1 24 | } 25 | ]; 26 | 27 | /** 28 | * An example of component testing. 29 | */ 30 | describe('ArticleList Component', () => { 31 | let routerConfig: Route[] = [ 32 | { path: 'articles/:id', component: ArticleComponent } 33 | ]; 34 | 35 | beforeEach(async(() => { 36 | TestBed.configureTestingModule({ 37 | imports: [ 38 | HttpModule, 39 | RouterTestingModule.withRoutes(routerConfig) 40 | ], 41 | providers: [ArticleService], 42 | declarations: [ArticleListComponent, ArticleComponent] 43 | }).compileComponents(); 44 | })); 45 | 46 | beforeEach(() => { 47 | this.fixture = TestBed.createComponent(ArticleListComponent); 48 | this.comp = this.fixture.componentInstance; 49 | this.articleService = this.fixture.debugElement.injector.get(ArticleService); 50 | this.getAllArticlesSpy = spyOn(this.articleService, 'getAll').and.returnValue(Observable.of(exampleArticleList)); 51 | this.el = this.fixture.debugElement.nativeElement; 52 | }); 53 | 54 | it('should get articles from service', fakeAsync(() => { 55 | expect(this.getAllArticlesSpy).toHaveBeenCalledTimes(0); 56 | this.fixture.detectChanges(); 57 | expect(this.getAllArticlesSpy).toHaveBeenCalledTimes(1); 58 | tick(); 59 | this.fixture.detectChanges(); 60 | expect(this.comp.articles).toEqual(exampleArticleList); 61 | })); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/app/article/article.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, fakeAsync, tick } from '@angular/core/testing'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { HttpModule } from '@angular/http'; 4 | import { Observable } from 'rxjs'; 5 | 6 | import { ArticleService } from '../../../src/app/article/article.service'; 7 | import { Article } from '../../../src/app/article/article.model'; 8 | import { ActivatedRouteStub } from '../../activated-route-stub'; 9 | import { ArticleComponent } from '../../../src/app/article/article.component'; 10 | 11 | const exampleArticle: Article = { 12 | id: 2, 13 | title: 'Good bye, world', 14 | body: 'Lorem ipsum dolor sit amet.', 15 | userId: 1 16 | }; 17 | 18 | /** 19 | * An example of component testing. 20 | */ 21 | describe('Article Component', () => { 22 | beforeEach(async(() => { 23 | TestBed.configureTestingModule({ 24 | imports: [ 25 | HttpModule 26 | ], 27 | providers: [ 28 | ArticleService, 29 | { provide: ActivatedRoute, useClass: ActivatedRouteStub } 30 | ], 31 | declarations: [ArticleComponent] 32 | }).compileComponents(); 33 | })); 34 | 35 | beforeEach(() => { 36 | this.fixture = TestBed.createComponent(ArticleComponent); 37 | this.comp = this.fixture.componentInstance; 38 | this.articleService = this.fixture.debugElement.injector.get(ArticleService); 39 | this.getArticleSpy = spyOn(this.articleService, 'get').and.returnValue(Observable.of(exampleArticle)); 40 | this.el = this.fixture.debugElement.nativeElement; 41 | this.fixture.debugElement.injector.get(ActivatedRoute).testParams = { id: exampleArticle.id } 42 | }); 43 | 44 | it('should get article from service', fakeAsync(() => { 45 | expect(this.getArticleSpy).toHaveBeenCalledTimes(0); 46 | this.fixture.detectChanges(); 47 | expect(this.getArticleSpy).toHaveBeenCalledWith(exampleArticle.id); 48 | tick(); 49 | this.fixture.detectChanges(); 50 | expect(this.comp.article).toEqual(exampleArticle); 51 | })); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/app/article/article.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { BaseRequestOptions, Response, ResponseOptions, Http } from '@angular/http'; 3 | import { MockBackend, MockConnection } from '@angular/http/testing'; 4 | 5 | import { Article } from '../../../src/app/article/article.model'; 6 | import { ArticleService } from '../../../src/app/article/article.service'; 7 | 8 | const exampleArticleList: Article[] = [ 9 | { 10 | id: 1, 11 | title: 'Hello, world', 12 | body: 'Lorem ipsum dolor sit amet.', 13 | userId: 1 14 | } 15 | ]; 16 | 17 | /** 18 | * An example of service testing with HTTP response mocking. 19 | */ 20 | describe('Article Service', () => { 21 | beforeEach(() => { 22 | TestBed.configureTestingModule({ 23 | providers: [ 24 | ArticleService, 25 | BaseRequestOptions, 26 | MockBackend, 27 | { 28 | provide: Http, 29 | useFactory: (backend: MockBackend, defaultOptions: BaseRequestOptions) => { 30 | return new Http(backend, defaultOptions) 31 | }, 32 | deps: [MockBackend, BaseRequestOptions] 33 | } 34 | ] 35 | }); 36 | }); 37 | 38 | it('should fetch single article', inject([ArticleService, MockBackend], (articleService: ArticleService, backend: MockBackend) => { 39 | const baseResponse = new Response(new ResponseOptions({ 40 | body: JSON.stringify(exampleArticleList[0]) 41 | })); 42 | 43 | backend.connections.subscribe((c: MockConnection) => { 44 | c.mockRespond(baseResponse); 45 | }); 46 | 47 | articleService.get(1).subscribe((data) => { 48 | expect(data).toEqual(exampleArticleList[0]); 49 | }); 50 | })); 51 | 52 | it('should fetch multiple articles', inject([ArticleService, MockBackend], (articleService: ArticleService, backend: MockBackend) => { 53 | const baseResponse = new Response(new ResponseOptions({ 54 | body: JSON.stringify(exampleArticleList) 55 | })); 56 | 57 | backend.connections.subscribe((c: MockConnection) => c.mockRespond(baseResponse)); 58 | 59 | articleService.getAll().subscribe((data) => { 60 | expect(data).toEqual(exampleArticleList); 61 | }); 62 | })); 63 | }); 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "noImplicitAny": false, 9 | "removeComments": false, 10 | "noEmitHelpers": true, 11 | "sourceMap": true, 12 | "types": ["node"] 13 | }, 14 | "include": [ 15 | "src/*.ts", 16 | "node_modules/@types/**/index.d.ts" 17 | ], 18 | "compileOnSave": false, 19 | "buildOnSave": false 20 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "inlineSourceMap": true 6 | } 7 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "directive-selector": [true, "attribute", "da", "camelCase"], 7 | "component-selector": [true, "element", "da", "kebab-case"], 8 | "use-input-property-decorator": true, 9 | "use-output-property-decorator": true, 10 | "use-host-property-decorator": true, 11 | "no-attribute-parameter-decorator": true, 12 | "no-input-rename": true, 13 | "no-output-rename": true, 14 | "no-forward-ref": true, 15 | "use-life-cycle-interface": true, 16 | "use-pipe-transform-interface": true, 17 | "pipe-naming": [true, "camelCase", "da"], 18 | "component-class-suffix": true, 19 | "directive-class-suffix": true, 20 | "import-destructuring-spacing": true, 21 | "templates-use-public": true, 22 | "no-access-missing-member": true, 23 | "invoke-injectable": true 24 | } 25 | } -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "modules", 3 | "out": "docs/app", 4 | "theme": "default", 5 | "ignoreCompilerErrors": "true", 6 | "experimentalDecorators": "true", 7 | "emitDecoratorMetadata": "true", 8 | "target": "ES5", 9 | "moduleResolution": "node", 10 | "preserveConstEnums": "true", 11 | "stripInternal": "true", 12 | "suppressExcessPropertyErrors": "true", 13 | "suppressImplicitAnyIndexErrors": "true", 14 | "module": "commonjs" 15 | } -------------------------------------------------------------------------------- /webpack.config.common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Please see webpack config reference for better understanding: 5 | * https://webpack.github.io/docs/configuration.html 6 | */ 7 | const webpack = require('webpack'); 8 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | 10 | module.exports = { 11 | /** 12 | * These parameters will be used for rendering `index.html`. 13 | */ 14 | metadata: { 15 | title: 'Demo Application', 16 | baseUrl: '/' 17 | }, 18 | 19 | entry: { 20 | 'polyfills': './src/polyfills.ts', 21 | 'vendor': './src/vendor.ts', 22 | 'app': './src/main.ts' 23 | }, 24 | 25 | resolve: { 26 | extensions: ['', '.ts', '.js', '.scss', '.html'], 27 | modulesDirectories: ['node_modules'] 28 | }, 29 | 30 | module: { 31 | loaders: [ 32 | /** 33 | * Loaders for TypeScript. 34 | * No need to exclude tests by `(spec|e2e)` mask here, as they are in separate directory. 35 | * 36 | * See project repository for details / configuration reference: 37 | * https://github.com/s-panferov/awesome-typescript-loader 38 | * https://github.com/TheLarkInn/angular2-template-loader 39 | */ 40 | { 41 | test: /\.ts$/, 42 | loaders: ['ts-loader', 'angular2-template-loader'] 43 | }, 44 | 45 | /** 46 | * Loaders for HTML templates, JSON files, SASS/SCSS stylesheets. See details at projects' repositories: 47 | * 48 | * https://github.com/webpack/json-loader 49 | * https://github.com/webpack/html-loader 50 | * https://github.com/gajus/to-string-loader 51 | */ 52 | {test: /\.json$/, loader: 'json-loader'}, 53 | {test: /\.html$/, loader: 'raw-loader'}, 54 | {test: /\.scss$/, loaders: ['raw-loader', 'sass-loader']} 55 | ] 56 | }, 57 | 58 | plugins: [ 59 | /** 60 | * Quote from webpack docs: "Assign the module and chunk ids by occurrence count. Ids that are used often get 61 | * lower (shorter) ids. This make ids predictable, reduces total file size and is recommended." 62 | * 63 | * See https://webpack.github.io/docs/list-of-plugins.html#occurrenceorderplugin 64 | */ 65 | new webpack.optimize.OccurenceOrderPlugin(true), 66 | 67 | /** 68 | * This plugin simplifies generation of `index.html` file. Especially useful for production environment, 69 | * where your files have hashes in their names. 70 | * 71 | * We have auto-injection disabled here, otherwise scripts will be automatically inserted at the end 72 | * of `body` element. 73 | * 74 | * See https://www.npmjs.com/package/html-webpack-plugin for details. 75 | * 76 | * TODO: Google Analytics and other stuff like that 77 | */ 78 | new HtmlWebpackPlugin({ 79 | title: 'Demo Application', 80 | template: 'src/index.ejs', 81 | chunksSortMode: 'dependency', 82 | inject: false 83 | }), 84 | 85 | /** 86 | * This plugin helps to share common code between pages. 87 | * 88 | * For more info see: 89 | * https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin 90 | * https://github.com/webpack/docs/wiki/optimization#multi-page-app 91 | */ 92 | new webpack.optimize.CommonsChunkPlugin({ 93 | name: ['vendor', 'polyfills'] 94 | }) 95 | ] 96 | }; 97 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * TODO: Comments 5 | */ 6 | const webpack = require('webpack'); 7 | const ENV = process.env.ENV = process.env.NODE_ENV = 'dev'; 8 | let config = require('./webpack.config.common'); 9 | 10 | config.devtool = 'inline-source-map'; 11 | 12 | config.output = { 13 | path: './dist', 14 | publicPath: 'http://localhost:9045/', 15 | filename: '[name].js', 16 | chunkFilename: '[id].chunk.js', 17 | sourceMapFilename: '[name].map' 18 | }; 19 | 20 | config.debug = true; 21 | 22 | config.devServer = { 23 | historyApiFallback: true, 24 | stats: 'minimal', 25 | outputPath: 'dist', 26 | host: 'localhost', 27 | port: 9045, 28 | watchOptions: { 29 | aggregateTimeout: 300, 30 | poll: 1000 31 | } 32 | }; 33 | 34 | /** 35 | * Quote from webpack docs: "Define free variables. Useful for having development builds with debug logging 36 | * or adding global constants." 37 | * 38 | * Note, that values are _evaluated_, not just assigned (this is why we use `JSON.stringify` here). 39 | */ 40 | config.plugins.push(new webpack.DefinePlugin({ 41 | 'ENV': JSON.stringify(ENV), 42 | 'process.env': { 43 | 'ENV': JSON.stringify(ENV) 44 | } 45 | })); 46 | 47 | module.exports = config; 48 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * TODO: Comments? 5 | * TODO: HMR 6 | */ 7 | 8 | switch (process.env.NODE_ENV) { 9 | case 'prod': 10 | case 'production': 11 | module.exports = require('./webpack.config.prod'); 12 | break; 13 | case 'test': 14 | case 'testing': 15 | module.exports = require('./webpack.config.test'); 16 | break; 17 | case 'dev': 18 | case 'development': 19 | default: 20 | module.exports = require('./webpack.config.dev'); 21 | break; 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * TODO: Comments 5 | */ 6 | const webpack = require('webpack'); 7 | const ENV = process.env.ENV = process.env.NODE_ENV = 'prod'; 8 | let config = require('./webpack.config.common'); 9 | 10 | config.devtool = 'source-map'; 11 | 12 | config.output = { 13 | path: './dist', 14 | publicPath: '/', 15 | filename: '[name].[hash].js', 16 | chunkFilename: '[id].[hash].chunk.js', 17 | sourceMapFilename: '[name].[hash].map' 18 | }; 19 | 20 | config.plugins.push( 21 | new webpack.NoErrorsPlugin(), 22 | new webpack.optimize.DedupePlugin(), 23 | new webpack.optimize.UglifyJsPlugin({ 24 | comments: false 25 | }) 26 | ); 27 | 28 | /** 29 | * See node-sass documentation for full list of options: 30 | * https://github.com/sass/node-sass#options 31 | */ 32 | config.sassLoader = { 33 | outputStyle: 'compressed' 34 | }; 35 | 36 | /** 37 | * Same purpose as in dev config. 38 | */ 39 | config.plugins.push(new webpack.DefinePlugin({ 40 | 'ENV': JSON.stringify(ENV), 41 | 'process.env': { 42 | 'ENV': JSON.stringify(ENV) 43 | } 44 | })); 45 | 46 | module.exports = config; 47 | -------------------------------------------------------------------------------- /webpack.config.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Config for test environment is pretty different from dev and prod, so we don't use common config as base. 3 | * 4 | * Karma watches the test entry points, so we don't need to define entry point here. 5 | */ 6 | module.exports = { 7 | /** 8 | * Inline source maps, generated by TypeScript compiler, will be used for code coverage. 9 | */ 10 | devtool: 'inline-source-map', 11 | 12 | verbose: true, 13 | 14 | resolve: { 15 | extensions: ['', '.ts', '.js', '.scss', '.html'], 16 | modulesDirectories: ['node_modules'] 17 | }, 18 | 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.ts$/, 23 | loaders: ['ts-loader', 'angular2-template-loader'] 24 | }, 25 | /** 26 | * These loaders are used in other environments as well, see `webpack.config.common.js`. 27 | */ 28 | {test: /\.json$/, loader: 'json-loader'}, 29 | {test: /\.html$/, loader: 'raw-loader'}, 30 | {test: /\.scss$/, loaders: ['raw-loader', 'sass-loader']} 31 | ], 32 | postLoaders: [ 33 | /** 34 | * Instruments source files for subsequent code coverage. 35 | * See https://github.com/deepsweet/istanbul-instrumenter-loader 36 | */ 37 | { 38 | test: /\.ts$/, 39 | loader: 'istanbul-instrumenter-loader?embedSource=true&noAutoWrap=true', 40 | exclude: [ 41 | 'node_modules', 42 | /\.(e2e|spec)\.ts$/ 43 | ] 44 | } 45 | ] 46 | }, 47 | 48 | /** 49 | * Enable inline source maps for code coverage report. 50 | */ 51 | ts: { 52 | configFileName: 'tsconfig.test.json' 53 | } 54 | }; 55 | --------------------------------------------------------------------------------