Compare commits

..

16 Commits

Author SHA1 Message Date
712d1a7c37 chore(release): v2.0.1 2016-09-23 11:28:36 -07:00
16601f9359 docs(changelog): add changelog for 2.0.1 2016-09-23 10:50:42 -07:00
b81e2e7a31 fix(upgrade): allow attribute selectors for components in ng2 which are not part of upgrade (#11808)
fixes #11280
2016-09-23 10:48:47 -07:00
98fac36706 docs(Component): API docs for .encapsulation and .interpolation 2016-09-23 10:22:24 -07:00
3e780c032e refactor: misc cleanup 2016-09-23 10:22:05 -07:00
e09882180e refactor(common): cleanup (#11668) 2016-09-23 10:21:58 -07:00
0e18c57a17 docs(core): mark TestBed as stable api and add preliminary docs (#11767)
TestBed was accidentaly ommited from the 'stable' api list during the API sweep before final. We do consider it to be stable.
2016-09-23 10:21:50 -07:00
51e2b9c073 docs(contributing): remove preview references (#11795) 2016-09-23 10:21:44 -07:00
f218e240d3 ci(BrowserStack): add Safari 10 (#11796) 2016-09-23 10:21:31 -07:00
af6b219f8e refactor(TemplateParser): clearer error message for on* binding (#11802)
fixes #11756
2016-09-23 10:21:15 -07:00
20addf5f9f chore(ISSUE_TEMPLATE): update Angular version field (#11821) 2016-09-23 10:21:02 -07:00
2860418a3c fix(forms): disable all radios with disable() 2016-09-23 10:19:58 -07:00
39e251eea7 fix(forms): support unbound disabled in ngModel (#11736) 2016-09-23 10:19:46 -07:00
d7d716d5db chore(zone.js): update to 0.6.25 (#11725) 2016-09-23 10:19:30 -07:00
a95d65241c fix(compiler): Safe property access expressions work in event bindings (#11724) 2016-09-23 10:19:24 -07:00
fdb22bd185 refactor: misc cleanup (#11654) 2016-09-23 10:18:38 -07:00
4456 changed files with 92316 additions and 281842 deletions

View File

@ -1,7 +1,3 @@
<!--
IF YOU DON'T FILL OUT THE FOLLOWING INFORMATION WE MIGHT CLOSE YOUR ISSUE WITHOUT INVESTIGATING
-->
**I'm submitting a ...** (check one with "x")
```
[ ] bug report => search github for a similar issue or PR before submitting
@ -9,18 +5,14 @@ IF YOU DON'T FILL OUT THE FOLLOWING INFORMATION WE MIGHT CLOSE YOUR ISSUE WITHOU
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
```
**Current behavior**
**Current behavior**
<!-- Describe how the bug manifests. -->
**Expected behavior**
<!-- Describe what the behavior would be without the bug. -->
**Minimal reproduction of the problem with instructions**
<!--
If the current behavior is a bug or you can illustrate your feature request better with an example,
please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via
https://plnkr.co or similar (you can use this template as a starting point: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5).
-->
**Reproduction of the problem**
<!-- If the current behavior is a bug or you can illustrate your feature request better with an example, please provide the steps to reproduce and if possible a minimal demo of the problem via https://plnkr.co or similar (you can use this template as a starting point: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5). -->
**What is the motivation / use case for changing the behavior?**
<!-- Describe the motivation or the concrete use case -->
@ -28,12 +20,12 @@ https://plnkr.co or similar (you can use this template as a starting point: http
**Please tell us about your environment:**
<!-- Operating system, IDE, package manager, HTTP server, ... -->
* **Angular version:** 2.0.X
* **Angular version:** 2.0.X
<!-- Check whether this is still an issue in the most recent Angular version -->
* **Browser:** [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ]
* **Browser:** [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ]
<!-- All browsers where this could be reproduced -->
* **Language:** [all | TypeScript X.X | ES6/7 | ES5]
* **Node (for AoT issues):** `node --version` =

7
.gitignore vendored
View File

@ -17,16 +17,11 @@ modules/.vscode
# Don't check in secret files
*secret.js
# Ignore npm/yarn debug log
# Ignore npm debug log
npm-debug.log
yarn-error.log
# build-analytics
.build-analytics
# rollup-test output
/modules/rollup-test/dist/
# angular.io
/aio/src/content/docs
/aio/dist

2
.nvmrc
View File

@ -1 +1 @@
6.9.5
6.6.0

View File

@ -1,252 +0,0 @@
# Configuration for pullapprove.com
#
# Approval access and primary role is determined by info in the project ownership spreadsheet:
# https://docs.google.com/spreadsheets/d/1-HIlzfbPYGsPr9KuYMe6bLfc4LXzPjpoALqtYRYTZB0/edit?pli=1#gid=0&vpid=A5
#
# === GitHub username to Full name map ===
#
# alexeagle - Alex Eagle
# alxhub - Alex Rickabaugh
# chuckjaz - Chuck Jazdzewski
# gkalpak - George Kalpakas
# IgorMinar - Igor Minar
# jasonaden - Jason Aden
# kara - Kara Erickson
# matsko - Matias Niemelä
# mhevery - Misko Hevery
# petebacondarwin - Pete Bacon Darwin
# pkozlowski-opensource - Pawel Kozlowski
# robwormald - Rob Wormald
# tbosch - Tobias Bosch
# vicb - Victor Berchet
# vikerman - Vikram Subramanian
version: 2
group_defaults:
required: 1
reset_on_reopened:
enabled: true
approve_by_comment:
enabled: false
groups:
root:
conditions:
files:
include:
- "*"
exclude:
- "aio/*"
- "integration/*"
- "modules/*"
- "tools/*"
users:
- IgorMinar
- mhevery
public-api:
conditions:
files:
include:
- "tools/public_api_guard/*"
users:
- IgorMinar
- mhevery
build-and-ci:
conditions:
files:
include:
- "*.yml"
- "*.json"
- "*.lock"
- "tools/*"
exclude:
- "tools/@angular/tsc-wrapped/*"
- "tools/public_api_guard/*"
- "aio/*"
users:
- IgorMinar #primary
- alexeagle
- jasonaden
- mhevery #fallback
integration:
conditions:
files:
- "integration/*"
users:
- alexeagle
- mhevery
- tbosch
- vicb
- IgorMinar #fallback
core:
conditions:
files:
- "packages/core/*"
users:
- tbosch #primary
- mhevery
- vicb
- IgorMinar #fallback
compiler/animations:
conditions:
files:
- "packages/compiler/src/animation/*"
users:
- matsko #primary
- tbosch
- IgorMinar #fallback
- mhevery #fallback
compiler/i18n:
conditions:
files:
- "packages/compiler/src/i18n/*"
users:
- vicb #primary
- tbosch
- IgorMinar #fallback
- mhevery #fallback
compiler:
conditions:
files:
- "packages/compiler/*"
users:
- tbosch #primary
- vicb
- chuckjaz
- mhevery
- IgorMinar #fallback
compiler-cli:
conditions:
files:
- "tools/@angular/tsc-wrapped/*"
- "packages/compiler-cli/*"
users:
- alexeagle
- chuckjaz
- tbosch
- IgorMinar #fallback
- mhevery #fallback
common:
conditions:
files:
- "packages/common/*"
users:
- pkozlowski-opensource #primary
- vicb
- IgorMinar #fallback
- mhevery #fallback
forms:
conditions:
files:
- "packages/forms/*"
users:
- kara #primary
# needs secondary
- IgorMinar #fallback
- mhevery #fallback
http:
conditions:
files:
- "packages/http/*"
users:
- vikerman #primary
- alxhub
- IgorMinar #fallback
- mhevery #fallback
language-service:
conditions:
files:
- "packages/language-service/*"
users:
- chuckjaz #primary
# needs secondary
- IgorMinar #fallback
- mhevery #fallback
router:
conditions:
files:
- "packages/router/*"
users:
- vicb #primary
# needs secondary
- IgorMinar #fallback
- mhevery #fallback
upgrade:
conditions:
files:
- "packages/upgrade/*"
users:
- petebacondarwin #primary
- gkalpak
- IgorMinar #fallback
- mhevery #fallback
platform-browser:
conditions:
files:
- "packages/platform-browser/*"
users:
- tbosch #primary
- vicb #secondary
- IgorMinar #fallback
- mhevery #fallback
platform-server:
conditions:
files:
- "packages/platform-server/*"
users:
- vikerman #primary
- alxhub
- vicb
- tbosch
- IgorMinar #fallback
- mhevery #fallback
platform-webworker:
conditions:
files:
- "packages/platform-webworker/*"
users:
- vicb #primary
- tbosch #secondary
- IgorMinar #fallback
- mhevery #fallback
benchpress:
conditions:
files:
- "packages/benchpress/*"
users:
- tbosch #primary
# needs secondary
- IgorMinar #fallback
- mhevery #fallback
angular.io:
conditions:
files:
- "aio/*"
users:
- IgorMinar
- robwormald
- petebacondarwin
- mhevery #fallback

View File

@ -1,7 +1,7 @@
language: node_js
sudo: false
node_js:
- '6.9.5'
- '6.6.0'
addons:
# firefox: "38.0"
@ -10,43 +10,30 @@ addons:
# needed to install g++ that is used by npms's native modules
- ubuntu-toolchain-r-test
packages:
# needed to install g++ that is used by npms's native modules
- g++-4.8
# https://docs.travis-ci.com/user/jwt
jwt:
# SAUCE_ACCESS_KEY<=secret for NGBUILDS_IO_KEY to work around travis-ci/travis-ci#7223, unencrypted value in valentine as NGBUILDS_IO_KEY>
# we alias NGBUILDS_IO_KEY to $SAUCE_ACCESS_KEY in env.sh and set the SAUCE_ACCESS_KEY there
- secure: "L7nrZwkAtFtYrP2DykPXgZvEKjkv0J/TwQ/r2QGxFTaBq4VZn+2Dw0YS7uCxoMqYzDwH0aAOqxoutibVpk8Z/16nE3tNmU5RzltMd6Xmt3qU2f/JDQLMo6PSlBodnjOUsDHJgmtrcbjhqrx/znA237BkNUu6UZRT7mxhXIZpn0U="
branches:
except:
- g3_v2_0
cache:
yarn: true
directories:
- ./node_modules
- ./.chrome/chromium
- ./aio/node_modules
env:
global:
# GITHUB_TOKEN_ANGULAR
# This is needed for the e2e Travis matrix task to publish packages to github for continuous packages delivery.
- secure: "fq/U7VDMWO8O8SnAQkdbkoSe2X92PVqg4d044HmRYVmcf6YbO48+xeGJ8yOk0pCBwl3ISO4Q2ot0x546kxfiYBuHkZetlngZxZCtQiFT9kyId8ZKcYdXaIW9OVdw3Gh3tQyUwDucfkVhqcs52D6NZjyE2aWZ4/d1V4kWRO/LMgo="
# FIREBASE_TOKEN
# This is needed for publishing builds to the "aio-staging" firebase site.
# TODO(i): the token was generated using the iminar@google account, we should switch to a shared/role-base account.
- secure: "MPx3UM77o5IlhT75PKHL0FXoB5tSXDc3vnCXCd1sRy4XUTZ9vjcV6nNuyqEf+SOw659bGbC1FI4mACGx1Q+z7MQDR85b1mcA9uSgHDkh+IR82CnCVdaX9d1RXafdJIArahxfmorbiiPPLyPIKggo7ituRm+2c+iraoCkE/pXxYg="
matrix:
# Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete.
- CI_MODE=e2e
- CI_MODE=js
- CI_MODE=saucelabs_required
- CI_MODE=browserstack_required
- CI_MODE=saucelabs_optional
- CI_MODE=browserstack_optional
- CI_MODE=docs_test
- CI_MODE=aio
- CI_MODE=js
- CI_MODE=e2e
- CI_MODE=saucelabs_required
- CI_MODE=browserstack_required
- CI_MODE=saucelabs_optional
- CI_MODE=browserstack_optional
matrix:
fast_finish: true
@ -54,19 +41,11 @@ matrix:
- env: "CI_MODE=saucelabs_optional"
- env: "CI_MODE=browserstack_optional"
before_install:
# source the env.sh script so that the exported variables are available to other scripts later on
- source ./scripts/ci/env.sh print
install:
- ./scripts/ci/install.sh
- ./scripts/ci-lite/install.sh
script:
- ./scripts/ci/build.sh
- ./scripts/ci/test.sh
# deploy is part of 'script' and not 'after_success' so that we fail the build if the deployment fails
- ./scripts/ci/deploy.sh
- ./scripts/ci/angular.sh
# all the scripts under this line will not quickly abort in case ${TRAVIS_TEST_RESULT} is 1 (job failure)
- ./scripts/ci/cleanup.sh
- ./scripts/ci/print-logs.sh
- ./scripts/ci-lite/build.sh && ./scripts/ci-lite/test.sh
after_script:
- ./scripts/ci-lite/cleanup.sh

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
# Pushing changes into the Angular tree
# Pushing changes into the Angular 2 tree
Please see [Using git with Angular repositories](https://docs.google.com/document/d/1h8nijFSaa1jG_UE8v4WP7glh5qOUXnYtAtJh_gwOQHI/edit)
for details about how we maintain a linear commit history, and the rules for committing.
@ -6,16 +6,29 @@ for details about how we maintain a linear commit history, and the rules for com
As a contributor, just read the instructions in [CONTRIBUTING.md](CONTRIBUTING.md) and send a pull request.
Someone with committer access will do the rest.
# Change approvals
## The `PR: merge` label and `presubmit-*` branches
Change approvals in our monorepo are managed via [pullapprove.com](https://about.pullapprove.com/) and are configured via the `.pullapprove.yaml` file.
We have automated the process for merging pull requests into master. Our goal is to minimize the disruption for
Angular committers and also prevent breakages on master.
When a PR has `pr_state: LGTM` and is ready to merge, you should add the `pr_action: merge` label.
Currently (late 2015), we need to ensure that each PR will cleanly merge into the Google-internal version control,
so the caretaker reviews the changes manually.
# Merging
After this review, the caretaker adds `zomg_admin: do_merge` which is restricted to admins only.
A robot running as [mary-poppins](https://github.com/mary-poppins)
is notified that the label was added by an authorized person,
and will create a new branch in the angular project, using the convention `presubmit-{username}-pr-{number}`.
Once a change has all the approvals either the last approver or the PR author (if PR author has the project collaborator status) should mark the PR with "PR: merge" label.
This signals to the caretaker that the PR should be merged.
(Note: if the automation fails, committers can instead push the commits to a branch following this naming scheme.)
# Who is the Caretaker?
When a Travis build succeeds for a presubmit branch named following the convention,
Travis will re-base the commits, merge to master, and close the PR automatically.
See [this explanation](https://twitter.com/IgorMinar/status/799365744806854656).
Finally, after merge `mary-poppins` removes the presubmit branch.
## Administration
The list of users who can trigger a merge by adding the `zomg_admin: do_merge` label is stored in our appengine app datastore.
Edit the contents of the [CoreTeamMember Table](
https://console.developers.google.com/project/angular2-automation/datastore/query?queryType=KindQuery&namespace=&kind=CoreTeamMember)

View File

@ -1,6 +1,6 @@
# Contributing to Angular
# Contributing to Angular 2
We would love for you to contribute to Angular and help make it even better than it is
We would love for you to contribute to Angular 2 and help make it even better than it is
today! As a contributor, here are the guidelines we would like you to follow:
- [Code of Conduct](#coc)
@ -17,26 +17,17 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
## <a name="question"></a> Got a Question or Problem?
Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [StackOverflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
If you have questions about how to *use* Angular, please direct them to the [Google Group][angular-group]
discussion list or [StackOverflow][stackoverflow]. Please note that the Angular team's capacity to answer usage questions is limited. We are also available on [Gitter][gitter].
StackOverflow is a much better place to ask questions since:
- there are thousands of people willing to help on StackOverflow
- questions and answers stay available for public viewing so your question / answer might help someone else
- StackOverflow's voting system assures that the best answers are prominently visible.
To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to StackOverflow.
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
## <a name="issue"></a> Found a Bug?
## <a name="issue"></a> Found an Issue?
If you find a bug in the source code, you can help us by
[submitting an issue](#submit-issue) to our [GitHub Repository][github]. Even better, you can
[submit a Pull Request](#submit-pr) with a fix.
## <a name="feature"></a> Missing a Feature?
You can *request* a new feature by [submitting an issue](#submit-issue) to our GitHub
Repository. If you would like to *implement* a new feature, please submit an issue with
## <a name="feature"></a> Want a Feature?
You can *request* a new feature by [submitting an issue](#submit-issue) to our [GitHub
Repository][github]. If you would like to *implement* a new feature, please submit an issue with
a proposal for your work first, to be sure that we can use it.
Please consider what kind of change it is:
@ -48,22 +39,24 @@ and help you to craft the change so that it is successfully accepted into the pr
## <a name="submit"></a> Submission Guidelines
### <a name="submit-issue"></a> Submitting an Issue
Before you submit an issue, search the archive, maybe your question was already answered.
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
If your issue appears to be a bug, and hasn't been reported, open a new issue.
Help us to maximize the effort we can spend fixing issues and adding new
features, by not reporting duplicate issues. Providing the following information will increase the
chances of your issue being dealt with quickly:
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like:
* **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
* **Angular Version** - what version of Angular is affected (e.g. 2.0.0-alpha.53)
* **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you
* **Browsers and Operating System** - is this a problem with all browsers?
* **Reproduce the Error** - provide a live example (using [Plunker][plunker],
[JSFiddle][jsfiddle] or [Runnable][runnable]) or a unambiguous set of steps
* **Related Issues** - has a similar issue been reported before?
* **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
causing the problem (line of code or commit)
- version of Angular used
- 3rd-party libraries and their versions
- and most importantly - a use-case that fails
A minimal reproduce scenario using http://plnkr.co/ allows us to quickly confirm a bug (or point out coding problem) as well as confirm that we are fixing the right problem. If plunker is not a suitable way to demonstrate the problem (for example for issues related to our npm packaging), please create a standalone git repository demonstrating the problem.
We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced.
You can file new issues by filling out our [new issue form](https://github.com/angular/angular/issues/new).
You can file new issues by providing the above information [here](https://github.com/angular/angular/issues/new).
### <a name="submit-pr"></a> Submitting a Pull Request (PR)
@ -101,7 +94,7 @@ Before you submit your Pull Request (PR) consider the following guidelines:
* In GitHub, send a pull request to `angular:master`.
* If we suggest changes then:
* Make the required updates.
* Re-run the Angular test suites to ensure tests are still passing.
* Re-run the Angular 2 test suites to ensure tests are still passing.
* Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
```shell
@ -191,44 +184,21 @@ If the commit reverts a previous commit, it should begin with `revert: `, follow
### Type
Must be one of the following:
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
* **docs**: Documentation only changes
* **feat**: A new feature
* **fix**: A bug fix
* **perf**: A code change that improves performance
* **refactor**: A code change that neither fixes a bug nor adds a feature
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
semi-colons, etc)
* **refactor**: A code change that neither fixes a bug nor adds a feature
* **perf**: A code change that improves performance
* **test**: Adding missing tests or correcting existing tests
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
* **chore**: Other changes that don't modify `src` or `test` files
### Scope
The scope should be the name of the npm package affected (as perceived by person reading changelog generated from commit messages.
The following is the list of supported scopes:
* **common**
* **compiler**
* **compiler-cli**
* **core**
* **forms**
* **http**
* **language-service**
* **platform-browser**
* **platform-browser-dynamic**
* **platform-server**
* **platform-webworker**
* **platform-webworker-dynamic**
* **router**
* **upgrade**
* **tsc-wrapped**
There is currently few exception to the "use package name" rule:
* **packaging**: used for changes that change the npm package layout in all of our packages, e.g. public path changes, package.json changes done to all packages, d.ts file/format changes, changes to bundles, etc.
* **changelog**: used for updating the release notes in CHANGELOG.md
* **aio**: used for docs-app (angular.io) related changes within the /aio directory of the repo
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`)
The scope could be anything specifying place of the commit change. For example
`Compiler`, `ElementInjector`, etc.
### Subject
The subject contains succinct description of the change:
@ -267,7 +237,7 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
[github]: https://github.com/angular/angular
[gitter]: https://gitter.im/angular/angular
[individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html
[js-style-guide]: https://google.github.io/styleguide/jsguide.html
[js-style-guide]: https://google.github.io/styleguide/javascriptguide.xml
[jsfiddle]: http://jsfiddle.net
[plunker]: http://plnkr.co/edit
[runnable]: http://runnable.com

View File

@ -1,6 +1,6 @@
# Building and Testing Angular
# Building and Testing Angular 2 for JS
This document describes how to set up your development environment to build and test Angular.
This document describes how to set up your development environment to build and test Angular 2 JS version.
It also explains the basic mechanics of using `git`, `node`, and `npm`.
* [Prerequisite Software](#prerequisite-software)
@ -71,24 +71,9 @@ particular `gulp` and `protractor` commands. If you prefer, you can drop this pa
Since global installs can become stale, and required versions can vary by project, we avoid their
use in these instructions.
*Option 2*: globally installing the package `npm-run` by running `npm install -g npm-run`
(you might need to prefix this command with `sudo`). You will then be able to run locally installed
package scripts by invoking: e.g., `npm-run gulp build`
(see [npm-run project page](https://github.com/timoxley/npm-run) for more details).
*Option 3*: defining a bash alias like `alias nbin='PATH=$(npm bin):$PATH'` as detailed in this
*Option 2*: defining a bash alias like `alias nbin='PATH=$(npm bin):$PATH'` as detailed in this
[Stackoverflow answer](http://stackoverflow.com/questions/9679932/how-to-use-package-installed-locally-in-node-modules/15157360#15157360) and used like this: e.g., `nbin gulp build`.
## Installing Bower Modules
Now run `bower` to install additional dependencies:
```shell
# Install other Angular project dependencies (bower.json)
bower install
```
## Windows only
In order to create the right symlinks, run **as administrator**:
@ -129,7 +114,7 @@ You should execute the 3 test suites before submitting a PR to github.
All the tests are executed on our Continuous Integration infrastructure and a PR could only be merged once the tests pass.
- CircleCI fails if your code is not formatted properly,
- Travis CI fails if any of the test suites described above fails.
- Travis CI fails if any of the test suite describe above fails.
## Update the public API tests
@ -139,10 +124,9 @@ If you happen to modify the public API of Angular, API golden files must be upda
$ gulp public-api:update
```
Note: The command `gulp public-api:enforce` fails when the API doesn't match the golden files. Make sure to rebuild
the project before trying to verify after an API change.
Note: The command `./test.sh tools` fails when the API doesn't match the golden files.
## <a name="clang-format"></a> Formatting your source code
## Formatting your source code
Angular uses [clang-format](http://clang.llvm.org/docs/ClangFormat.html) to format the source code. If the source code
is not properly formatted, the CI will fail and the PR can not be merged.
@ -153,38 +137,4 @@ You can automatically format your code by running:
$ gulp format
```
## Linting/verifying your source code
You can check that your code is properly formatted and adheres to coding style by running:
``` shell
$ gulp lint
```
## Publishing snapshot builds
When the `master` branch successfully builds on Travis, it automatically publishes build artifacts
to repositories in the Angular org, eg. the `@angular/core` package is published to
http://github.com/angular/core-builds.
The ES2015 version of Angular is published to a different branch in these repos, for example
http://github.com/angular/core-builds#master-es2015
You may find that your un-merged change needs some validation from external participants.
Rather than requiring them to pull your Pull Request and build Angular locally, you can
publish the `*-builds` snapshots just like our Travis build does.
First time, you need to create the github repositories:
``` shell
$ export TOKEN=[get one from https://github.com/settings/tokens]
$ CREATE_REPOS=1 ./scripts/publish/publish-build-artifacts.sh [github username]
```
For subsequent snapshots, just run
``` shell
$ ./scripts/publish/publish-build-artifacts.sh [github username]
```
The script will publish the build snapshot to a branch with the same name as your current branch,
and create it if it doesn't exist.

View File

@ -1,6 +1,6 @@
The MIT License
Copyright (c) 2014-2017 Google, Inc. http://angular.io
Copyright (c) 2014-2016 Google, Inc. http://angular.io
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,7 +1,7 @@
Naming Conventions in Angular
Naming Conventions in Angular2
---
In general Angular should follow TypeScript naming conventions.
In general Angular2 should follow TypeScript naming conventions.
See: https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines

View File

@ -4,14 +4,18 @@
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/pr?style=flat)](http://issuestats.com/github/angular/angular)
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/issue?style=flat)](http://issuestats.com/github/angular/angular)
[![npm version](https://badge.fury.io/js/%40angular%2Fcore.svg)](https://badge.fury.io/js/%40angular%2Fcore)
[![Downloads](http://img.shields.io/npm/dm/angular2.svg)](https://npmjs.org/package/angular2)
[![Sauce Test Status](https://saucelabs.com/browser-matrix/angular2-ci.svg)](https://saucelabs.com/u/angular2-ci)
[![Sauce Test Status](https://saucelabs.com/browser-matrix/angular2-ci.svg)](https://saucelabs.com/u/angular2-ci)
*Safari (7+), iOS (7+), Edge (14) and IE mobile (11) are tested on [BrowserStack][browserstack].*
Angular
=========
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript (JS) and other languages.
Angular is a development platform for building mobile and desktop web applications. This is the
repository for [Angular 2][ng2] Typescript/JavaScript (JS).
Angular2 for [Dart][dart] can be found at [dart-lang/angular2][ng2dart].
## Quickstart
@ -26,5 +30,9 @@ guidelines for [contributing][contributing] and then check out one of our issues
[browserstack]: https://www.browserstack.com/
[contributing]: http://github.com/angular/angular/blob/master/CONTRIBUTING.md
[dart]: http://www.dartlang.org
[quickstart]: https://angular.io/docs/ts/latest/quickstart.html
[ng]: http://angular.io
[ng2]: http://angular.io
[ngDart]: http://angulardart.org
[ngJS]: http://angularjs.org
[ng2dart]: https://github.com/dart-lang/angular2

View File

@ -1,62 +0,0 @@
# Saved Responses for Angular's Issue Tracker
The following are canned responses that the Angular team should use to close issues on our issue tracker that fall into the listed resolution categories.
Since GitHub currently doesn't allow us to have a repository-wide or organization-wide list of [saved replies](https://help.github.com/articles/working-with-saved-replies/), these replies need to be maintained by individual team members. Since the responses can be modified in the future, all responses are versioned to simplify the process of keeping the responses up to date.
## Angular: Already Fixed (v1)
```
Thanks for reporting this issue. Luckily it has already been fixed in one of the recent releases. Please update to the most recent version to resolve the problem.
If after upgrade the problem still exists in your application please open a new issue and provide a plunker reproducing the problem and describing the difference between the expected and current behavior. You can use this plunker template: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5?p=catalogue
```
## Angular: Don't Understand (v1)
```
I'm sorry but we don't understand the problem you are reporting.
If the problem still exists please open a new issue and provide a plunker reproducing the problem and describing the difference between the expected and current behavior. You can use this plunker template: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5?p=catalogue
```
## Angular: Duplicate (v1)
```
Thanks for reporting this issue. However this issue is a duplicate of an existing issue #<ISSUE_NUMBER>. Please subscribe to that issue for future updates.
```
## Angular: Insufficient Information Provided (v1)
```
Thanks for reporting this issue. However, you didn't provide sufficient information for us to understand and reproduce the problem. Please check out [our submission guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-submitting-an-issue) to understand why we can't act on issues that are lacking important information.
If the problem still persists, please file a new issue and ensure you provide all of the required information when filling out the issue template.
```
## Angular: Issue Outside of Angular (v1)
```
I'm sorry but this issue is not caused by Angular. Please contact the author(s) of project <PROJECT NAME> or file issue on their issue tracker.
```
## Angular: Non-reproducible (v1)
```
I'm sorry but we can't reproduce the problem following the instructions you provided.
If the problem still exists please open a new issue following [our submission guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-submitting-an-issue).
```
## Angular: Obsolete (v1)
```
Thanks for reporting this issue. This issue is now obsolete due to changes in the recent releases. Please update to the most recent Angular version.
If the problem still persists, please file a new issue and ensure you provide the version of Angular affected and include the steps to reproduce the problem when filling out the issue template.
```
## Angular: Support Request (v1)
```
Hello, we reviewed this issue and determined that it doesn't fall into the bug report or feature request category. This issue tracker is not suitable for support requests, please repost your issue on [StackOverflow](http://stackoverflow.com/) using tag `angular`.
If you are wondering why we don't resolve support issues via the issue tracker, please [check out this explanation](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-got-a-question-or-problem).
```

View File

@ -1,4 +1,4 @@
# Developer Tools for Angular
# Developer Tools for Angular 2
Here you will find a collection of tools and tips for keeping your application
perform well and contain fewer bugs.

View File

@ -1,7 +1,7 @@
# Triage Process and Github Labels for Angular
# Triage Process and Github Labels for Angular 2
This document describes how the Angular team uses labels and milestones
to triage issues on github. The basic idea of the process is that
to triage issues on github. The basic idea of the new process is that
caretaker only assigns a component and type (bug, feature) label. The
owner of the component than is in full control of how the issues should
be triaged further.
@ -17,27 +17,26 @@ with it.
* `comp: animations`: `@matsko`
* `comp: benchpress`: `@tbosch`
* `comp: build & ci`: `@IgorMinar` -- All build and CI scripts
* `comp: build/ci`: `@IgorMinar` -- All build and CI scripts
* `comp: common`: `@mhevery` -- This includes core components / pipes.
* `comp: core & compiler`: `@tbosch` -- Because core and compiler are very
* `comp: core/compiler`: `@tbosch` -- Because core and compiler are very
intertwined, we will be treating them as one.
* `comp: forms`: `@kara`
* `comp: http`: `@jeffbcross`
* `comp: i18n`: `@vicb`
* `comp: language service`: `@chuckjaz`
* `comp: metadata-extractor`: `@chuckjaz`
* `comp: router`: `@vicb`
* `comp: router`: `@vsavkin`
* `comp: testing`: `@juliemr`
* `comp: upgrade`: `@mhevery`
* `comp: web-worker`: `@vicb`
* `comp: zones`: `@mhevery`
* `comp: zone`: `@mhevery`
There are few components which are cross-cutting. They don't have
a clear location in the source tree. We will treat them as a component
even thought no specific source tree is associated with them.
* `comp: docs`: `@naomiblack`
* `comp: packaging`: `@IgorMinar`
* `comp: documentation`: `@naomiblack`
* `comp: packaging`: `@mhevery`
* `comp: performance`: `@tbosch`
* `comp: security`: `@IgorMinar`
@ -54,11 +53,11 @@ What kind of problem is this?
## Caretaker Triage Process
It is the caretaker's responsibility to assign `comp: *` to each new
issue as they come in. The reason why we limit the responsibility of the
caretaker to this one label is that it is likely that without domain
knowledge the caretaker could mislabel issues or lack knowledge of
duplicate issues.
It is the caretaker's responsibility to assign `comp: *` and `type: *`
to each new issue as they come in. The reason why we limit the
responsibility of the caretaker to these two labels is that it is
unlikely that without domain knowledge the caretaker could add any
additional labels of value.
## Component's owner Triage Process
@ -69,37 +68,11 @@ process for their component.
It will be up to the component owner to determine the order in which the
issues within the component will be resolved.
Several owners have adopted the issue categorization based on
[user pain](http://www.lostgarden.com/2008/05/improving-bug-triage-with-user-pain.html)
used by AngularJS. In this system every issue is assigned frequency and
severity based on which the total user pain score is calculated.
Following is the definition of various frequency and severity levels:
1. `freq(score): *` How often does this issue come up? How many developers does this affect?
* low (1) - obscure issue affecting a handful of developers
* moderate (2) - impacts auxiliary usage patterns, only small number of applications are affected
* high (3) - impacts primary usage patterns, affecting most Angular apps
* critical (4) - impacts all Angular apps
1. `severity(score): *` - How bad is the issue?
* inconvenience (1) - causes ugly/boilerplate code in apps
* confusing (2) - unexpected or inconsistent behavior; hard-to-debug
* broken expected use (3) - it's hard or impossible for a developer using Angular to accomplish something that Angular should be able to do
* memory leak (4)
* regression (5) - functionality that used to work no longer works in a new release due to an unintentional change
* security issue (6)
These criteria are then used to calculate a "user pain" score as follows:
`pain = severity × frequency`
### Assigning Issues to Milestones
Any issue that is being worked on must have:
* An `Assignee`: The person doing the work.
* An `assignee`: The person doing the work.
* A `Milestone`: When we expect to complete this work.
We aim to only have at most three milestones open at a time:

View File

@ -1,72 +0,0 @@
{
"project": {
"version": "1.0.0-beta.32.3",
"name": "site"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"content",
"app/search/search-worker.js",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.json",
"prefix": "aio",
"styles": [
"styles.scss"
],
"scripts": [
],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"files": "src/**/*.ts",
"project": "src/tsconfig.json"
},
{
"files": "e2e/**/*.ts",
"project": "e2e/tsconfig.json"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "scss",
"component": {},
"prefixInterfaces": false,
"inline": {
"style": false,
"template": false
},
"spec": {
"class": false,
"component": true,
"directive": true,
"module": false,
"pipe": true,
"service": true
}
}
}

View File

@ -1,5 +0,0 @@
{
"projects": {
"staging": "aio-staging"
}
}

View File

@ -1,31 +0,0 @@
# Site
This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.26.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
Before running the tests make sure you are serving the app via `ng serve`.
## Deploying to GitHub Pages
Run `ng github-pages:deploy` to deploy to GitHub Pages.
## Further help
To get more help on the `angular-cli` use `ng help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@ -1,3 +0,0 @@
scripts-js/lib
scripts-js/node_modules
scripts-js/**/test

View File

@ -1,154 +0,0 @@
# Image metadata and config
FROM debian:jessie
LABEL name="angular.io PR preview" \
description="This image implements the PR preview functionality for angular.io." \
vendor="Angular" \
version="1.0"
VOLUME /aio-secrets
VOLUME /var/www/aio-builds
EXPOSE 80 443
# Build-time args and env vars
ARG AIO_BUILDS_DIR=/var/www/aio-builds
ARG TEST_AIO_BUILDS_DIR=/tmp/aio-builds
ARG AIO_DOMAIN_NAME=ngbuilds.io
ARG TEST_AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME.localhost
ARG AIO_GITHUB_ORGANIZATION=angular
ARG TEST_AIO_GITHUB_ORGANIZATION=angular
ARG AIO_GITHUB_TEAM_SLUGS=angular-core
ARG TEST_AIO_GITHUB_TEAM_SLUGS=angular-core
ARG AIO_NGINX_HOSTNAME=$AIO_DOMAIN_NAME
ARG TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_DOMAIN_NAME
ARG AIO_NGINX_PORT_HTTP=80
ARG TEST_AIO_NGINX_PORT_HTTP=8080
ARG AIO_NGINX_PORT_HTTPS=443
ARG TEST_AIO_NGINX_PORT_HTTPS=4433
ARG AIO_REPO_SLUG=angular/angular
ARG TEST_AIO_REPO_SLUG=test-repo/test-slug
ARG AIO_UPLOAD_HOSTNAME=upload.localhost
ARG TEST_AIO_UPLOAD_HOSTNAME=upload.localhost
ARG AIO_UPLOAD_MAX_SIZE=20971520
ARG TEST_AIO_UPLOAD_MAX_SIZE=20971520
ARG AIO_UPLOAD_PORT=3000
ARG TEST_AIO_UPLOAD_PORT=3001
ENV AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR \
AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME TEST_AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME \
AIO_GITHUB_ORGANIZATION=$AIO_GITHUB_ORGANIZATION TEST_AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION \
AIO_GITHUB_TEAM_SLUGS=$AIO_GITHUB_TEAM_SLUGS TEST_AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS \
AIO_LOCALCERTS_DIR=/etc/ssl/localcerts TEST_AIO_LOCALCERTS_DIR=/etc/ssl/localcerts-test \
AIO_NGINX_HOSTNAME=$AIO_NGINX_HOSTNAME TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME \
AIO_NGINX_PORT_HTTP=$AIO_NGINX_PORT_HTTP TEST_AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP \
AIO_NGINX_PORT_HTTPS=$AIO_NGINX_PORT_HTTPS TEST_AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS \
AIO_REPO_SLUG=$AIO_REPO_SLUG TEST_AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG \
AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \
AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \
AIO_UPLOAD_HOSTNAME=$AIO_UPLOAD_HOSTNAME TEST_AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME \
AIO_UPLOAD_MAX_SIZE=$AIO_UPLOAD_MAX_SIZE TEST_AIO_UPLOAD_MAX_SIZE=$TEST_AIO_UPLOAD_MAX_SIZE \
AIO_UPLOAD_PORT=$AIO_UPLOAD_PORT TEST_AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT \
NODE_ENV=production
# Create directory for logs
RUN mkdir /var/log/aio
# Add extra package sources
RUN apt-get update -y && apt-get install -y curl
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_6.x | bash -
RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
# Install packages
RUN apt-get update -y && apt-get install -y \
chkconfig \
cron \
dnsmasq \
nano \
nginx \
nodejs \
openssl \
rsyslog \
yarn
RUN yarn global add pm2@2
# Set up cronjobs
COPY cronjobs/aio-builds-cleanup /etc/cron.d/
RUN chmod 0744 /etc/cron.d/aio-builds-cleanup
RUN crontab /etc/cron.d/aio-builds-cleanup
# Set up dnsmasq
COPY dnsmasq/dnsmasq.conf /etc/
RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|" /etc/dnsmasq.conf
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|" /etc/dnsmasq.conf
RUN sed -i "s|{{\$TEST_AIO_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|" /etc/dnsmasq.conf
RUN sed -i "s|{{\$TEST_AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|" /etc/dnsmasq.conf
# Set up SSL/TLS certificates
COPY nginx/create-selfsigned-cert.sh /tmp/
RUN chmod a+x /tmp/create-selfsigned-cert.sh
RUN /tmp/create-selfsigned-cert.sh "selfcert-prod" "$AIO_NGINX_HOSTNAME" "$AIO_LOCALCERTS_DIR"
RUN /tmp/create-selfsigned-cert.sh "selfcert-test" "$TEST_AIO_NGINX_HOSTNAME" "$TEST_AIO_LOCALCERTS_DIR"
RUN rm /tmp/create-selfsigned-cert.sh
RUN update-ca-certificates
# Set up nginx (for production and testing)
RUN rm /etc/nginx/sites-enabled/*
COPY nginx/aio-builds.conf /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$AIO_BUILDS_DIR|" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$AIO_DOMAIN_NAME|" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|" /etc/nginx/sites-available/aio-builds-prod.conf
RUN ln -s /etc/nginx/sites-available/aio-builds-prod.conf /etc/nginx/sites-enabled/aio-builds-prod.conf
COPY nginx/aio-builds.conf /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$TEST_AIO_DOMAIN_NAME|" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|" /etc/nginx/sites-available/aio-builds-test.conf
RUN ln -s /etc/nginx/sites-available/aio-builds-test.conf /etc/nginx/sites-enabled/aio-builds-test.conf
# Set up pm2
RUN pm2 startup systemv -u root > /dev/null
RUN chkconfig pm2-root on
# Set up the shell scripts
COPY scripts-sh/ $AIO_SCRIPTS_SH_DIR/
RUN chmod a+x $AIO_SCRIPTS_SH_DIR/*
RUN find $AIO_SCRIPTS_SH_DIR -maxdepth 1 -type f -printf "%P\n" \
| while read file; do ln -s $AIO_SCRIPTS_SH_DIR/$file /usr/local/bin/aio-${file%.*}; done
# Set up the Node.js scripts
COPY scripts-js/ $AIO_SCRIPTS_JS_DIR/
WORKDIR $AIO_SCRIPTS_JS_DIR/
RUN yarn install --production
# Set up health check
HEALTHCHECK --interval=5m CMD /usr/local/bin/aio-health-check
# Go!
WORKDIR /
CMD aio-init && tail -f /dev/null

View File

@ -1,2 +0,0 @@
# Periodically clean up builds that do not correspond to currently open PRs
0 4 * * * root /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1

View File

@ -1,16 +0,0 @@
# Do not read /etc/resolv.conf. Get servers from this file instead.
no-resolv
server=8.8.8.8
server=8.8.4.4
# Listen for DHCP and DNS requests only on this address.
listen-address=127.0.0.1
# Force an IP addres for these domains.
address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1
address=/{{$AIO_UPLOAD_HOSTNAME}}/127.0.0.1
address=/{{$TEST_AIO_NGINX_HOSTNAME}}/127.0.0.1
address=/{{$TEST_AIO_UPLOAD_HOSTNAME}}/127.0.0.1
# Run as root (required from inside docker container).
user=root

View File

@ -1,66 +0,0 @@
# Serve PR-preview requests
server {
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{40})\.";
listen {{$AIO_NGINX_PORT_HTTP}};
listen [::]:{{$AIO_NGINX_PORT_HTTP}};
listen {{$AIO_NGINX_PORT_HTTPS}} ssl;
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl;
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
root {{$AIO_BUILDS_DIR}}/$pr/$sha;
disable_symlinks on from=$document_root;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
# Handle all other requests
server {
server_name _;
listen {{$AIO_NGINX_PORT_HTTP}} default_server;
listen [::]:{{$AIO_NGINX_PORT_HTTP}};
listen {{$AIO_NGINX_PORT_HTTPS}} ssl default_server;
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl;
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
# Health check
location "~^\/health-check\/?$" {
add_header Content-Type text/plain;
return 200 '';
}
# Upload builds
location "~^\/create-build\/(?<pr>[1-9][0-9]*)\/(?<sha>[0-9a-f]{40})\/?$" {
if ($request_method != "POST") {
add_header Allow "POST";
return 405;
}
client_body_temp_path /tmp/aio-create-builds;
client_body_buffer_size 128K;
client_max_body_size {{$AIO_UPLOAD_MAX_SIZE}};
client_body_in_file_only on;
proxy_pass_request_headers on;
proxy_set_header X-FILE $request_body_file;
proxy_set_body off;
proxy_redirect off;
proxy_method GET;
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri;
resolver 127.0.0.1;
}
# Everything else
location / {
return 404;
}
}

View File

@ -1,20 +0,0 @@
#!/bin/bash
set -eu -o pipefail
# Variables
confFile=/tmp/$1.conf
domainName=$2
outDir=$3
# Create certificate
cp /etc/ssl/openssl.cnf "$confFile"
echo "[subjectAltName]" >> "$confFile"
echo "subjectAltName = DNS:$domainName, DNS:*.$domainName" >> "$confFile"
mkdir -p $outDir
openssl req -days 365 -newkey rsa:2048 -nodes -sha256 -x509 \
-config "$confFile" -extensions subjectAltName -subj "/CN=$domainName" \
-out "$outDir/$domainName.crt" -keyout "$outDir/$domainName.key"
chmod -R 400 "$outDir"
cp "$outDir/$domainName.crt" /usr/local/share/ca-certificates

View File

@ -1 +0,0 @@
/dist/

View File

@ -1,71 +0,0 @@
// Imports
import * as fs from 'fs';
import * as path from 'path';
import * as shell from 'shelljs';
import {GithubPullRequests} from '../common/github-pull-requests';
import {assertNotMissingOrEmpty} from '../common/utils';
// Classes
export class BuildCleaner {
// Constructor
constructor(protected buildsDir: string, protected repoSlug: string, protected githubToken: string) {
assertNotMissingOrEmpty('buildsDir', buildsDir);
assertNotMissingOrEmpty('repoSlug', repoSlug);
assertNotMissingOrEmpty('githubToken', githubToken);
}
// Methods - Public
public cleanUp(): Promise<void> {
return Promise.all([
this.getExistingBuildNumbers(),
this.getOpenPrNumbers(),
]).then(([existingBuilds, openPrs]) => this.removeUnnecessaryBuilds(existingBuilds, openPrs));
}
// Methods - Protected
protected getExistingBuildNumbers(): Promise<number[]> {
return new Promise((resolve, reject) => {
fs.readdir(this.buildsDir, (err, files) => {
if (err) {
return reject(err);
}
const buildNumbers = files.
map(Number). // Convert string to number
filter(Boolean); // Ignore NaN (or 0), because they are not builds
resolve(buildNumbers);
});
});
}
protected getOpenPrNumbers(): Promise<number[]> {
const githubPullRequests = new GithubPullRequests(this.githubToken, this.repoSlug);
return githubPullRequests.
fetchAll('open').
then(prs => prs.map(pr => pr.number));
}
protected removeDir(dir: string) {
try {
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
(shell as any).chmod('-R', 'a+w', dir);
shell.rm('-rf', dir);
} catch (err) {
console.error(`ERROR: Unable to remove '${dir}' due to:`, err);
}
}
protected removeUnnecessaryBuilds(existingBuildNumbers: number[], openPrNumbers: number[]) {
const toRemove = existingBuildNumbers.filter(num => !openPrNumbers.includes(num));
console.log(`Existing builds: ${existingBuildNumbers.length}`);
console.log(`Open pull requests: ${openPrNumbers.length}`);
console.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
toRemove.
map(num => path.join(this.buildsDir, String(num))).
forEach(dir => this.removeDir(dir));
}
}

View File

@ -1,21 +0,0 @@
// Imports
import {getEnvVar} from '../common/utils';
import {BuildCleaner} from './build-cleaner';
// Constants
const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN', true);
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
// Run
_main();
// Functions
function _main() {
const buildCleaner = new BuildCleaner(AIO_BUILDS_DIR, AIO_REPO_SLUG, AIO_GITHUB_TOKEN);
buildCleaner.cleanUp().catch(err => {
console.error('ERROR:', err);
process.exit(1);
});
}

View File

@ -1,110 +0,0 @@
// Imports
import {IncomingMessage} from 'http';
import * as https from 'https';
import {assertNotMissingOrEmpty} from './utils';
// Constants
const GITHUB_HOSTNAME = 'api.github.com';
// Interfaces - Types
interface RequestParams {
[key: string]: string | number;
}
type RequestParamsOrNull = RequestParams | null;
// Classes
export class GithubApi {
protected requestHeaders: {[key: string]: string};
// Constructor
constructor(githubToken: string) {
assertNotMissingOrEmpty('githubToken', githubToken);
this.requestHeaders = {
'Authorization': `token ${githubToken}`,
'User-Agent': `Node/${process.versions.node}`,
};
}
// Methods - Public
public get<T>(pathname: string, params?: RequestParamsOrNull): Promise<T> {
const path = this.buildPath(pathname, params);
return this.request<T>('get', path);
}
public post<T>(pathname: string, params?: RequestParamsOrNull, data?: any): Promise<T> {
const path = this.buildPath(pathname, params);
return this.request<T>('post', path, data);
}
// Methods - Protected
protected buildPath(pathname: string, params?: RequestParamsOrNull): string {
if (params == null) {
return pathname;
}
const search = (params === null) ? '' : this.serializeSearchParams(params);
const joiner = search && '?';
return `${pathname}${joiner}${search}`;
}
protected getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> {
const perPage = 100;
const params = {
...baseParams,
page: currentPage,
per_page: perPage,
};
return this.get<T[]>(pathname, params).then(items => {
if (items.length < perPage) {
return items;
}
return this.getPaginated(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
});
}
protected request<T>(method: string, path: string, data: any = null): Promise<T> {
return new Promise<T>((resolve, reject) => {
const options = {
headers: {...this.requestHeaders},
host: GITHUB_HOSTNAME,
method,
path,
};
const onError = (statusCode: number, responseText: string) => {
const url = `https://${GITHUB_HOSTNAME}${path}`;
reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`);
};
const onSuccess = (responseText: string) => {
try { resolve(JSON.parse(responseText)); } catch (err) { reject(err); }
};
const onResponse = (res: IncomingMessage) => {
const statusCode = res.statusCode || -1;
const isSuccess = (200 <= statusCode) && (statusCode < 400);
let responseText = '';
res.
on('data', d => responseText += d).
on('end', () => isSuccess ? onSuccess(responseText) : onError(statusCode, responseText)).
on('error', reject);
};
https.
request(options, onResponse).
on('error', reject).
end(data && JSON.stringify(data));
});
}
protected serializeSearchParams(params: RequestParams): string {
return Object.keys(params).
filter(key => params[key] != null).
map(key => `${key}=${encodeURIComponent(String(params[key]))}`).
join('&');
}
}

View File

@ -1,44 +0,0 @@
// Imports
import {assertNotMissingOrEmpty} from '../common/utils';
import {GithubApi} from './github-api';
// Interfaces - Types
export interface PullRequest {
number: number;
user: {login: string};
}
export type PullRequestState = 'all' | 'closed' | 'open';
// Classes
export class GithubPullRequests extends GithubApi {
// Constructor
constructor(githubToken: string, protected repoSlug: string) {
super(githubToken);
assertNotMissingOrEmpty('repoSlug', repoSlug);
}
// Methods - Public
public addComment(pr: number, body: string): Promise<void> {
if (!(pr > 0)) {
throw new Error(`Invalid PR number: ${pr}`);
} else if (!body) {
throw new Error(`Invalid or empty comment body: ${body}`);
}
return this.post<void>(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body});
}
public fetch(pr: number): Promise<PullRequest> {
return this.get<PullRequest>(`/repos/${this.repoSlug}/pulls/${pr}`);
}
public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> {
console.log(`Fetching ${state} pull requests...`);
const pathname = `/repos/${this.repoSlug}/pulls`;
const params = {state};
return this.getPaginated<PullRequest>(pathname, params);
}
}

View File

@ -1,45 +0,0 @@
// Imports
import {assertNotMissingOrEmpty} from '../common/utils';
import {GithubApi} from './github-api';
// Interfaces - Types
interface Team {
id: number;
slug: string;
}
interface TeamMembership {
state: string;
}
// Classes
export class GithubTeams extends GithubApi {
// Constructor
constructor(githubToken: string, protected organization: string) {
super(githubToken);
assertNotMissingOrEmpty('organization', organization);
}
// Methods - Public
public fetchAll(): Promise<Team[]> {
return this.getPaginated<Team>(`/orgs/${this.organization}/teams`);
}
public isMemberById(username: string, teamIds: number[]): Promise<boolean> {
const getMembership = (teamId: number) =>
this.get<TeamMembership>(`/teams/${teamId}/memberships/${username}`).
then(membership => membership.state === 'active').
catch(() => false);
const reduceFn = (promise: Promise<boolean>, teamId: number) =>
promise.then(isMember => isMember || getMembership(teamId));
return teamIds.reduce(reduceFn, Promise.resolve(false));
}
public isMemberBySlug(username: string, teamSlugs: string[]): Promise<boolean> {
return this.fetchAll().
then(teams => teams.filter(team => teamSlugs.includes(team.slug)).map(team => team.id)).
then(teamIds => this.isMemberById(username, teamIds)).
catch(() => false);
}
}

View File

@ -1,23 +0,0 @@
export const runTests = (specFiles: string[], helpers?: string[]) => {
// We can't use `import` here, because of the following mess:
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
//
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
// `jasmine-core` module and the `jasmine` module).
// tslint:disable-next-line: no-var-requires variable-name
const Jasmine = require('jasmine');
const config = {
helpers,
random: true,
spec_files: specFiles,
stopSpecOnExpectationFailure: true,
};
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
const runner = new Jasmine();
runner.loadConfig(config);
runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1));
runner.execute();
};

View File

@ -1,17 +0,0 @@
// Functions
export const assertNotMissingOrEmpty = (name: string, value: string | null | undefined) => {
if (!value) {
throw new Error(`Missing or empty required parameter '${name}'!`);
}
};
export const getEnvVar = (name: string, isOptional = false): string => {
const value = process.env[name];
if (!isOptional && !value) {
console.error(`ERROR: Missing required environment variable '${name}'!`);
process.exit(1);
}
return value || '';
};

View File

@ -1,81 +0,0 @@
// Imports
import * as cp from 'child_process';
import {EventEmitter} from 'events';
import * as fs from 'fs';
import * as path from 'path';
import * as shell from 'shelljs';
import {assertNotMissingOrEmpty} from '../common/utils';
import {CreatedBuildEvent} from './build-events';
import {UploadError} from './upload-error';
// Classes
export class BuildCreator extends EventEmitter {
// Constructor
constructor(protected buildsDir: string) {
super();
assertNotMissingOrEmpty('buildsDir', buildsDir);
}
// Methods - Public
public create(pr: string, sha: string, archivePath: string): Promise<any> {
const prDir = path.join(this.buildsDir, pr);
const shaDir = path.join(prDir, sha);
let dirToRemoveOnError: string;
return Promise.
all([this.exists(prDir), this.exists(shaDir)]).
then(([prDirExisted, shaDirExisted]) => {
if (shaDirExisted) {
throw new UploadError(403, `Request to overwrite existing directory: ${shaDir}`);
}
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
return Promise.resolve().
then(() => shell.mkdir('-p', shaDir)).
then(() => this.extractArchive(archivePath, shaDir)).
then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha)));
}).
catch(err => {
if (dirToRemoveOnError) {
shell.rm('-rf', dirToRemoveOnError);
}
if (!(err instanceof UploadError)) {
err = new UploadError(500, `Error while uploading to directory: ${shaDir}\n${err}`);
}
throw err;
});
}
// Methods - Protected
protected exists(fileOrDir: string): Promise<boolean> {
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
}
protected extractArchive(inputFile: string, outputDir: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
const cmd = `tar --extract --gzip --directory "${outputDir}" --file "${inputFile}"`;
cp.exec(cmd, (err, _stdout, stderr) => {
if (err) {
return reject(err);
}
if (stderr) {
console.warn(stderr);
}
try {
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
(shell as any).chmod('-R', 'a-w', outputDir);
shell.rm('-f', inputFile);
resolve();
} catch (err) {
reject(err);
}
});
});
}
}

View File

@ -1,15 +0,0 @@
// Classes
export class BuildEvent {
// Constructor
constructor(public type: string, public pr: number, public sha: string) {}
}
export class CreatedBuildEvent extends BuildEvent {
// Properties - Public, Static
public static type = 'build.created';
// Constructor
constructor(pr: number, sha: string) {
super(CreatedBuildEvent.type, pr, sha);
}
}

View File

@ -1,78 +0,0 @@
// Imports
import * as jwt from 'jsonwebtoken';
import {GithubPullRequests} from '../common/github-pull-requests';
import {GithubTeams} from '../common/github-teams';
import {assertNotMissingOrEmpty} from '../common/utils';
import {UploadError} from './upload-error';
// Interfaces - Types
interface JwtPayload {
slug: string;
'pull-request': number;
}
// Classes
export class BuildVerifier {
// Properties - Protected
protected githubPullRequests: GithubPullRequests;
protected githubTeams: GithubTeams;
// Constructor
constructor(protected secret: string, githubToken: string, protected repoSlug: string, organization: string,
protected allowedTeamSlugs: string[]) {
assertNotMissingOrEmpty('secret', secret);
assertNotMissingOrEmpty('githubToken', githubToken);
assertNotMissingOrEmpty('repoSlug', repoSlug);
assertNotMissingOrEmpty('organization', organization);
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
this.githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
this.githubTeams = new GithubTeams(githubToken, organization);
}
// Methods - Public
public getPrAuthorTeamMembership(pr: number): Promise<{author: string, isMember: boolean}> {
return Promise.resolve().
then(() => this.githubPullRequests.fetch(pr)).
then(prInfo => prInfo.user.login).
then(author => this.githubTeams.isMemberBySlug(author, this.allowedTeamSlugs).
then(isMember => ({author, isMember})));
}
public verify(expectedPr: number, authHeader: string): Promise<void> {
return Promise.resolve().
then(() => this.extractJwtString(authHeader)).
then(jwtString => this.verifyJwt(expectedPr, jwtString)).
then(jwtPayload => this.verifyPr(jwtPayload['pull-request'])).
catch(err => { throw new UploadError(403, `Error while verifying upload for PR ${expectedPr}: ${err}`); });
}
// Methods - Protected
protected extractJwtString(input: string): string {
return input.replace(/^token +/i, '');
}
protected verifyJwt(expectedPr: number, token: string): Promise<JwtPayload> {
return new Promise((resolve, reject) => {
jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => {
if (err) {
reject(err.message || err);
} else if (payload.slug !== this.repoSlug) {
reject(`jwt slug invalid. expected: ${this.repoSlug}`);
} else if (payload['pull-request'] !== expectedPr) {
reject(`jwt pull-request invalid. expected: ${expectedPr}`);
} else {
resolve(payload);
}
});
});
}
protected verifyPr(pr: number): Promise<void> {
return this.getPrAuthorTeamMembership(pr).
then(({author, isMember}) => isMember ? Promise.resolve() : Promise.reject(
`User '${author}' is not an active member of any of the following teams: ` +
`${this.allowedTeamSlugs.join(', ')}`,
));
}
}

View File

@ -1,39 +0,0 @@
// Imports
import {getEnvVar} from '../common/utils';
import {BuildVerifier} from './build-verifier';
// Run
_main();
// Functions
function _main() {
const secret = 'unused';
const githubToken = getEnvVar('AIO_GITHUB_TOKEN');
const repoSlug = getEnvVar('AIO_REPO_SLUG');
const organization = getEnvVar('AIO_GITHUB_ORGANIZATION');
const allowedTeamSlugs = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(',');
const pr = +getEnvVar('AIO_PREVERIFY_PR');
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, organization, allowedTeamSlugs);
// Exit codes:
// - 0: The PR author is a member.
// - 1: The PR author is not a member.
// - 2: An error occurred.
buildVerifier.getPrAuthorTeamMembership(pr).
then(({author, isMember}) => {
if (isMember) {
process.exit(0);
} else {
const errorMessage = `User '${author}' is not an active member of any of the following teams: ` +
`${allowedTeamSlugs.join(', ')}`;
onError(errorMessage, 1);
}
}).
catch(err => onError(err, 2));
}
function onError(err: string, exitCode: number) {
console.error(err);
process.exit(exitCode || 1);
}

View File

@ -1,10 +0,0 @@
// Imports
import {GithubPullRequests} from '../common/github-pull-requests';
import {BuildVerifier} from './build-verifier';
// Run
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
GithubPullRequests.prototype.addComment = () => Promise.resolve();
BuildVerifier.prototype.verify = () => Promise.resolve();
// tslint:disable-next-line: no-var-requires
require('./index');

View File

@ -1,35 +0,0 @@
// TODO(gkalpak): Find more suitable way to run as `www-data`.
process.setuid('www-data');
// Imports
import {getEnvVar} from '../common/utils';
import {uploadServerFactory} from './upload-server-factory';
// Constants
const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
const AIO_DOMAIN_NAME = getEnvVar('AIO_DOMAIN_NAME');
const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION');
const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS');
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
const AIO_PREVIEW_DEPLOYMENT_TOKEN = getEnvVar('AIO_PREVIEW_DEPLOYMENT_TOKEN');
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
const AIO_UPLOAD_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME');
const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT');
// Run
_main();
// Functions
function _main() {
uploadServerFactory.
create({
buildsDir: AIO_BUILDS_DIR,
domainName: AIO_DOMAIN_NAME,
githubOrganization: AIO_GITHUB_ORGANIZATION,
githubTeamSlugs: AIO_GITHUB_TEAM_SLUGS.split(','),
githubToken: AIO_GITHUB_TOKEN,
repoSlug: AIO_REPO_SLUG,
secret: AIO_PREVIEW_DEPLOYMENT_TOKEN,
}).
listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME);
}

View File

@ -1,8 +0,0 @@
// Classes
export class UploadError extends Error {
// Constructor
constructor(public status: number = 500, message?: string) {
super(message);
Object.setPrototypeOf(this, UploadError.prototype);
}
}

View File

@ -1,117 +0,0 @@
// Imports
import * as express from 'express';
import * as http from 'http';
import {GithubPullRequests} from '../common/github-pull-requests';
import {assertNotMissingOrEmpty} from '../common/utils';
import {BuildCreator} from './build-creator';
import {CreatedBuildEvent} from './build-events';
import {BuildVerifier} from './build-verifier';
import {UploadError} from './upload-error';
// Constants
const AUTHORIZATION_HEADER = 'AUTHORIZATION';
const X_FILE_HEADER = 'X-FILE';
// Interfaces - Types
interface UploadServerConfig {
buildsDir: string;
domainName: string;
githubOrganization: string;
githubTeamSlugs: string[];
githubToken: string;
repoSlug: string;
secret: string;
}
// Classes
class UploadServerFactory {
// Methods - Public
public create({
buildsDir,
domainName,
githubOrganization,
githubTeamSlugs,
githubToken,
repoSlug,
secret,
}: UploadServerConfig): http.Server {
assertNotMissingOrEmpty('domainName', domainName);
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs);
const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName);
const middleware = this.createMiddleware(buildVerifier, buildCreator);
const httpServer = http.createServer(middleware);
httpServer.on('listening', () => {
const info = httpServer.address();
console.info(`Up and running (and listening on ${info.address}:${info.port})...`);
});
return httpServer;
}
// Methods - Protected
protected createBuildCreator(buildsDir: string, githubToken: string, repoSlug: string,
domainName: string): BuildCreator {
const buildCreator = new BuildCreator(buildsDir);
const githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
buildCreator.on(CreatedBuildEvent.type, ({pr, sha}: CreatedBuildEvent) => {
const body = `The angular.io preview for ${sha.slice(0, 7)} is available [here][1].\n\n` +
`[1]: https://pr${pr}-${sha}.${domainName}/`;
githubPullRequests.addComment(pr, body);
});
return buildCreator;
}
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
const middleware = express();
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
const pr = req.params[0];
const sha = req.params[1];
const archive = req.header(X_FILE_HEADER);
const authHeader = req.header(AUTHORIZATION_HEADER);
if (!authHeader) {
this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req);
} else if (!archive) {
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
}
buildVerifier.
verify(+pr, authHeader).
then(() => buildCreator.create(pr, sha, archive)).
then(() => res.sendStatus(201)).
catch(err => this.respondWithError(res, err));
});
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
middleware.get('*', req => this.throwRequestError(404, 'Unknown resource', req));
middleware.all('*', req => this.throwRequestError(405, 'Unsupported method', req));
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
return middleware;
}
protected respondWithError(res: express.Response, err: any) {
if (!(err instanceof UploadError)) {
err = new UploadError(500, String((err && err.message) || err));
}
const statusText = http.STATUS_CODES[err.status] || '???';
console.error(`Upload error: ${err.status} - ${statusText}`);
console.error(err.message);
res.status(err.status).end(err.message);
}
protected throwRequestError(status: number, error: string, req: express.Request) {
throw new UploadError(status, `${error} in request: ${req.method} ${req.originalUrl}`);
}
}
// Exports
export const uploadServerFactory = new UploadServerFactory();

View File

@ -1,191 +0,0 @@
// Imports
import * as cp from 'child_process';
import * as fs from 'fs';
import * as http from 'http';
import * as path from 'path';
import * as shell from 'shelljs';
import {getEnvVar} from '../common/utils';
// Constans
const SERVER_USER = 'www-data';
const TEST_AIO_BUILDS_DIR = getEnvVar('TEST_AIO_BUILDS_DIR');
const TEST_AIO_NGINX_HOSTNAME = getEnvVar('TEST_AIO_NGINX_HOSTNAME');
const TEST_AIO_NGINX_PORT_HTTP = +getEnvVar('TEST_AIO_NGINX_PORT_HTTP');
const TEST_AIO_NGINX_PORT_HTTPS = +getEnvVar('TEST_AIO_NGINX_PORT_HTTPS');
const TEST_AIO_UPLOAD_HOSTNAME = getEnvVar('TEST_AIO_UPLOAD_HOSTNAME');
const TEST_AIO_UPLOAD_MAX_SIZE = +getEnvVar('TEST_AIO_UPLOAD_MAX_SIZE');
const TEST_AIO_UPLOAD_PORT = +getEnvVar('TEST_AIO_UPLOAD_PORT');
// Interfaces - Types
export interface CmdResult { success: boolean; err: Error; stdout: string; stderr: string; }
export interface FileSpecs { content?: string; size?: number; }
export type CleanUpFn = () => void;
export type TestSuiteFactory = (scheme: string, port: number) => void;
export type VerifyCmdResultFn = (result: CmdResult) => void;
// Classes
class Helper {
// Properties - Public
public get buildsDir() { return TEST_AIO_BUILDS_DIR; }
public get nginxHostname() { return TEST_AIO_NGINX_HOSTNAME; }
public get nginxPortHttp() { return TEST_AIO_NGINX_PORT_HTTP; }
public get nginxPortHttps() { return TEST_AIO_NGINX_PORT_HTTPS; }
public get serverUser() { return SERVER_USER; }
public get uploadHostname() { return TEST_AIO_UPLOAD_HOSTNAME; }
public get uploadPort() { return TEST_AIO_UPLOAD_PORT; }
public get uploadMaxSize() { return TEST_AIO_UPLOAD_MAX_SIZE; }
// Properties - Protected
protected cleanUpFns: CleanUpFn[] = [];
protected portPerScheme: {[scheme: string]: number} = {
http: this.nginxPortHttp,
https: this.nginxPortHttps,
};
// Constructor
constructor() {
shell.mkdir('-p', this.buildsDir);
shell.exec(`chown -R ${this.serverUser} ${this.buildsDir}`);
}
// Methods - Public
public cleanUp() {
while (this.cleanUpFns.length) {
// Clean-up fns remove themselves from the list.
this.cleanUpFns[0]();
}
if (fs.readdirSync(this.buildsDir).length) {
throw new Error(`Directory '${this.buildsDir}' is not empty after clean-up.`);
}
}
public createDummyArchive(pr: string, sha: string, archivePath: string): CleanUpFn {
const inputDir = path.join(this.buildsDir, 'uploaded', pr, sha);
const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`;
const cmd2 = `chown ${this.serverUser} ${archivePath}`;
const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true);
shell.exec(cmd1);
shell.exec(cmd2);
cleanUpTemp();
return this.createCleanUpFn(() => shell.rm('-rf', archivePath));
}
public createDummyBuild(pr: string, sha: string, force = false): CleanUpFn {
const prDir = path.join(this.buildsDir, pr);
const shaDir = path.join(prDir, sha);
const idxPath = path.join(shaDir, 'index.html');
const barPath = path.join(shaDir, 'foo', 'bar.js');
this.writeFile(idxPath, {content: `PR: ${pr} | SHA: ${sha} | File: /index.html`}, force);
this.writeFile(barPath, {content: `PR: ${pr} | SHA: ${sha} | File: /foo/bar.js`}, force);
shell.exec(`chown -R ${this.serverUser} ${prDir}`);
return this.createCleanUpFn(() => shell.rm('-rf', prDir));
}
public deletePrDir(pr: string) {
const prDir = path.join(this.buildsDir, pr);
if (fs.existsSync(prDir)) {
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
(shell as any).chmod('-R', 'a+w', prDir);
shell.rm('-rf', prDir);
}
}
public readBuildFile(pr: string, sha: string, relFilePath: string): string {
const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath);
return fs.readFileSync(absFilePath, 'utf8');
}
public runCmd(cmd: string, opts: cp.ExecFileOptions = {}): Promise<CmdResult> {
return new Promise(resolve => {
const proc = cp.exec(cmd, opts, (err, stdout, stderr) => resolve({success: !err, err, stdout, stderr}));
this.createCleanUpFn(() => proc.kill());
});
}
public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory) {
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
}
public verifyResponse(status: number | [number, string], regex = /^/): VerifyCmdResultFn {
let statusCode: number;
let statusText: string;
if (Array.isArray(status)) {
statusCode = status[0];
statusText = status[1];
} else {
statusCode = status;
statusText = http.STATUS_CODES[statusCode];
}
return (result: CmdResult) => {
const [headers, body] = result.stdout.
split(/(?:\r?\n){2,}/).
map(s => s.trim()).
slice(-2);
if (!result.success) {
console.log('Stdout:', result.stdout);
console.log('Stderr:', result.stderr);
console.log('Error:', result.err);
}
expect(result.success).toBe(true);
expect(headers).toContain(`${statusCode} ${statusText}`);
expect(body).toMatch(regex);
};
}
public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string): CleanUpFn {
const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath);
return this.writeFile(absFilePath, {content}, true);
}
public writeFile(filePath: string, {content, size}: FileSpecs, force = false): CleanUpFn {
if (!force && fs.existsSync(filePath)) {
throw new Error(`Refusing to overwrite existing file '${filePath}'.`);
}
let cleanUpTarget = filePath;
while (!fs.existsSync(path.dirname(cleanUpTarget))) {
cleanUpTarget = path.dirname(cleanUpTarget);
}
shell.mkdir('-p', path.dirname(filePath));
if (size) {
// Create a file of the specified size.
cp.execSync(`fallocate -l ${size} ${filePath}`);
} else {
// Create a file with the specified content.
fs.writeFileSync(filePath, content || '');
}
shell.exec(`chown ${this.serverUser} ${filePath}`);
return this.createCleanUpFn(() => shell.rm('-rf', cleanUpTarget));
}
// Methods - Protected
protected createCleanUpFn(fn: Function): CleanUpFn {
const cleanUpFn = () => {
const idx = this.cleanUpFns.indexOf(cleanUpFn);
if (idx !== -1) {
this.cleanUpFns.splice(idx, 1);
fn();
}
};
this.cleanUpFns.push(cleanUpFn);
return cleanUpFn;
}
}
// Exports
export const helper = new Helper();

View File

@ -1,6 +0,0 @@
// Imports
import {runTests} from '../common/run-tests';
// Run
const specFiles = [`${__dirname}/**/*.e2e.js`];
runTests(specFiles);

View File

@ -1,232 +0,0 @@
// Imports
import * as path from 'path';
import {helper as h} from './helper';
// Tests
h.runForAllSupportedSchemes((scheme, port) => describe(`nginx (on ${scheme.toUpperCase()})`, () => {
const hostname = h.nginxHostname;
const host = `${hostname}:${port}`;
const pr = '9';
const sha9 = '9'.repeat(40);
const sha0 = '0'.repeat(40);
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
afterEach(() => h.cleanUp());
describe(`pr<pr>-<sha>.${host}/*`, () => {
beforeEach(() => {
h.createDummyBuild(pr, sha9);
h.createDummyBuild(pr, sha0);
});
it('should return /index.html', done => {
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
const bodyegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
Promise.all([
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyegex)),
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyegex)),
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyegex)),
]).then(done);
});
it('should return /foo/bar.js', done => {
const bodyegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/bar.js`).
then(h.verifyResponse(200, bodyegex)).
then(done);
});
it('should respond with 403 for directories', done => {
Promise.all([
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/`).then(h.verifyResponse(403)),
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo`).then(h.verifyResponse(403)),
]).then(done);
});
it('should respond with 404 for unknown paths', done => {
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz.css`).
then(h.verifyResponse(404)).
then(done);
});
it('should respond with 404 for unknown PRs/SHAs', done => {
const otherPr = 54321;
const otherSha = '8'.repeat(40);
Promise.all([
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${sha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${sha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}9.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherSha}.${host}`).then(h.verifyResponse(404)),
]).then(done);
});
it('should respond with 404 if the subdomain format is wrong', done => {
Promise.all([
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://prx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://xx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://p${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://r${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${pr}${sha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${pr}_${sha9}.${host}`).then(h.verifyResponse(404)),
]).then(done);
});
it('should reject PRs with leading zeros', done => {
h.runCmd(`curl -iL ${scheme}://pr0${pr}-${sha9}.${host}`).
then(h.verifyResponse(404)).
then(done);
});
it('should accept SHAs with leading zeros (but not ignore them)', done => {
const bodyegex = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
Promise.all([
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${sha9}.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha0}.${host}`).then(h.verifyResponse(200, bodyegex)),
]).then(done);
});
});
describe(`${host}/health-check`, () => {
it('should respond with 200', done => {
Promise.all([
h.runCmd(`curl -iL ${scheme}://${host}/health-check`).then(h.verifyResponse(200)),
h.runCmd(`curl -iL ${scheme}://${host}/health-check/`).then(h.verifyResponse(200)),
]).then(done);
});
it('should respond with 404 if the path does not match exactly', done => {
Promise.all([
h.runCmd(`curl -iL ${scheme}://${host}/health-check/foo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/health-check-foo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/health-checknfoo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/foo/health-check`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/foo-health-check`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/foonhealth-check`).then(h.verifyResponse(404)),
]).then(done);
});
});
describe(`${host}/create-build/<pr>/<sha>`, () => {
it('should disallow non-POST requests', done => {
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
Promise.all([
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
]).then(done);
});
it(`should reject files larger than ${h.uploadMaxSize}B (according to header)`, done => {
const headers = `--header "Content-Length: ${1.5 * h.uploadMaxSize}"`;
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
h.runCmd(`curl -iLX POST ${headers} ${url}`).
then(h.verifyResponse([413, 'Request Entity Too Large'])).
then(done);
});
it(`should reject files larger than ${h.uploadMaxSize}B (without header)`, done => {
const filePath = path.join(h.buildsDir, 'snapshot.tar.gz');
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
h.writeFile(filePath, {size: 1.5 * h.uploadMaxSize});
h.runCmd(`curl -iLX POST --data-binary "@${filePath}" ${url}`).
then(h.verifyResponse([413, 'Request Entity Too Large'])).
then(done);
});
it('should pass requests through to the upload server', done => {
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/${pr}/${sha9}`).
then(h.verifyResponse(401, /Missing or empty 'AUTHORIZATION' header/)).
then(done);
});
it('should respond with 404 for unknown paths', done => {
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
Promise.all([
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)),
]).then(done);
});
it('should reject PRs with leading zeros', done => {
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/0${pr}/${sha9}`).
then(h.verifyResponse(404)).
then(done);
});
it('should accept SHAs with leading zeros (but not ignore them)', done => {
const cmdPrefix = `curl -iLX POST ${scheme}://${host}/create-build/${pr}`;
const bodyRegex = /Missing or empty 'AUTHORIZATION' header/;
Promise.all([
h.runCmd(`${cmdPrefix}/0${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/${sha0}`).then(h.verifyResponse(401, bodyRegex)),
]).then(done);
});
});
describe(`${host}/*`, () => {
it('should respond with 404 for unkown URLs (even if the resource exists)', done => {
['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => {
const absFilePath = path.join(h.buildsDir, relFilePath);
h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
});
Promise.all([
h.runCmd(`curl -iL ${scheme}://${host}/index.html`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://foo.${host}/index.html`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://foo.${host}/`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://foo.${host}`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/foo.js`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)),
]).then(done);
});
});
}));

View File

@ -1,84 +0,0 @@
// Imports
import * as path from 'path';
import {helper as h} from './helper';
// Tests
h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => {
const hostname = h.nginxHostname;
const host = `${hostname}:${port}`;
const pr9 = '9';
const sha9 = '9'.repeat(40);
const sha0 = '0'.repeat(40);
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
const getFile = (pr: string, sha: string, file: string) =>
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha}.${host}/${file}`);
const uploadBuild = (pr: string, sha: string, archive: string) => {
const curlPost = 'curl -iLX POST --header "Authorization: Token FOO"';
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
};
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
afterEach(() => {
h.deletePrDir(pr9);
h.cleanUp();
});
it('should be able to upload and serve a build for a new PR', done => {
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath).
then(() => Promise.all([
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
])).
then(done);
});
it('should be able to upload and serve a build for an existing PR', done => {
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`;
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`);
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyBuild(pr9, sha0);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath).
then(() => Promise.all([
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(200, idxContentRegex0)),
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex0)),
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
])).
then(done);
});
it('should not be able to overwrite a build', done => {
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
h.createDummyBuild(pr9, sha9);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath).
then(h.verifyResponse(403)).
then(() => Promise.all([
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
])).
then(done);
});
}));

View File

@ -1,265 +0,0 @@
// Imports
import * as fs from 'fs';
import * as path from 'path';
import {CmdResult, helper as h} from './helper';
// Tests
describe('upload-server (on HTTP)', () => {
const hostname = h.uploadHostname;
const port = h.uploadPort;
const host = `${hostname}:${port}`;
const pr = '9';
const sha9 = '9'.repeat(40);
const sha0 = '0'.repeat(40);
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
afterEach(() => h.cleanUp());
describe(`${host}/create-build/<pr>/<sha>`, () => {
const authorizationHeader = `--header "Authorization: Token FOO"`;
const xFileHeader = `--header "X-File: ${h.buildsDir}/snapshot.tar.gz"`;
const curl = `curl -iL ${authorizationHeader} ${xFileHeader}`;
it('should disallow non-GET requests', done => {
const url = `http://${host}/create-build/${pr}/${sha9}`;
const bodyRegex = /^Unsupported method/;
Promise.all([
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405, bodyRegex)),
]).then(done);
});
it('should reject requests without an \'AUTHORIZATION\' header', done => {
const headers1 = '';
const headers2 = '--header "AUTHORIXATION: "';
const url = `http://${host}/create-build/${pr}/${sha9}`;
const bodyRegex = /^Missing or empty 'AUTHORIZATION' header/;
Promise.all([
h.runCmd(`curl -iL ${headers1} ${url}`).then(h.verifyResponse(401, bodyRegex)),
h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(401, bodyRegex)),
]).then(done);
});
it('should reject requests without an \'X-FILE\' header', done => {
const headers1 = authorizationHeader;
const headers2 = `${authorizationHeader} --header "X-FILE: "`;
const url = `http://${host}/create-build/${pr}/${sha9}`;
const bodyRegex = /^Missing or empty 'X-FILE' header/;
Promise.all([
h.runCmd(`curl -iL ${headers1} ${url}`).then(h.verifyResponse(400, bodyRegex)),
h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(400, bodyRegex)),
]).then(done);
});
it('should respond with 404 for unknown paths', done => {
const cmdPrefix = `${curl} http://${host}`;
Promise.all([
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)),
]).then(done);
});
it('should reject PRs with leading zeros', done => {
h.runCmd(`${curl} http://${host}/create-build/0${pr}/${sha9}`).
then(h.verifyResponse(404)).
then(done);
});
it('should accept SHAs with leading zeros (but not ignore them)', done => {
Promise.all([
h.runCmd(`${curl} http://${host}/create-build/${pr}/0${sha9}`).then(h.verifyResponse(404)),
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha0}`).then(h.verifyResponse(500)),
]).then(done);
});
it('should not overwrite existing builds', done => {
h.createDummyBuild(pr, sha9);
expect(h.readBuildFile(pr, sha9, 'index.html')).toContain('index.html');
h.writeBuildFile(pr, sha9, 'index.html', 'My content');
expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content');
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
then(h.verifyResponse(403, /^Request to overwrite existing directory/)).
then(() => expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content')).
then(done);
});
it('should delete the PR directory on error (for new PR)', done => {
const prDir = path.join(h.buildsDir, pr);
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
then(h.verifyResponse(500)).
then(() => expect(fs.existsSync(prDir)).toBe(false)).
then(done);
});
it('should only delete the SHA directory on error (for existing PR)', done => {
const prDir = path.join(h.buildsDir, pr);
const shaDir = path.join(prDir, sha9);
h.createDummyBuild(pr, sha0);
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
then(h.verifyResponse(500)).
then(() => {
expect(fs.existsSync(shaDir)).toBe(false);
expect(fs.existsSync(prDir)).toBe(true);
}).
then(done);
});
describe('on successful upload', () => {
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
let uploadPromise: Promise<CmdResult>;
beforeEach(() => {
h.createDummyArchive(pr, sha9, archivePath);
uploadPromise = h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`);
});
afterEach(() => h.deletePrDir(pr));
it('should respond with 201', done => {
uploadPromise.then(h.verifyResponse(201)).then(done);
});
it('should extract the contents of the uploaded file', done => {
uploadPromise.
then(() => {
expect(h.readBuildFile(pr, sha9, 'index.html')).toContain(`uploaded/${pr}`);
expect(h.readBuildFile(pr, sha9, 'foo/bar.js')).toContain(`uploaded/${pr}`);
}).
then(done);
});
it(`should create files/directories owned by '${h.serverUser}'`, done => {
const shaDir = path.join(h.buildsDir, pr, sha9);
const idxPath = path.join(shaDir, 'index.html');
const barPath = path.join(shaDir, 'foo', 'bar.js');
uploadPromise.
then(() => Promise.all([
h.runCmd(`find ${shaDir}`),
h.runCmd(`find ${shaDir} -user ${h.serverUser}`),
])).
then(([{stdout: allFiles}, {stdout: userFiles}]) => {
expect(userFiles).toBe(allFiles);
expect(userFiles).toContain(shaDir);
expect(userFiles).toContain(idxPath);
expect(userFiles).toContain(barPath);
}).
then(done);
});
it('should delete the uploaded file', done => {
expect(fs.existsSync(archivePath)).toBe(true);
uploadPromise.
then(() => expect(fs.existsSync(archivePath)).toBe(false)).
then(done);
});
it('should make the build directory non-writable', done => {
const shaDir = path.join(h.buildsDir, pr, sha9);
const idxPath = path.join(shaDir, 'index.html');
const barPath = path.join(shaDir, 'foo', 'bar.js');
// See https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588.
const isNotWritable = (fileOrDir: string) => {
const mode = fs.statSync(fileOrDir).mode;
// tslint:disable-next-line: no-bitwise
return !(mode & parseInt('222', 8));
};
uploadPromise.
then(() => {
expect(isNotWritable(shaDir)).toBe(true);
expect(isNotWritable(idxPath)).toBe(true);
expect(isNotWritable(barPath)).toBe(true);
}).
then(done);
});
});
});
describe(`${host}/health-check`, () => {
it('should respond with 200', done => {
Promise.all([
h.runCmd(`curl -iL http://${host}/health-check`).then(h.verifyResponse(200)),
h.runCmd(`curl -iL http://${host}/health-check/`).then(h.verifyResponse(200)),
]).then(done);
});
it('should respond with 404 if the path does not match exactly', done => {
Promise.all([
h.runCmd(`curl -iL http://${host}/health-check/foo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL http://${host}/health-check-foo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL http://${host}/health-checknfoo`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL http://${host}/foo/health-check`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL http://${host}/foo-health-check`).then(h.verifyResponse(404)),
h.runCmd(`curl -iL http://${host}/foonhealth-check`).then(h.verifyResponse(404)),
]).then(done);
});
});
describe(`${host}/*`, () => {
it('should respond with 404 for GET requests to unknown URLs', done => {
const bodyRegex = /^Unknown resource/;
Promise.all([
h.runCmd(`curl -iL http://${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iL http://${host}/`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iL http://${host}`).then(h.verifyResponse(404, bodyRegex)),
]).then(done);
});
it('should respond with 405 for non-GET requests to any URL', done => {
const bodyRegex = /^Unsupported method/;
Promise.all([
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(405, bodyRegex)),
]).then(done);
});
});
});

View File

@ -1,43 +0,0 @@
{
"name": "aio-scripts-js",
"version": "1.0.0",
"description": "Performing various tasks on PR build artifacts for angular.io.",
"repository": "https://github.com/angular/angular.git",
"author": "Angular",
"license": "MIT",
"scripts": {
"prebuild": "yarn run clean",
"build": "tsc",
"build-watch": "yarn run tsc -- --watch",
"clean": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
"dev": "concurrently --kill-others --raw --success first \"yarn run build-watch\" \"yarn run test-watch\"",
"lint": "tslint --project tsconfig.json",
"pre~~test-only": "yarn run lint",
"~~test-only": "node dist/test",
"pretest": "yarn run build",
"test": "yarn run ~~test-only",
"pretest-watch": "yarn run build",
"test-watch": "nodemon --exec \"yarn run ~~test-only\" --watch dist"
},
"dependencies": {
"express": "^4.14.1",
"jasmine": "^2.5.3",
"jsonwebtoken": "^7.3.0",
"shelljs": "^0.7.6"
},
"devDependencies": {
"@types/express": "^4.0.35",
"@types/jasmine": "^2.5.43",
"@types/jsonwebtoken": "^7.2.0",
"@types/node": "^7.0.5",
"@types/shelljs": "^0.7.0",
"@types/supertest": "^2.0.0",
"concurrently": "^3.3.0",
"eslint": "^3.15.0",
"eslint-plugin-jasmine": "^2.2.0",
"nodemon": "^1.11.0",
"supertest": "^3.0.0",
"tslint": "^4.4.2",
"typescript": "^2.1.6"
}
}

View File

@ -1,318 +0,0 @@
// Imports
import * as fs from 'fs';
import * as shell from 'shelljs';
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
// Tests
describe('BuildCleaner', () => {
let cleaner: BuildCleaner;
beforeEach(() => cleaner = new BuildCleaner('/foo/bar', 'baz/qux', '12345'));
describe('constructor()', () => {
it('should throw if \'buildsDir\' is empty', () => {
expect(() => new BuildCleaner('', '/baz/qux', '12345')).
toThrowError('Missing or empty required parameter \'buildsDir\'!');
});
it('should throw if \'repoSlug\' is empty', () => {
expect(() => new BuildCleaner('/foo/bar', '', '12345')).
toThrowError('Missing or empty required parameter \'repoSlug\'!');
});
it('should throw if \'githubToken\' is empty', () => {
expect(() => new BuildCleaner('/foo/bar', 'baz/qux', '')).
toThrowError('Missing or empty required parameter \'githubToken\'!');
});
});
describe('cleanUp()', () => {
let cleanerGetExistingBuildNumbersSpy: jasmine.Spy;
let cleanerGetOpenPrNumbersSpy: jasmine.Spy;
let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy;
let existingBuildsDeferred: {resolve: Function, reject: Function};
let openPrsDeferred: {resolve: Function, reject: Function};
let promise: Promise<void>;
beforeEach(() => {
cleanerGetExistingBuildNumbersSpy = spyOn(cleaner as any, 'getExistingBuildNumbers').and.callFake(() => {
return new Promise((resolve, reject) => existingBuildsDeferred = {resolve, reject});
});
cleanerGetOpenPrNumbersSpy = spyOn(cleaner as any, 'getOpenPrNumbers').and.callFake(() => {
return new Promise((resolve, reject) => openPrsDeferred = {resolve, reject});
});
cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner as any, 'removeUnnecessaryBuilds');
promise = cleaner.cleanUp();
});
it('should return a promise', () => {
expect(promise).toEqual(jasmine.any(Promise));
});
it('should get the existing builds', () => {
expect(cleanerGetExistingBuildNumbersSpy).toHaveBeenCalled();
});
it('should get the open PRs', () => {
expect(cleanerGetOpenPrNumbersSpy).toHaveBeenCalled();
});
it('should reject if \'getExistingBuildNumbers()\' rejects', done => {
promise.catch(err => {
expect(err).toBe('Test');
done();
});
existingBuildsDeferred.reject('Test');
});
it('should reject if \'getOpenPrNumbers()\' rejects', done => {
promise.catch(err => {
expect(err).toBe('Test');
done();
});
openPrsDeferred.reject('Test');
});
it('should reject if \'removeUnnecessaryBuilds()\' rejects', done => {
promise.catch(err => {
expect(err).toBe('Test');
done();
});
cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.reject('Test'));
existingBuildsDeferred.resolve();
openPrsDeferred.resolve();
});
it('should pass existing builds and open PRs to \'removeUnnecessaryBuilds()\'', done => {
promise.then(() => {
expect(cleanerRemoveUnnecessaryBuildsSpy).toHaveBeenCalledWith('foo', 'bar');
done();
});
existingBuildsDeferred.resolve('foo');
openPrsDeferred.resolve('bar');
});
it('should resolve with the value returned by \'removeUnnecessaryBuilds()\'', done => {
promise.then(result => {
expect(result).toBe('Test');
done();
});
cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.resolve('Test'));
existingBuildsDeferred.resolve();
openPrsDeferred.resolve();
});
});
// Protected methods
describe('getExistingBuildNumbers()', () => {
let fsReaddirSpy: jasmine.Spy;
let readdirCb: (err: any, files?: string[]) => void;
let promise: Promise<number[]>;
beforeEach(() => {
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb);
promise = (cleaner as any).getExistingBuildNumbers();
});
it('should return a promise', () => {
expect(promise).toEqual(jasmine.any(Promise));
});
it('should get the contents of the builds directory', () => {
expect(fsReaddirSpy).toHaveBeenCalled();
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/foo/bar');
});
it('should reject if an error occurs while getting the files', done => {
promise.catch(err => {
expect(err).toBe('Test');
done();
});
readdirCb('Test');
});
it('should resolve with the returned files (as numbers)', done => {
promise.then(result => {
expect(result).toEqual([12, 34, 56]);
done();
});
readdirCb(null, ['12', '34', '56']);
});
it('should ignore files with non-numeric (or zero) names', done => {
promise.then(result => {
expect(result).toEqual([12, 34, 56]);
done();
});
readdirCb(null, ['12', 'foo', '34', 'bar', '56', '000']);
});
});
describe('getOpenPrNumbers()', () => {
let prDeferred: {resolve: Function, reject: Function};
let promise: Promise<number[]>;
beforeEach(() => {
spyOn(GithubPullRequests.prototype, 'fetchAll').and.callFake(() => {
return new Promise((resolve, reject) => prDeferred = {resolve, reject});
});
promise = (cleaner as any).getOpenPrNumbers();
});
it('should return a promise', () => {
expect(promise).toEqual(jasmine.any(Promise));
});
it('should fetch open PRs via \'GithubPullRequests\'', () => {
expect(GithubPullRequests.prototype.fetchAll).toHaveBeenCalledWith('open');
});
it('should reject if an error occurs while fetching PRs', done => {
promise.catch(err => {
expect(err).toBe('Test');
done();
});
prDeferred.reject('Test');
});
it('should resolve with the numbers of the fetched PRs', done => {
promise.then(prNumbers => {
expect(prNumbers).toEqual([1, 2, 3]);
done();
});
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
});
});
describe('removeDir()', () => {
let shellChmodSpy: jasmine.Spy;
let shellRmSpy: jasmine.Spy;
beforeEach(() => {
shellChmodSpy = spyOn(shell, 'chmod');
shellRmSpy = spyOn(shell, 'rm');
});
it('should remove the specified directory and its content', () => {
(cleaner as any).removeDir('/foo/bar');
expect(shellRmSpy).toHaveBeenCalledWith('-rf', '/foo/bar');
});
it('should make the directory and its content writable before removing', () => {
shellRmSpy.and.callFake(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar'));
(cleaner as any).removeDir('/foo/bar');
expect(shellRmSpy).toHaveBeenCalled();
});
it('should catch errors and log them', () => {
const consoleErrorSpy = spyOn(console, 'error');
shellRmSpy.and.callFake(() => { throw 'Test'; });
(cleaner as any).removeDir('/foo/bar');
expect(consoleErrorSpy).toHaveBeenCalled();
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain('Unable to remove \'/foo/bar\'');
expect(consoleErrorSpy.calls.argsFor(0)[1]).toBe('Test');
});
});
describe('removeUnnecessaryBuilds()', () => {
let consoleLogSpy: jasmine.Spy;
let cleanerRemoveDirSpy: jasmine.Spy;
beforeEach(() => {
consoleLogSpy = spyOn(console, 'log');
cleanerRemoveDirSpy = spyOn(cleaner as any, 'removeDir');
});
it('should log the number of existing builds, open PRs and builds to be removed', () => {
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
expect(console.log).toHaveBeenCalledWith('Existing builds: 3');
expect(console.log).toHaveBeenCalledWith('Open pull requests: 4');
expect(console.log).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
});
it('should construct full paths to directories (by prepending \'buildsDir\')', () => {
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []);
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2');
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
});
it('should remove the builds that do not correspond to open PRs', () => {
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]);
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(2);
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
cleanerRemoveDirSpy.calls.reset();
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]);
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(0);
cleanerRemoveDirSpy.calls.reset();
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []);
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4);
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2');
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/4');
cleanerRemoveDirSpy.calls.reset();
});
});
});

View File

@ -1,410 +0,0 @@
// Imports
import {EventEmitter} from 'events';
import {ClientRequest, IncomingMessage} from 'http';
import * as https from 'https';
import {GithubApi} from '../../lib/common/github-api';
// Tests
describe('GithubApi', () => {
let api: GithubApi;
beforeEach(() => api = new GithubApi('12345'));
describe('constructor()', () => {
it('should throw if \'githubToken\' is missing or empty', () => {
expect(() => new GithubApi('')).toThrowError('Missing or empty required parameter \'githubToken\'!');
});
});
describe('get()', () => {
let apiBuildPathSpy: jasmine.Spy;
let apiRequestSpy: jasmine.Spy;
beforeEach(() => {
apiBuildPathSpy = spyOn(api as any, 'buildPath');
apiRequestSpy = spyOn(api as any, 'request');
});
it('should call \'buildPath()\' with the pathname and params', () => {
api.get('/foo', {bar: 'baz'});
expect(apiBuildPathSpy).toHaveBeenCalled();
expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]);
});
it('should call \'request()\' with the correct method', () => {
api.get('/foo');
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('get');
});
it('should call \'request()\' with the correct path', () => {
apiBuildPathSpy.and.returnValue('/foo/bar');
api.get('foo');
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar');
});
it('should not pass data to \'request()\'', () => {
(api.get as Function)('foo', {}, {});
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[2]).toBeUndefined();
});
});
describe('post()', () => {
let apiBuildPathSpy: jasmine.Spy;
let apiRequestSpy: jasmine.Spy;
beforeEach(() => {
apiBuildPathSpy = spyOn(api as any, 'buildPath');
apiRequestSpy = spyOn(api as any, 'request');
});
it('should call \'buildPath()\' with the pathname and params', () => {
api.post('/foo', {bar: 'baz'});
expect(apiBuildPathSpy).toHaveBeenCalled();
expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]);
});
it('should call \'request()\' with the correct method', () => {
api.post('/foo');
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('post');
});
it('should call \'request()\' with the correct path', () => {
apiBuildPathSpy.and.returnValue('/foo/bar');
api.post('/foo');
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar');
});
it('should pass the data to \'request()\'', () => {
api.post('/foo', {}, {bar: 'baz'});
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[2]).toEqual({bar: 'baz'});
});
});
// Protected methods
describe('buildPath()', () => {
it('should return the pathname if no params', () => {
expect((api as any).buildPath('/foo')).toBe('/foo');
expect((api as any).buildPath('/foo', undefined)).toBe('/foo');
expect((api as any).buildPath('/foo', null)).toBe('/foo');
});
it('should append the params to the pathname', () => {
expect((api as any).buildPath('/foo', {bar: 'baz'})).toBe('/foo?bar=baz');
});
it('should join the params with \'&\'', () => {
expect((api as any).buildPath('/foo', {bar: 1, baz: 2})).toBe('/foo?bar=1&baz=2');
});
it('should ignore undefined/null params', () => {
expect((api as any).buildPath('/foo', {bar: undefined, baz: null})).toBe('/foo');
});
it('should encode param values as URI components', () => {
expect((api as any).buildPath('/foo', {bar: 'b a&z'})).toBe('/foo?bar=b%20a%26z');
});
});
describe('getPaginated()', () => {
let deferreds: {resolve: Function, reject: Function}[];
beforeEach(() => {
deferreds = [];
spyOn(api, 'get').and.callFake(() => new Promise((resolve, reject) => deferreds.push({resolve, reject})));
});
it('should return a promise', () => {
expect((api as any).getPaginated()).toEqual(jasmine.any(Promise));
});
it('should call \'get()\' with the correct pathname and params', () => {
(api as any).getPaginated('/foo/bar');
(api as any).getPaginated('/foo/bar', {baz: 'qux'});
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 0, per_page: 100});
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 0, per_page: 100});
});
it('should reject if the request fails', done => {
(api as any).getPaginated('/foo/bar').catch((err: any) => {
expect(err).toBe('Test');
done();
});
deferreds[0].reject('Test');
});
it('should resolve with the returned items', done => {
const items = [{id: 1}, {id: 2}];
(api as any).getPaginated('/foo/bar').then((data: any) => {
expect(data).toEqual(items);
done();
});
deferreds[0].resolve(items);
});
it('should iteratively call \'get()\' to fetch all items', done => {
// Create an array or 250 objects.
const allItems = '.'.repeat(250).split('').map((_, i) => ({id: i}));
const apiGetSpy = api.get as jasmine.Spy;
(api as any).getPaginated('/foo/bar', {baz: 'qux'}).then((data: any) => {
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
expect(apiGetSpy).toHaveBeenCalledTimes(3);
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(0)]);
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(1)]);
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(2)]);
expect(data).toEqual(allItems);
done();
});
deferreds[0].resolve(allItems.slice(0, 100));
setTimeout(() => {
deferreds[1].resolve(allItems.slice(100, 200));
setTimeout(() => {
deferreds[2].resolve(allItems.slice(200));
}, 0);
}, 0);
});
});
describe('request()', () => {
let httpsRequestSpy: jasmine.Spy;
let latestRequest: ClientRequest;
beforeEach(() => {
const originalRequest = https.request;
httpsRequestSpy = spyOn(https, 'request').and.callFake((...args: any[]) => {
latestRequest = originalRequest.apply(https, args);
spyOn(latestRequest, 'on').and.callThrough();
spyOn(latestRequest, 'end');
return latestRequest;
});
});
it('should return a promise', () => {
expect((api as any).request()).toEqual(jasmine.any(Promise));
});
it('should call \'https.request()\' with the correct options', () => {
(api as any).request('method', 'path');
expect(httpsRequestSpy).toHaveBeenCalled();
expect(httpsRequestSpy.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({
headers: jasmine.objectContaining({
'User-Agent': `Node/${process.versions.node}`,
}),
host: 'api.github.com',
method: 'method',
path: 'path',
}));
});
it('should call specify an \'Authorization\' header if \'githubToken\' is present', () => {
(api as any).request('method', 'path');
expect(httpsRequestSpy).toHaveBeenCalled();
expect(httpsRequestSpy.calls.argsFor(0)[0].headers).toEqual(jasmine.objectContaining({
Authorization: 'token 12345',
}));
});
it('should reject on request error', done => {
(api as any).request('method', 'path').catch((err: any) => {
expect(err).toBe('Test');
done();
});
latestRequest.emit('error', 'Test');
});
it('should send the request (i.e. call \'end()\')', () => {
(api as any).request('method', 'path');
expect(latestRequest.end).toHaveBeenCalled();
});
it('should \'JSON.stringify\' and send the data along with the request', () => {
(api as any).request('method', 'path');
expect(latestRequest.end).toHaveBeenCalledWith(null);
(api as any).request('method', 'path', {key: 'value'});
expect(latestRequest.end).toHaveBeenCalledWith('{"key":"value"}');
});
describe('onResponse', () => {
let promise: Promise<void>;
let respond: (statusCode: number) => IncomingMessage;
beforeEach(() => {
promise = (api as any).request('method', 'path');
respond = (statusCode: number) => {
const mockResponse = new EventEmitter() as IncomingMessage;
mockResponse.statusCode = statusCode;
const onResponse = httpsRequestSpy.calls.argsFor(0)[1];
onResponse(mockResponse);
return mockResponse;
};
});
it('should reject on response error', done => {
promise.catch(err => {
expect(err).toBe('Test');
done();
});
const res = respond(200);
res.emit('error', 'Test');
});
it('should reject if returned statusCode is <200', done => {
promise.catch(err => {
expect(err).toContain('failed');
expect(err).toContain('status: 199');
done();
});
const res = respond(199);
res.emit('end');
});
it('should reject if returned statusCode is >=400', done => {
promise.catch(err => {
expect(err).toContain('failed');
expect(err).toContain('status: 400');
done();
});
const res = respond(400);
res.emit('end');
});
it('should include the response text in the rejection message', done => {
promise.catch(err => {
expect(err).toContain('Test');
done();
});
const res = respond(500);
res.emit('data', 'Test');
res.emit('end');
});
it('should resolve if returned statusCode is <=200 <400', done => {
promise.then(done);
const res = respond(200);
res.emit('data', '{}');
res.emit('end');
});
it('should resolve with the response text \'JSON.parsed\'', done => {
promise.then(data => {
expect(data).toEqual({foo: 'bar'});
done();
});
const res = respond(300);
res.emit('data', '{"foo":"bar"}');
res.emit('end');
});
it('should collect and concatenate the whole response text', done => {
promise.then(data => {
expect(data).toEqual({foo: 'bar', baz: 'qux'});
done();
});
const res = respond(300);
res.emit('data', '{"foo":');
res.emit('data', '"bar","baz"');
res.emit('data', ':"qux"}');
res.emit('end');
});
it('should reject if the response text is malformed JSON', done => {
promise.catch(err => {
expect(err).toEqual(jasmine.any(SyntaxError));
done();
});
const res = respond(300);
res.emit('data', '}');
res.emit('end');
});
});
});
});

View File

@ -1,117 +0,0 @@
// Imports
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
// Tests
describe('GithubPullRequests', () => {
describe('constructor()', () => {
it('should throw if \'githubToken\' is missing or empty', () => {
expect(() => new GithubPullRequests('', 'foo/bar')).
toThrowError('Missing or empty required parameter \'githubToken\'!');
});
it('should throw if \'repoSlug\' is missing or empty', () => {
expect(() => new GithubPullRequests('12345', '')).
toThrowError('Missing or empty required parameter \'repoSlug\'!');
});
});
describe('addComment()', () => {
let prs: GithubPullRequests;
let deferred: {resolve: Function, reject: Function};
beforeEach(() => {
prs = new GithubPullRequests('12345', 'foo/bar');
spyOn(prs, 'post').and.callFake(() => new Promise((resolve, reject) => deferred = {resolve, reject}));
});
it('should return a promise', () => {
expect(prs.addComment(42, 'body')).toEqual(jasmine.any(Promise));
});
it('should throw if the PR number is invalid', () => {
expect(() => prs.addComment(-1337, 'body')).toThrowError(`Invalid PR number: -1337`);
expect(() => prs.addComment(NaN, 'body')).toThrowError(`Invalid PR number: NaN`);
});
it('should throw if the comment body is invalid or empty', () => {
expect(() => prs.addComment(42, '')).toThrowError(`Invalid or empty comment body: `);
});
it('should call \'post()\' with the correct pathname, params and data', () => {
prs.addComment(42, 'body');
expect(prs.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'});
});
it('should reject if the request fails', done => {
prs.addComment(42, 'body').catch(err => {
expect(err).toBe('Test');
done();
});
deferred.reject('Test');
});
it('should resolve with the returned response', done => {
prs.addComment(42, 'body').then(data => {
expect(data).toEqual('Test');
done();
});
deferred.resolve('Test');
});
});
describe('fetchAll()', () => {
let prs: GithubPullRequests;
let prsGetPaginatedSpy: jasmine.Spy;
beforeEach(() => {
prs = new GithubPullRequests('12345', 'foo/bar');
prsGetPaginatedSpy = spyOn(prs as any, 'getPaginated');
spyOn(console, 'log');
});
it('should call \'getPaginated()\' with the correct pathname and params', () => {
const expectedPathname = '/repos/foo/bar/pulls';
prs.fetchAll('all');
prs.fetchAll('closed');
prs.fetchAll('open');
expect(prsGetPaginatedSpy).toHaveBeenCalledTimes(3);
expect(prsGetPaginatedSpy.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]);
expect(prsGetPaginatedSpy.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]);
expect(prsGetPaginatedSpy.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]);
});
it('should default to \'all\' if no state is specified', () => {
prs.fetchAll();
expect(prsGetPaginatedSpy).toHaveBeenCalledWith('/repos/foo/bar/pulls', {state: 'all'});
});
it('should forward the value returned by \'getPaginated()\'', () => {
prsGetPaginatedSpy.and.returnValue('Test');
expect(prs.fetchAll()).toBe('Test');
});
});
});

View File

@ -1,232 +0,0 @@
// Imports
import {GithubTeams} from '../../lib/common/github-teams';
// Tests
describe('GithubTeams', () => {
describe('constructor()', () => {
it('should throw if \'githubToken\' is missing or empty', () => {
expect(() => new GithubTeams('', 'org')).
toThrowError('Missing or empty required parameter \'githubToken\'!');
});
it('should throw if \'organization\' is missing or empty', () => {
expect(() => new GithubTeams('12345', '')).
toThrowError('Missing or empty required parameter \'organization\'!');
});
});
describe('fetchAll()', () => {
let teams: GithubTeams;
let teamsGetPaginatedSpy: jasmine.Spy;
beforeEach(() => {
teams = new GithubTeams('12345', 'foo');
teamsGetPaginatedSpy = spyOn(teams as any, 'getPaginated');
});
it('should call \'getPaginated()\' with the correct pathname and params', () => {
teams.fetchAll();
expect(teamsGetPaginatedSpy).toHaveBeenCalledWith('/orgs/foo/teams');
});
it('should forward the value returned by \'getPaginated()\'', () => {
teamsGetPaginatedSpy.and.returnValue('Test');
expect(teams.fetchAll()).toBe('Test');
});
});
describe('isMemberById()', () => {
let teams: GithubTeams;
let teamsGetSpy: jasmine.Spy;
beforeEach(() => {
teams = new GithubTeams('12345', 'foo');
teamsGetSpy = spyOn(teams, 'get');
});
it('should return a promise', () => {
expect(teams.isMemberById('user', [1])).toEqual(jasmine.any(Promise));
});
it('should resolve with false if called with an empty array', done => {
teams.isMemberById('user', []).then(isMember => {
expect(isMember).toBe(false);
expect(teamsGetSpy).not.toHaveBeenCalled();
done();
});
});
it('should call \'get()\' with the correct pathname', done => {
teamsGetSpy.and.returnValue(Promise.resolve(null));
teams.isMemberById('user', [1]).then(() => {
expect(teamsGetSpy).toHaveBeenCalledWith('/teams/1/memberships/user');
done();
});
});
it('should resolve with false if \'get()\' rejects', done => {
teamsGetSpy.and.returnValue(Promise.reject(null));
teams.isMemberById('user', [1]).then(isMember => {
expect(isMember).toBe(false);
expect(teamsGetSpy).toHaveBeenCalled();
done();
});
});
it('should resolve with false if the membership is not active', done => {
teamsGetSpy.and.returnValue(Promise.resolve({state: 'pending'}));
teams.isMemberById('user', [1]).then(isMember => {
expect(isMember).toBe(false);
expect(teamsGetSpy).toHaveBeenCalled();
done();
});
});
it('should resolve with true if the membership is active', done => {
teamsGetSpy.and.returnValue(Promise.resolve({state: 'active'}));
teams.isMemberById('user', [1]).then(isMember => {
expect(isMember).toBe(true);
done();
});
});
it('should sequentially call \'get()\' until an active membership is found', done => {
const trainedResponses: {[pathname: string]: Promise<{state: string}>} = {
'/teams/1/memberships/user': Promise.resolve({state: 'pending'}),
'/teams/2/memberships/user': Promise.reject(null),
'/teams/3/memberships/user': Promise.resolve({state: 'active'}),
};
teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]);
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
expect(isMember).toBe(true);
expect(teamsGetSpy).toHaveBeenCalledTimes(3);
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
done();
});
});
it('should resolve with false if no active membership is found', done => {
const trainedResponses: {[pathname: string]: Promise<{state: string}>} = {
'/teams/1/memberships/user': Promise.resolve({state: 'pending'}),
'/teams/2/memberships/user': Promise.reject(null),
'/teams/3/memberships/user': Promise.resolve({state: 'not active'}),
'/teams/4/memberships/user': Promise.reject(null),
};
teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]);
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
expect(isMember).toBe(false);
expect(teamsGetSpy).toHaveBeenCalledTimes(4);
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
expect(teamsGetSpy.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user');
done();
});
});
});
describe('isMemberBySlug()', () => {
let teams: GithubTeams;
let teamsFetchAllSpy: jasmine.Spy;
let teamsIsMemberByIdSpy: jasmine.Spy;
beforeEach(() => {
teams = new GithubTeams('12345', 'foo');
const mockResponse = Promise.resolve([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]);
teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.returnValue(mockResponse);
teamsIsMemberByIdSpy = spyOn(teams, 'isMemberById');
});
it('should return a promise', () => {
expect(teams.isMemberBySlug('user', ['team-slug'])).toEqual(jasmine.any(Promise));
});
it('should call \'fetchAll()\'', () => {
teams.isMemberBySlug('user', ['team-slug']);
expect(teamsFetchAllSpy).toHaveBeenCalled();
});
it('should resolve with false if \'fetchAll()\' rejects', done => {
teamsFetchAllSpy.and.returnValue(Promise.reject(null));
teams.isMemberBySlug('user', ['team-slug']).then(isMember => {
expect(isMember).toBe(false);
done();
});
});
it('should call \'isMemberById()\' with the correct params if no team is found', done => {
teams.isMemberBySlug('user', ['no-match']).then(() => {
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', []);
done();
});
});
it('should call \'isMemberById()\' with the correct params if teams are found', done => {
const spy = teamsIsMemberByIdSpy;
Promise.all([
teams.isMemberBySlug('user', ['team1']).then(() => expect(spy).toHaveBeenCalledWith('user', [1])),
teams.isMemberBySlug('user', ['team2']).then(() => expect(spy).toHaveBeenCalledWith('user', [2])),
teams.isMemberBySlug('user', ['team1', 'team2']).then(() => expect(spy).toHaveBeenCalledWith('user', [1, 2])),
]).then(done);
});
it('should resolve with false if \'isMemberById()\' rejects', done => {
teamsIsMemberByIdSpy.and.returnValue(Promise.reject(null));
teams.isMemberBySlug('user', ['team1']).then(isMember => {
expect(isMember).toBe(false);
expect(teamsIsMemberByIdSpy).toHaveBeenCalled();
done();
});
});
it('should resolve with the value \'isMemberById()\' resolves with', done => {
teamsIsMemberByIdSpy.and.returnValues(Promise.resolve(false), Promise.resolve(true));
Promise.all([
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(false)),
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(true)),
]).then(() => {
expect(teamsIsMemberByIdSpy).toHaveBeenCalledTimes(2);
done();
});
});
});
});

View File

@ -1,81 +0,0 @@
// Imports
import {assertNotMissingOrEmpty, getEnvVar} from '../../lib/common/utils';
// Tests
describe('utils', () => {
describe('assertNotMissingOrEmpty()', () => {
it('should throw if passed an empty value', () => {
expect(() => assertNotMissingOrEmpty('foo', undefined)).
toThrowError('Missing or empty required parameter \'foo\'!');
expect(() => assertNotMissingOrEmpty('bar', null)).toThrowError('Missing or empty required parameter \'bar\'!');
expect(() => assertNotMissingOrEmpty('baz', '')).toThrowError('Missing or empty required parameter \'baz\'!');
});
it('should not throw if passed a non-empty value', () => {
expect(() => assertNotMissingOrEmpty('foo', ' ')).not.toThrow();
expect(() => assertNotMissingOrEmpty('bar', 'bar')).not.toThrow();
expect(() => assertNotMissingOrEmpty('baz', 'b a z')).not.toThrow();
});
});
describe('getEnvVar()', () => {
const emptyVar = '$$test_utils_getEnvVar_empty$$';
const nonEmptyVar = '$$test_utils_getEnvVar_nonEmpty$$';
const undefinedVar = '$$test_utils_getEnvVar_undefined$$';
beforeEach(() => {
process.env[emptyVar] = '';
process.env[nonEmptyVar] = 'foo';
});
afterEach(() => {
delete process.env[emptyVar];
delete process.env[nonEmptyVar];
});
it('should return an environment variable', () => {
expect(getEnvVar(nonEmptyVar)).toBe('foo');
});
it('should exit with an error if the environment variable is not defined', () => {
const consoleErrorSpy = spyOn(console, 'error');
const processExitSpy = spyOn(process, 'exit');
getEnvVar(undefinedVar);
expect(consoleErrorSpy).toHaveBeenCalled();
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain(undefinedVar);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it('should exit with an error if the environment variable is empty', () => {
const consoleErrorSpy = spyOn(console, 'error');
const processExitSpy = spyOn(process, 'exit');
getEnvVar(emptyVar);
expect(consoleErrorSpy).toHaveBeenCalled();
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain(emptyVar);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it('should return an empty string if an undefined variable is optional', () => {
expect(getEnvVar(undefinedVar, true)).toBe('');
});
it('should return an empty string if an empty variable is optional', () => {
expect(getEnvVar(emptyVar, true)).toBe('');
});
});
});

View File

@ -1,6 +0,0 @@
declare namespace jasmine {
export interface DoneFn extends Function {
(): void;
fail: (message: Error | string) => void;
}
}

View File

@ -1,7 +0,0 @@
// Imports
import {runTests} from '../lib/common/run-tests';
// Run
const specFiles = [`${__dirname}/**/*.spec.js`];
const helpers = [`${__dirname}/helpers.js`];
runTests(specFiles, helpers);

View File

@ -1,320 +0,0 @@
// Imports
import * as cp from 'child_process';
import {EventEmitter} from 'events';
import * as fs from 'fs';
import * as shell from 'shelljs';
import {BuildCreator} from '../../lib/upload-server/build-creator';
import {CreatedBuildEvent} from '../../lib/upload-server/build-events';
import {UploadError} from '../../lib/upload-server/upload-error';
import {expectToBeUploadError} from './helpers';
// Tests
describe('BuildCreator', () => {
const pr = '9';
const sha = '9'.repeat(40);
const archive = 'snapshot.tar.gz';
const buildsDir = 'builds/dir';
const prDir = `${buildsDir}/${pr}`;
const shaDir = `${prDir}/${sha}`;
let bc: BuildCreator;
beforeEach(() => bc = new BuildCreator(buildsDir));
describe('constructor()', () => {
it('should throw if \'buildsDir\' is missing or empty', () => {
expect(() => new BuildCreator('')).toThrowError('Missing or empty required parameter \'buildsDir\'!');
});
it('should extend EventEmitter', () => {
expect(bc).toEqual(jasmine.any(BuildCreator));
expect(bc).toEqual(jasmine.any(EventEmitter));
expect(Object.getPrototypeOf(bc)).toBe(BuildCreator.prototype);
});
});
describe('create()', () => {
let bcEmitSpy: jasmine.Spy;
let bcExistsSpy: jasmine.Spy;
let bcExtractArchiveSpy: jasmine.Spy;
let shellMkdirSpy: jasmine.Spy;
let shellRmSpy: jasmine.Spy;
beforeEach(() => {
bcEmitSpy = spyOn(bc, 'emit');
bcExistsSpy = spyOn(bc as any, 'exists');
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
shellMkdirSpy = spyOn(shell, 'mkdir');
shellRmSpy = spyOn(shell, 'rm');
});
it('should return a promise', done => {
const promise = bc.create(pr, sha, archive);
promise.then(done); // Do not complete the test (and release the spies) synchronously
// to avoid running the actual `extractArchive()`.
expect(promise).toEqual(jasmine.any(Promise));
});
it('should throw if the build does already exist', done => {
bcExistsSpy.and.returnValue(true);
bc.create(pr, sha, archive).catch(err => {
expectToBeUploadError(err, 403, `Request to overwrite existing directory: ${shaDir}`);
done();
});
});
it('should create the build directory (and any parent directories)', done => {
bc.create(pr, sha, archive).
then(() => expect(shellMkdirSpy).toHaveBeenCalledWith('-p', shaDir)).
then(done);
});
it('should extract the archive contents into the build directory', done => {
bc.create(pr, sha, archive).
then(() => expect(bcExtractArchiveSpy).toHaveBeenCalledWith(archive, shaDir)).
then(done);
});
it('should emit a CreatedBuildEvent on success', done => {
let emitted = false;
bcEmitSpy.and.callFake((type: string, evt: CreatedBuildEvent) => {
expect(type).toBe(CreatedBuildEvent.type);
expect(evt).toEqual(jasmine.any(CreatedBuildEvent));
expect(evt.pr).toBe(+pr);
expect(evt.sha).toBe(sha);
emitted = true;
});
bc.create(pr, sha, archive).
then(() => expect(emitted).toBe(true)).
then(done);
});
describe('on error', () => {
it('should abort and skip further operations if it fails to create the directories', done => {
shellMkdirSpy.and.throwError('');
bc.create(pr, sha, archive).catch(() => {
expect(shellMkdirSpy).toHaveBeenCalled();
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
done();
});
});
it('should abort and skip further operations if it fails to extract the archive', done => {
bcExtractArchiveSpy.and.throwError('');
bc.create(pr, sha, archive).catch(() => {
expect(shellMkdirSpy).toHaveBeenCalled();
expect(bcExtractArchiveSpy).toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
done();
});
});
it('should delete the PR directory (for new PR)', done => {
bcExtractArchiveSpy.and.throwError('');
bc.create(pr, sha, archive).catch(() => {
expect(shellRmSpy).toHaveBeenCalledWith('-rf', prDir);
done();
});
});
it('should delete the SHA directory (for existing PR)', done => {
bcExistsSpy.and.callFake((path: string) => path !== shaDir);
bcExtractArchiveSpy.and.throwError('');
bc.create(pr, sha, archive).catch(() => {
expect(shellRmSpy).toHaveBeenCalledWith('-rf', shaDir);
done();
});
});
it('should reject with an UploadError', done => {
shellMkdirSpy.and.callFake(() => {throw 'Test'; });
bc.create(pr, sha, archive).catch(err => {
expectToBeUploadError(err, 500, `Error while uploading to directory: ${shaDir}\nTest`);
done();
});
});
it('should pass UploadError instances unmodified', done => {
shellMkdirSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
bc.create(pr, sha, archive).catch(err => {
expectToBeUploadError(err, 543, 'Test');
done();
});
});
});
});
// Protected methods
describe('exists()', () => {
let fsAccessSpy: jasmine.Spy;
let fsAccessCbs: Function[];
beforeEach(() => {
fsAccessCbs = [];
fsAccessSpy = spyOn(fs, 'access').and.callFake((_: string, cb: Function) => fsAccessCbs.push(cb));
});
it('should return a promise', () => {
expect((bc as any).exists('foo')).toEqual(jasmine.any(Promise));
});
it('should call \'fs.access()\' with the specified argument', () => {
(bc as any).exists('foo');
expect(fs.access).toHaveBeenCalledWith('foo', jasmine.any(Function));
});
it('should resolve with \'true\' if \'fs.access()\' succeeds', done => {
Promise.
all([(bc as any).exists('foo'), (bc as any).exists('bar')]).
then(results => expect(results).toEqual([true, true])).
then(done);
fsAccessCbs[0]();
fsAccessCbs[1](null);
});
it('should resolve with \'false\' if \'fs.access()\' errors', done => {
Promise.
all([(bc as any).exists('foo'), (bc as any).exists('bar')]).
then(results => expect(results).toEqual([false, false])).
then(done);
fsAccessCbs[0]('Error');
fsAccessCbs[1](new Error());
});
});
describe('extractArchive()', () => {
let consoleWarnSpy: jasmine.Spy;
let shellChmodSpy: jasmine.Spy;
let shellRmSpy: jasmine.Spy;
let cpExecSpy: jasmine.Spy;
let cpExecCbs: Function[];
beforeEach(() => {
cpExecCbs = [];
consoleWarnSpy = spyOn(console, 'warn');
shellChmodSpy = spyOn(shell, 'chmod');
shellRmSpy = spyOn(shell, 'rm');
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: Function) => cpExecCbs.push(cb));
});
it('should return a promise', () => {
expect((bc as any).extractArchive('foo', 'bar')).toEqual(jasmine.any(Promise));
});
it('should "gunzip" and "untar" the input file into the output directory', () => {
const cmd = 'tar --extract --gzip --directory "output/dir" --file "input/file"';
(bc as any).extractArchive('input/file', 'output/dir');
expect(cpExecSpy).toHaveBeenCalledWith(cmd, jasmine.any(Function));
});
it('should log (as a warning) any stderr output if extracting succeeded', done => {
(bc as any).extractArchive('foo', 'bar').
then(() => expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr')).
then(done);
cpExecCbs[0](null, 'This is stdout', 'This is stderr');
});
it('should make the build directory non-writable', done => {
(bc as any).extractArchive('foo', 'bar').
then(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a-w', 'bar')).
then(done);
cpExecCbs[0]();
});
it('should delete the uploaded file on success', done => {
(bc as any).extractArchive('input/file', 'output/dir').
then(() => expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file')).
then(done);
cpExecCbs[0]();
});
describe('on error', () => {
it('should abort and skip further operations if it fails to extract the archive', done => {
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
expect(shellChmodSpy).not.toHaveBeenCalled();
expect(shellRmSpy).not.toHaveBeenCalled();
expect(err).toBe('Test');
done();
});
cpExecCbs[0]('Test');
});
it('should abort and skip further operations if it fails to make non-writable', done => {
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
expect(shellChmodSpy).toHaveBeenCalled();
expect(shellRmSpy).not.toHaveBeenCalled();
expect(err).toBe('Test');
done();
});
shellChmodSpy.and.callFake(() => { throw 'Test'; });
cpExecCbs[0]();
});
it('should abort and reject if it fails to remove the uploaded file', done => {
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
expect(shellChmodSpy).toHaveBeenCalled();
expect(shellRmSpy).toHaveBeenCalled();
expect(err).toBe('Test');
done();
});
shellRmSpy.and.callFake(() => { throw 'Test'; });
cpExecCbs[0]();
});
});
});
});

View File

@ -1,61 +0,0 @@
// Imports
import {BuildEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
// Tests
describe('BuildEvent', () => {
let evt: BuildEvent;
beforeEach(() => evt = new BuildEvent('foo', 42, 'bar'));
it('should have a \'type\' property', () => {
expect(evt.type).toBe('foo');
});
it('should have a \'pr\' property', () => {
expect(evt.pr).toBe(42);
});
it('should have a \'sha\' property', () => {
expect(evt.sha).toBe('bar');
});
});
describe('CreatedBuildEvent', () => {
let evt: CreatedBuildEvent;
beforeEach(() => evt = new CreatedBuildEvent(42, 'bar'));
it('should have a static \'type\' property', () => {
expect(CreatedBuildEvent.type).toBe('build.created');
});
it('should extend BuildEvent', () => {
expect(evt).toEqual(jasmine.any(CreatedBuildEvent));
expect(evt).toEqual(jasmine.any(BuildEvent));
expect(Object.getPrototypeOf(evt)).toBe(CreatedBuildEvent.prototype);
});
it('should automatically set the \'type\'', () => {
expect(evt.type).toBe(CreatedBuildEvent.type);
});
it('should have a \'pr\' property', () => {
expect(evt.pr).toBe(42);
});
it('should have a \'sha\' property', () => {
expect(evt.sha).toBe('bar');
});
});

View File

@ -1,253 +0,0 @@
// Imports
import * as jwt from 'jsonwebtoken';
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
import {GithubTeams} from '../../lib/common/github-teams';
import {BuildVerifier} from '../../lib/upload-server/build-verifier';
import {expectToBeUploadError} from './helpers';
// Tests
describe('BuildVerifier', () => {
const defaultConfig = {
allowedTeamSlugs: ['team1', 'team2'],
githubToken: 'githubToken',
organization: 'organization',
repoSlug: 'repo/slug',
secret: 'secret',
};
let bv: BuildVerifier;
// Helpers
const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => {
const cfg = {...defaultConfig, ...partialConfig};
return new BuildVerifier(cfg.secret, cfg.githubToken, cfg.repoSlug, cfg.organization,
cfg.allowedTeamSlugs);
};
beforeEach(() => bv = createBuildVerifier());
describe('constructor()', () => {
['secret', 'githubToken', 'repoSlug', 'organization', 'allowedTeamSlugs'].forEach(param => {
it(`should throw if '${param}' is missing or empty`, () => {
expect(() => createBuildVerifier({[param]: ''})).
toThrowError(`Missing or empty required parameter '${param}'!`);
});
});
it('should throw if \'allowedTeamSlugs\' is an empty array', () => {
expect(() => createBuildVerifier({allowedTeamSlugs: []})).
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
});
});
describe('verify()', () => {
const pr = 9;
const defaultJwt = {
'exp': Math.floor(Date.now() / 1000) + 30,
'iat': Math.floor(Date.now() / 1000) - 30,
'iss': 'Travis CI, GmbH',
'pull-request': pr,
'slug': defaultConfig.repoSlug,
};
let bvGetPrAuthorTeamMembership: jasmine.Spy;
// Heleprs
const createAuthHeader = (partialJwt: Partial<typeof defaultJwt> = {}, secret: string = defaultConfig.secret) =>
`Token ${jwt.sign({...defaultJwt, ...partialJwt}, secret)}`;
beforeEach(() => {
bvGetPrAuthorTeamMembership = spyOn(bv, 'getPrAuthorTeamMembership').
and.returnValue(Promise.resolve({author: 'some-author', isMember: true}));
});
it('should return a promise', () => {
expect(bv.verify(pr, createAuthHeader())).toEqual(jasmine.any(Promise));
});
it('should fail if the authorization header is invalid', done => {
bv.verify(pr, 'foo').catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: jwt malformed';
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should fail if the secret is invalid', done => {
bv.verify(pr, createAuthHeader({}, 'foo')).catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: invalid signature';
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should fail if the issuer is invalid', done => {
bv.verify(pr, createAuthHeader({iss: 'not valid'})).catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: ' +
`jwt issuer invalid. expected: ${defaultJwt.iss}`;
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should fail if the token has expired', done => {
bv.verify(pr, createAuthHeader({exp: 0})).catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: jwt expired';
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should fail if the repo slug does not match', done => {
bv.verify(pr, createAuthHeader({slug: 'foo/bar'})).catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: ' +
`jwt slug invalid. expected: ${defaultConfig.repoSlug}`;
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should fail if the PR does not match', done => {
bv.verify(pr, createAuthHeader({'pull-request': 1337})).catch(err => {
const errorMessage = 'Error while verifying upload for PR 9: ' +
`jwt pull-request invalid. expected: ${pr}`;
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should not fail if the token is valid', done => {
bv.verify(pr, createAuthHeader()).then(done);
});
it('should not fail even if the token has been issued in the future', done => {
const in30s = Math.floor(Date.now() / 1000) + 30;
bv.verify(pr, createAuthHeader({iat: in30s})).then(done);
});
it('should call \'getPrAuthorTeamMembership()\' if the token is valid', done => {
bv.verify(pr, createAuthHeader()).then(() => {
expect(bvGetPrAuthorTeamMembership).toHaveBeenCalledWith(pr);
done();
});
});
it('should fail if \'getPrAuthorTeamMembership()\' rejects', done => {
bvGetPrAuthorTeamMembership.and.callFake(() => Promise.reject('Test'));
bv.verify(pr, createAuthHeader()).catch(err => {
expectToBeUploadError(err, 403, `Error while verifying upload for PR ${pr}: Test`);
done();
});
});
it('should fail if \'getPrAuthorTeamMembership()\' reports no membership', done => {
const errorMessage = `Error while verifying upload for PR ${pr}: User 'test' is not an active member of any of ` +
'the following teams: team1, team2';
bvGetPrAuthorTeamMembership.and.returnValue(Promise.resolve({author: 'test', isMember: false}));
bv.verify(pr, createAuthHeader()).catch(err => {
expectToBeUploadError(err, 403, errorMessage);
done();
});
});
it('should succeed if everything checks outs', done => {
bv.verify(pr, createAuthHeader()).then(done);
});
});
describe('getPrAuthorTeamMembership()', () => {
const pr = 9;
let prsFetchSpy: jasmine.Spy;
let teamsIsMemberBySlugSpy: jasmine.Spy;
beforeEach(() => {
prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch').
and.returnValue(Promise.resolve({user: {login: 'username'}}));
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').
and.returnValue(Promise.resolve(true));
});
it('should return a promise', () => {
expect(bv.getPrAuthorTeamMembership(pr)).toEqual(jasmine.any(Promise));
});
it('should fetch the corresponding PR', done => {
bv.getPrAuthorTeamMembership(pr).then(() => {
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
done();
});
});
it('should fail if fetching the PR errors', done => {
prsFetchSpy.and.callFake(() => Promise.reject('Test'));
bv.getPrAuthorTeamMembership(pr).catch(err => {
expect(err).toBe('Test');
done();
});
});
it('should verify the PR author\'s membership in the specified teams', done => {
bv.getPrAuthorTeamMembership(pr).then(() => {
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
done();
});
});
it('should fail if verifying membership errors', done => {
teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test'));
bv.getPrAuthorTeamMembership(pr).catch(err => {
expect(err).toBe('Test');
done();
});
});
it('should return the PR\'s author and whether they are members', done => {
teamsIsMemberBySlugSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
Promise.all([
bv.getPrAuthorTeamMembership(pr).then(({author, isMember}) => {
expect(author).toBe('username');
expect(isMember).toBe(true);
}),
bv.getPrAuthorTeamMembership(pr).then(({author, isMember}) => {
expect(author).toBe('username');
expect(isMember).toBe(false);
}),
]).then(done);
});
});
});

View File

@ -1,11 +0,0 @@
import {UploadError} from '../../lib/upload-server/upload-error';
export const expectToBeUploadError = (actual: UploadError, status?: number, message?: string) => {
expect(actual).toEqual(jasmine.any(UploadError));
if (status != null) {
expect(actual.status).toBe(status);
}
if (message != null) {
expect(actual.message).toBe(message);
}
};

View File

@ -1,39 +0,0 @@
// Imports
import {UploadError} from '../../lib/upload-server/upload-error';
// Tests
describe('UploadError', () => {
let err: UploadError;
beforeEach(() => err = new UploadError(999, 'message'));
it('should extend Error', () => {
expect(err).toEqual(jasmine.any(UploadError));
expect(err).toEqual(jasmine.any(Error));
expect(Object.getPrototypeOf(err)).toBe(UploadError.prototype);
});
it('should have a \'status\' property', () => {
expect(err.status).toBe(999);
});
it('should have a \'message\' property', () => {
expect(err.message).toBe('message');
});
it('should have a 500 \'status\' by default', () => {
expect(new UploadError().status).toBe(500);
});
it('should have an empty \'message\' by default', () => {
expect(new UploadError().message).toBe('');
expect(new UploadError(999).message).toBe('');
});
});

View File

@ -1,405 +0,0 @@
// Imports
import * as express from 'express';
import * as http from 'http';
import * as supertest from 'supertest';
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
import {BuildCreator} from '../../lib/upload-server/build-creator';
import {CreatedBuildEvent} from '../../lib/upload-server/build-events';
import {BuildVerifier} from '../../lib/upload-server/build-verifier';
import {uploadServerFactory as usf} from '../../lib/upload-server/upload-server-factory';
// Tests
describe('uploadServerFactory', () => {
const defaultConfig = {
buildsDir: 'builds/dir',
domainName: 'domain.name',
githubOrganization: 'organization',
githubTeamSlugs: ['team1', 'team2'],
githubToken: '12345',
repoSlug: 'repo/slug',
secret: 'secret',
};
// Helpers
const createUploadServer = (partialConfig: Partial<typeof defaultConfig> = {}) =>
usf.create({...defaultConfig, ...partialConfig});
describe('create()', () => {
let usfCreateMiddlewareSpy: jasmine.Spy;
beforeEach(() => {
usfCreateMiddlewareSpy = spyOn(usf as any, 'createMiddleware').and.callThrough();
});
it('should throw if \'buildsDir\' is missing or empty', () => {
expect(() => createUploadServer({buildsDir: ''})).
toThrowError('Missing or empty required parameter \'buildsDir\'!');
});
it('should throw if \'domainName\' is missing or empty', () => {
expect(() => createUploadServer({domainName: ''})).
toThrowError('Missing or empty required parameter \'domainName\'!');
});
it('should throw if \'githubToken\' is missing or empty', () => {
expect(() => createUploadServer({githubToken: ''})).
toThrowError('Missing or empty required parameter \'githubToken\'!');
});
it('should throw if \'githubOrganization\' is missing or empty', () => {
expect(() => createUploadServer({githubOrganization: ''})).
toThrowError('Missing or empty required parameter \'organization\'!');
});
it('should throw if \'githubTeamSlugs\' is missing or empty', () => {
expect(() => createUploadServer({githubTeamSlugs: []})).
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
});
it('should throw if \'repoSlug\' is missing or empty', () => {
expect(() => createUploadServer({repoSlug: ''})).
toThrowError('Missing or empty required parameter \'repoSlug\'!');
});
it('should throw if \'secret\' is missing or empty', () => {
expect(() => createUploadServer({secret: ''})).
toThrowError('Missing or empty required parameter \'secret\'!');
});
it('should return an http.Server', () => {
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
const server = createUploadServer();
expect(server).toBe(httpCreateServerSpy.calls.mostRecent().returnValue);
});
it('should create and use an appropriate BuildCreator', () => {
const usfCreateBuildCreatorSpy = spyOn(usf as any, 'createBuildCreator').and.callThrough();
createUploadServer();
const buildCreator: BuildCreator = usfCreateBuildCreatorSpy.calls.mostRecent().returnValue;
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(jasmine.any(BuildVerifier), buildCreator);
expect(usfCreateBuildCreatorSpy).toHaveBeenCalledWith('builds/dir', '12345', 'repo/slug', 'domain.name');
});
it('should create and use an appropriate middleware', () => {
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
createUploadServer();
const middleware: express.Express = usfCreateMiddlewareSpy.calls.mostRecent().returnValue;
const buildVerifier = jasmine.any(BuildVerifier);
const buildCreator = jasmine.any(BuildCreator);
expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware);
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildVerifier, buildCreator);
});
it('should log the server address info on \'listening\'', () => {
const consoleInfoSpy = spyOn(console, 'info');
const server = createUploadServer('builds/dir');
server.address = () => ({address: 'foo', family: '', port: 1337});
expect(consoleInfoSpy).not.toHaveBeenCalled();
server.emit('listening');
expect(consoleInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...');
});
});
// Protected methods
describe('createBuildCreator()', () => {
let buildCreator: BuildCreator;
beforeEach(() => {
buildCreator = (usf as any).createBuildCreator(
defaultConfig.buildsDir,
defaultConfig.githubToken,
defaultConfig.repoSlug,
defaultConfig.domainName,
);
});
it('should pass the \'buildsDir\' to the BuildCreator', () => {
expect((buildCreator as any).buildsDir).toBe('builds/dir');
});
it('should post a comment on GitHub on \'build.created\'', () => {
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
const commentBody = 'The angular.io preview for 1234567 is available [here][1].\n\n' +
'[1]: https://pr42-1234567890.domain.name/';
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'});
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
});
it('should pass the correct \'githubToken\' and \'repoSlug\' to GithubPullRequests', () => {
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'});
const prs = prsAddCommentSpy.calls.mostRecent().object;
expect(prs).toEqual(jasmine.any(GithubPullRequests));
expect((prs as any).repoSlug).toBe('repo/slug');
expect((prs as any).requestHeaders.Authorization).toContain('12345');
});
});
describe('createMiddleware()', () => {
let buildVerifier: BuildVerifier;
let buildCreator: BuildCreator;
let agent: supertest.SuperTest<supertest.Test>;
// Helpers
const promisifyRequest = (req: supertest.Request) =>
new Promise((resolve, reject) => req.end(err => err ? reject(err) : resolve()));
const verifyRequests = (reqs: supertest.Request[], done: jasmine.DoneFn) =>
Promise.all(reqs.map(promisifyRequest)).then(done, done.fail);
beforeEach(() => {
buildVerifier = new BuildVerifier(
defaultConfig.secret,
defaultConfig.githubToken,
defaultConfig.repoSlug,
defaultConfig.githubOrganization,
defaultConfig.githubTeamSlugs,
);
buildCreator = new BuildCreator(defaultConfig.buildsDir);
agent = supertest.agent((usf as any).createMiddleware(buildVerifier, buildCreator));
spyOn(console, 'error');
});
describe('GET /create-build/<pr>/<sha>', () => {
const pr = '9';
const sha = '9'.repeat(40);
let buildVerifierVerifySpy: jasmine.Spy;
let buildCreatorCreateSpy: jasmine.Spy;
beforeEach(() => {
buildVerifierVerifySpy = spyOn(buildVerifier, 'verify').and.returnValue(Promise.resolve());
buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(Promise.resolve());
});
it('should respond with 405 for non-GET requests', done => {
verifyRequests([
agent.put(`/create-build/${pr}/${sha}`).expect(405),
agent.post(`/create-build/${pr}/${sha}`).expect(405),
agent.patch(`/create-build/${pr}/${sha}`).expect(405),
agent.delete(`/create-build/${pr}/${sha}`).expect(405),
], done);
});
it('should respond with 401 for requests without an \'AUTHORIZATION\' header', done => {
const url = `/create-build/${pr}/${sha}`;
const responseBody = `Missing or empty 'AUTHORIZATION' header in request: GET ${url}`;
verifyRequests([
agent.get(url).expect(401, responseBody),
agent.get(url).set('AUTHORIZATION', '').expect(401, responseBody),
], done);
});
it('should respond with 400 for requests without an \'X-FILE\' header', done => {
const url = `/create-build/${pr}/${sha}`;
const responseBody = `Missing or empty 'X-FILE' header in request: GET ${url}`;
const request1 = agent.get(url).set('AUTHORIZATION', 'foo');
const request2 = agent.get(url).set('AUTHORIZATION', 'foo').set('X-FILE', '');
verifyRequests([
request1.expect(400, responseBody),
request2.expect(400, responseBody),
], done);
});
it('should respond with 404 for unknown paths', done => {
verifyRequests([
agent.get(`/foo/create-build/${pr}/${sha}`).expect(404),
agent.get(`/foo-create-build/${pr}/${sha}`).expect(404),
agent.get(`/fooncreate-build/${pr}/${sha}`).expect(404),
agent.get(`/create-build/foo/${pr}/${sha}`).expect(404),
agent.get(`/create-build-foo/${pr}/${sha}`).expect(404),
agent.get(`/create-buildnfoo/${pr}/${sha}`).expect(404),
agent.get(`/create-build/pr${pr}/${sha}`).expect(404),
agent.get(`/create-build/${pr}/${sha}42`).expect(404),
], done);
});
it('should call \'BuildVerifier#verify()\' with the correct arguments', done => {
const req = agent.
get(`/create-build/${pr}/${sha}`).
set('AUTHORIZATION', 'foo').
set('X-FILE', 'bar');
promisifyRequest(req).
then(() => expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo')).
then(done, done.fail);
});
it('should propagate errors from BuildVerifier', done => {
buildVerifierVerifySpy.and.callFake(() => Promise.reject('Test'));
const req = agent.
get(`/create-build/${pr}/${sha}`).
set('AUTHORIZATION', 'foo').
set('X-FILE', 'bar').
expect(500, 'Test');
promisifyRequest(req).
then(() => {
expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo');
expect(buildCreatorCreateSpy).not.toHaveBeenCalled();
}).
then(done, done.fail);
});
it('should call \'BuildCreator#create()\' with the correct arguments', done => {
const req = agent.
get(`/create-build/${pr}/${sha}`).
set('AUTHORIZATION', 'foo').
set('X-FILE', 'bar');
promisifyRequest(req).
then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar')).
then(done, done.fail);
});
it('should propagate errors from BuildCreator', done => {
buildCreatorCreateSpy.and.callFake(() => Promise.reject('Test'));
const req = agent.
get(`/create-build/${pr}/${sha}`).
set('AUTHORIZATION', 'foo').
set('X-FILE', 'bar').
expect(500, 'Test');
verifyRequests([req], done);
});
it('should respond with 201 on successful upload', done => {
const req = agent.
get(`/create-build/${pr}/${sha}`).
set('AUTHORIZATION', 'foo').
set('X-FILE', 'bar').
expect(201, http.STATUS_CODES[201]);
verifyRequests([req], done);
});
it('should reject PRs with leading zeros', done => {
verifyRequests([agent.get(`/create-build/0${pr}/${sha}`).expect(404)], done);
});
it('should accept SHAs with leading zeros (but not ignore them)', done => {
const sha41 = '0'.repeat(41);
const sha40 = '0'.repeat(40);
const request41 = agent.get(`/create-build/${pr}/${sha41}`);
const request40 = agent.get(`/create-build/${pr}/${sha40}`).
set('AUTHORIZATION', 'foo').
set('X-FILE', 'bar');
Promise.all([
promisifyRequest(request41.expect(404)),
promisifyRequest(request40.expect(201)),
]).then(done, done.fail);
});
});
describe('GET /health-check', () => {
it('should respond with 200', done => {
verifyRequests([
agent.get('/health-check').expect(200),
agent.get('/health-check/').expect(200),
], done);
});
it('should respond with 405 for non-GET requests', done => {
verifyRequests([
agent.put('/health-check').expect(405),
agent.post('/health-check').expect(405),
agent.patch('/health-check').expect(405),
agent.delete('/health-check').expect(405),
], done);
});
it('should respond with 404 if the path does not match exactly', done => {
verifyRequests([
agent.get('/health-check/foo').expect(404),
agent.get('/health-check-foo').expect(404),
agent.get('/health-checknfoo').expect(404),
agent.get('/foo/health-check').expect(404),
agent.get('/foo-health-check').expect(404),
agent.get('/foonhealth-check').expect(404),
], done);
});
});
describe('GET *', () => {
it('should respond with 404', done => {
const responseBody = 'Unknown resource in request: GET /some/url';
verifyRequests([agent.get('/some/url').expect(404, responseBody)], done);
});
});
describe('ALL *', () => {
it('should respond with 405', done => {
const responseFor = (method: string) => `Unsupported method in request: ${method.toUpperCase()} /some/url`;
verifyRequests([
agent.put('/some/url').expect(405, responseFor('put')),
agent.post('/some/url').expect(405, responseFor('post')),
agent.patch('/some/url').expect(405, responseFor('patch')),
agent.delete('/some/url').expect(405, responseFor('delete')),
], done);
});
});
});
});

View File

@ -1,28 +0,0 @@
{
"compilerOptions": {
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,
"inlineSourceMap": true,
"lib": [
"es2016"
],
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "dist",
"pretty": true,
"rootDir": ".",
"skipLibCheck": true,
"strictNullChecks": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
]
},
"include": [
"lib/**/*",
"test/**/*"
]
}

View File

@ -1,15 +0,0 @@
{
"extends": "tslint:recommended",
"rules": {
"array-type": [true, "array"],
"arrow-parens": [true, "ban-single-arg-parens"],
"interface-name": [true, "never-prefix"],
"max-classes-per-file": [true, 4],
"no-consecutive-blank-lines": [true, 2],
"no-console": false,
"no-namespace": [true, "allow-declarations"],
"no-string-literal": false,
"quotemark": [true, "single"],
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +0,0 @@
#!/bin/bash
set -e -o pipefail
node $AIO_SCRIPTS_JS_DIR/dist/lib/clean-up >> /var/log/aio/clean-up.log 2>&1

View File

@ -1,53 +0,0 @@
#!/bin/bash
set +e -o pipefail
# Variables
exitCode=0
# Helpers
function reportStatus {
local lastExitCode=$?
echo "$1: $([[ $lastExitCode -eq 0 ]] && echo OK || echo NOT OK)"
[[ $lastExitCode -eq 0 ]] || exitCode=1
}
# Check services
services=(
rsyslog
cron
nginx
pm2-root
)
for s in ${services[@]}; do
service $s status > /dev/null
reportStatus "Service '$s'"
done
# Check servers
origins=(
http://$AIO_UPLOAD_HOSTNAME:$AIO_UPLOAD_PORT
http://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTP
https://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTPS
)
for o in ${origins[@]}; do
curl --fail --silent $o/health-check > /dev/null
reportStatus "Server '$o'"
done
# Check resolution of external URLs
origins=(
https://google.com
)
for o in ${origins[@]}; do
curl --fail --silent $o > /dev/null
reportStatus "External URL '$o'"
done
# Exit
exit $exitCode

View File

@ -1,15 +0,0 @@
#!/bin/bash
set -e -o pipefail
exec >> /var/log/aio/init.log
exec 2>&1
# Start the services
echo [`date`] - Starting services...
service rsyslog start
service cron start
service dnsmasq start
service nginx start
service pm2-root start
aio-upload-server-prod start
echo [`date`] - Services started successfully.

View File

@ -1,15 +0,0 @@
#!/bin/bash
set -e -o pipefail
# Set up env variables for production
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null)
# Start the upload-server instance
# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user.
# (Currently, there doesn't seem to be a straight forward way.)
action=$([ "$1" == "stop" ] && echo "stop" || echo "start")
pm2 $action $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server \
--log /var/log/aio/upload-server-prod.log \
--name aio-upload-server-prod \
${@:2}

View File

@ -1,29 +0,0 @@
#!/bin/bash
set -e -o pipefail
# Set up env variables for testing
export AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR
export AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME
export AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION
export AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$TEST_AIO_PREVIEW_DEPLOYMENT_TOKEN
export AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG
export AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME
export AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/TEST_GITHUB_TOKEN 2>/dev/null || echo "TEST_GITHUB_TOKEN")
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/TEST_PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null || echo "TEST_PREVIEW_DEPLOYMENT_TOKEN")
# Start the upload-server instance
# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user.
# (Currently, there doesn't seem to be a straight forward way.)
appName=aio-upload-server-test
if [[ "$1" == "stop" ]]; then
pm2 delete $appName
else
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server/index-test.js \
--log /var/log/aio/upload-server-test.log \
--name $appName \
--no-autorestart \
${@:2}
fi

View File

@ -1,40 +0,0 @@
#!/bin/bash
set -e -o pipefail
logFile=/var/log/aio/verify-setup.log
uploadServerLogFile=/var/log/aio/upload-server-verify-setup.log
exec 3>&1
exec >> $logFile
exec 2>&1
echo "[`date`] - Starting verification..."
# Helpers
function countdown {
message=$1
secs=$2
while [ $secs -gt 0 ]; do
echo -ne "$message in $secs...\033[0K\r"
sleep 1
: $((secs--))
done
echo -ne "\033[0K\r"
}
function onExit {
aio-upload-server-test stop
echo -e "Full logs in '$logFile'.\n" > /dev/fd/3
}
# Setup EXIT trap
trap 'onExit' EXIT
# Start an upload-server instance for testing
aio-upload-server-test start --log $uploadServerLogFile
# Give the upload-server some time to start :(
countdown "Starting" 5 > /dev/fd/3
# Run the tests
node $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup | tee /dev/fd/3

View File

@ -1,52 +0,0 @@
# VM Setup - Set up secrets
## Overview
Necessary secrets:
1. `GITHUB_TOKEN`
- Used for:
- Retrieving open PRs without rate-limiting.
- Retrieving PR author.
- Retrieving members of the `angular-core` team.
- Posting comments with preview links on PRs.
2. `PREVIEW_DEPLOYMENT_TOKEN`
- Used for:
- Decoding the JWT tokens received with `/create-build` requests.
**Note:**
`TEST_GITHUB_TOKEN` and `TEST_PREVIEW_DEPLOYMENT_TOKEN` can also be created similar to their
non-TEST counterparts and they will be loaded when running `aio-verify-setup`, but it is currently
not clear if/how they can be used in tests.
## Create secrets
1. `GITHUB_TOKEN`
- Visit https://github.com/settings/tokens.
- Generate new token with the `public_repo` scope.
2. `PREVIEW_DEPLOYMENT_TOKEN`
- Just generate a hard-to-guess character sequence.
- Add it to `.travis.yml` under `addons -> jwt -> secure`.
Can be added automatically with: `travis encrypt --add addons.jwt PREVIEW_DEPLOYMENT_TOKEN=<access-key>`
**Note:**
Due to [travis-ci/travis-ci#7223](https://github.com/travis-ci/travis-ci/issues/7223) it is not
currently possible to use the JWT addon (as described above) for anything other than the
`SAUCE_ACCESS_KEY` variable. You can get creative, though...
**WARNING**
TO avoid arbitrary uploads, make sure the `PREVIEW_DEPLOYMENT_TOKEN` is NOT printed in the Travis log.
## Save secrets on the VM
- `sudo mkdir /aio-secrets`
- `sudo touch /aio-secrets/GITHUB_TOKEN`
- Insert `<github-token>` into `/aio-secrets/GITHUB_TOKEN`.
- `sudo touch /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN`
- Insert `<access-token>` into `/aio-secrets/PREVIEW_DEPLOYMENT_TOKEN`.
- `sudo chmod 400 /aio-secrets/*`

View File

@ -1,35 +0,0 @@
# VM Setup - Set up docker
## Install docker
_Debian (jessie):_
- `sudo apt-get update`
- `sudo apt-get install -y apt-transport-https ca-certificates curl git software-properties-common`
- `curl -fsSL https://apt.dockerproject.org/gpg | sudo apt-key add -`
- `apt-key fingerprint 58118E89F3A912897C070ADBF76221572C52609D`
- `sudo add-apt-repository "deb https://apt.dockerproject.org/repo/ debian-$(lsb_release -cs) main"`
- `sudo apt-get update`
- `sudo apt-get -y install docker-engine`
_Ubuntu (16.04):_
- `sudo apt-get update`
- `sudo apt-get install -y curl git linux-image-extra-$(uname -r) linux-image-extra-virtual`
- `sudo apt-get install -y apt-transport-https ca-certificates`
- `curl -fsSL https://yum.dockerproject.org/gpg | sudo apt-key add -`
- `apt-key fingerprint 58118E89F3A912897C070ADBF76221572C52609D`
- `sudo add-apt-repository "deb https://apt.dockerproject.org/repo/ ubuntu-$(lsb_release -cs) main"`
- `sudo apt-get update`
- `sudo apt-get -y install docker-engine`
## Start the docker
- `sudo service docker start`
## Test docker
- `sudo docker run hello-world`
## Start docker on boot
- `sudo systemctl enable docker`

View File

@ -1,17 +0,0 @@
# VM setup - Attach persistent disk
## Create `aio-builds` persistent disk (if not already exists)
- Follow instructions [here](https://cloud.google.com/compute/docs/disks/add-persistent-disk#create_disk).
- `sudo mkfs.ext4 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/disk/by-id/google-aio-builds`
## Mount disk
- `sudo mkdir -p /mnt/disks/aio-builds`
- `sudo mount -o discard,defaults /dev/disk/by-id/google-aio-builds /mnt/disks/aio-builds`
- `sudo chmod a+w /mnt/disks/aio-builds`
## Mount disk on boot
- ``echo UUID=`sudo blkid -s UUID -o value /dev/disk/by-id/google-aio-builds` \
/mnt/disks/aio-builds ext4 discard,defaults,nofail 0 2 | sudo tee -a /etc/fstab``

View File

@ -1,31 +0,0 @@
# VM Setup Instructions
- Set up secrets (access tokens, passwords, etc)
- Set up docker
- Attach persistent disk
- Build docker image (+ checkout repo)
- Run image (+ setup for run on boot)
## Build image
- `<aio-builds-setup-dir>/build.sh [<name>[:<tag>] [--build-arg <NAME>=<value> ...]]`
## Run image
- `sudo docker run \
-d \
--dns 127.0.0.1 \
--name <instance-name> \
-p 80:80 \
-p 443:443 \
--restart unless-stopped \
[-v <host-cert-dir>:/etc/ssl/localcerts:ro] \
-v <host-secrets-dir>:/aio-secrets:ro \
-v <host-builds-dir>:/var/www/aio-builds \
<name>[:<tag>]
`
## Questions
- Do we care to keep logs (e.g. cron, nginx, aio-upload-server, aio-clean-up, pm2) outside of the container?
- Instead of creating new comments for each commit, update the original comment?

View File

@ -1,16 +0,0 @@
#!/bin/bash
set -eux -o pipefail
# Set up env
source "`dirname $0`/env.sh"
readonly defaultImageNameAndTag="aio-builds:latest"
# Build `scripts-js/`
cd "$SCRIPTS_JS_DIR"
yarn install
yarn run build
cd -
# Create docker image
readonly nameAndOptionalTag=${1:-$defaultImageNameAndTag}
sudo docker build --tag $nameAndOptionalTag ${@:2} $DOCKERBUILD_DIR

View File

@ -1,5 +0,0 @@
#!/bin/bash
readonly THIS_DIR=$(cd $(dirname $0); pwd)
readonly DOCKERBUILD_DIR="$THIS_DIR/../dockerbuild"
readonly SCRIPTS_JS_DIR="$DOCKERBUILD_DIR/scripts-js"

View File

@ -1,11 +0,0 @@
#!/bin/bash
set -eux -o pipefail
# Set up env
source "`dirname $0`/env.sh"
# Test `scripts-js/`
cd "$SCRIPTS_JS_DIR"
yarn install
yarn test
cd -

View File

@ -1,13 +0,0 @@
#!/bin/bash
set -eux -o pipefail
# Set up env
source "`dirname $0`/env.sh"
# Preverify PR
AIO_GITHUB_ORGANIZATION="angular" \
AIO_GITHUB_TEAM_SLUGS="angular-core" \
AIO_GITHUB_TOKEN=$(echo ${GITHUB_TEAM_MEMBERSHIP_CHECK_KEY} | rev) \
AIO_REPO_SLUG=$TRAVIS_REPO_SLUG \
AIO_PREVERIFY_PR=$TRAVIS_PULL_REQUEST \
node "$SCRIPTS_JS_DIR/dist/lib/upload-server/index-preverify-pr"

View File

@ -1,94 +0,0 @@
@cheatsheetSection
Template syntax
@cheatsheetIndex 2
@description
@cheatsheetItem
syntax:
`<input [value]="firstName">`|`[value]`
description:
Binds property `value` to the result of expression `firstName`.
@cheatsheetItem
syntax:
`<div [attr.role]="myAriaRole">`|`[attr.role]`
description:
Binds attribute `role` to the result of expression `myAriaRole`.
@cheatsheetItem
syntax:
`<div [class.extra-sparkle]="isDelightful">`|`[class.extra-sparkle]`
description:
Binds the presence of the CSS class `extra-sparkle` on the element to the truthiness of the expression `isDelightful`.
@cheatsheetItem
syntax:
`<div [style.width.px]="mySize">`|`[style.width.px]`
description:
Binds style property `width` to the result of expression `mySize` in pixels. Units are optional.
@cheatsheetItem
syntax:
`<button (click)="readRainbow($event)">`|`(click)`
description:
Calls method `readRainbow` when a click event is triggered on this button element (or its children) and passes in the event object.
@cheatsheetItem
syntax:
`<div title="Hello {{ponyName}}">`|`{{ponyName}}`
description:
Binds a property to an interpolated string, for example, "Hello Seabiscuit". Equivalent to:
`<div [title]="'Hello ' + ponyName">`
@cheatsheetItem
syntax:
`<p>Hello {{ponyName}}</p>`|`{{ponyName}}`
description:
Binds text content to an interpolated string, for example, "Hello Seabiscuit".
@cheatsheetItem
syntax:
`<my-cmp [(title)]="name">`|`[(title)]`
description:
Sets up two-way data binding. Equivalent to: `<my-cmp [title]="name" (titleChange)="name=$event">`
@cheatsheetItem
syntax:
`<video #movieplayer ...>
<button (click)="movieplayer.play()">
</video>`|`#movieplayer`|`(click)`
description:
Creates a local variable `movieplayer` that provides access to the `video` element instance in data-binding and event-binding expressions in the current template.
@cheatsheetItem
syntax:
`<p *myUnless="myExpression">...</p>`|`*myUnless`
description:
The `*` symbol turns the current element into an embedded template. Equivalent to:
`<ng-template [myUnless]="myExpression"><p>...</p></ng-template>`
@cheatsheetItem
syntax:
`<p>Card No.: {{cardNumber | myCardNumberFormatter}}</p>`|`{{cardNumber | myCardNumberFormatter}}`
description:
Transforms the current value of expression `cardNumber` via the pipe called `myCardNumberFormatter`.
@cheatsheetItem
syntax:
`<p>Employer: {{employer?.companyName}}</p>`|`{{employer?.companyName}}`
description:
The safe navigation operator (`?`) means that the `employer` field is optional and if `undefined`, the rest of the expression should be ignored.
@cheatsheetItem
syntax:
`<svg:rect x="0" y="0" width="100" height="100"/>`|`svg:`
description:
An SVG snippet template needs an `svg:` prefix on its root element to disambiguate the SVG element from an HTML component.
@cheatsheetItem
syntax:
`<svg>
<rect x="0" y="0" width="100" height="100"/>
</svg>`|`svg`
description:
An `<svg>` root element is detected as an SVG element automatically, without the prefix.

File diff suppressed because it is too large Load Diff

View File

@ -1,623 +0,0 @@
@title
Ahead-of-Time Compilation
@intro
Learn how to use Ahead-of-time compilation
@description
This cookbook describes how to radically improve performance by compiling _Ahead of Time_ (AOT)
during a build process.
{@a toc}
## Table of Contents
* [Overview](#overview)
* [_Ahead-of-Time_ vs _Just-in-Time_](#aot-jit)
* [Compile with AOT](#compile)
* [Bootstrap](#bootstrap)
* [Tree Shaking](#tree-shaking)
* [Load the bundle](#load)
* [Serve the app](#serve)
* [Workflow and convenience script](#workflow)
* [Source Code](#source-code)
* [Tour of Heroes](#toh)
{@a overview}
## Overview
An Angular application consist largely of components and their HTML templates.
Before the browser can render the application,
the components and templates must be converted to executable JavaScript by the _Angular compiler_.
<a href="https://www.youtube.com/watch?v=kW9cJsvcsGo" target="_blank">Watch compiler author Tobias Bosch explain the Angular Compiler</a> at AngularConnect 2016.You can compile the app in the browser, at runtime, as the application loads, using the **_Just-in-Time_ (JIT) compiler**.
This is the standard development approach shown throughout the documentation.
It's great .. but it has shortcomings.
JIT compilation incurs a runtime performance penalty.
Views take longer to render because of the in-browser compilation step.
The application is bigger because it includes the Angular compiler
and a lot of library code that the application won't actually need.
Bigger apps take longer to transmit and are slower to load.
Compilation can uncover many component-template binding errors.
JIT compilation discovers them at runtime which is later than we'd like.
The **_Ahead-of-Time_ (AOT) compiler** can catch template errors early and improve performance
by compiling at build time as you'll learn in this chapter.
{@a aot-jit}
## _Ahead-of-time_ (AOT) vs _Just-in-time_ (JIT)
There is actually only one Angular compiler. The difference between AOT and JIT is a matter of timing and tooling.
With AOT, the compiler runs once at build time using one set of libraries;
With JIT it runs every time for every user at runtime using a different set of libraries.
### Why do AOT compilation?
*Faster rendering*
With AOT, the browser downloads a pre-compiled version of the application.
The browser loads executable code so it can render the application immediately, without waiting to compile the app first.
*Fewer asynchronous requests*
The compiler _inlines_ external html templates and css style sheets within the application JavaScript,
eliminating separate ajax requests for those source files.
*Smaller Angular framework download size*
There's no need to download the Angular compiler if the app is already compiled.
The compiler is roughly half of Angular itself, so omitting it dramatically reduces the application payload.
*Detect template errors earlier*
The AOT compiler detects and reports template binding errors during the build step
before users can see them.
*Better security*
AOT compiles HTML templates and components into JavaScript files long before they are served to the client.
With no templates to read and no risky client-side HTML or JavaScript evaluation,
there are fewer opportunities for injection attacks.
{@a compile}
## Compile with AOT
### Prepare for offline compilation
Take the <a href='../guide/setup.html'>Setup</a> as a starting point.
A few minor changes to the lone `app.component` lead to these two class and html files:
<md-tab-group>
<md-tab label="src/app/app.component.html">
{@example 'cb-aot-compiler/ts/src/app/app.component.html'}
</md-tab>
<md-tab label="src/app/app.component.ts">
{@example 'cb-aot-compiler/ts/src/app/app.component.ts'}
</md-tab>
</md-tab-group>
Install a few new npm dependencies with the following command:
<code-example language="none" class="code-shell">
npm install @angular/compiler-cli @angular/platform-server --save
</code-example>
You will run the `ngc` compiler provided in the `@angular/compiler-cli` npm package
instead of the TypeScript compiler (`tsc`).
`ngc` is a drop-in replacement for `tsc` and is configured much the same way.
`ngc` requires its own `tsconfig.json` with AOT-oriented settings.
Copy the original `src/tsconfig.json` to a file called `tsconfig-aot.json` (on the project root),
then modify it to look as follows.
{@example 'cb-aot-compiler/ts/tsconfig-aot.json'}
The `compilerOptions` section is unchanged except for one property.
**Set the `module` to `es2015`**.
This is important as explained later in the [Tree Shaking](#tree-shaking) section.
What's really new is the `ngc` section at the bottom called `angularCompilerOptions`.
Its `"genDir"` property tells the compiler
to store the compiled output files in a new `aot` folder.
The `"skipMetadataEmit" : true` property prevents the compiler from generating metadata files with the compiled application.
Metadata files are not necessary when targeting TypeScript files, so there is no reason to include them.
***Component-relative Template URLS***
The AOT compiler requires that `@Component` URLS for external templates and css files be _component-relative_.
That means that the value of `@Component.templateUrl` is a URL value _relative_ to the component class file.
For example, an `'app.component.html'` URL means that the template file is a sibling of its companion `app.component.ts` file.
While JIT app URLs are more flexible, stick with _component-relative_ URLs for compatibility with AOT compilation.
JIT-compiled applications that use the SystemJS loader and _component-relative_ URLs *must set the* `@Component.moduleId` *property to* `module.id`.
The `module` object is undefined when an AOT-compiled app runs.
The app fails with a null reference error unless you assign a global `module` value in the `index.html` like this:
{@example 'cb-aot-compiler/ts/src/index.html' region='moduleId'}
Setting a global `module` is a temporary expedient.
### Compiling the application
Initiate AOT compilation from the command line using the previously installed `ngc` compiler by executing:
<code-example language="none" class="code-shell">
node_modules/.bin/ngc -p tsconfig-aot.json
</code-example>
Windows users should surround the `ngc` command in double quotes:
<code-example format='.'>
"node_modules/.bin/ngc" -p tsconfig-aot.json
</code-example>
`ngc` expects the `-p` switch to point to a `tsconfig.json` file or a folder containing a `tsconfig.json` file.
After `ngc` completes, look for a collection of _NgFactory_ files in the `aot` folder (the folder specified as `genDir` in `tsconfig-aot.json`).
These factory files are essential to the compiled application.
Each component factory creates an instance of the component at runtime by combining the original class file
and a JavaScript representation of the component's template.
Note that the original component class is still referenced internally by the generated factory.
The curious can open the `aot/app.component.ngfactory.ts` to see the original Angular template syntax
in its intermediate, compiled-to-TypeScript form.
JIT compilation generates these same _NgFactories_ in memory where they are largely invisible.
AOT compilation reveals them as separate, physical files.
~~~ {.alert.is-important}
Do not edit the _NgFactories_! Re-compilation replaces these files and all edits will be lost.
~~~
{@a bootstrap}
## Bootstrap
The AOT path changes application bootstrapping.
Instead of bootstrapping `AppModule`, you bootstrap the application with the generated module factory, `AppModuleNgFactory`.
Make a copy of `main.ts` and name it `main-jit.ts`.
This is the JIT version; set it aside as you may need it [later](#run-jit "Running with JIT").
Open `main.ts` and convert it to AOT compilation.
Switch from the `platformBrowserDynamic.bootstrap` used in JIT compilation to
`platformBrowser().bootstrapModuleFactory` and pass in the AOT-generated `AppModuleNgFactory`.
Here is AOT bootstrap in `main.ts` next to the original JIT version:
<md-tab-group>
<md-tab label="src/main.ts">
{@example 'cb-aot-compiler/ts/src/main.ts'}
</md-tab>
<md-tab label="src/main-jit.ts">
{@example 'cb-aot-compiler/ts/src/main-jit.ts'}
</md-tab>
</md-tab-group>
Be sure to recompile with `ngc`!
{@a tree-shaking}
## Tree Shaking
AOT compilation sets the stage for further optimization through a process called _Tree Shaking_.
A Tree Shaker walks the dependency graph, top to bottom, and _shakes out_ unused code like
dead needles in a Christmas tree.
Tree Shaking can greatly reduce the downloaded size of the application
by removing unused portions of both source and library code.
In fact, most of the reduction in small apps comes from removing unreferenced Angular features.
For example, this demo application doesn't use anything from the `@angular/forms` library.
There is no reason to download Forms-related Angular code and tree shaking ensures that you don't.
Tree Shaking and AOT compilation are separate steps.
Tree Shaking can only target JavaScript code.
AOT compilation converts more of the application to JavaScript,
which in turn makes more of the application "Tree Shakable".
### Rollup
This cookbook illustrates a Tree Shaking utility called _Rollup_.
Rollup statically analyzes the application by following the trail of `import` and `export` statements.
It produces a final code _bundle_ that excludes code that is exported, but never imported.
Rollup can only Tree Shake `ES2015` modules which have `import` and `export` statements.
Recall that `tsconfig-aot.json` is configured to produce `ES2015` modules.
It's not important that the code itself be written with `ES2015` syntax such as `class` and `const`.
What matters is that the code uses ES `import` and `export` statements rather than `require` statements.Install the Rollup dependencies with this command:
<code-example format='.'>
npm install rollup rollup-plugin-node-resolve rollup-plugin-commonjs rollup-plugin-uglify --save-dev
</code-example>
Next, create a configuration file (`rollup-config.js`)
in the project root directory to tell Rollup how to process the application.
The cookbook configuration file looks like this.
{@example 'cb-aot-compiler/ts/rollup-config.js'}
It tells Rollup that the app entry point is `src/app/main.js` .
The `dest` attribute tells Rollup to create a bundle called `build.js` in the `dist` folder.
It overrides the default `onwarn` method in order to skip annoying messages about the AOT compiler's use of the `this` keyword.
Then there are plugins.
### Rollup Plugins
Optional plugins filter and transform the Rollup inputs and output.
*RxJS*
Rollup expects application source code to use `ES2015` modules.
Not all external dependencies are published as `ES2015` modules.
In fact, most are not. Many of them are published as _CommonJS_ modules.
The _RxJs_ observable library is an essential Angular dependency published as an ES5 JavaScript _CommonJS_ module.
Luckily there is a Rollup plugin that modifies _RxJs_
to use the ES `import` and `export` statements that Rollup requires.
Rollup then preserves in the final bundle the parts of `RxJS` referenced by the application.
{@example 'cb-aot-compiler/ts/rollup-config.js' region='commonjs'}
*Minification*
Rollup Tree Shaking reduces code size considerably. Minification makes it smaller still.
This cookbook relies on the _uglify_ Rollup plugin to minify and mangle the code.
{@example 'cb-aot-compiler/ts/rollup-config.js' region='uglify'}
In a production setting, you would also enable gzip on the web server to compress
the code into an even smaller package going over the wire.
### Run Rollup
Execute the Rollup process with this command:
<code-example format='.'>
node_modules/.bin/rollup -c rollup-config.js
</code-example>
Windows users should surround the `rollup` command in double quotes:
<code-example format='.'>
"node_modules/.bin/rollup" -c rollup-config.js
</code-example>
{@a load}
## Load the Bundle
Loading the generated application bundle does not require a module loader like SystemJS.
Remove the scripts that concern SystemJS.
Instead, load the bundle file using a single `script` tag **_after_** the `</body>` tag:
{@example 'cb-aot-compiler/ts/src/index.html' region='bundle'}
{@a serve}
## Serve the app
You'll need a web server to host the application.
Use the same _Lite Server_ employed elsewhere in the documentation:
<code-example language="none" class="code-shell">
npm run lite
</code-example>
The server starts, launches a browser, and the app should appear.
{@a source-code}
## AOT QuickStart Source Code
Here's the pertinent source code:
<md-tab-group>
<md-tab label="src/app/app.component.html">
{@example 'cb-aot-compiler/ts/src/app/app.component.html'}
</md-tab>
<md-tab label="src/app/app.component.ts">
{@example 'cb-aot-compiler/ts/src/app/app.component.ts'}
</md-tab>
<md-tab label="src/main.ts">
{@example 'cb-aot-compiler/ts/src/main.ts'}
</md-tab>
<md-tab label="src/index.html">
{@example 'cb-aot-compiler/ts/src/index.html'}
</md-tab>
<md-tab label="tsconfig-aot.json">
{@example 'cb-aot-compiler/ts/tsconfig-aot.json'}
</md-tab>
<md-tab label="rollup-config.js">
{@example 'cb-aot-compiler/ts/rollup-config.js'}
</md-tab>
</md-tab-group>
{@a workflow}
## Workflow and convenience script
You'll rebuild the AOT version of the application every time you make a change.
Those _npm_ commands are long and difficult to remember.
Add the following _npm_ convenience script to the `package.json` so you can compile and rollup in one command.Open a terminal window and try it.
<code-example language="none" class="code-shell">
npm run build:aot
</code-example>
{@a run-jit}
### And JIT too!
AOT compilation and rollup together take several seconds.
You may be able to develop iteratively a little faster with SystemJS and JIT.
The same source code can be built both ways. Here's one way to do that.
* Make a copy of `index.html` and call it `index-jit.html`.
* Delete the script at the bottom of `index-jit.html` that loads `bundle.js`
* Restore the SystemJS scripts like this:
{@example 'cb-aot-compiler/ts/src/index-jit.html' region='jit'}
Notice the slight change to the `system.import` which now specifies `src/app/main-jit`.
That's the JIT version of the bootstrap file that we preserved [above](#bootstrap)
Open a _different_ terminal window and enter.
<code-example language="none" class="code-shell">
npm start
</code-example>
That compiles the app with JIT and launches the server.
The server loads `index.html` which is still the AOT version (confirm in the browser console).
Change the address bar to `index-jit.html` and it loads the JIT version (confirm in the browser console).
Develop as usual.
The server and TypeScript compiler are in "watch mode" so your changes are reflected immediately in the browser.
To see those changes in AOT, switch to the original terminal and re-run `npm run build:aot`.
When it finishes, go back to the browser and back-button to the AOT version in the (default) `index.html`.
Now you can develop JIT and AOT, side-by-side.
{@a toh}
## Tour of Heroes
The sample above is a trivial variation of the QuickStart app.
In this section you apply what you've learned about AOT compilation and Tree Shaking
to an app with more substance, the tutorial [_Tour of Heroes_](../tutorial/toh-pt6.html).
### JIT in development, AOT in production
Today AOT compilation and Tree Shaking take more time than is practical for development. That will change soon.
For now, it's best to JIT compile in development and switch to AOT compilation before deploying to production.
Fortunately, the source code can be compiled either way without change _if_ you account for a few key differences.
***index.html***
The JIT and AOT apps require their own `index.html` files because they setup and launch so differently.
Here they are for comparison:
<md-tab-group>
<md-tab label="aot/index.html (AOT)">
{@example 'toh-6/ts/aot/index.html'}
</md-tab>
<md-tab label="src/index.html (JIT)">
{@example 'toh-6/ts/src/index.html'}
</md-tab>
</md-tab-group>
The JIT version relies on `SystemJS` to load individual modules.
Its scripts appear in its `index.html`.
The AOT version loads the entire application in a single script, `aot/dist/build.js`.
It does not need `SystemJS`, so that script is absent from its `index.html`
***main.ts***
JIT and AOT applications boot in much the same way but require different Angular libraries to do so.
The key differences, covered in the [Bootstrap](#bootstrap) section above,
are evident in these `main` files which can and should reside in the same folder:
<md-tab-group>
<md-tab label="main-aot.ts (AOT)">
{@example 'toh-6/ts/src/main-aot.ts'}
</md-tab>
<md-tab label="main.ts (JIT)">
{@example 'toh-6/ts/src/main.ts'}
</md-tab>
</md-tab-group>
***TypeScript configuration***
JIT-compiled applications transpile to `commonjs` modules.
AOT-compiled applications transpile to _ES2015_/_ES6_ modules to facilitate Tree Shaking.
AOT requires its own TypeScript configuration settings as well.
You'll need separate TypeScript configuration files such as these:
<md-tab-group>
<md-tab label="tsconfig-aot.json (AOT)">
{@example 'toh-6/ts/tsconfig-aot.json'}
</md-tab>
<md-tab label="src/tsconfig.json (JIT)">
{@example 'toh-6/ts/src/tsconfig.1.json'}
</md-tab>
</md-tab-group>
~~~ {.callout.is-helpful}
<header>
@Types and node modules
</header>
In the file structure of _this particular sample project_,
the `node_modules` folder happens to be two levels up from the project root.
Therefore, `"typeRoots"` must be set to `"../../node_modules/@types/"`.
In a more typical project, `node_modules` would be a sibling of `tsconfig-aot.json`
and `"typeRoots"` would be set to `"node_modules/@types/"`.
Edit your `tsconfig-aot.json` to fit your project's file structure.
~~~
### Tree Shaking
Rollup does the Tree Shaking as before.
{@example 'toh-6/ts/rollup-config.js'}
### Running the application
~~~ {.alert.is-important}
The general audience instructions for running the AOT build of the Tour of Heroes app are not ready.
The following instructions presuppose that you have cloned the
<a href="https://github.com/angular/angular.io" target="_blank">angular.io</a>
github repository and prepared it for development as explained in the repo's README.md.
The _Tour of Heroes_ source code is in the `public/docs/_examples/toh-6/ts` folder.
~~~
Run the JIT-compiled app with `npm start` as for all other JIT examples.
Compiling with AOT presupposes certain supporting files, most of them discussed above.
<md-tab-group>
<md-tab label="src/index.html">
{@example 'toh-6/ts/src/index.html'}
</md-tab>
<md-tab label="copy-dist-files.js">
{@example 'toh-6/ts/copy-dist-files.js'}
</md-tab>
<md-tab label="rollup-config.js">
{@example 'toh-6/ts/rollup-config.js'}
</md-tab>
<md-tab label="tsconfig-aot.json">
{@example 'toh-6/ts/tsconfig-aot.json'}
</md-tab>
</md-tab-group>
Extend the `scripts` section of the `package.json` with these npm scripts:Copy the AOT distribution files into the `/aot` folder with the node script:
<code-example language="none" class="code-shell">
node copy-dist-files
</code-example>
You won't do that again until there are updates to `zone.js` or the `core-js` shim for old browsers.Now AOT-compile the app and launch it with the `lite` server:
<code-example language="none" class="code-shell">
npm run build:aot && npm run serve:aot
</code-example>
### Inspect the Bundle
It's fascinating to see what the generated JavaScript bundle looks like after Rollup.
The code is minified, so you won't learn much from inspecting the bundle directly.
But the <a href="https://github.com/danvk/source-map-explorer/blob/master/README.md" target="_blank">source-map-explorer</a>
tool can be quite revealing.
Install it:
<code-example language="none" class="code-shell">
npm install source-map-explorer --save-dev
</code-example>
Run the following command to generate the map.
<code-example language="none" class="code-shell">
node_modules/.bin/source-map-explorer aot/dist/build.js
</code-example>
The `source-map-explorer` analyzes the source map generated with the bundle and draws a map of all dependencies,
showing exactly which application and Angular modules and classes are included in the bundle.
Here's the map for _Tour of Heroes_.
<a href="assets/images/cookbooks/aot-compiler/toh6-bundle.png" target="_blank" title="View larger image">
<figure class='image-display'>
<img src="assets/images/cookbooks/aot-compiler/toh6-bundle.png" alt="TOH-6-bundle"> </img>
</figure>
</a>

View File

@ -1,301 +0,0 @@
@title
Component Interaction
@intro
Share information between different directives and components
@description
<a id="top"></a>This cookbook contains recipes for common component communication scenarios
in which two or more components share information.
<a id="toc"></a>## Table of contents
[Pass data from parent to child with input binding](#parent-to-child)
[Intercept input property changes with a setter](#parent-to-child-setter)
[Intercept input property changes with *ngOnChanges*](#parent-to-child-on-changes)
[Parent listens for child event](#child-to-parent)
[Parent interacts with child via a *local variable*](#parent-to-child-local-var)
[Parent calls a *ViewChild*](#parent-to-view-child)
[Parent and children communicate via a service](#bidirectional-service)
**See the <live-example name="cb-component-communication"></live-example>**.
<a id="parent-to-child"></a>## Pass data from parent to child with input binding
`HeroChildComponent` has two ***input properties***,
typically adorned with [@Input decorations](../guide/template-syntax.html#inputs-outputs).
{@example 'cb-component-communication/ts/src/app/hero-child.component.ts'}
The second `@Input` aliases the child component property name `masterName` as `'master'`.
The `HeroParentComponent` nests the child `HeroChildComponent` inside an `*ngFor` repeater,
binding its `master` string property to the child's `master` alias
and each iteration's `hero` instance to the child's `hero` property.
{@example 'cb-component-communication/ts/src/app/hero-parent.component.ts'}
The running application displays three heroes:
<figure class='image-display'>
<img src="assets/images/cookbooks/component-communication/parent-to-child.png" alt="Parent-to-child"> </img>
</figure>
### Test it
E2E test that all children were instantiated and displayed as expected:
{@example 'cb-component-communication/e2e-spec.ts' region='parent-to-child'}
[Back to top](#top)
<a id="parent-to-child-setter"></a>## Intercept input property changes with a setter
Use an input property setter to intercept and act upon a value from the parent.
The setter of the `name` input property in the child `NameChildComponent`
trims the whitespace from a name and replaces an empty value with default text.
{@example 'cb-component-communication/ts/src/app/name-child.component.ts'}
Here's the `NameParentComponent` demonstrating name variations including a name with all spaces:
{@example 'cb-component-communication/ts/src/app/name-parent.component.ts'}
<figure class='image-display'>
<img src="assets/images/cookbooks/component-communication/setter.png" alt="Parent-to-child-setter"> </img>
</figure>
### Test it
E2E tests of input property setter with empty and non-empty names:
{@example 'cb-component-communication/e2e-spec.ts' region='parent-to-child-setter'}
[Back to top](#top)
<a id="parent-to-child-on-changes"></a>## Intercept input property changes with *ngOnChanges*
Detect and act upon changes to input property values with the `ngOnChanges` method of the `OnChanges` lifecycle hook interface.
May prefer this approach to the property setter when watching multiple, interacting input properties.
Learn about `ngOnChanges` in the [LifeCycle Hooks](../guide/lifecycle-hooks.html) chapter.This `VersionChildComponent` detects changes to the `major` and `minor` input properties and composes a log message reporting these changes:
{@example 'cb-component-communication/ts/src/app/version-child.component.ts'}
The `VersionParentComponent` supplies the `minor` and `major` values and binds buttons to methods that change them.
{@example 'cb-component-communication/ts/src/app/version-parent.component.ts'}
Here's the output of a button-pushing sequence:
<figure class='image-display'>
<img src="assets/images/cookbooks/component-communication/parent-to-child-on-changes.gif" alt="Parent-to-child-onchanges"> </img>
</figure>
### Test it
Test that ***both*** input properties are set initially and that button clicks trigger
the expected `ngOnChanges` calls and values:
{@example 'cb-component-communication/e2e-spec.ts' region='parent-to-child-onchanges'}
[Back to top](#top)
<a id="child-to-parent"></a>## Parent listens for child event
The child component exposes an `EventEmitter` property with which it `emits`events when something happens.
The parent binds to that event property and reacts to those events.
The child's `EventEmitter` property is an ***output property***,
typically adorned with an [@Output decoration](../guide/template-syntax.html#inputs-outputs)
as seen in this `VoterComponent`:
{@example 'cb-component-communication/ts/src/app/voter.component.ts'}
Clicking a button triggers emission of a `true` or `false` (the boolean *payload*).
The parent `VoteTakerComponent` binds an event handler (`onVoted`) that responds to the child event
payload (`$event`) and updates a counter.
{@example 'cb-component-communication/ts/src/app/votetaker.component.ts'}
The framework passes the event argument &mdash; represented by `$event` &mdash; to the handler method,
and the method processes it:
<figure class='image-display'>
<img src="assets/images/cookbooks/component-communication/child-to-parent.gif" alt="Child-to-parent"> </img>
</figure>
### Test it
Test that clicking the *Agree* and *Disagree* buttons update the appropriate counters:
{@example 'cb-component-communication/e2e-spec.ts' region='child-to-parent'}
[Back to top](#top)
## Parent interacts with child via *local variable*
A parent component cannot use data binding to read child properties
or invoke child methods. We can do both
by creating a template reference variable for the child element
and then reference that variable *within the parent template*
as seen in the following example.
<a id="countdown-timer-example"></a>
We have a child `CountdownTimerComponent` that repeatedly counts down to zero and launches a rocket.
It has `start` and `stop` methods that control the clock and it displays a
countdown status message in its own template.
{@example 'cb-component-communication/ts/src/app/countdown-timer.component.ts'}
Let's see the `CountdownLocalVarParentComponent` that hosts the timer component.
{@example 'cb-component-communication/ts/src/app/countdown-parent.component.ts' region='lv'}
The parent component cannot data bind to the child's
`start` and `stop` methods nor to its `seconds` property.
We can place a local variable (`#timer`) on the tag (`<countdown-timer>`) representing the child component.
That gives us a reference to the child component itself and the ability to access
*any of its properties or methods* from within the parent template.
In this example, we wire parent buttons to the child's `start` and `stop` and
use interpolation to display the child's `seconds` property.
Here we see the parent and child working together.
<figure class='image-display'>
<img src="assets/images/cookbooks/component-communication/countdown-timer-anim.gif" alt="countdown timer"> </img>
</figure>
{@a countdown-tests}
### Test it
Test that the seconds displayed in the parent template
match the seconds displayed in the child's status message.
Test also that clicking the *Stop* button pauses the countdown timer:
{@example 'cb-component-communication/e2e-spec.ts' region='countdown-timer-tests'}
[Back to top](#top)
<a id="parent-to-view-child"></a>## Parent calls a *ViewChild*
The *local variable* approach is simple and easy. But it is limited because
the parent-child wiring must be done entirely within the parent template.
The parent component *itself* has no access to the child.
We can't use the *local variable* technique if an instance of the parent component *class*
must read or write child component values or must call child component methods.
When the parent component *class* requires that kind of access,
we ***inject*** the child component into the parent as a *ViewChild*.
We'll illustrate this technique with the same [Countdown Timer](#countdown-timer-example) example.
We won't change its appearance or behavior.
The child [CountdownTimerComponent](#countdown-timer-example) is the same as well.
We are switching from the *local variable* to the *ViewChild* technique
solely for the purpose of demonstration.Here is the parent, `CountdownViewChildParentComponent`:
{@example 'cb-component-communication/ts/src/app/countdown-parent.component.ts' region='vc'}
It takes a bit more work to get the child view into the parent component *class*.
We import references to the `ViewChild` decorator and the `AfterViewInit` lifecycle hook.
We inject the child `CountdownTimerComponent` into the private `timerComponent` property
via the `@ViewChild` property decoration.
The `#timer` local variable is gone from the component metadata.
Instead we bind the buttons to the parent component's own `start` and `stop` methods and
present the ticking seconds in an interpolation around the parent component's `seconds` method.
These methods access the injected timer component directly.
The `ngAfterViewInit` lifecycle hook is an important wrinkle.
The timer component isn't available until *after* Angular displays the parent view.
So we display `0` seconds initially.
Then Angular calls the `ngAfterViewInit` lifecycle hook at which time it is *too late*
to update the parent view's display of the countdown seconds.
Angular's unidirectional data flow rule prevents us from updating the parent view's
in the same cycle. We have to *wait one turn* before we can display the seconds.
We use `setTimeout` to wait one tick and then revise the `seconds` method so
that it takes future values from the timer component.
### Test it
Use [the same countdown timer tests](#countdown-tests) as before.[Back to top](#top)
<a id="bidirectional-service"></a>## Parent and children communicate via a service
A parent component and its children share a service whose interface enables bi-directional communication
*within the family*.
The scope of the service instance is the parent component and its children.
Components outside this component subtree have no access to the service or their communications.
This `MissionService` connects the `MissionControlComponent` to multiple `AstronautComponent` children.
{@example 'cb-component-communication/ts/src/app/mission.service.ts'}
The `MissionControlComponent` both provides the instance of the service that it shares with its children
(through the `providers` metadata array) and injects that instance into itself through its constructor:
{@example 'cb-component-communication/ts/src/app/missioncontrol.component.ts'}
The `AstronautComponent` also injects the service in its constructor.
Each `AstronautComponent` is a child of the `MissionControlComponent` and therefore receives its parent's service instance:
{@example 'cb-component-communication/ts/src/app/astronaut.component.ts'}
Notice that we capture the `subscription` and unsubscribe when the `AstronautComponent` is destroyed.
This is a memory-leak guard step. There is no actual risk in this app because the
lifetime of a `AstronautComponent` is the same as the lifetime of the app itself.
That *would not* always be true in a more complex application.
We do not add this guard to the `MissionControlComponent` because, as the parent,
it controls the lifetime of the `MissionService`.The *History* log demonstrates that messages travel in both directions between
the parent `MissionControlComponent` and the `AstronautComponent` children,
facilitated by the service:
<figure class='image-display'>
<img src="assets/images/cookbooks/component-communication/bidirectional-service.gif" alt="bidirectional-service"> </img>
</figure>
### Test it
Tests click buttons of both the parent `MissionControlComponent` and the `AstronautComponent` children
and verify that the *History* meets expectations:
{@example 'cb-component-communication/e2e-spec.ts' region='bidirectional-service'}
[Back to top](#top)

View File

@ -1,194 +0,0 @@
@title
Component-relative Paths
@intro
Use relative URLs for component templates and styles.
@description
## Write *Component-Relative* URLs to component templates and style files
Our components often refer to external template and style files.
We identify those files with a URL in the `templateUrl` and `styleUrls` properties of the `@Component` metadata
as seen here:
{@example 'cb-component-relative-paths/ts/src/app/some.component.ts' region='absolute-config'}
By default, we *must* specify the full path back to the application root.
We call this an ***absolute path*** because it is *absolute* with respect to the application root.
There are two problems with an *absolute path*:
1. We have to remember the full path back to the application root.
1. We have to update the URL when we move the component around in the application files structure.
It would be much easier to write and maintain our application components if we could specify template and style locations
*relative* to their component class file.
*We can!*
~~~ {.alert.is-important}
We can if we build our application as `commonjs` modules and load those modules
with a suitable package loader such as `systemjs` or `webpack`.
Learn why [below](#why-default).
The Angular CLI uses these technologies and defaults to the
*component-relative path* approach described here.
CLI users can skip this chapter or read on to understand
how it works.
~~~
## _Component-Relative_ Paths
Our goal is to specify template and style URLs *relative* to their component class files,
hence the term ***component-relative path***.
The key to success is following a convention that puts related component files in well-known locations.
We recommend keeping component template and component-specific style files as *siblings* of their
companion component class files.
Here we see the three files for `SomeComponent` sitting next to each other in the `app` folder.
<aio-filetree>
<aio-folder>
app
<aio-file>
some.component.css
</aio-file>
<aio-file>
some.component.html
</aio-file>
<aio-file>
some.component.ts
</aio-file>
</aio-folder>
</aio-filetree>
We'll have more files and folders &mdash; and greater folder depth &mdash; as our application grows.
We'll be fine as long as the component files travel together as the inseparable siblings they are.
### Set the *moduleId*
Having adopted this file structure convention, we can specify locations of the template and style files
relative to the component class file simply by setting the `moduleId` property of the `@Component` metadata like this
{@example 'cb-component-relative-paths/ts/src/app/some.component.ts' region='module-id'}
We strip the `src/app/` base path from the `templateUrl` and `styleUrls` and replace it with `./`.
The result looks like this:
{@example 'cb-component-relative-paths/ts/src/app/some.component.ts' region='relative-config'}
~~~ {.alert.is-helpful}
Webpack users may prefer [an alternative approach](#webpack).
~~~
## Source
**We can see the <live-example name="cb-component-relative-paths"></live-example>**
and download the source code from there
or simply read the pertinent source here.
<md-tab-group>
<md-tab label="src/app/some.component.ts">
{@example 'cb-component-relative-paths/ts/src/app/some.component.ts'}
</md-tab>
<md-tab label="src/app/some.component.html">
{@example 'cb-component-relative-paths/ts/src/app/some.component.html'}
</md-tab>
<md-tab label="src/app/some.component.css">
{@example 'cb-component-relative-paths/ts/src/app/some.component.css'}
</md-tab>
<md-tab label="src/app/app.component.ts">
{@example 'cb-component-relative-paths/ts/src/app/app.component.ts'}
</md-tab>
</md-tab-group>
{@a why-default}
## Appendix: why *component-relative* is not the default
A *component-relative* path is obviously superior to an *absolute* path.
Why did Angular default to the *absolute* path?
Why do *we* have to set the `moduleId`? Why can't Angular set it?
First, let's look at what happens if we use a relative path and omit the `moduleId`.
`EXCEPTION: Failed to load some.component.html`
Angular can't find the file so it throws an error.
Why can't Angular calculate the template and style URLs from the component file's location?
Because the location of the component can't be determined without the developer's help.
Angular apps can be loaded in many ways: from individual files, from SystemJS packages, or
from CommonJS packages, to name a few.
We might generate modules in any of several formats.
We might not be writing modular code at all!
With this diversity of packaging and module load strategies,
it's not possible for Angular to know with certainty where these files reside at runtime.
The only location Angular can be sure of is the URL of the `index.html` home page, the application root.
So by default it resolves template and style paths relative to the URL of `index.html`.
That's why we previously wrote our file URLs with an `app/` base path prefix.
But *if* we follow the recommended guidelines and we write modules in `commonjs` format
and we use a module loader that *plays nice*,
*then* we &mdash; the developers of the application &mdash;
know that the semi-global `module.id` variable is available and contains
the absolute URL of the component class module file.
That knowledge enables us to tell Angular where the *component* file is
by setting the `moduleId`:
{@example 'cb-component-relative-paths/ts/src/app/some.component.ts' region='module-id'}
{@a webpack}
## Webpack: load templates and styles
Webpack developers have an alternative to `moduleId`.
They can load templates and styles at runtime by adding `./` at the beginning of the `template` and `styles` / `styleUrls`
properties that reference *component-relative URLS.
{@example 'webpack/ts/src/app/app.component.ts'}
Webpack will do a `require` behind the scenes to load the templates and styles. Read more [here](../guide/webpack.html#highlights).
See the [Introduction to Webpack](../guide/webpack.html).

View File

@ -1,921 +0,0 @@
@title
Dependency Injection
@intro
Techniques for Dependency Injection
@description
Dependency Injection is a powerful pattern for managing code dependencies.
In this cookbook we will explore many of the features of Dependency Injection (DI) in Angular.
<a id="toc"></a>## Table of contents
[Application-wide dependencies](#app-wide-dependencies)
[External module configuration](#external-module-configuration)
[*@Injectable* and nested service dependencies](#nested-dependencies)
[Limit service scope to a component subtree](#service-scope)
[Multiple service instances (sandboxing)](#multiple-service-instances)
[Qualify dependency lookup with *@Optional* and *@Host*](#qualify-dependency-lookup)
[Inject the component's DOM element](#component-element)
[Define dependencies with providers](#providers)
* [The *provide* object literal](#provide)
* [useValue - the *value provider*](#usevalue)
* [useClass - the *class provider*](#useclass)
* [useExisting - the *alias provider*](#useexisting)
* [useFactory - the *factory provider*](#usefactory)
[Provider token alternatives](#tokens)
* [class-interface](#class-interface)
* [OpaqueToken](#opaque-token)
[Inject into a derived class](#di-inheritance)
[Find a parent component by injection](#find-parent)
* [Find parent with a known component type](#known-parent)
* [Cannot find a parent by its base class](#base-parent)
* [Find a parent by its class-interface](#class-interface-parent)
* [Find a parent in a tree of parents (*@SkipSelf*)](#parent-tree)
* [A *provideParent* helper function](#provideparent)
[Break circularities with a forward class reference (*forwardRef*)](#forwardref)
**See the <live-example name="cb-dependency-injection"></live-example>**
of the code supporting this cookbook.
<a id="app-wide-dependencies"></a>## Application-wide dependencies
Register providers for dependencies used throughout the application in the root application component, `AppComponent`.
In the following example, we import and register several services
(the `LoggerService`, `UserContext`, and the `UserService`)
in the `@Component` metadata `providers` array.
{@example 'cb-dependency-injection/ts/src/app/app.component.ts' region='import-services'}
All of these services are implemented as classes.
Service classes can act as their own providers which is why listing them in the `providers` array
is all the registration we need.
A *provider* is something that can create or deliver a service.
Angular creates a service instance from a class provider by "new-ing" it.
Learn more about providers [below](#providers).Now that we've registered these services,
Angular can inject them into the constructor of *any* component or service, *anywhere* in the application.
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='ctor'}
{@example 'cb-dependency-injection/ts/src/app/user-context.service.ts' region='ctor'}
<a id="external-module-configuration"></a>
## External module configuration
We often register providers in the `NgModule` rather than in the root application component.
We do this when (a) we expect the service to be injectable everywhere
or (b) we must configure another application global service _before it starts_.
We see an example of the second case here, where we configure the Component Router with a non-default
[location strategy](../guide/router.html#location-strategy) by listing its provider
in the `providers` list of the `AppModule`.
{@example 'cb-dependency-injection/ts/src/app/app.module.ts' region='providers'}
{@a injectable}
{@a nested-dependencies}
## *@Injectable* and nested service dependencies
The consumer of an injected service does not know how to create that service.
It shouldn't care.
It's the dependency injection's job to create and cache that service.
Sometimes a service depends on other services ... which may depend on yet other services.
Resolving these nested dependencies in the correct order is also the framework's job.
At each step, the consumer of dependencies simply declares what it requires in its constructor and the framework takes over.
For example, we inject both the `LoggerService` and the `UserContext` in the `AppComponent`.
{@example 'cb-dependency-injection/ts/src/app/app.component.ts' region='ctor'}
The `UserContext` in turn has dependencies on both the `LoggerService` (again) and
a `UserService` that gathers information about a particular user.
{@example 'cb-dependency-injection/ts/src/app/user-context.service.ts' region='injectables'}
When Angular creates an`AppComponent`, the dependency injection framework creates an instance of the `LoggerService` and
starts to create the `UserContextService`.
The `UserContextService` needs the `LoggerService`, which the framework already has, and the `UserService`, which it has yet to create.
The `UserService` has no dependencies so the dependency injection framework can just `new` one into existence.
The beauty of dependency injection is that the author of `AppComponent` didn't care about any of this.
The author simply declared what was needed in the constructor (`LoggerService` and `UserContextService`) and the framework did the rest.
Once all the dependencies are in place, the `AppComponent` displays the user information:
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/logged-in-user.png" alt="Logged In User"> </img>
</figure>
### *@Injectable()*
Notice the `@Injectable()`decorator on the `UserContextService` class.
{@example 'cb-dependency-injection/ts/src/app/user-context.service.ts' region='injectable'}
That decorator makes it possible for Angular to identify the types of its two dependencies, `LoggerService` and `UserService`.
Technically, the `@Injectable()`decorator is only _required_ for a service class that has _its own dependencies_.
The `LoggerService` doesn't depend on anything. The logger would work if we omitted `@Injectable()`
and the generated code would be slightly smaller.
But the service would break the moment we gave it a dependency and we'd have to go back and
and add `@Injectable()` to fix it. We add `@Injectable()` from the start for the sake of consistency and to avoid future pain.
~~~ {.alert.is-helpful}
Although we recommend applying `@Injectable` to all service classes, do not feel bound by it.
Some developers prefer to add it only where needed and that's a reasonable policy too.
~~~
The `AppComponent` class had two dependencies as well but no `@Injectable()`.
It didn't need `@Injectable()` because that component class has the `@Component` decorator.
In Angular with TypeScript, a *single* decorator &mdash; *any* decorator &mdash; is sufficient to identify dependency types.
<a id="service-scope"></a>
## Limit service scope to a component subtree
All injected service dependencies are singletons meaning that,
for a given dependency injector ("injector"), there is only one instance of service.
But an Angular application has multiple dependency injectors, arranged in a tree hierarchy that parallels the component tree.
So a particular service can be *provided* (and created) at any component level and multiple times
if provided in multiple components.
By default, a service dependency provided in one component is visible to all of its child components and
Angular injects the same service instance into all child components that ask for that service.
Accordingly, dependencies provided in the root `AppComponent` can be injected into *any* component *anywhere* in the application.
That isn't always desirable.
Sometimes we want to restrict service availability to a particular region of the application.
We can limit the scope of an injected service to a *branch* of the application hierarchy
by providing that service *at the sub-root component for that branch*.
Here we provide the `HeroService` to the `HeroesBaseComponent` by listing it in the `providers` array:
{@example 'cb-dependency-injection/ts/src/app/sorted-heroes.component.ts' region='injection'}
When Angular creates the `HeroesBaseComponent`, it also creates a new instance of `HeroService`
that is visible only to the component and its children (if any).
We could also provide the `HeroService` to a *different* component elsewhere in the application.
That would result in a *different* instance of the service, living in a *different* injector.
We examples of such scoped `HeroService` singletons appear throughout the accompanying sample code,
including the `HeroBiosComponent`, `HeroOfTheMonthComponent`, and `HeroesBaseComponent`.
Each of these components has its own `HeroService` instance managing its own independent collection of heroes.
~~~ {.alert.is-helpful}
### Take a break!
This much Dependency Injection knowledge may be all that many Angular developers
ever need to build their applications. It doesn't always have to be more complicated.
~~~
<a id="multiple-service-instances"></a>
## Multiple service instances (sandboxing)
Sometimes we want multiple instances of a service at *the same level of the component hierarchy*.
A good example is a service that holds state for its companion component instance.
We need a separate instance of the service for each component.
Each service has its own work-state, isolated from the service-and-state of a different component.
We call this *sandboxing* because each service and component instance has its own sandbox to play in.
<a id="hero-bios-component"></a>
Imagine a `HeroBiosComponent` that presents three instances of the `HeroBioComponent`.
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='simple'}
Each `HeroBioComponent` can edit a single hero's biography.
A `HeroBioComponent` relies on a `HeroCacheService` to fetch, cache, and perform other persistence operations on that hero.
{@example 'cb-dependency-injection/ts/src/app/hero-cache.service.ts' region='service'}
Clearly the three instances of the `HeroBioComponent` can't share the same `HeroCacheService`.
They'd be competing with each other to determine which hero to cache.
Each `HeroBioComponent` gets its *own* `HeroCacheService` instance
by listing the `HeroCacheService` in its metadata `providers` array.
{@example 'cb-dependency-injection/ts/src/app/hero-bio.component.ts' region='component'}
The parent `HeroBiosComponent` binds a value to the `heroId`.
The `ngOnInit` pass that `id` to the service which fetches and caches the hero.
The getter for the `hero` property pulls the cached hero from the service.
And the template displays this data-bound property.
Find this example in <live-example name="cb-dependency-injection">live code</live-example>
and confirm that the three `HeroBioComponent` instances have their own cached hero data.
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/hero-bios.png" alt="Bios"> </img>
</figure>
{@a optional}
{@a qualify-dependency-lookup}
## Qualify dependency lookup with *@Optional* and *@Host*
We learned that dependencies can be registered at any level in the component hierarchy.
When a component requests a dependency, Angular starts with that component's injector and walks up the injector tree
until it finds the first suitable provider. Angular throws an error if it can't find the dependency during that walk.
We *want* this behavior most of the time.
But sometimes we need to limit the search and/or accommodate a missing dependency.
We can modify Angular's search behavior with the `@Host` and `@Optional` qualifying decorators,
used individually or together.
The `@Optional` decorator tells Angular to continue when it can't find the dependency.
Angular sets the injection parameter to `null` instead.
The `@Host` decorator stops the upward search at the *host component*.
The host component is typically the component requesting the dependency.
But when this component is projected into a *parent* component, that parent component becomes the host.
We look at this second, more interesting case in our next example.
### Demonstration
The `HeroBiosAndContactsComponent` is a revision of the `HeroBiosComponent` that we looked at [above](#hero-bios-component).
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='hero-bios-and-contacts'}
Focus on the template:
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='template'}
We've inserted a `<hero-contact>` element between the `<hero-bio>` tags.
Angular *projects* (*transcludes*) the corresponding `HeroContactComponent` into the `HeroBioComponent` view,
placing it in the `<ng-content>` slot of the `HeroBioComponent` template:
{@example 'cb-dependency-injection/ts/src/app/hero-bio.component.ts' region='template'}
It looks like this, with the hero's telephone number from `HeroContactComponent` projected above the hero description:
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/hero-bio-and-content.png" alt="bio and contact"> </img>
</figure>
Here's the `HeroContactComponent` which demonstrates the qualifying decorators that we're talking about in this section:
{@example 'cb-dependency-injection/ts/src/app/hero-contact.component.ts' region='component'}
Focus on the constructor parameters
{@example 'cb-dependency-injection/ts/src/app/hero-contact.component.ts' region='ctor-params'}
The `@Host()` function decorating the `heroCache` property ensures that
we get a reference to the cache service from the parent `HeroBioComponent`.
Angular throws if the parent lacks that service, even if a component higher in the component tree happens to have that service.
A second `@Host()` function decorates the `loggerService` property.
We know the only `LoggerService` instance in the app is provided at the `AppComponent` level.
The host `HeroBioComponent` doesn't have its own `LoggerService` provider.
Angular would throw an error if we hadn't also decorated the property with the `@Optional()` function.
Thanks to `@Optional()`, Angular sets the `loggerService` to null and the rest of the component adapts.
We'll come back to the `elementRef` property shortly.Here's the `HeroBiosAndContactsComponent` in action.
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/hero-bios-and-contacts.png" alt="Bios with contact into"> </img>
</figure>
If we comment out the `@Host()` decorator, Angular now walks up the injector ancestor tree
until it finds the logger at the `AppComponent` level. The logger logic kicks in and the hero display updates
with the gratuitous "!!!", indicating that the logger was found.
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/hero-bio-contact-no-host.png" alt="Without @Host"> </img>
</figure>
On the other hand, if we restore the `@Host()` decorator and comment out `@Optional`,
the application fails for lack of the required logger at the host component level.
<br>
`EXCEPTION: No provider for LoggerService! (HeroContactComponent -> LoggerService)`
<a id="component-element"></a>## Inject the component's element
On occasion we might need to access a component's corresponding DOM element.
Although we strive to avoid it, many visual effects and 3rd party tools (such as jQuery)
require DOM access.
To illustrate, we've written a simplified version of the `HighlightDirective` from
the [Attribute Directives](../guide/attribute-directives.html) chapter.
{@example 'cb-dependency-injection/ts/src/app/highlight.directive.ts'}
The directive sets the background to a highlight color when the user mouses over the
DOM element to which it is applied.
Angular set the constructor's `el` parameter to the injected `ElementRef` which is
a wrapper around that DOM element.
Its `nativeElement` property exposes the DOM element for the directive to manipulate.
The sample code applies the directive's `myHighlight` attribute to two `<div>` tags,
first without a value (yielding the default color) and then with an assigned color value.
{@example 'cb-dependency-injection/ts/src/app/app.component.html' region='highlight'}
The following image shows the effect of mousing over the `<hero-bios-and-contacts>` tag.
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/highlight.png" alt="Highlighted bios"> </img>
</figure>
<a id="providers"></a>
## Define dependencies with providers
In this section we learn to write providers that deliver dependent services.
### Background
We get a service from a dependency injector by giving it a ***token***.
We usually let Angular handle this transaction for us by specifying a constructor parameter and its type.
The parameter type serves as the injector lookup *token*.
Angular passes this token to the injector and assigns the result to the parameter.
Here's a typical example:
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='ctor'}
Angular asks the injector for the service associated with the `LoggerService` and
and assigns the returned value to the `logger` parameter.
Where did the injector get that value?
It may already have that value in its internal container.
If it doesn't, it may be able to make one with the help of a ***provider***.
A *provider* is a recipe for delivering a service associated with a *token*.
If the injector doesn't have a provider for the requested *token*, it delegates the request
to its parent injector, where the process repeats until there are no more injectors.
If the search is futile, the injector throws an error ... unless the request was [optional](#optional).
Let's return our attention to providers themselves.A new injector has no providers.
Angular initializes the injectors it creates with some providers it cares about.
We have to register our _own_ application providers manually,
usually in the `providers` array of the `Component` or `Directive` metadata:
{@example 'cb-dependency-injection/ts/src/app/app.component.ts' region='providers'}
### Defining providers
The simple class provider is the most typical by far.
We mention the class in the `providers` array and we're done.
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='class-provider'}
It's that simple because the most common injected service is an instance of a class.
But not every dependency can be satisfied by creating a new instance of a class.
We need other ways to deliver dependency values and that means we need other ways to specify a provider.
The `HeroOfTheMonthComponent` example demonstrates many of the alternatives and why we need them.
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/hero-of-month.png" alt="Hero of the month" width="300px"> </img>
</figure>
It's visually simple: a few properties and the output of a logger. The code behind it gives us plenty to talk about.
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='hero-of-the-month'}
{@a provide}
#### The *provide* object literal
The `provide` object literal takes a *token* and a *definition object*.
The *token* is usually a class but [it doesn't have to be](#tokens).
The *definition* object has one main property, (e.g. `useValue`) that indicates how the provider
should create or return the provided value.
{@a usevalue}
#### useValue - the *value provider*
Set the `useValue` property to a ***fixed value*** that the provider can return as the dependency object.
Use this technique to provide *runtime configuration constants* such as web-site base addresses and feature flags.
We often use a *value provider* in a unit test to replace a production service with a fake or mock.
The `HeroOfTheMonthComponent` example has two *value providers*.
The first provides an instance of the `Hero` class;
the second specifies a literal string resource:
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='use-value'}
The `Hero` provider token is a class which makes sense because the value is a `Hero`
and the consumer of the injected hero would want the type information.
The `TITLE` provider token is *not a class*.
It's a special kind of provider lookup key called an [OpaqueToken](#opaquetoken).
We often use an `OpaqueToken` when the dependency is a simple value like a string, a number, or a function.
The value of a *value provider* must be defined *now*. We can't create the value later.
Obviously the title string literal is immediately available.
The `someHero` variable in this example was set earlier in the file:
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='some-hero'}
The other providers create their values *lazily* when they're needed for injection.
{@a useclass}
#### useClass - the *class provider*
The `useClass` provider creates and returns new instance of the specified class.
Use this technique to ***substitute an alternative implementation*** for a common or default class.
The alternative could implement a different strategy, extend the default class,
or fake the behavior of the real class in a test case.
We see two examples in the `HeroOfTheMonthComponent`:
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='use-class'}
The first provider is the *de-sugared*, expanded form of the most typical case in which the
class to be created (`HeroService`) is also the provider's injection token.
We wrote it in this long form to de-mystify the preferred short form.
The second provider substitutes the `DateLoggerService` for the `LoggerService`.
The `LoggerService` is already registered at the `AppComponent` level.
When _this component_ requests the `LoggerService`, it receives the `DateLoggerService` instead.
This component and its tree of child components receive the `DateLoggerService` instance.
Components outside the tree continue to receive the original `LoggerService` instance.The `DateLoggerService` inherits from `LoggerService`; it appends the current date/time to each message:
{@example 'cb-dependency-injection/ts/src/app/date-logger.service.ts' region='date-logger-service'}
{@a useexisting}
#### useExisting - the *alias provider*
The `useExisting` provider maps one token to another.
In effect, the first token is an ***alias*** for the service associated with second token,
creating ***two ways to access the same service object***.
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='use-existing'}
Narrowing an API through an aliasing interface is _one_ important use case for this technique.
We're aliasing for that very purpose here.
Imagine that the `LoggerService` had a large API (it's actually only three methods and a property).
We want to shrink that API surface to just the two members exposed by the `MinimalLogger` [*class-interface*](#class-interface):
{@example 'cb-dependency-injection/ts/src/app/date-logger.service.ts' region='minimal-logger'}
The constructor's `logger` parameter is typed as `MinimalLogger` so only its two members are visible in TypeScript:
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/minimal-logger-intellisense.png" alt="MinimalLogger restricted API"> </img>
</figure>
Angular actually sets the `logger` parameter to the injector's full version of the `LoggerService`
which happens to be the `DateLoggerService` thanks to the override provider registered previously via `useClass`.
The following image, which displays the logging date, confirms the point:
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/date-logger-entry.png" alt="DateLoggerService entry" width="300px"> </img>
</figure>
{@a usefactory}
#### useFactory - the *factory provider*
The `useFactory` provider creates a dependency object by calling a factory function
as seen in this example.
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='use-factory'}
Use this technique to ***create a dependency object***
with a factory function whose inputs are some ***combination of injected services and local state***.
The *dependency object* doesn't have to be a class instance. It could be anything.
In this example, the *dependency object* is a string of the names of the runners-up
to the "Hero of the Month" contest.
The local state is the number `2`, the number of runners-up this component should show.
We execute `runnersUpFactory` immediately with `2`.
The `runnersUpFactory` itself isn't the provider factory function.
The true provider factory function is the function that `runnersUpFactory` returns.
{@example 'cb-dependency-injection/ts/src/app/runners-up.ts' region='factory-synopsis'}
That returned function takes a winning `Hero` and a `HeroService` as arguments.
Angular supplies these arguments from injected values identified by
the two *tokens* in the `deps` array.
The two `deps` values are *tokens* that the injector uses
to provide these factory function dependencies.
After some undisclosed work, the function returns the string of names
and Angular injects it into the `runnersUp` parameter of the `HeroOfTheMonthComponent`.
The function retrieves candidate heroes from the `HeroService`,
takes `2` of them to be the runners-up, and returns their concatenated names.
Look at the <live-example name="cb-dependency-injection"></live-example>
for the full source code.
{@a tokens}
## Provider token alternatives: the *class-interface* and *OpaqueToken*
Angular dependency injection is easiest when the provider *token* is a class
that is also the type of the returned dependency object (what we usually call the *service*).
But the token doesn't have to be a class and even when it is a class,
it doesn't have to be the same type as the returned object.
That's the subject of our next section.
<a id="class-interface"></a>
### class-interface
In the previous *Hero of the Month* example, we used the `MinimalLogger` class
as the token for a provider of a `LoggerService`.
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='use-existing'}
The `MinimalLogger` is an abstract class.
{@example 'cb-dependency-injection/ts/src/app/date-logger.service.ts' region='minimal-logger'}
We usually inherit from an abstract class.
But `LoggerService` doesn't inherit from `MinimalLogger`. *No class* inherits from it.
Instead, we use it like an interface.
Look again at the declaration for `DateLoggerService`
{@example 'cb-dependency-injection/ts/src/app/date-logger.service.ts' region='date-logger-service-signature'}
`DateLoggerService` inherits (extends) from `LoggerService`, not `MinimalLogger`.
The `DateLoggerService` *implements* `MinimalLogger` as if `MinimalLogger` were an *interface*.
We call a class used in this way a ***class-interface***.
The key benefit of a *class-interface* is that we can get the strong-typing of an interface
and we can ***use it as a provider token*** in the same manner as a normal class.
A ***class-interface*** should define *only* the members that its consumers are allowed to call.
Such a narrowing interface helps decouple the concrete class from its consumers.
The `MinimalLogger` defines just two of the `LoggerClass` members.
#### Why *MinimalLogger* is a class and not an interface
We can't use an interface as a provider token because
interfaces are not JavaScript objects.
They exist only in the TypeScript design space.
They disappear after the code is transpiled to JavaScript.
A provider token must be a real JavaScript object of some kind:
a function, an object, a string ... a class.
Using a class as an interface gives us the characteristics of an interface in a JavaScript object.
The minimize memory cost, the class should have *no implementation*.
The `MinimalLogger` transpiles to this unoptimized, pre-minified JavaScript:
{@example 'cb-dependency-injection/ts/src/app/date-logger.service.ts' region='minimal-logger-transpiled'}
It never grows larger no matter how many members we add *as long as they are typed but not implemented*.
{@a opaque-token}
### OpaqueToken
Dependency objects can be simple values like dates, numbers and strings or
shapeless objects like arrays and functions.
Such objects don't have application interfaces and therefore aren't well represented by a class.
They're better represented by a token that is both unique and symbolic,
a JavaScript object that has a friendly name but won't conflict with
another token that happens to have the same name.
The `OpaqueToken` has these characteristics.
We encountered them twice in the *Hero of the Month* example,
in the *title* value provider and in the *runnersUp* factory provider.
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='provide-opaque-token'}
We created the `TITLE` token like this:
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='opaque-token'}
{@a di-inheritance}
## Inject into a derived class
We must take care when writing a component that inherits from another component.
If the base component has injected dependencies,
we must re-provide and re-inject them in the derived class
and then pass them down to the base class through the constructor.
In this contrived example, `SortedHeroesComponent` inherits from `HeroesBaseComponent`
to display a *sorted* list of heroes.
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/sorted-heroes.png" alt="Sorted Heroes"> </img>
</figure>
The `HeroesBaseComponent` could stand on its own.
It demands its own instance of the `HeroService` to get heroes
and displays them in the order they arrive from the database.
{@example 'cb-dependency-injection/ts/src/app/sorted-heroes.component.ts' region='heroes-base'}
We strongly prefer simple constructors. They should do little more than initialize variables.
This rule makes the component safe to construct under test without fear that it will do something dramatic like talk to the server.
That's why we call the `HeroService` from within the `ngOnInit` rather than the constructor.
We explain the mysterious `afterGetHeroes` below.Users want to see the heroes in alphabetical order.
Rather than modify the original component, we sub-class it and create a
`SortedHeroesComponent` that sorts the heroes before presenting them.
The `SortedHeroesComponent` lets the base class fetch the heroes.
(we said it was contrived).
Unfortunately, Angular cannot inject the `HeroService` directly into the base class.
We must provide the `HeroService` again for *this* component,
then pass it down to the base class inside the constructor.
{@example 'cb-dependency-injection/ts/src/app/sorted-heroes.component.ts' region='sorted-heroes'}
Now take note of the `afterGetHeroes` method.
Our first instinct was to create an `ngOnInit` method in `SortedHeroesComponent` and do the sorting there.
But Angular calls the *derived* class's `ngOnInit` *before* calling the base class's `ngOnInit`
so we'd be sorting the heroes array *before they arrived*. That produces a nasty error.
Overriding the base class's `afterGetHeroes` method solves the problem
These complications argue for *avoiding component inheritance*.
{@a find-parent}
## Find a parent component by injection
Application components often need to share information.
We prefer the more loosely coupled techniques such as data binding and service sharing.
But sometimes it makes sense for one component to have a direct reference to another component
perhaps to access values or call methods on that component.
Obtaining a component reference is a bit tricky in Angular.
Although an Angular application is a tree of components,
there is no public API for inspecting and traversing that tree.
There is an API for acquiring a child reference
(checkout `Query`, `QueryList`, `ViewChildren`, and `ContentChildren`).
There is no public API for acquiring a parent reference.
But because every component instance is added to an injector's container,
we can use Angular dependency injection to reach a parent component.
This section describes some techniques for doing that.
<a id="known-parent"></a>
### Find a parent component of known type
We use standard class injection to acquire a parent component whose type we know.
In the following example, the parent `AlexComponent` has several children including a `CathyComponent`:
{@a alex}
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-1'}
*Cathy* reports whether or not she has access to *Alex*
after injecting an `AlexComponent` into her constructor:
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='cathy'}
We added the [@Optional](#optional) qualifier for safety but
the <live-example name="cb-dependency-injection"></live-example>
confirms that the `alex` parameter is set.
<a id="base-parent"></a>
### Cannot find a parent by its base class
What if we do *not* know the concrete parent component class?
A re-usable component might be a child of multiple components.
Imagine a component for rendering breaking news about a financial instrument.
For sound (cough) business reasons, this news component makes frequent calls
directly into its parent instrument as changing market data stream by.
The app probably defines more than a dozen financial instrument components.
If we're lucky, they all implement the same base class
whose API our `NewsComponent` understands.
Looking for components that implement an interface would be better.
That's not possible because TypeScript interfaces disappear from the transpiled JavaScript
which doesn't support interfaces. There's no artifact we could look for.We're not claiming this is good design.
We are asking *can a component inject its parent via the parent's base class*?
The sample's `CraigComponent` explores this question. [Looking back](#alex)
we see that the `Alex` component *extends* (*inherits*) from a class named `Base`.
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-class-signature'}
The `CraigComponent` tries to inject `Base` into its `alex` constructor parameter and reports if it succeeded.
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='craig'}
Unfortunately, this does not work.
The <live-example name="cb-dependency-injection"></live-example>
confirms that the `alex` parameter is null.
*We cannot inject a parent by its base class.*
<a id="class-interface-parent"></a>
### Find a parent by its class-interface
We can find a parent component with a [class-interface](#class-interface).
The parent must cooperate by providing an *alias* to itself in the name of a *class-interface* token.
Recall that Angular always adds a component instance to its own injector;
that's why we could inject *Alex* into *Cathy* [earlier](#known-parent).
We write an [*alias provider*](#useexisting) &mdash; a `provide` object literal with a `useExisting` definition &mdash;
that creates an *alternative* way to inject the same component instance
and add that provider to the `providers` array of the `@Component` metadata for the `AlexComponent`:
{@a alex-providers}
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-providers'}
[Parent](#parent-token) is the provider's *class-interface* token.
The [*forwardRef*](#forwardref) breaks the circular reference we just created by having the `AlexComponent` refer to itself.
*Carol*, the third of *Alex*'s child components, injects the parent into its `parent` parameter, the same way we've done it before:
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='carol-class'}
Here's *Alex* and family in action:
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/alex.png" alt="Alex in action"> </img>
</figure>
{@a parent-tree}
### Find the parent in a tree of parents
Imagine one branch of a component hierarchy: *Alice* -> *Barry* -> *Carol*.
Both *Alice* and *Barry* implement the `Parent` *class-interface*.
*Barry* is the problem. He needs to reach his parent, *Alice*, and also be a parent to *Carol*.
That means he must both *inject* the `Parent` *class-interface* to get *Alice* and
*provide* a `Parent` to satisfy *Carol*.
Here's *Barry*:
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='barry'}
*Barry*'s `providers` array looks just like [*Alex*'s](#alex-providers).
If we're going to keep writing [*alias providers*](#useexisting) like this we should create a [helper function](#provideparent).
For now, focus on *Barry*'s constructor:
<md-tab-group>
<md-tab label="Barry's constructor">
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='barry-ctor'}
</md-tab>
<md-tab label="Carol's constructor">
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='carol-ctor'}
</md-tab>
</md-tab-group>
It's identical to *Carol*'s constructor except for the additional `@SkipSelf` decorator.
`@SkipSelf` is essential for two reasons:
1. It tells the injector to start its search for a `Parent` dependency in a component *above* itself,
which *is* what parent means.
2. Angular throws a cyclic dependency error if we omit the `@SkipSelf` decorator.
`Cannot instantiate cyclic dependency! (BethComponent -> Parent -> BethComponent)`
Here's *Alice*, *Barry* and family in action:
<figure class='image-display'>
<img src="assets/images/cookbooks/dependency-injection/alice.png" alt="Alice in action"> </img>
</figure>
{@a parent-token}
### The *Parent* class-interface
We [learned earlier](#class-interface) that a *class-interface* is an abstract class used as an interface rather than as a base class.
Our example defines a `Parent` *class-interface* .
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='parent'}
The `Parent` *class-interface* defines a `name` property with a type declaration but *no implementation*.,
The `name` property is the only member of a parent component that a child component can call.
Such a narrowing interface helps decouple the child component class from its parent components.
A component that could serve as a parent *should* implement the *class-interface* as the `AliceComponent` does:
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alice-class-signature'}
Doing so adds clarity to the code. But it's not technically necessary.
Although the `AlexComponent` has a `name` property (as required by its `Base` class)
its class signature doesn't mention `Parent`:
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-class-signature'}
The `AlexComponent` *should* implement `Parent` as a matter of proper style.
It doesn't in this example *only* to demonstrate that the code will compile and run without the interface
{@a provideparent}
### A *provideParent* helper function
Writing variations of the same parent *alias provider* gets old quickly,
especially this awful mouthful with a [*forwardRef*](#forwardref):
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-providers'}
We can extract that logic into a helper function like this:
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='provide-the-parent'}
Now we can add a simpler, more meaningful parent provider to our components:
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alice-providers'}
We can do better. The current version of the helper function can only alias the `Parent` *class-interface*.
Our application might have a variety of parent types, each with its own *class-interface* token.
Here's a revised version that defaults to `parent` but also accepts an optional second parameter for a different parent *class-interface*.
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='provide-parent'}
And here's how we could use it with a different parent type:
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='beth-providers'}
{@a forwardref}
## Break circularities with a forward class reference (*forwardRef*)
The order of class declaration matters in TypeScript.
We can't refer directly to a class until it's been defined.
This isn't usually a problem, especially if we adhere to the recommended *one class per file* rule.
But sometimes circular references are unavoidable.
We're in a bind when class 'A refers to class 'B' and 'B' refers to 'A'.
One of them has to be defined first.
The Angular `forwardRef` function creates an *indirect* reference that Angular can resolve later.
The *Parent Finder* sample is full of circular class references that are impossible to break.
We face this dilemma when a class makes *a reference to itself*
as does the `AlexComponent` in its `providers` array.
The `providers` array is a property of the `@Component` decorator function which must
appear *above* the class definition.
We break the circularity with `forwardRef`:
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-providers'}

View File

@ -1,138 +0,0 @@
@title
Dynamic Component Loader
@intro
Load components dynamically
@description
Component templates are not always fixed. An application may need to load new components at runtime.
In this cookbook we show how to use `ComponentFactoryResolver` to add components dynamically.
<a id="toc"></a>## Table of contents
[Dynamic Component Loading](#dynamic-loading)
[Where to load the component](#where-to-load)
[Loading components](#loading-components)
<a id="dynamic-loading"></a>## Dynamic Component Loading
The following example shows how to build a dynamic ad banner.
The hero agency is planning an ad campaign with several different ads cycling through the banner.
New ad components are added frequently by several different teams. This makes it impractical to use a template with a static component structure.
Instead we need a way to load a new component without a fixed reference to the component in the ad banner's template.
Angular comes with its own API for loading components dynamically. In the following sections you will learn how to use it.
<a id="where-to-load"></a>## Where to load the component
Before components can be added we have to define an anchor point to mark where components can be inserted dynamically.
The ad banner uses a helper directive called `AdDirective` to mark valid insertion points in the template.
{@example 'cb-dynamic-component-loader/ts/src/app/ad.directive.ts'}
`AdDirective` injects `ViewContainerRef` to gain access to the view container of the element that will become the host of the dynamically added component.
<a id="loading-components"></a>## Loading components
The next step is to implement the ad banner. Most of the implementation is in `AdBannerComponent`.
We start by adding a `template` element with the `AdDirective` directive applied.
<md-tab-group>
<md-tab label="ad-banner.component.ts">
{@example 'cb-dynamic-component-loader/ts/src/app/ad-banner.component.ts'}
</md-tab>
<md-tab label="ad.service.ts">
{@example 'cb-dynamic-component-loader/ts/src/app/ad.service.ts'}
</md-tab>
<md-tab label="ad-item.ts">
{@example 'cb-dynamic-component-loader/ts/src/app/ad-item.ts'}
</md-tab>
<md-tab label="app.module.ts">
{@example 'cb-dynamic-component-loader/ts/src/app/app.module.ts'}
</md-tab>
<md-tab label="app.component">
{@example 'cb-dynamic-component-loader/ts/src/app/app.component.ts'}
</md-tab>
</md-tab-group>
The `template` element decorated with the `ad-host` directive marks where dynamically loaded components will be added.
Using a `template` element is recommended since it doesn't render any additional output.
{@example 'cb-dynamic-component-loader/ts/src/app/ad-banner.component.ts' region='ad-host'}
### Resolving Components
`AdBanner` takes an array of `AdItem` objects as input. `AdItem` objects specify the type of component to load and any data to bind to the component.
The ad components making up the ad campaign are returned from `AdService`.
Passing an array of components to `AdBannerComponent` allows for a dynamic list of ads without static elements in the template.
`AdBannerComponent` cycles through the array of `AdItems` and loads the corresponding components on an interval. Every 3 seconds a new component is loaded.
`ComponentFactoryResolver` is used to resolve a `ComponentFactory` for each specific component. The component factory is need to create an instance of the component.
`ComponentFactories` are generated by the Angular compiler.
Generally the compiler will generate a component factory for any component referenced in a template.
With dynamically loaded components there are no selector references in the templates since components are loaded at runtime. In order to ensure that the compiler will still generate a factory, dynamically loaded components have to be added to their `NgModule`'s `entryComponents` array.
{@example 'cb-dynamic-component-loader/ts/src/app/app.module.ts' region='entry-components'}
Components are added to the template by calling `createComponent` on the `ViewContainerRef` reference.
`createComponent` returns a reference to the loaded component. The component reference can be used to pass input data or call methods to interact with the component.
In the Ad banner, all components implement a common `AdComponent` interface to standardize the api for passing data to the components.
Two sample components and the `AdComponent` interface are shown below:
<md-tab-group>
<md-tab label="hero-job-ad.component.ts">
{@example 'cb-dynamic-component-loader/ts/src/app/hero-job-ad.component.ts'}
</md-tab>
<md-tab label="hero-profile.component.ts">
{@example 'cb-dynamic-component-loader/ts/src/app/hero-profile.component.ts'}
</md-tab>
<md-tab label="ad.component.ts">
{@example 'cb-dynamic-component-loader/ts/src/app/ad.component.ts'}
</md-tab>
</md-tab-group>
The final ad banner looks like this:
<figure class='image-display'>
<img src="assets/images/cookbooks/dynamic-component-loader/ads.gif" alt="Ads"> </img>
</figure>

View File

@ -1,168 +0,0 @@
@title
Dynamic Forms
@intro
Render dynamic forms with FormGroup
@description
We can't always justify the cost and time to build handcrafted forms,
especially if we'll need a great number of them, they're similar to each other, and they change frequently
to meet rapidly changing business and regulatory requirements.
It may be more economical to create the forms dynamically, based on metadata that describe the business object model.
In this cookbook we show how to use `formGroup` to dynamically render a simple form with different control types and validation.
It's a primitive start.
It might evolve to support a much richer variety of questions, more graceful rendering, and superior user experience.
All such greatness has humble beginnings.
In our example we use a dynamic form to build an online application experience for heroes seeking employment.
The agency is constantly tinkering with the application process.
We can create the forms on the fly *without changing our application code*.
<a id="toc"></a>## Table of contents
[Bootstrap](#bootstrap)
[Question Model](#object-model)
[Form Component](#form-component)
[Questionnaire Metadata](#questionnaire-metadata)
[Dynamic Template](#dynamic-template)
**See the <live-example name="cb-dynamic-form"></live-example>**.
<a id="bootstrap"></a>## Bootstrap
We start by creating an `NgModule` called `AppModule`.
In our example we will be using Reactive Forms.
Reactive Forms belongs to a different `NgModule` called `ReactiveFormsModule`, so in order to access any Reactive Forms directives, we have to import `ReactiveFormsModule` from the `@angular/forms` library.
We bootstrap our `AppModule` in main.ts.
<md-tab-group>
<md-tab label="app.module.ts">
{@example 'cb-dynamic-form/ts/src/app/app.module.ts'}
</md-tab>
<md-tab label="main.ts">
{@example 'cb-dynamic-form/ts/src/main.ts'}
</md-tab>
</md-tab-group>
<a id="object-model"></a>## Question Model
The next step is to define an object model that can describe all scenarios needed by the form functionality.
The hero application process involves a form with a lot of questions.
The "question" is the most fundamental object in the model.
We have created `QuestionBase` as the most fundamental question class.
{@example 'cb-dynamic-form/ts/src/app/question-base.ts'}
From this base we derived two new classes in `TextboxQuestion` and `DropdownQuestion` that represent Textbox and Dropdown questions.
The idea is that the form will be bound to specific question types and render the appropriate controls dynamically.
`TextboxQuestion` supports multiple html5 types like text, email, url etc via the `type` property.
{@example 'cb-dynamic-form/ts/src/app/question-textbox.ts'}
`DropdownQuestion` presents a list of choices in a select box.
{@example 'cb-dynamic-form/ts/src/app/question-dropdown.ts'}
Next we have defined `QuestionControlService`, a simple service for transforming our questions to a `FormGroup`.
In a nutshell, the form group consumes the metadata from the question model and allows us to specify default values and validation rules.
{@example 'cb-dynamic-form/ts/src/app/question-control.service.ts'}
<a id="form-component"></a>## Question form components
Now that we have defined the complete model we are ready to create components to represent the dynamic form.
`DynamicFormComponent` is the entry point and the main container for the form.
<md-tab-group>
<md-tab label="dynamic-form.component.html">
{@example 'cb-dynamic-form/ts/src/app/dynamic-form.component.html'}
</md-tab>
<md-tab label="dynamic-form.component.ts">
{@example 'cb-dynamic-form/ts/src/app/dynamic-form.component.ts'}
</md-tab>
</md-tab-group>
It presents a list of questions, each question bound to a `<df-question>` component element.
The `<df-question>` tag matches the `DynamicFormQuestionComponent`,
the component responsible for rendering the details of each _individual_ question based on values in the data-bound question object.
<md-tab-group>
<md-tab label="dynamic-form-question.component.html">
{@example 'cb-dynamic-form/ts/src/app/dynamic-form-question.component.html'}
</md-tab>
<md-tab label="dynamic-form-question.component.ts">
{@example 'cb-dynamic-form/ts/src/app/dynamic-form-question.component.ts'}
</md-tab>
</md-tab-group>
Notice this component can present any type of question in our model.
We only have two types of questions at this point but we can imagine many more.
The `ngSwitch` determines which type of question to display.
In both components we're relying on Angular's **formGroup** to connect the template HTML to the
underlying control objects, populated from the question model with display and validation rules.
`formControlName` and `formGroup` are directives defined in `ReactiveFormsModule`. Our templates can can access these directives directly since we imported `ReactiveFormsModule` from `AppModule`.
<a id="questionnaire-metadata"></a>## Questionnaire data`DynamicFormComponent` expects the list of questions in the form of an array bound to `@Input() questions`.
The set of questions we have defined for the job application is returned from the `QuestionService`.
In a real app we'd retrieve these questions from storage.
The key point is that we control the hero job application questions entirely through the objects returned from `QuestionService`.
Questionnaire maintenance is a simple matter of adding, updating, and removing objects from the `questions` array.
{@example 'cb-dynamic-form/ts/src/app/question.service.ts'}
Finally, we display an instance of the form in the `AppComponent` shell.
{@example 'cb-dynamic-form/ts/src/app/app.component.ts'}
<a id="dynamic-template"></a>## Dynamic Template
Although in this example we're modelling a job application for heroes, there are no references to any specific hero question
outside the objects returned by `QuestionService`.
This is very important since it allows us to repurpose the components for any type of survey
as long as it's compatible with our *question* object model.
The key is the dynamic data binding of metadata used to render the form
without making any hardcoded assumptions about specific questions.
In addition to control metadata, we are also adding validation dynamically.
The *Save* button is disabled until the form is in a valid state.
When the form is valid, we can click *Save* and the app renders the current form values as JSON.
This proves that any user input is bound back to the data model.
Saving and retrieving the data is an exercise for another time.
The final form looks like this:
<figure class='image-display'>
<img src="assets/images/cookbooks/dynamic-form/dynamic-form.png" alt="Dynamic-Form"> </img>
</figure>
[Back to top](#top)

View File

@ -1,486 +0,0 @@
@title
Form Validation
@intro
Validate user's form entries
@description
{@a top}
We can improve overall data quality by validating user input for accuracy and completeness.
In this cookbook we show how to validate user input in the UI and display useful validation messages
using first the template-driven forms and then the reactive forms approach.
Learn more about these choices in the [Forms chapter.](../guide/forms.html)
{@a toc}
## Table of Contents
[Simple Template-Driven Forms](#template1)
[Template-Driven Forms with validation messages in code](#template2)
[Reactive Forms with validation in code](#reactive)
[Custom validation](#custom-validation)
[Testing](#testing)
{@a live-example}
**Try the live example to see and download the full cookbook source code**
<live-example name="cb-form-validation" embedded=true img="cookbooks/form-validation/plunker.png">
</live-example>
{@a template1}
## Simple Template-Driven Forms
In the template-driven approach, you arrange
[form elements](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms_in_HTML) in the component's template.
You add Angular form directives (mostly directives beginning `ng...`) to help
Angular construct a corresponding internal control model that implements form functionality.
We say that the control model is _implicit_ in the template.
To validate user input, you add [HTML validation attributes](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation)
to the elements. Angular interprets those as well, adding validator functions to the control model.
Angular exposes information about the state of the controls including
whether the user has "touched" the control or made changes and if the control values are valid.
In the first template validation example,
we add more HTML to read that control state and update the display appropriately.
Here's an excerpt from the template html for a single input box control bound to the hero name:
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.html' region='name-with-error-msg'}
Note the following:
- The `<input>` element carries the HTML validation attributes: `required`, `minlength`, and `maxlength`.
- We set the `name` attribute of the input box to `"name"` so Angular can track this input element and associate it
with an Angular form control called `name` in its internal control model.
- We use the `[(ngModel)]` directive to two-way data bind the input box to the `hero.name` property.
- We set a template variable (`#name`) to the value `"ngModel"` (always `ngModel`).
This gives us a reference to the Angular `NgModel` directive
associated with this control that we can use _in the template_
to check for control states such as `valid` and `dirty`.
- The `*ngIf` on `<div>` element reveals a set of nested message `divs` but only if there are "name" errors and
the control is either `dirty` or `touched`.
- Each nested `<div>` can present a custom message for one of the possible validation errors.
We've prepared messages for `required`, `minlength`, and `maxlength`.
The full template repeats this kind of layout for each data entry control on the form.
#### Why check _dirty_ and _touched_?
We shouldn't show errors for a new hero before the user has had a chance to edit the value.
The checks for `dirty` and `touched` prevent premature display of errors.
Learn about `dirty` and `touched` in the [Forms](../guide/forms.html) chapter.The component class manages the hero model used in the data binding
as well as other code to support the view.
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.ts' region='class'}
Use this template-driven validation technique when working with static forms with simple, standard validation rules.
Here are the complete files for the first version of `HeroFormTemplateCompononent` in the template-driven approach:
<md-tab-group>
<md-tab label="template/hero-form-template1.component.html">
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.html'}
</md-tab>
<md-tab label="template/hero-form-template1.component.ts">
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.ts'}
</md-tab>
</md-tab-group>
{@a template2}
## Template-Driven Forms with validation messages in code
While the layout is straightforward,
there are obvious shortcomings with the way we handle validation messages:
* It takes a lot of HTML to represent all possible error conditions.
This gets out of hand when there are many controls and many validation rules.
* We're not fond of so much JavaScript logic in HTML.
* The messages are static strings, hard-coded into the template.
We often require dynamic messages that we should shape in code.
We can move the logic and the messages into the component with a few changes to
the template and component.
Here's the hero name again, excerpted from the revised template ("Template 2"), next to the original version:
<md-tab-group>
<md-tab label="hero-form-template2.component.html (name #2)">
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.html' region='name-with-error-msg'}
</md-tab>
<md-tab label="hero-form-template1.component.html (name #1)">
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.html' region='name-with-error-msg'}
</md-tab>
</md-tab-group>
The `<input>` element HTML is almost the same. There are noteworthy differences:
- The hard-code error message `<divs>` are gone.
- There's a new attribute, `forbiddenName`, that is actually a custom validation directive.
It invalidates the control if the user enters "bob" anywhere in the name ([try it](#live-example)).
We discuss [custom validation directives](#custom-validation) later in this cookbook.
- The `#name` template variable is gone because we no longer refer to the Angular control for this element.
- Binding to the new `formErrors.name` property is sufficent to display all name validation error messages.
#### Component class
The original component code stays the same.
We _added_ new code to acquire the Angular form control and compose error messages.
The first step is to acquire the form control that Angular created from the template by querying for it.
Look back at the top of the component template where we set the
`#heroForm` template variable in the `<form>` element:
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.html' region='form-tag'}
The `heroForm` variable is a reference to the control model that Angular derived from the template.
We tell Angular to inject that model into the component class's `currentForm` property using a `@ViewChild` query:
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.ts' region='view-child'}
Some observations:
- Angular `@ViewChild` queries for a template variable when you pass it
the name of that variable as a string (`'heroForm'` in this case).
- The `heroForm` object changes several times during the life of the component, most notably when we add a new hero.
We'll have to re-inspect it periodically.
- Angular calls the `ngAfterViewChecked` [lifecycle hook method](../guide/lifecycle-hooks.html#afterview)
when anything changes in the view.
That's the right time to see if there's a new `heroForm` object.
- When there _is_ a new `heroForm` model, we subscribe to its `valueChanged` _Observable_ property.
The `onValueChanged` handler looks for validation errors after every user keystroke.
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.ts' region='handler'}
The `onValueChanged` handler interprets user data entry.
The `data` object passed into the handler contains the current element values.
The handler ignores them. Instead, it iterates over the fields of the component's `formErrors` object.
The `formErrors` is a dictionary of the hero fields that have validation rules and their current error messages.
Only two hero properties have validation rules, `name` and `power`.
The messages are empty strings when the hero data are valid.
For each field, the handler
- clears the prior error message if any
- acquires the field's corresponding Angular form control
- if such a control exists _and_ its been changed ("dirty") _and_ its invalid ...
- the handler composes a consolidated error message for all of the control's errors.
We'll need some error messages of course, a set for each validated property, one message per validation rule:
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.ts' region='messages'}
Now every time the user makes a change, the `onValueChanged` handler checks for validation errors and produces messages accordingly.
### Is this an improvement?
Clearly the template got substantially smaller while the component code got substantially larger.
It's not easy to see the benefit when there are just three fields and only two of them have validation rules.
Consider what happens as we increase the number of validated fields and rules.
In general, HTML is harder to read and maintain than code.
The initial template was already large and threatening to get rapidly worse as we add more validation message `<divs>`.
After moving the validation messaging to the component,
the template grows more slowly and proportionally.
Each field has approximately the same number of lines no matter its number of validation rules.
The component also grows proportionally, at the rate of one line per validated field
and one line per validation message.
Both trends are manageable.
Now that the messages are in code, we have more flexibility. We can compose messages more intelligently.
We can refactor the messages out of the component, perhaps to a service class that retrieves them from the server.
In short, there are more opportunities to improve message handling now that text and logic have moved from template to code.
### _FormModule_ and template-driven forms
Angular has two different forms modules &mdash; `FormsModule` and `ReactiveFormsModule` &mdash;
that correspond with the two approaches to form development.
Both modules come from the same `@angular/forms` library package.
We've been reviewing the "Template-driven" approach which requires the `FormsModule`
Here's how we imported it in the `HeroFormTemplateModule`.
{@example 'cb-form-validation/ts/src/app/template/hero-form-template.module.ts'}
We haven't talked about the `SharedModule` or its `SubmittedComponent` which appears at the bottom of every
form template in this cookbook.
They're not germane to the validation story. Look at the [live example](#live-example) if you're interested.
{@a reactive}
## Reactive Forms
In the template-driven approach, you markup the template with form elements, validation attributes,
and `ng...` directives from the Angular `FormsModule`.
At runtime, Angular interprets the template and derives its _form control model_.
**Reactive Forms** takes a different approach.
You create the form control model in code. You write the template with form elements
and`form...` directives from the Angular `ReactiveFormsModule`.
At runtime, Angular binds the template elements to your control model based on your instructions.
This approach requires a bit more effort. *You have to write the control model and manage it*.
In return, you can
* add, change, and remove validation functions on the fly
* manipulate the control model dynamically from within the component
* [test](#testing) validation and control logic with isolated unit tests.
The third cookbook sample re-writes the hero form in _reactive forms_ style.
### Switch to the _ReactiveFormsModule_
The reactive forms classes and directives come from the Angular `ReactiveFormsModule`, not the `FormsModule`.
The application module for the "Reactive Forms" feature in this sample looks like this:
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.module.ts'}
The "Reactive Forms" feature module and component are in the `src/app/reactive` folder.
Let's focus on the `HeroFormReactiveComponent` there, starting with its template.
### Component template
We begin by changing the `<form>` tag so that it binds the Angular `formGroup` directive in the template
to the `heroForm` property in the component class.
The `heroForm` is the control model that the component class builds and maintains.
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.html' region='form-tag'}
Then we modify the template HTML elements to match the _reactive forms_ style.
Here is the "name" portion of the template again, revised for reactive forms and compared with the template-driven version:
<md-tab-group>
<md-tab label="hero-form-reactive.component.html (name #3)">
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.html' region='name-with-error-msg'}
</md-tab>
<md-tab label="hero-form-template1.component.html (name #2)">
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.html' region='name-with-error-msg'}
</md-tab>
</md-tab-group>
Key changes:
- the validation attributes are gone (except `required`) because we'll be validating in code.
- `required` remains, not for validation purposes (we'll cover that in the code),
but rather for css styling and accessibility.
A future version of reactive forms will add the `required` HTML validation attribute to the DOM element
(and perhaps the `aria-required` attribute) when the control has the `required` validator function.
Until then, apply the `required` attribute _and_ add the `Validator.required` function
to the control model, as we'll do below.
- the `formControlName` replaces the `name` attribute; it serves the same
purpose of correlating the input box with the Angular form control.
- the two-way `[(ngModel)]` binding is gone.
The reactive approach does not use data binding to move data into and out of the form controls.
We do that in code.
The retreat from data binding is a principle of the reactive paradigm rather than a technical limitation.### Component class
The component class is now responsible for defining and managing the form control model.
Angular no longer derives the control model from the template so we can no longer query for it.
We create the Angular form control model explicitly with the help of the `FormBuilder`.
Here's the section of code devoted to that process, paired with the template-driven code it replaces:
<md-tab-group>
<md-tab label="reactive/hero-form-reactive.component.ts (FormBuilder)">
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.ts' region='form-builder'}
</md-tab>
<md-tab label="template/hero-form-template2.component.ts (ViewChild)">
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.ts' region='view-child'}
</md-tab>
</md-tab-group>
- we inject the `FormBuilder` in a constructor.
- we call a `buildForm` method in the `ngOnInit` [lifecycle hook method](../guide/lifecycle-hooks.html#hooks-overview)
because that's when we'll have the hero data. We'll call it again in the `addHero` method.
A real app would retrieve the hero asynchronously from a data service, a task best performed in the `ngOnInit` hook.- the `buildForm` method uses the `FormBuilder` (`fb`) to declare the form control model.
Then it attaches the same `onValueChanged` handler (there's a one line difference)
to the form's `valueChanged` event and calls it immediately
to set error messages for the new control model.
#### _FormBuilder_ declaration
The `FormBuilder` declaration object specifies the three controls of the sample's hero form.
Each control spec is a control name with an array value.
The first array element is the current value of the corresponding hero field.
The (optional) second value is a validator function or an array of validator functions.
Most of the validator functions are stock validators provided by Angular as static methods of the `Validators` class.
Angular has stock validators that correspond to the standard HTML validation attributes.
The `forbiddenNames` validator on the `"name"` control is a custom validator,
discussed in a separate [section below](#custom-validation).
Learn more about `FormBuilder` in a _forthcoming_ chapter on reactive forms.
#### Committing hero value changes
In two-way data binding, the user's changes flow automatically from the controls back to the data model properties.
Reactive forms do not use data binding to update data model properties.
The developer decides _when and how_ to update the data model from control values.
This sample updates the model twice:
1. when the user submits the form
1. when the user chooses to add a new hero
The `onSubmit` method simply replaces the `hero` object with the combined values of the form:
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.ts' region='on-submit'}
This example is "lucky" in that the `heroForm.value` properties _just happen_ to
correspond _exactly_ to the hero data object properties.The `addHero` method discards pending changes and creates a brand new `hero` model object.
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.ts' region='add-hero'}
Then it calls `buildForm` again which replaces the previous `heroForm` control model with a new one.
The `<form>` tag's `[formGroup]` binding refreshes the page with the new control model.
Here's the complete reactive component file, compared to the two template-driven component files.
<md-tab-group>
<md-tab label="reactive/hero-form-reactive.component.ts (#3)">
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.ts'}
</md-tab>
<md-tab label="template/hero-form-template2.component.ts (#2)">
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.ts'}
</md-tab>
<md-tab label="template/hero-form-template1.component.ts (#1)">
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.ts'}
</md-tab>
</md-tab-group>
Run the [live example](#live-example) to see how the reactive form behaves
and to compare all of the files in this cookbook sample.
{@a custom-validation}
## Custom validation
This cookbook sample has a custom `forbiddenNamevalidator` function that's applied to both the
template-driven and the reactive form controls. It's in the `src/app/shared` folder
and declared in the `SharedModule`.
Here's the `forbiddenNamevalidator` function itself:
{@example 'cb-form-validation/ts/src/app/shared/forbidden-name.directive.ts' region='custom-validator'}
The function is actually a factory that takes a regular expression to detect a _specific_ forbidden name
and returns a validator function.
In this sample, the forbidden name is "bob";
the validator rejects any hero name containing "bob".
Elsewhere it could reject "alice" or any name that the configuring regular expression matches.
The `forbiddenNamevalidator` factory returns the configured validator function.
That function takes an Angular control object and returns _either_
null if the control value is valid _or_ a validation error object.
The validation error object typically has a property whose name is the validation key ('forbiddenName')
and whose value is an arbitrary dictionary of values that we could insert into an error message (`{name}`).
Learn more about validator functions in a _forthcoming_ chapter on custom form validation.#### Custom validation directive
In the reactive forms component we added a configured `forbiddenNamevalidator`
to the bottom of the `'name'` control's validator function list.
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.ts' region='name-validators'}
In the template-driven component template, we add the selector (`forbiddenName`) of a custom _attribute directive_ to the name's input box
and configured it to reject "bob".
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.html' region='name-input'}
The corresponding `ForbiddenValidatorDirective` is a wrapper around the `forbiddenNamevalidator`.
Angular forms recognizes the directive's role in the validation process because the directive registers itself
with the `NG_VALIDATORS` provider, a provider with an extensible collection of validation directives.
{@example 'cb-form-validation/ts/src/app/shared/forbidden-name.directive.ts' region='directive-providers'}
The rest of the directive is unremarkable and we present it here without further comment.
{@example 'cb-form-validation/ts/src/app/shared/forbidden-name.directive.ts' region='directive'}
See the [Attribute Directives](../guide/attribute-directives.html) chapter.
{@a testing}
## Testing Considerations
We can write _isolated unit tests_ of validation and control logic in _Reactive Forms_.
_Isolated unit tests_ probe the component class directly, independent of its
interactions with its template, the DOM, other dependencies, or Angular itself.
Such tests have minimal setup, are quick to write, and easy to maintain.
They do not require the `Angular TestBed` or asynchronous testing practices.
That's not possible with _Template-driven_ forms.
The template-driven approach relies on Angular to produce the control model and
to derive validation rules from the HTML validation attributes.
You must use the `Angular TestBed` to create component test instances,
write asynchronous tests, and interact with the DOM.
While not difficult, this takes more time, work and skill &mdash;
factors that tend to diminish test code coverage and quality.

View File

@ -1,620 +0,0 @@
@title
Internationalization (i18n)
@intro
Translate the app's template text into multiple languages.
@description
{@a top}
Angular's _internationalization_ (_i18n_) tools help make your app available in multiple languages.
## Table of contents
* [Angular and i18n template translation](#angular-i18n)
* [Mark text with the _i18n_ attribute](#i18n-attribute)
* [Add _i18n-..._ translation attributes](#translate-attributes)
* [Handle singular and plural](#cardinality)
* [Select among alternative texts](#select)
* [Create a translation source file with the **_ng-xi18n_ extraction tool**](#ng-xi18n)
* [Translate text messages](#translate)
* [Merge the completed translation file into the app](#merge)
* [Merge with the JIT compiler](#jit)
* [Internationalization with the AOT compiler](#aot)
* [Translation file maintenance and _id_ changes](#maintenance)
**Try this** <live-example name="cb-i18n" title="i18n Example in Spanish">live example</live-example>
of a JIT-compiled app, translated into Spanish.
{@a angular-i18n}
## Angular and _i18n_ template translation
Application internationalization is a challenging, many-faceted effort that
takes dedication and enduring commitment.
Angular's _i18n_ internationalization facilities can help.
This page describes the _i18n_ tools available to assist translation of component template text
into multiple languages.
Practitioners of _internationalization_ refer to a translatable text as a "_message_".
This page uses the words "_text_" and "_message_" interchangably and in the combination, "_text message_".
The _i18n_ template translation process has four phases:
1. Mark static text messages in your component templates for translation.
1. An angular _i18n_ tool extracts the marked messages into an industry standard translation source file.
1. A translator edits that file, translating the extracted text messages into the target language,
and returns the file to you.
1. The Angular compiler imports the completed translation files,
replaces the original messages with translated text, and generates a new version of the application
in the target language.
You need to build and deploy a separate version of the application for each supported language.
{@a i18n-attribute}
## Mark text with the _i18n_ attribute
The Angular `i18n` attribute is a marker for translatable content.
Place it on every element tag whose fixed text should be translated.
~~~ {.alert.is-helpful}
`i18n` is not an Angular _directive_.
It's a custom _attribute_, recognized by Angular tools and compilers.
After translation, the compiler removes it.
~~~
In the accompanying sample, an `<h1>` tag displays a simple English language greeting
that you translate into Spanish:
{@example 'cb-i18n/ts/src/app/app.component.1.html' region='greeting'}
Add the `i18n` attribute to the tag to mark it for translation.
{@example 'cb-i18n/ts/src/app/app.component.1.html' region='i18n-attribute'}
### Help the translator with a _description_ and _intent_
In order to translate it accurately, the translator may
need a description of the message.
Assign a description to the i18n attribute:
{@example 'cb-i18n/ts/src/app/app.component.1.html' region='i18n-attribute-desc'}
In order to deliver a correct translation, the translator may need to
know your _intent_&mdash;the true _meaning_ of the text
within _this particular_ application context.
In front of the description, add some contextual meaning to the assigned string,
separating it from the description with the `|` character (`<meaning>|<description>`):
{@example 'cb-i18n/ts/src/app/app.component.html' region='i18n-attribute-meaning'}
While all appearances of a message with the _same_ meaning have the _same_ translation,
a message with *a variety of possible meanings* could have different translations.
The Angular extraction tool preserves both the _meaning_ and the _description_ in the translation source file
to facilitiate contextually-specific translations.
### Translate text without creating an element
Suppose there is a stretch of text that you'd like to translate.
You could wrap it in a `<span>` tag but for some reason (CSS comes to mind)
you don't want to create a new DOM element merely to facilitate translation.
Here are two techniques to try.
(1) Wrap the text in an `<ng-container>` element. The `<ng-container>` is never renderered:
{@example 'cb-i18n/ts/src/app/app.component.html' region='i18n-ng-container'}
(2) Wrap the text in a pair of HTML comments:
{@example 'cb-i18n/ts/src/app/app.component.html' region='i18n-with-comment'}
{@a translate-attributes}
## Add _i18n-..._ translation attributes
You've added an image to your template. You care about accessibility too so you add a `title` attribute:
{@example 'cb-i18n/ts/src/app/app.component.1.html' region='i18n-title'}
The `title` attribute needs to be translated.
Angular i18n support has more translation attributes in the form,`i18n-x`, where `x` is the
name of the attribute to translate.
To translate the `title` on the `img` tag from the previous example, write:
{@example 'cb-i18n/ts/src/app/app.component.html' region='i18n-title-translate'}
You can also assign a meaning and a description with the `i18n-x="<meaning>|<description>"` syntax.
{@a cardinality}
## Handle singular and plural
Different languages have different pluralization rules.
Suppose your application says something about a collection of wolves.
In English, depending upon the number of wolves, you could display "no wolves", "one wolf", "two wolves", or "a wolf pack".
Other languages might express the _cardinality_ differently.
Here's how you could mark up the component template to display the phrase appropriate to the number of wolves:
{@example 'cb-i18n/ts/src/app/app.component.html' region='i18n-plural'}
* The first parameter is the key. It is bound to the component property (`wolves`)
that determines the number of wolves.
* The second parameter identifies this as a `plural` translation type.
* The third parameter defines a pluralization pattern consisting of pluralization
categories and their matching values.
Pluralization categories include:
* =0
* =1
* =5
* few
* other
Put the default _English_ translation in braces (`{}`) next to the pluralization category.
* When you're talking about one wolf, you could write `=1 {one wolf}`.
* For zero wolves, you could write `=0 {no wolves}`.
* For two wolves, you could write `=2 {two wolves}`.
You could keep this up for three, four, and every other number of wolves.
Or you could specify the **`other`** category as a catch-all for any unmatched cardinality
and write something like: `other {a wolf pack}`.
This syntax conforms to the
<a href="http://userguide.icu-project.org/formatparse/messages" target="_blank" title="ICU Message Format">ICU Message Format</a>
that derives from the
<a href="http://cldr.unicode.org/" target="_blank" title="CLDR">Common Locale Data Repository (CLDR),</a>
which specifies the
<a href="http://unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules" target="_blank" title="Pluralization Rules">pluralization rules</a>.
{@a select}
## Select among alternative texts
The application displays different text depending upon whether the hero is male or female.
These text alternatives require translation too.
You can handle this with a `select` translation.
A `select` also follows the
<a href="http://userguide.icu-project.org/formatparse/messages" target="_blank" title="ICU Message Format">ICU message syntax</a>.
You choose among alternative translation based on a string value instead of a number.
The following format message in the component template binds to the component's `gender`
property, which outputs either an "m" or an "f".
The message maps those values to the appropriate translation:
{@example 'cb-i18n/ts/src/app/app.component.html' region='i18n-select'}
{@a ng-xi18n}
## Create a translation source file with the _ng-xi18n_ tool
Use the **_ng-xi18n_ extraction tool** to extract the `i18n`-marked texts
into a translation source file in an industry standard format.
This is an Angular CLI tool in the `@angular/compiler-cli` npm package.
If you haven't already installed the CLI and its `platform-server` peer dependency, do so now:
<code-example language="sh" class="code-shell">
npm install @angular/compiler-cli @angular/platform-server --save
</code-example>
Open a terminal window at the root of the application project and enter the `ng-xi18n` command:
<code-example language="sh" class="code-shell">
./node_modules/.bin/ng-xi18n
</code-example>
Windows users may have to quote the command like this: `"./node_modules/.bin/ng-xi18n"`
By default, the tool generates a translation file named **`messages.xlf`** in the
<a href="https://en.wikipedia.org/wiki/XLIFF" target="_blank">XML Localisation Interchange File Format (XLIFF, version 1.2)</a>.
{@a other-formats}
### Other translation formats
You can generate a file named **`messages.xmb`** in the
<a href="http://cldr.unicode.org/development/development-process/design-proposals/xmb" target="_blank">XML Message Bundle (XMB)</a> format
by adding the `--i18nFormat=xmb` flag.
<code-example language="sh" class="code-shell">
./node_modules/.bin/ng-xi18n --i18nFormat=xmb
</code-example>
This sample sticks with the _XLIFF_ format.
{@a ng-xi18n-options}
### Other options
You may have to specify additional options.
For example, if the `tsconfig.json` TypeScript configuration
file is located somewhere other than in the root folder,
you must identify the path to it with the `-p` option:
<code-example language="sh" class="code-shell">
./node_modules/.bin/ng-xi18n -p path/to/tsconfig.json
./node_modules/.bin/ng-xi18n --i18nFormat=xmb -p path/to/tsconfig.json
</code-example>
{@a npm-i18n-script}
### Add an _npm_ script for convenience
Consider adding a convenience shortcut to the `scripts` section of the `package.json`
to make the command easier to remember and run:
<code-example format='.' language='sh'>
"scripts": {
"i18n": "ng-xi18n",
...
}
</code-example>
Now you can issue command variations such as these:
<code-example language="sh" class="code-shell">
npm run i18n
npm run i18n -- -p path/to/tsconfig.json
npm run i18n -- --i18nFormat=xmb -p path/to/tsconfig.json
</code-example>
Note the `--` flag before the options.
It tells _npm_ to pass every flag thereafter to `ng-xi18n`.
{@a translate}
## Translate text messages
The `ng-xi18n` command generates a translation source file
in the project root folder named `messages.xlf`.
The next step is to translate the English language template
text into the specific language translation
files. The cookbook sample creates a Spanish translation file.
{@a localization-folder}
### Create a localization folder
You will probably translate into more than one other language so it's a good idea
for the project structure to reflect your entire internationalization effort.
One approach is to dedicate a folder to localization and store related assets
(for example, internationalization files) there.
Localization and internationalization are
<a href="https://en.wikipedia.org/wiki/Internationalization_and_localization" target="_blank">different but closely related terms</a>.This cookbook follows that suggestion. It has a `locale` folder under the `src/`.
Assets within the folder carry a filename extension that matches a language-culture code from a
<a href="https://msdn.microsoft.com/en-us/library/ee825488(v=cs.20).aspx" target="_blank">well-known codeset</a>.
Make a copy of the `messages.xlf` file, put it in the `locale` folder and
rename it `messages.es.xlf`for the Spanish language translation.
Do the same for each target language.
### Translate text nodes
In the real world, you send the `messages.es.xlf` file to a Spanish translator who fills in the translations
using one of the
<a href="https://en.wikipedia.org/wiki/XLIFF#Editors" target="_blank">many XLIFF file editors</a>.
This sample file is easy to translate without a special editor or knowledge of Spanish.
Open `messages.es.xlf` and find the first `<trans-unit>` section:
{@example 'cb-i18n/ts/src/locale/messages.es.xlf.html' region='translated-hello'}
This XML element represents the translation of the `<h1>` greeting tag you marked with the `i18n` attribute.
Using the _source_, _description_, and _meaning_ elements to guide your translation,
replace the `<target/>` tag with the Spanish greeting:
{@example 'cb-i18n/ts/src/locale/messages.es.xlf.html' region='translated-hello'}
~~~ {.alert.is-important}
Note that the tool generates the `id`. **Don't touch it.**
Its value depends on the content of the message and its assigned meaning.
Change either factor and the `id` changes as well.
See the **[translation file maintenance discussion](#maintenance)**.
~~~
Translate the other text nodes the same way:
{@example 'cb-i18n/ts/src/locale/messages.es.xlf.html' region='translated-other-nodes'}
{@a translate-plural-select}
## Translate _plural_ and _select_
Translating _plural_ and _select_ messages is a little tricky.
The `<source>` tag is empty for `plural` and `select` translation
units, which makes them hard to correlate with the original template.
The `XLIFF` format doesn't yet support the ICU rules; it soon will.
However, the `XMB` format does support the ICU rules.
You'll just have to look for them in relation to other translation units that you recognize from elsewhere in the source template.
In this example, you know the translation unit for the `select` must be just below the translation unit for the logo.
### Translate _plural_
To translate a `plural`, translate its ICU format match values:
{@example 'cb-i18n/ts/src/locale/messages.es.xlf.html' region='translated-plural'}
### Translate _select_
The `select` behaves a little differently. Here again is the ICU format message in the component template:
{@example 'cb-i18n/ts/src/app/app.component.html' region='i18n-select'}
The extraction tool broke that into _two_ translation units.
The first unit contains the text that was _outside_ the `select`.
In place of the `select` is a placeholder, `<x id="ICU">`, that represents the `select` message.
Translate the text and leave the placeholder where it is.
{@example 'cb-i18n/ts/src/locale/messages.es.xlf.html' region='translate-select-1'}
The second translation unit, immediately below the first one, contains the `select` message. Translate that.
{@example 'cb-i18n/ts/src/locale/messages.es.xlf.html' region='translate-select-2'}
Here they are together, after translation:
{@example 'cb-i18n/ts/src/locale/messages.es.xlf.html' region='translated-select'}
<div class='l-main-content'>
</div>
The entire template translation is complete. It's
time to incorporate that translation into the application.
<div id='app-pre-translation'>
</div>
### The app before translation
When the previous steps finish, the sample app _and_ its translation file are as follows:
<md-tab-group>
<md-tab label="src/app/app.component.html">
{@example 'cb-i18n/ts/src/app/app.component.html'}
</md-tab>
<md-tab label="src/app/app.component.ts">
{@example 'cb-i18n/ts/src/app/app.component.ts'}
</md-tab>
<md-tab label="src/app/app.module.ts">
{@example 'cb-i18n/ts/src/app/app.module.ts'}
</md-tab>
<md-tab label="src/main.ts">
{@example 'cb-i18n/ts/src/main.1.ts'}
</md-tab>
<md-tab label="src/locale/messages.es.xlf">
{@example 'cb-i18n/ts/src/locale/messages.es.xlf.html'}
</md-tab>
</md-tab-group>
{@a merge}
## Merge the completed translation file into the app
To merge the translated text into component templates,
compile the application with the completed translation file.
The process is the same whether the file is in `.xlf` format or
in another format (`.xlif` and `.xtb`) that Angular understands.
You provide the Angular compiler with three new pieces of information:
* the translation file
* the translation file format
* the <a href="https://en.wikipedia.org/wiki/XLIFF" target="_blank">_Locale ID_</a>
(`es` or `en-US` for instance)
_How_ you provide this information depends upon whether you compile with
the JIT (_Just-in-Time_) compiler or the AOT (_Ahead-of-Time_) compiler.
* With [JIT](#jit), you provide the information at bootstrap time.
* With [AOT](#aot), you pass the information as `ngc` options.
{@a jit}
### Merge with the JIT compiler
The JIT compiler compiles the application in the browser as the application loads.
Translation with the JIT compiler is a dynamic process of:
1. Determining the language version for the current user.
2. Importing the appropriate language translation file as a string constant.
3. Creating corresponding translation providers to guide the JIT compiler.
4. Bootstrapping the application with those providers.
Open `index.html` and revise the launch script as follows:
{@example 'cb-i18n/ts/src/index.html' region='i18n'}
In this sample, the user's language is hardcoded as a global `document.locale` variable
in the `index.html`.
{@a text-plugin}
### SystemJS Text plugin
Notice the SystemJS mapping of `text` to a `systemjs-text-plugin.js`.
With the help of a text plugin, SystemJS can read any file as raw text and
return the contents as a string.
You'll need it to import the language translation file.
SystemJS doesn't ship with a raw text plugin but it's easy to add.
Create the following `systemjs-text-plugin.js` in the `src/` folder:
{@example 'cb-i18n/ts/src/systemjs-text-plugin.js'}
### Create translation providers
Three providers tell the JIT compiler how to translate the template texts for a particular language
while compiling the application:
* `TRANSLATIONS` is a string containing the content of the translation file.
* `TRANSLATIONS_FORMAT` is the format of the file: `xlf`, `xlif` or `xtb`.
* `LOCALE_ID` is the locale of the target language.
The `getTranslationProviders` function in the following `src/app/i18n-providers.ts`
creates those providers based on the user's _locale_
and the corresponding translation file:
{@example 'cb-i18n/ts/src/app/i18n-providers.ts'}
1. It gets the locale from the global `document.locale` variable that was set in `index.html`.
1. If there is no locale or the language is U.S. English (`en-US`), there is no need to translate.
The function returns an empty `noProviders` array as a `Promise`.
It must return a `Promise` because this function could read a translation file asynchronously from the server.
1. It creates a transaction filename from the locale according to the name and location convention
[described earlier](#localization-folder).
1. The `getTranslationsWithSystemJs` method reads the translation and returns the contents as a string.
Notice that it appends `!text` to the filename, telling SystemJS to use the [text plugin](#text-plugin).
1. The callback composes a providers array with the three translation providers.
1. Finally, `getTranslationProviders` returns the entire effort as a promise.
### Bootstrap the app with translation providers
The Angular `bootstrapModule` method has a second, _options_ parameter
that can influence the behavior of the compiler.
You'll create an _options_ object with the translation providers from `getTranslationProviders`
and pass it to `bootstrapModule`.
Open the `src/main.ts` and modify the bootstrap code as follows:
{@example 'cb-i18n/ts/src/main.ts'}
Notice that it waits for the `getTranslationProviders` promise to resolve before
bootstrapping the app.
The app is now _internationalized_ for English and Spanish and there is a clear path for adding
more languages.
{@a aot}
### _Internationalize_ with the AOT compiler
The JIT compiler translates the application into the target language
while compiling dynamically in the browser.
That's flexible but may not be fast enough for your users.
The AOT (_Ahead-of-Time_) compiler is part of a build process that
produces a small, fast, ready-to-run application package.
When you internationalize with the AOT compiler, you pre-build
a separate application package for each
language. Then in the host web page (`index.html`),
you determine which language the user needs
and serve the appropriate application package.
This cookbook doesn't cover how to build multiple application packages and
serve them according to the user's language preference.
It does explain the few steps necessary to tell the AOT compiler to apply a translations file.
Internationalization with the AOT compiler requires
some setup specifically for AOT compilation.
Start with the application project as shown
[just before merging the translation file](#app-pre-translation)
and refer to the [AOT cookbook](aot-compiler.html) to make it _AOT-ready_.
Next, issue an `ngc` compile command for each supported language (including English).
The result is a separate version of the application for each language.
Tell AOT how to translate by adding three options to the `ngc` command:
* `--i18nFile`: the path to the translation file
* `--locale`: the name of the locale
* `--i18nFormat`: the format of the localization file
For this sample, the Spanish language command would be
<code-example language="sh" class="code-shell">
./node_modules/.bin/ngc --i18nFile=./locale/messages.es.xlf --locale=es --i18nFormat=xlf
</code-example>
Windows users may have to quote the command:
<code-example language="sh" class="code-shell">
"./node_modules/.bin/ngc" --i18nFile=./locale/messages.es.xlf --locale=es --i18nFormat=xlf
</code-example>
{@a maintenance}
## Translation file maintenance and _id_ changes
As the application evolves, you will change the _i18n_ markup
and re-run the `ng-xi18n` extraction tool many times.
The _new_ markup that you add is not a problem;
but _most_ changes to _existing_ markup trigger
generation of _new_ `id`s for the affected translation units.
After an `id` changes, the translation files are no longer in-sync.
**All translated versions of the application will fail** during re-compilation.
The error messages identify the old `id`s that are no longer valid but
they don't tell you what the new `id`s should be.
**Commit all translation message files to source control**,
especially the English source `messages.xlf`.
The difference between the old and the new `messages.xlf` file
help you find and update `id` changes across your translation files.

View File

@ -1,28 +0,0 @@
@title
Cookbook
@intro
A collection of recipes for common Angular application scenarios
@description
The *Cookbook* offers answers to common implementation questions.
Each cookbook chapter is a collection of recipes focused on a particular Angular feature or application challenge
such as data binding, cross-component interaction, and communicating with a remote server via HTTP.
The cookbook is just getting started. Many more recipes are on the way.
Each cookbook chapter links to a live sample with every recipe included.
Recipes are deliberately brief and code-centric.
Each recipe links to a chapter of the Developer Guide or the API Guide
where you can learn more about the purpose, context, and design choices behind the code snippets.
## Feedback
The cookbook is a perpetual *work-in-progress*.
We welcome feedback! Leave a comment by clicking the icon in upper right corner of the banner.
Post *documentation* issues and pull requests on the
[angular.io](https://github.com/angular/angular.io) github repository.
Post issues with *Angular itself* to the [angular](https://github.com/angular/angular) github repository.

View File

@ -1,103 +0,0 @@
@title
Set the Document Title
@intro
Setting the document or window title using the Title service.
@description
{@a top}
Our app should be able to make the browser title bar say whatever we want it to say.
This cookbook explains how to do it.**See the <live-example name="cb-set-document-title"></live-example>**.
<table>
<tr>
<td>
To see the browser title bar change in the live example,
open it again in the Plunker editor by clicking the icon in the upper right,
then pop out the preview window by clicking the blue 'X' button in the upper right corner.
</td>
<td>
<img src='assets/images/devguide/plunker-switch-to-editor-button.png' width="200px" height="70px" alt="pop out the window" align="right"> </img> <br> </br> <img src='assets/images/devguide/plunker-separate-window-button.png' width="200px" height="47px" alt="pop out the window" align="right"> </img>
</td>
</tr>
</table>
## The problem with *&lt;title&gt;*
The obvious approach is to bind a property of the component to the HTML `<title>` like this:
<code-example format=''>
&lt;title&gt;{{This_Does_Not_Work}}&lt;/title&gt;
</code-example>
Sorry but that won't work.
The root component of our application is an element contained within the `<body>` tag.
The HTML `<title>` is in the document `<head>`, outside the body, making it inaccessible to Angular data binding.
We could grab the browser `document` object and set the title manually.
That's dirty and undermines our chances of running the app outside of a browser someday.
Running your app outside a browser means that you can take advantage of server-side
pre-rendering for near-instant first app render times and for SEO. It means you could run from
inside a Web Worker to improve your app's responsiveness by using multiple threads. And it
means that you could run your app inside Electron.js or Windows Universal to deliver it to the desktop.
## Use the *Title* service
Fortunately, Angular bridges the gap by providing a `Title` service as part of the *Browser platform*.
The [Title](../api/platform-browser/index/Title-class.html) service is a simple class that provides an API
for getting and setting the current HTML document title:
* `getTitle() : string` &mdash; Gets the title of the current HTML document.
* `setTitle( newTitle : string )` &mdash; Sets the title of the current HTML document.
Let's inject the `Title` service into the root `AppComponent` and expose a bindable `setTitle` method that calls it:
{@example 'cb-set-document-title/ts/src/app/app.component.ts' region='class'}
We bind that method to three anchor tags and, voilà!
<figure class='image-display'>
<img src="assets/images/cookbooks/set-document-title/set-title-anim.gif" alt="Set title"> </img>
</figure>
Here's the complete solution
<md-tab-group>
<md-tab label="src/main.ts">
{@example 'cb-set-document-title/ts/src/main.ts'}
</md-tab>
<md-tab label="src/app/app.module.ts">
{@example 'cb-set-document-title/ts/src/app/app.module.ts'}
</md-tab>
<md-tab label="src/app/app.component.ts">
{@example 'cb-set-document-title/ts/src/app/app.component.ts'}
</md-tab>
</md-tab-group>
## Why we provide the *Title* service in *bootstrap*
We generally recommended providing application-wide services in the root application component, `AppComponent`.
Here we recommend registering the title service during bootstrapping,
a location we reserve for configuring the runtime Angular environment.
That's exactly what we're doing.
The `Title` service is part of the Angular *browser platform*.
If we bootstrap our application into a different platform,
we'll have to provide a different `Title` service that understands the concept of a "document title" for that specific platform.
Ideally the application itself neither knows nor cares about the runtime environment.[Back to top](#top)

View File

@ -1,868 +0,0 @@
@title
TypeScript to JavaScript
@intro
Convert Angular TypeScript examples into ES6 and ES5 JavaScript
@description
Anything you can do with Angular in _TypeScript_, you can also do
in JavaScript. Translating from one language to the other is mostly a
matter of changing the way you organize your code and access Angular APIs.
_TypeScript_ is a popular language option for Angular development.
Most code examples on the Internet as well as on this site are written in _TypeScript_.
This cookbook contains recipes for translating _TypeScript_
code examples to _ES6_ and to _ES5_ so that JavaScript developers
can read and write Angular apps in their preferred dialect.
{@a toc}
## Table of contents
[_TypeScript_ to _ES6_ to _ES5_](#from-ts)<br>
[Modularity: imports and exports](#modularity)<br>
[Classes and Class Metadata](#class-metadata)<br>
[_ES5_ DSL](#dsl)<br>
[Interfaces](#interfaces)<br>
[Input and Output Metadata](#io-decorators)<br>
[Dependency Injection](#dependency-injection)<br>
[Host Binding](#host-binding)<br>
[View and Child Decorators](#view-child-decorators)<br>
[AOT compilation in _TypeScript_ Only](#aot)<br>
**Run and compare the live <live-example name="cb-ts-to-js">_TypeScript_</live-example> and <live-example name="cb-ts-to-js" lang="js">JavaScript</live-example>
code shown in this cookbook.**
{@a from-ts}
## _TypeScript_ to _ES6_ to _ES5_
_TypeScript_
<a href="https://www.typescriptlang.org" target="_blank" title='"TypeScript is a typed, superset of JavaScript"'>is a typed superset of _ES6 JavaScript_</a>.
&nbsp; _ES6 JavaScript_ is a superset of _ES5 JavaScript_. &nbsp; _ES5_ is the kind of JavaScript that runs natively in all modern browsers.
The transformation of _TypeScript_ code all the way down to _ES5_ code can be seen as "shedding" features.
The downgrade progression is
* _TypeScript_ to _ES6-with-decorators_
* _ES6-with-decorators_ to _ES6-without-decorators_ ("_plain ES6_")
* _ES6-without-decorators_ to _ES5_
When translating from _TypeScript_ to _ES6-with-decorators_, remove
[class property access modifiers](http://www.typescriptlang.org/docs/handbook/classes.html#public-private-and-protected-modifiers)
such as `public` and `private`.
Remove most of the
[type declarations](https://www.typescriptlang.org/docs/handbook/basic-types.html),
such as `:string` and `:boolean`
but **keep the constructor parameter types which are used for dependency injection**.
From _ES6-with-decorators_ to _plain ES6_, remove all
[decorators](https://www.typescriptlang.org/docs/handbook/decorators.html)
and the remaining types.
You must declare properties in the class constructor (`this.title = '...'`) rather than in the body of the class.
Finally, from _plain ES6_ to _ES5_, the main missing features are `import`
statements and `class` declarations.
For _plain ES6_ transpilation you can _start_ with a setup similar to the
[_TypeScript_ quickstart](https://github.com/angular/quickstart) and adjust the application code accordingly.
Transpile with [Babel](https://babeljs.io/) using the `es2015` preset.
To use decorators and annotations with Babel, install the
[`angular2`](https://github.com/shuhei/babel-plugin-angular2-annotations) preset as well.
{@a modularity}
## Importing and Exporting
### Importing Angular Code
In both _TypeScript_ and _ES6_, you import Angular classes, functions, and other members with _ES6_ `import` statements.
In _ES5_, you access the Angular entities of the [the Angular packages](../glossary.html#scoped-package)
through the global `ng` object.
Anything you can import from `@angular` is a nested member of this `ng` object:
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/app.module.ts' region='ng2import'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/app.module.es6' region='ng2import'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/app.module.es6' region='ng2import'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/app.module.js' region='ng2import'}
</md-tab>
</md-tab-group>
### Exporting Application Code
Each file in a _TypeScript_ or _ES6_ Angular application constitutes an _ES6_ module.
When you want to make something available to other modules, you `export` it.
_ES5_ lacks native support for modules.
In an Angular _ES5_ application, you load each file manually by adding a `<script>` tag to `index.html`.
~~~ {.alert.is-important}
The order of `<script>` tags is often significant.
You must load a file that defines a public JavaScript entity before a file that references that entity.
~~~
The best practice in _ES5_ is to create a form of modularity that avoids polluting the global scope.
Add one application namespace object such as `app` to the global `document`.
Then each code file "exports" public entities by attaching them to that namespace object, e.g., `app.HeroComponent`.
You could factor a large application into several sub-namespaces
which leads to "exports" along the lines of `app.heroQueries.HeroComponent`.
Every _ES5_ file should wrap code in an
[Immediately Invoked Function Expression (IIFE)](https://en.wikipedia.org/wiki/Immediately-invoked_function_expression)
to limit unintentional leaking of private symbols into the global scope.
Here is a `HeroComponent` as it might be defined and "exported" in each of the four language variants.
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero.component.ts' region='appexport'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero.component.es6' region='appexport'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/hero.component.es6' region='appexport'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/hero.component.js' region='appexport'}
</md-tab>
</md-tab-group>
### Importing Application Code
In _TypeScript_ and _ES6_ apps, you `import` things that have been exported from other modules.
In _ES5_ you use the shared namespace object to access "exported" entities from other files.
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/app.module.ts' region='appimport'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/app.module.es6' region='appimport'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/app.module.es6' region='appimport'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/app.module.js' region='appimport'}
</md-tab>
</md-tab-group>
~~~ {.alert.is-helpful}
Alternatively, you can use a module loader such as Webpack or
Browserify in an Angular JavaScript project. In such a project, you would
use _CommonJS_ modules and the `require` function to load Angular framework code.
Then use `module.exports` and `require` to export and import application code.
~~~
{@a class-metadata}
## Classes and Class Metadata
### Classes
Most Angular _TypeScript_ and _ES6_ code is written as classes.
Properties and method parameters of _TypeScript_ classes may be marked with the access modifiers
`private`, `internal`, and `public`.
Remove these modifiers when translating to JavaScript.
Most type declarations (e.g, `:string` and `:boolean`) should be removed when translating to JavaScript.
When translating to _ES6-with-decorators_, ***do not remove types from constructor parameters!***
Look for types in _TypeScript_ property declarations.
In general it is better to initialize such properties with default values because
many browser JavaScript engines can generate more performant code.
When _TypeScript_ code follows this same advice, it can infer the property types
and there is nothing to remove during translation.
In _ES6-without-decorators_, properties of classes must be assigned inside the constructor.
_ES5_ JavaScript has no classes.
Use the constructor function pattern instead, adding methods to the prototype.
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero.component.ts' region='class'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero.component.es6' region='class'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/hero.component.es6' region='class'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/hero.component.js' region='constructorproto'}
</md-tab>
</md-tab-group>
### Metadata
When writing in _TypeScript_ or _ES6-with-decorators_,
provide configuration and metadata by adorning a class with one or more *decorators*.
For example, you supply metadata to a component class by preceding its definition with a
[`@Component`](../api/core/index/Component-decorator.html) decorator function whose
argument is an object literal with metadata properties.
In _plain ES6_, you provide metadata by attaching an `annotations` array to the _class_.
Each item in the array is a new instance of a metadata decorator created with a similar metadata object literal.
In _ES5_, you also provide an `annotations` array but you attach it to the _constructor function_ rather than to a class.
See these variations side-by-side:
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero.component.ts' region='metadata'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero.component.es6' region='metadata'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/hero.component.es6' region='metadata'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/hero.component.js' region='metadata'}
</md-tab>
</md-tab-group>
***External Template file***
A large component template is often kept in a separate template file.
{@example 'cb-ts-to-js/ts/src/app/hero-title.component.html'}
The component (`HeroTitleComponent` in this case) then references the template file in its metadata `templateUrl` property:
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero-title.component.ts' region='templateUrl'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-title.component.es6' region='templateUrl'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/hero-title.component.es6' region='templateUrl'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/hero-title.component.js' region='templateUrl'}
</md-tab>
</md-tab-group>
Note that both the _TypeScript_ and _ES6_ `templateUrl` properties identify the location of the template file _relative to the component module_.
All three metadata configurations specify the `moduleId` property
so that Angular can calculate the proper module address.
The _ES5_ approach shown here does not support modules and therefore there is no way to calculate a _module-relative URL_.
The `templateUrl` for the _ES5_ code must specify the _path from the project root_ and
omits the irrelevant `moduleId` property.
With the right tooling, the `moduleId` may not be needed in the other JavaScript dialects either.
But it's safest to provide it anyway.
{@a dsl}
## _ES5_ DSL
This _ES5_ pattern of creating a constructor and annotating it with metadata is so common that Angular
provides a convenience API to make it a little more compact and locates the metadata above the constructor,
as you would if you wrote in _TypeScript_ or _ES6-with-decorators_.
This _API_ (_Application Programming Interface_) is commonly known as the _ES5 DSL_ (_Domain Specific Language_).
Set an application namespace property (e.g., `app.HeroDslComponent`) to the result of an `ng.core.Component` function call.
Pass the same metadata object to `ng.core.Component` as you did before.
Then chain a call to the `Class` method which takes an object defining the class constructor and instance methods.
Here is an example of the `HeroComponent`, re-written with the DSL,
next to the original _ES5_ version for comparison:
<md-tab-group>
<md-tab label="ES5 JavaScript with DSL">
{@example 'cb-ts-to-js/js/src/app/hero.component.js' region='dsl'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/hero.component.js'}
</md-tab>
</md-tab-group>
~~~ {.callout.is-helpful}
<header>
Name the constructor
</header>
A **named** constructor displays clearly in the console log
if the component throws a runtime error.
An **unnamed** constructor displays as an anonymous function (e.g., `class0`)
which is impossible to find in the source code.
~~~
### Properties with getters and setters
_TypeScript_ and _ES6_ support with getters and setters.
Here's an example of a read-only _TypeScript_ property with a getter
that prepares a toggle-button label for the next clicked state:
{@example 'cb-ts-to-js/ts/src/app/hero-queries.component.ts' region='defined-property'}
This _TypeScript_ "getter" property is transpiled to an _ES5_
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty"
target="_blank" title="Defined Properties">defined property</a>.
The _ES5 DSL_ does not support _defined properties_ directly
but you can still create them by extracting the "class" prototype and
adding the _defined property_ in raw JavaScript like this:
{@example 'cb-ts-to-js/js/src/app/hero-queries.component.js' region='defined-property'}
### DSL for other classes
There are similar DSLs for other decorated classes.
You can define a directive with `ng.core.Directive`:
<code-example>
app.MyDirective = ng.core.Directive({
selector: '[myDirective]'
}).Class({
...
});
</code-example>
and a pipe with `ng.core.Pipe`:
<code-example>
app.MyPipe = ng.core.Pipe({
name: 'myPipe'
}).Class({
...
});
</code-example>
{@a interfaces}
## Interfaces
A _TypeScript_ interface helps ensure that a class implements the interface's members correctly.
We strongly recommend Angular interfaces where appropriate.
For example, the component class that implements the `ngOnInit` lifecycle hook method
should implement the `OnInit` interface.
_TypeScript_ interfaces exist for developer convenience and are not used by Angular at runtime.
They have no physical manifestation in the generated JavaScript code.
Just implement the methods and ignore interfaces when translating code samples from _TypeScript_ to JavaScript.
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero-lifecycle.component.ts'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-lifecycle.component.es6'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/hero-lifecycle.component.es6'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/hero-lifecycle.component.js'}
</md-tab>
<md-tab label="ES5 JavaScript with DSL">
{@example 'cb-ts-to-js/js/src/app/hero-lifecycle.component.js' region='dsl'}
</md-tab>
</md-tab-group>
{@a io-decorators}
## Input and Output Metadata
### Input and Output Decorators
In _TypeScript_ and _ES6-with-decorators_, you often add metadata to class _properties_ with _property decorators_.
For example, you apply [`@Input` and `@Output` property decorators](../guide/template-syntax.html#inputs-outputs)
to public class properties that will be the target of data binding expressions in parent components.
There is no equivalent of a property decorator in _ES5_ or _plain ES6_.
Fortunately, every property decorator has an equivalent representation in a class decorator metadata property.
A _TypeScript_ `@Input` property decorator can be represented by an item in the `Component` metadata's `inputs` array.
You already know how to add `Component` or `Directive` class metadata in _any_ JavaScript dialect so
there's nothing fundamentally new about adding another property.
But note that what would have been _separate_ `@Input` and `@Output` property decorators for each class property are
combined in the metadata `inputs` and `outputs` _arrays_.
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/confirm.component.ts'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/confirm.component.es6'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/confirm.component.es6'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/confirm.component.js'}
</md-tab>
<md-tab label="ES5 JavaScript with DSL">
{@example 'cb-ts-to-js/js/src/app/confirm.component.js' region='dsl'}
</md-tab>
</md-tab-group>
In the previous example, one of the public-facing binding names (`cancelMsg`)
differs from the corresponding class property name (`notOkMsg`).
That's OK but you must tell Angular about it so that it can map an external binding of `cancelMsg`
to the component's `notOkMsg` property.
In _TypeScript_ and _ES6-with-decorators_,
you specify the special binding name in the argument to the property decorator.
In _ES5_ and _plain ES6_ code, convey this pairing with the `propertyName: bindingName` syntax in the class metadata.
## Dependency Injection
Angular relies heavily on [Dependency Injection](../guide/dependency-injection.html) to provide services to the objects it creates.
When Angular creates a new component, directive, pipe or another service,
it sets the class constructor parameters to instances of services provided by an _Injector_.
The developer must tell Angular what to inject into each parameter.
### Injection by Class Type
The easiest and most popular technique in _TypeScript_ and _ES6-with-decorators_ is to set the constructor parameter type
to the class associated with the service to inject.
The _TypeScript_ transpiler writes parameter type information into the generated JavaScript.
Angular reads that information at runtime and locates the corresponding service in the appropriate _Injector_..
The _ES6-with-decorators_ transpiler does essentially the same thing using the same parameter-typing syntax.
_ES5_ and _plain ES6_ lack types so you must identify "injectables" by attaching a **`parameters`** array to the constructor function.
Each item in the array specifies the service's injection token.
As with _TypeScript_ the most popular token is a class,
or rather a _constructor function_ that represents a class in _ES5_ and _plain ES6_.
The format of the `parameters` array varies:
* _plain ES6_ &mdash; nest each constructor function in a sub-array.
* _ES5_ &mdash; simply list the constructor functions.
When writing with _ES5 DSL_, set the `Class.constructor` property to
an array whose first parameters are the injectable constructor functions and whose
last parameter is the class constructor itself.
This format should be familiar to AngularJS developers.
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero-di.component.ts'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-di.component.es6'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/hero-di.component.es6'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/hero-di.component.js'}
</md-tab>
<md-tab label="ES5 JavaScript with DSL">
{@example 'cb-ts-to-js/js/src/app/hero-di.component.js' region='dsl'}
</md-tab>
</md-tab-group>
### Injection with the @Inject decorator
Sometimes the dependency injection token isn't a class or constructor function.
In _TypeScript_ and _ES6-with-decorators_, you precede the class constructor parameter
by calling the `@Inject()` decorator with the injection token.
In the following example, the token is the string `'heroName'`.
The other JavaScript dialects add a `parameters` array to the class contructor function.
Each item constains a new instance of `Inject`:
* _plain ES6_ &mdash; each item is a new instance of `Inject(token)` in a sub-array.
* _ES5_ &mdash; simply list the string tokens.
When writing with _ES5 DSL_, set the `Class.constructor` property to a function definition
array as before. Create a new instance of `ng.core.Inject(token)` for each parameter.
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero-di-inject.component.ts'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-di-inject.component.es6'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/hero-di-inject.component.es6'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/hero-di-inject.component.js'}
</md-tab>
<md-tab label="ES5 JavaScript with DSL">
{@example 'cb-ts-to-js/js/src/app/hero-di-inject.component.js' region='dsl'}
</md-tab>
</md-tab-group>
### Additional Injection Decorators
You can qualify injection behavior with injection decorators from `@angular/core`.
In _TypeScript_ and _ES6-with-decorators_,
you precede the constructor parameters with injection qualifiers such as:
* [`@Optional`](../api/core/index/Optional-decorator.html) sets the parameter to `null` if the service is missing
* [`@Attribute`](../api/core/index/Attribute-interface.html) to inject a host element attribute value
* [`@ContentChild`](../api/core/index/ContentChild-decorator.html) to inject a content child
* [`@ViewChild`](../api/core/index/ViewChild-decorator.html) to inject a view child
* [`@Host`](../api/core/index/Host-decorator.html) to inject a service in this component or its host
* [`@SkipSelf`](../api/core/index/SkipSelf-decorator.html) to inject a service provided in an ancestor of this component
In _plain ES6_ and _ES5_, create an instance of the equivalent injection qualifier in a nested array within the `parameters` array.
For example, you'd write `new Optional()` in _plain ES6_ and `new ng.core.Optional()` in _ES5_.
When writing with _ES5 DSL_, set the `Class.constructor` property to a function definition
array as before. Use a nested array to define a parameter's complete injection specification.
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero-title.component.ts'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-title.component.es6'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/hero-title.component.es6'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/hero-title.component.js'}
</md-tab>
<md-tab label="ES5 JavaScript with DSL">
{@example 'cb-ts-to-js/js/src/app/hero-title.component.js' region='dsl'}
</md-tab>
</md-tab-group>
In the example above, there is no provider for the `'titlePrefix'` token.
Without `Optional`, Angular would raise an error.
With `Optional`, Angular sets the constructor parameter to `null`
and the component displays the title without a prefix.
{@a host-binding}
## Host Binding
Angular supports bindings to properties and events of the _host element_ which is the
element whose tag matches the component selector.
### Host Decorators
In _TypeScript_ and _ES6-with-decorators_, you can use host property decorators to bind a host
element to a component or directive.
The [`@HostBinding`](../api/core/index/HostBinding-interface.html) decorator
binds host element properties to component data properties.
The [`@HostListener`](../api/core/index/HostListener-interface.html) decorator binds
host element events to component event handlers.
In _plain ES6_ or _ES5_, add a `host` attribute to the component metadata to achieve the
same effect as `@HostBinding` and `@HostListener`.
The `host` value is an object whose properties are host property and listener bindings:
* Each key follows regular Angular binding syntax: `[property]` for host bindings
or `(event)` for host listeners.
* Each value identifies the corresponding component property or method.
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero-host.component.ts'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-host.component.es6'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/hero-host.component.es6'}
</md-tab>
<md-tab label="ES5 JavaScript">
{@example 'cb-ts-to-js/js/src/app/hero-host.component.js'}
</md-tab>
<md-tab label="ES5 JavaScript with DSL">
{@example 'cb-ts-to-js/js/src/app/hero-host.component.js' region='dsl'}
</md-tab>
</md-tab-group>
### Host Metadata
Some developers prefer to specify host properties and listeners
in the component metadata.
They'd _rather_ do it the way you _must_ do it _ES5_ and _plain ES6_.
The following re-implementation of the `HeroComponent` reminds us that _any property metadata decorator_
can be expressed as component or directive metadata in both _TypeScript_ and _ES6-with-decorators_.
These particular _TypeScript_ and _ES6_ code snippets happen to be identical.
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero-host-meta.component.ts'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-host-meta.component.es6'}
</md-tab>
</md-tab-group>
{@a view-child-decorators}
### View and Child Decorators
Several _property_ decorators query a component's nested view and content components.
_View_ children are associated with element tags that appear _within_ the component's template.
_Content_ children are associated with elements that appear _between_ the component's element tags;
they are projected into an `<ng-content>` slot in the component's template. The [`@ViewChild`](../api/core/index/ViewChild-decorator.html) and
[`@ViewChildren`](../api/core/index/ViewChildren-decorator.html) property decorators
allow a component to query instances of other components that are used in
its view.
In _ES5_ and _ES6_, you access a component's view children by adding a `queries` property to the component metadata.
The `queries` property value is a hash map.
* each _key_ is the name of a component property that will hold the view child or children.
* each _value_ is a new instance of either `ViewChild` or `ViewChildren`.
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero-queries.component.ts' region='view'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-queries.component.es6' region='view'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/hero-queries.component.es6' region='view'}
</md-tab>
<md-tab label="ES5 JavaScript with DSL">
{@example 'cb-ts-to-js/js/src/app/hero-queries.component.js' region='view'}
</md-tab>
</md-tab-group>
The [`@ContentChild`](../api/core/index/ContentChild-decorator.html) and
[`@ContentChildren`](../api/core/index/ContentChildren-decorator.html) property decorators
allow a component to query instances of other components that have been projected
into its view from elsewhere.
They can be added in the same way as [`@ViewChild`](../api/core/index/ViewChild-decorator.html) and
[`@ViewChildren`](../api/core/index/ViewChildren-decorator.html).
<md-tab-group>
<md-tab label="TypeScript">
{@example 'cb-ts-to-js/ts/src/app/hero-queries.component.ts' region='content'}
</md-tab>
<md-tab label="ES6 JavaScript with decorators">
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-queries.component.es6' region='content'}
</md-tab>
<md-tab label="ES6 JavaScript">
{@example 'cb-ts-to-js/js-es6/src/app/hero-queries.component.es6' region='content'}
</md-tab>
<md-tab label="ES5 JavaScript with DSL">
{@example 'cb-ts-to-js/js/src/app/hero-queries.component.js' region='content'}
</md-tab>
</md-tab-group>
~~~ {.alert.is-helpful}
In _TypeScript_ and _ES6-with-decorators_ you can also use the `queries` metadata
instead of the `@ViewChild` and `@ContentChild` property decorators.
~~~
{@a aot}
## AOT Compilation in _TypeScript_ only
Angular offers two modes of template compilation, JIT (_Just-in-Time_) and
[AOT (_Ahead-of-Time_)](aot-compiler.html).
Currently the AOT compiler only works with _TypeScript_ applications because, in part, it generates
_TypeScript_ files as an intermediate result.
**AOT is not an option for pure JavaScript applications** at this time.

View File

@ -1,171 +0,0 @@
@title
Visual Studio 2015 QuickStart
@intro
Use Visual Studio 2015 with the QuickStart files
@description
<a id="top"></a>Some developers prefer Visual Studio as their Integrated Development Environment (IDE).
This cookbook describes the steps required to set up and use the
Angular QuickStart files in **Visual Studio 2015 within an ASP.NET 4.x project**.
There is no *live example* for this cookbook because it describes Visual Studio, not the application.
<a id="asp-net-4"></a>## ASP.NET 4.x Project
This cookbook explains how to set up the QuickStart files with an **ASP.NET 4.x project** in
Visual Studio 2015.
If you prefer a `File | New Project` experience and are using **ASP.NET Core**,
then consider the _experimental_
<a href="http://blog.stevensanderson.com/2016/10/04/angular2-template-for-visual-studio/" target="_blank">ASP.NET Core + Angular template for Visual Studio 2015</a>.
Note that the resulting code does not map to the docs. Adjust accordingly.
The steps are as follows:
- [Prerequisite](#prereq1): Install Node.js
- [Prerequisite](#prereq2): Install Visual Studio 2015 Update 3
- [Prerequisite](#prereq3): Configure External Web tools
- [Prerequisite](#prereq4): Install TypeScript 2 for Visual Studio 2015
- [Step 1](#download): Download the QuickStart files
- [Step 2](#create-project): Create the Visual Studio ASP.NET project
- [Step 3](#copy): Copy the QuickStart files into the ASP.NET project folder
- [Step 4](#restore): Restore required packages
- [Step 5](#build-and-run): Build and run the app
<h2 id='prereq1'>
Prerequisite: Node.js
</h2>
Install **[Node.js® and npm](https://nodejs.org/en/download/)**
if they are not already on your machine.
**Verify that you are running node version `4.6.x` or greater, and npm `3.x.x` or greater**
by running `node -v` and `npm -v` in a terminal/console window.
Older versions produce errors.
<h2 id='prereq2'>
Prerequisite: Visual Studio 2015 Update 3
</h2>
The minimum requirement for developing Angular applications with Visual Studio is Update 3.
Earlier versions do not follow the best practices for developing applications with TypeScript.
To view your version of Visual Studio 2015, go to `Help | About Visual Studio`.
If you don't have it, install **[Visual Studio 2015 Update 3](https://www.visualstudio.com/en-us/news/releasenotes/vs2015-update3-vs)**.
Or use `Tools | Extensions and Updates` to update to Update 3 directly from Visual Studio 2015.
<h2 id='prereq3'>
Prerequisite: Configure External Web tools
</h2>
Configure Visual Studio to use the global external web tools instead of the tools that ship with Visual Studio:
* Open the **Options** dialog with `Tools` | `Options`
* In the tree on the left, select `Projects and Solutions` | `External Web Tools`.
* On the right, move the `$(PATH)` entry above the `$(DevEnvDir`) entries. This tells Visual Studio to
use the external tools (such as npm) found in the global path before using its own version of the external tools.
* Click OK to close the dialog.
* Restart Visual Studio for this change to take effect.
Visual Studio will now look first for external tools in the current workspace and
if not found then look in the global path and if it is not found there, Visual Studio
will use its own versions of the tools.
<h2 id='prereq4'>
Prerequisite: Install TypeScript 2 for Visual Studio 2015
</h2>
While Visual Studio Update 3 ships with TypeScript support out of the box, it currently doesnt ship with TypeScript 2,
which you need to develop Angular applications.
To install TypeScript 2:
* Download and install **[TypeScript 2.0 for Visual Studio 2015](http://download.microsoft.com/download/6/D/8/6D8381B0-03C1-4BD2-AE65-30FF0A4C62DA/TS2.0.3-TS-release20-nightly-20160921.1/TypeScript_Dev14Full.exe)**
* OR install it with npm: `npm install -g typescript@2.0`.
You can find out more about TypeScript 2 support in Visual studio **[here](https://blogs.msdn.microsoft.com/typescript/2016/09/22/announcing-typescript-2-0/)**
At this point, Visual Studio is ready. Its a good idea to close Visual Studio and
restart it to make sure everything is clean.
<h2 id='download'>
Step 1: Download the QuickStart files
</h2>
[Download the QuickStart source](https://github.com/angular/quickstart)
from github. If you downloaded as a zip file, extract the files.
<h2 id='create-project'>
Step 2: Create the Visual Studio ASP.NET project
</h2>
Create the ASP.NET 4.x project in the usual way as follows:
* In Visual Studio, select `File` | `New` | `Project` from the menu.
* In the template tree, select `Templates` | `Visual C#` (or `Visual Basic`) | `Web`.
* Select the `ASP.NET Web Application` template, give the project a name, and click OK.
* Select the desired ASP.NET 4.5.2 template and click OK.
In this cookbook we'll select the `Empty` template with no added folders,
no authentication and no hosting. Pick the template and options appropriate for your project.
<h2 id='copy'>
Step 3: Copy the QuickStart files into the ASP.NET project folder
</h2>
Copy the QuickStart files we downloaded from github into the folder containing the `.csproj` file.
Include the files in the Visual Studio project as follows:
* Click the `Show All Files` button in Solution Explorer to reveal all of the hidden files in the project.
* Right-click on each folder/file to be included in the project and select `Include in Project`.
Minimally, include the following folder/files:
* app folder (answer *No* if asked to search for TypeScript Typings)
* styles.css
* index.html
* package.json
* tsconfig.json
<h2 id='restore'>
Step 4: Restore the required packages
</h2>
Restore the packages required for an Angular application as follows:
* Right-click on the `package.json` file in Solution Explorer and select `Restore Packages`.
<br>This uses `npm` to install all of the packages defined in the `package.json` file.
It may take some time.
* If desired, open the Output window (`View` | `Output`) to watch the npm commands execute.
* Ignore the warnings.
* When the restore is finished, a message should say: `npm command completed with exit code 0`.
* Click the `Refresh` icon in Solution Explorer.
* **Do not** include the `node_modules` folder in the project. Let it be a hidden project folder.
<h2 id='build-and-run'>
Step 5: Build and run the app
</h2>
First, ensure that `index.html` is set as the start page.
Right-click `index.html` in Solution Explorer and select option `Set As Start Page`.
Build and launch the app with debugger by clicking the **Run** button or press `F5`.
It's faster to run without the debugger by pressing `Ctrl-F5`.The default browser opens and displays the QuickStart sample application.
Try editing any of the project files. *Save* and refresh the browser to
see the changes.
<h2 id='routing'>
Note on Routing Applications
</h2>
If this application used the Angular router, a browser refresh could return a *404 - Page Not Found*.
Look at the address bar. Does it contain a navigation url (a "deep link") ... any path other than `/` or `/index.html`?
You'll have to configure the server to return `index.html` for these requests.
Until you do, remove the navigation path and refresh again.

View File

@ -1,17 +0,0 @@
# _boilerplate files
!_boilerplate/*
*/*/src/styles.css
*/*/src/systemjs.config.js
*/*/src/tsconfig.json
*/*/bs-config.e2e.json
*/*/bs-config.json
*/*/package.json
*/*/tslint.json
# example files
_test-output
protractor-helpers.js
*/e2e-spec.js
**/ts/**/*.js
**/js-es6*/**/*.js
**/ts-snippets/**/*.js

View File

@ -1,14 +0,0 @@
{
"open": false,
"logLevel": "silent",
"port": 8080,
"server": {
"baseDir": "src",
"routes": {
"/node_modules": "node_modules"
},
"middleware": {
"0": null
}
}
}

View File

@ -1,8 +0,0 @@
{
"server": {
"baseDir": "src",
"routes": {
"/node_modules": "node_modules"
}
}
}

View File

@ -1,4 +0,0 @@
{
"build": "build",
"run": "serve"
}

View File

@ -1,44 +0,0 @@
{
"name": "angular-examples",
"version": "1.0.0",
"private": true,
"description": "Example package.json, only contains needed scripts for examples. See _examples/package.json for master package.json.",
"scripts": {
"build": "tsc -p src/",
"build:watch": "tsc -p src/ -w",
"build:e2e": "tsc -p e2e/",
"serve": "lite-server -c=bs-config.json",
"serve:e2e": "lite-server -c=bs-config.e2e.json",
"prestart": "npm run build",
"start": "concurrently \"npm run build:watch\" \"npm run serve\"",
"pree2e": "webdriver-manager update && npm run build:e2e",
"e2e": "concurrently \"npm run serve:e2e\" \"npm run protractor\" --kill-others --success first",
"protractor": "protractor protractor.config.js",
"pretest": "npm run build",
"test": "concurrently \"npm run build:watch\" \"karma start karma.conf.js\"",
"pretest:once": "npm run build",
"test:once": "karma start karma.conf.js --single-run",
"lint": "tslint ./src/**/*.ts -t verbose",
"build:upgrade": "tsc",
"serve:upgrade": "http-server",
"build:cli": "ng build --no-progress",
"serve:cli": "http-server dist/",
"build:aot": "ngc -p tsconfig-aot.json && rollup -c rollup-config.js",
"serve:aot": "lite-server -c bs-config.aot.json",
"start:webpack": "webpack-dev-server --inline --progress --port 8080",
"test:webpack": "karma start karma.webpack.conf.js",
"build:webpack": "rimraf dist && webpack --config config/webpack.prod.js --bail",
"build:babel": "babel src -d src --extensions \".es6\" --source-maps",
"copy-dist-files": "node ./copy-dist-files.js",
"i18n": "ng-xi18n"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {},
"devDependencies": {
"angular-cli": "^1.0.0-beta.26"
},
"repository": {}
}

View File

@ -1,10 +0,0 @@
{
"description": "QuickStart",
"basePath": "src/",
"files": [
"app/app.component.ts",
"index.html"
],
"open": "app/app.component.ts",
"tags": ["quickstart"]
}

View File

@ -1,116 +0,0 @@
/* #docregion , quickstart, toh */
/* Master Styles */
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}
h2, h3 {
color: #444;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
/* #enddocregion quickstart */
body, input[text], button {
color: #888;
font-family: Cambria, Georgia;
}
/* #enddocregion toh */
a {
cursor: pointer;
cursor: hand;
}
button {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #aaa;
cursor: auto;
}
/* Navigation link styles */
nav a {
padding: 5px 10px;
text-decoration: none;
margin-right: 10px;
margin-top: 10px;
display: inline-block;
background-color: #eee;
border-radius: 4px;
}
nav a:visited, a:link {
color: #607D8B;
}
nav a:hover {
color: #039be5;
background-color: #CFD8DC;
}
nav a.active {
color: #039be5;
}
/* items class */
.items {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 24em;
}
.items li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.items li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.items li.selected {
background-color: #CFD8DC;
color: white;
}
.items li.selected:hover {
background-color: #BBD8DC;
}
.items .text {
position: relative;
top: -3px;
}
.items .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
/* #docregion toh */
/* everywhere else */
* {
font-family: Arial, Helvetica, sans-serif;
}

View File

@ -1,42 +0,0 @@
/**
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
// other libraries
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.js',
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);

View File

@ -1,88 +0,0 @@
/**
* WEB VERSION FOR CURRENT ANGULAR BUILD
* (based on systemjs.config.js in angular.io)
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*
* UNTESTED !
*/
(function (global) {
System.config({
// DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
transpiler: 'ts',
typescriptOptions: {
// Copy of compiler options in standard tsconfig.json
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es2015", "dom"],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
},
meta: {
'typescript': {
"exports": "ts"
}
},
paths: {
// paths serve as alias
'npm:': 'https://unpkg.com/',
'ng:': 'https://cdn.rawgit.com/angular/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'ng:core-builds/master/bundles/core.umd.js',
'@angular/common': 'ng:common-builds/master/bundles/common.umd.js',
'@angular/compiler': 'ng:compiler-builds/master/bundles/compiler.umd.js',
'@angular/platform-browser': 'ng:platform-browser-builds/master/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'ng:platform-browser-dynamic-builds/master/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'ng:http-builds/master/bundles/http.umd.js',
'@angular/router': 'ng:router-builds/master/bundles/router.umd.js',
'@angular/router/upgrade': 'ng:router-builds/master/bundles/router-upgrade.umd.js',
'@angular/forms': 'ng:forms-builds/master/bundles/forms.umd.js',
'@angular/upgrade': 'ng:upgrade-builds/master/bundles/upgrade.umd.js',
'@angular/upgrade/static': 'ng:upgrade-builds/master/bundles/upgrade-static.umd.js',
// angular testing umd bundles (overwrite the shim mappings)
'@angular/core/testing': 'ng:core-builds/master/bundles/core-testing.umd.js',
'@angular/common/testing': 'ng:common-builds/master/bundles/common-testing.umd.js',
'@angular/compiler/testing': 'ng:compiler-builds/master/bundles/compiler-testing.umd.js',
'@angular/platform-browser/testing': 'ng:platform-browser-builds/master/bundles/platform-browser-testing.umd.js',
'@angular/platform-browser-dynamic/testing': 'ng:platform-browser-dynamic-builds/master/bundles/platform-browser-dynamic-testing.umd.js',
'@angular/http/testing': 'ng:http-builds/master/bundles/http-testing.umd.js',
'@angular/router/testing': 'ng:router-builds/master/bundles/router-testing.umd.js',
'@angular/forms/testing': 'ng:forms-builds/master/bundles/forms-testing.umd.js',
// other libraries
'rxjs': 'npm:rxjs@5.0.1',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
'typescript': 'npm:typescript@2.0.10/lib/typescript.js',
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.ts',
defaultExtension: 'ts'
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);
/*
Copyright 2016 Google Inc. All Rights Reserved.
Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
*/

View File

@ -1,75 +0,0 @@
/**
* WEB ANGULAR VERSION
* (based on systemjs.config.js in angular.io)
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
// DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
transpiler: 'ts',
typescriptOptions: {
// Copy of compiler options in standard tsconfig.json
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es2015", "dom"],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
},
meta: {
'typescript': {
"exports": "ts"
}
},
paths: {
// paths serve as alias
'npm:': 'https://unpkg.com/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
// other libraries
'rxjs': 'npm:rxjs@5.0.1',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
'typescript': 'npm:typescript@2.0.10/lib/typescript.js',
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.ts',
defaultExtension: 'ts'
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);
/*
Copyright 2016 Google Inc. All Rights Reserved.
Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
*/

Some files were not shown because too many files have changed in this diff Show More