Compare commits

..

407 Commits

Author SHA1 Message Date
14a2d1a6f7 docs: add changelog for 4.0.2 2017-04-11 14:56:13 -07:00
61e089931f release: cut the 4.0.2 release 2017-04-11 14:50:12 -07:00
cbf30cb101 ci: add jasonaden to the router 2017-04-11 14:38:46 -07:00
2f41b52e78 fix(router): fix query param parsing 2017-04-11 14:38:46 -07:00
dcf60da16d refactor(router): cleanup & simplifications 2017-04-11 14:38:45 -07:00
9c9f409364 test(router): enable running the campaign in non-ES6 browsers
Closes #15806.
2017-04-11 14:38:45 -07:00
b44b983c1f test(router): test preloading when a module is already loaded 2017-04-11 14:38:45 -07:00
978f80985c fix(router): the preloader use the module from the loaded config 2017-04-11 14:38:45 -07:00
c10e50cf38 fix(tsc-wrapped): ensure valid path separators in metadata
* Fixes that `tsc-wrapped` stores invalid path separators in the bundled metadata files. Previous errors could have been: `Cannot find module '.corecoordinationnique-selection-dispatcher'.` (See https://github.com/angular/material2/issues/3834)
* Fixes failing tests on Windows. Now all tooling tests are green on Windows.

Related to #15403
2017-04-11 14:38:45 -07:00
168a2eb5bf fix(language-service): detect when there isn't a tsconfig.json
Fixes #15874
2017-04-11 14:38:44 -07:00
c9c7acd484 fix(language-service): parse extended i18n forms 2017-04-11 14:34:15 -07:00
5b99533315 fix(language-service): initialize static reflector correctly
Fixes #15768
2017-04-11 14:34:14 -07:00
037805b741 style(router): fix typo in router preloader method 2017-04-11 14:34:14 -07:00
0861fda69c fix(language-service): avoid throwing exceptions when reporting metadata errors 2017-04-11 14:34:13 -07:00
feae7b6059 fix(language-service): resolve any parameter types to any result 2017-04-11 14:34:12 -07:00
e4277a0869 fix(language-service): improve resilience to incomplete information 2017-04-11 14:34:12 -07:00
1864ccb3dd fix(compiler): fix inheritance for AOT with summaries (#15583)
Allows to inherit ctor args, lifecycle hooks and statics from a class
in another compilation unit. 
Will error if trying to inherit from a class in another compilation unit 
that has an `@Component` / `@Directive` / `@Pipe` / `@NgModule`.
2017-04-11 14:34:11 -07:00
6b79ab5abe docs: add changelog for 4.0.1 2017-03-29 16:27:40 -07:00
53c12a84dc release: cut the 4.0.1 release 2017-03-29 16:26:06 -07:00
ca665303f4 fix(core): fix inheritance in JIT mode for TS 2.1 (#15599)
Fixes #15502
2017-03-29 16:19:15 -07:00
0fe4985756 fix(language-service): correctly determine base members of types (#15600)
Fixes #15460
2017-03-29 16:19:09 -07:00
74087cb39d docs(http): remove deprecated stuff and outdated plunkrs (#15598) 2017-03-29 15:18:03 -07:00
902bb2f026 fix(compiler): throw when a component defines both template and templateUrl (#15572)
Closes #15566
2017-03-29 10:29:00 -07:00
23bf34853c refactor(router): cleanup & simplification (#15436) 2017-03-29 10:15:00 -07:00
8c4b963927 fix(core): Update types for TypeScript nullability support (#15472) 2017-03-29 10:14:55 -07:00
bfa4f70204 fix(language-service): don't require reflect-metadata module to be provided (#15569)
Fixes #15568
2017-03-29 10:14:48 -07:00
d481f6d150 docs(core): fix API docs for Injector.get 2017-03-29 10:14:38 -07:00
fd6114561b perf(router): don't create new serializer every time UrlTree.toString is called (#15565) 2017-03-29 10:11:21 -07:00
c82851172e fix(animations): make sure style calculations are not computed too early (#15540)
Closes #15507
2017-03-29 10:11:06 -07:00
75478b2078 fix(router): should run CanActivate after CanDeactivate guards
Closes #14059
Closes #15467
2017-03-29 10:11:00 -07:00
cdbb3dbd2a refactor(router): fix tests structure 2017-03-29 10:10:53 -07:00
e72124c888 fix(core): fix the key/value differ (#15539)
fixes #15457
2017-03-29 10:10:43 -07:00
b8c0a97e35 fix(core): check for undefined on normalizeDebugBindingValue (#15503)
DebugServices is parsing false atributes values incorrectly.
Parse5 expects a string value for attributes, but currently boolean is being sent.

Closes #15494
2017-03-29 10:10:37 -07:00
5597fd3180 fix(language-service): improve performance of updateModuleAnalysis() (#15543) 2017-03-29 10:10:26 -07:00
a88413f871 fix(compiler): ignore errors when evaluating base classes (#15560)
Fixes #15536
2017-03-29 10:10:22 -07:00
aa116524e6 refactor(router): improve flatten fn
closes #15505
2017-03-29 10:10:10 -07:00
4a5ad7ba30 refactor(router): use object spread operator instead of merge fn 2017-03-29 10:10:06 -07:00
d74e4d0633 fix(core): improve error msg for invalid KeyValueDiffer.diff arg (#15489)
Closes #15402
2017-03-29 10:09:55 -07:00
a2c2b87aa3 fix(language-service): be resilient to invalidate ordering (#15470)
Fixes #15466
2017-03-29 10:09:19 -07:00
8f4ea3e4b8 docs(core): fix typo and example in InjectionToken doc (#15449)
The doc included an example that didn't use InjectionToken.
2017-03-29 10:08:22 -07:00
bf25e94f19 fix(language-service): guard access to Symbol.members (#15529)
Fixes #15528 

What is the current behavior?
The language service access TypeScript's Symbol.members without checking for null or undefined.
What is the new behavior?
The access is guarded.
2017-03-29 10:08:06 -07:00
7983414e6a ci: add tbosch and vicb as approvers of the language service (#15530) 2017-03-29 10:07:58 -07:00
8248eba3e2 docs: in doc comments, replace [aA]ngular2 with Angular (#15463) 2017-03-29 10:07:44 -07:00
a65487528f fix(compiler): allow single quotes into named interpolations (#15461)
Fixes #15318
2017-03-29 10:07:32 -07:00
426b3a19b7 refactor: use object spread operator rather than merge (#15426) 2017-03-29 10:07:14 -07:00
2360676a7b fix(router): shouldn't execute CanLoad when a route has been loaded
Closes #14475
Closes #15438
2017-03-29 10:07:01 -07:00
ce3e03ff1a refactor(router): polishing 2017-03-29 10:06:52 -07:00
858c11cf7b docs: clarify querying all descendants (#15400)
Fixes #14417
Updated example to illustrate @ContentChildren default behavior (only query direct children), and how to query for nested elements/all descendants.
2017-03-29 10:06:09 -07:00
95afaf495b test(compiler): refactor i18n integration test 2017-03-24 14:35:10 -07:00
d92930e975 docs: fixed broken links (#15455)
Closes #15446
2017-03-24 08:14:23 -07:00
c2892dada3 docs: revert the move of CONTRIBUTING.md to docs/CONTRIBUTING.md
Because the root path has a special meeting for GitHub, Jeremy, unicorns and red arrows.
2017-03-23 20:57:49 -07:00
b2b1195534 docs: update the contributing link one more time :-) 2017-03-23 19:26:25 -07:00
e1b09e3bcc docs: update CONTRIBUTING link in README.md 2017-03-23 19:25:46 -07:00
c65b75443e docs: add the 4.0.0 code name to the changelog 2017-03-23 17:46:02 -07:00
db0dca3fc1 docs: add a note about exclusion of symbols prefixed with ɵ from our public api surface (#15440) 2017-03-23 17:21:52 -07:00
7a715b2403 docs: add changelog for 4.0.0 2017-03-23 16:48:54 -07:00
1ba296644d release: cut the 4.0.0 release 2017-03-23 16:45:16 -07:00
b800a0c824 fix: prevent strictNullChecks support until #15432 is fixed (#15434) 2017-03-23 14:54:19 -07:00
0dda01e37c fix(compiler): correctly handle when toString is exported (#15430)
Fixes #15420
2017-03-23 13:38:01 -07:00
c8ab5cb0c5 fix(compiler): assume queries with no matches as static (#15429)
This has the side effect of allowing `@Input` and `@ContentChild`
on the same property if the query is static (see the bug description
for details).

Fixes #15417
2017-03-23 13:37:45 -07:00
92084f2b6a fix(platform-browser): setAttribute should work with xmlns namespace (#14874)
Closes #14865
2017-03-23 12:52:06 -07:00
08f2f08d74 fix(router): should pass new data to Observable when query params change (#15387)
Fixes #15290
2017-03-23 10:43:14 -07:00
376088da70 build(aio): delete content folder before doc-gen (#15414)
This should prevent false-positive e2e test runs where stale files
are left lying around.
2017-03-23 10:23:09 -07:00
73808dd38b build(aio): upgrade to @angular@4.0.0-rc.6 (#15412) 2017-03-22 23:52:43 -07:00
ee03418b10 release: cut the 4.0.0-rc.6 release 2017-03-22 23:09:47 -07:00
da700d1842 docs: move markdown docs from root dir to docs/ and remove obsolete files (#15410) 2017-03-22 22:50:12 -07:00
de87c47dd9 docs: spelling errors 2017-03-22 21:28:24 -07:00
1060805a1f docs: fix spelling 2017-03-22 21:28:24 -07:00
08d86751b9 fix(compiler): only log template deprecation warning once (#15364) 2017-03-22 21:26:53 -07:00
9319b5f329 docs(common): update ngFor docs to new as syntax (#15166) 2017-03-22 17:18:22 -07:00
26f6bd4d3b ci(travis): update excluded branch to g3 (#15391) 2017-03-22 17:15:38 -07:00
edb2571a59 docs: spelling error (#15406) 2017-03-22 17:14:56 -07:00
98cb974796 docs(router): fix typo in ParamMap api doc (#15397) 2017-03-22 17:14:11 -07:00
8b414222aa fix(aio): do not fallback to index.html for file requests (#15401)
Previously, all URLs were rewritten to `index.html` in order to support
deep-linking. This works when navigating to URLs that correspond to existing
resources. E.g. navigating to `/tutorial` returns `index.html` and then the
`DocViewer` takes over and requests `tutorial.json`.
Navigating to a non-existent URL (e.g. `/foo`), will return `index.html`, which
in turn requests (the non-existent) `foo.json` and throws an error when trying
to parse the returned `index.html` as JSON.

This commit fixes it by only rewriting URLs that do not request a file (i.e. do
not include a `.` in the last path segment).

Fixes #15398
2017-03-22 15:31:47 -07:00
ea49a95bd9 fix(upgrade): component injectors should not link the module injector tree (#15385) 2017-03-22 15:22:38 -07:00
a50d79df47 fix(compiler-cli): adding missing format xliff for the extractor (#15386)
To generate XLF files with ng-xi18n we could use the format parameter "xlf" or "xlif". The real name is "xliff" not "xlif", so this probably was a typo. This PR adds "xliff" as can be expected
2017-03-22 15:12:02 -07:00
64285a2171 docs: update 4.0.0-rc.5 release schedule (#15288) 2017-03-22 13:26:12 -07:00
941f194a83 docs: spelling errors (#15375) 2017-03-22 13:25:35 -07:00
8b4edcc7ad build(aio): output {@example} tags as <code-example> elements (#15382) 2017-03-22 13:24:40 -07:00
c58499786c fix(tsc-wrapped): use windows friendly path normalization in bundler (#15374)
Fixes #15289

PR Close #15374
2017-03-21 22:30:57 -05:00
1bcbcfd56f revert: build(aio): implement prerendering (#15346)
This reverts commit d0bc83ca27.

Protractor-based prerendering is flakey on Travis and takes several minutes to
complete, slowing down the build. Prerendering has a lower impact now that we
use a ServiceWorker. We will revisit in the future (probably using a
`PlatformServer`-based approach).

PR Close #15346
2017-03-21 19:05:36 -05:00
90d2518d9a fix(compiler): look for flat module resources using declaration module path (#15367)
`ngc` would look for flat module resources relative to the flat module index.
`ngc` now looks for flat module resources relative to the `.d.ts` file that declarates
the component.

Fixes #15221

PR Close #15367
2017-03-21 19:05:03 -05:00
7354949763 feat(tsc-wrapped): record original location of flattened symbols (#15367)
Added an "origins" section to the flat module `.metadata.json` files
that records where the original symbols was declared. This allows
correctly calculating relative path references recorded in metadata.
2017-03-21 19:05:00 -05:00
5efc86069f fix(forms): make composition event buffering configurable (#15256)
This commit fixes a regression where `ngModel` no longer syncs
letter by letter on Android devices, and instead syncs at the
end of every word. This broke when we introduced buffering of
IME events so IMEs like Pinyin keyboards or Katakana keyboards
wouldn't display composition strings. Unfortunately, iOS devices
and Android devices have opposite event behavior. Whereas iOS
devices fire composition events for IME keyboards only, Android
fires composition events for Latin-language keyboards. For
this reason, languages like English don't work as expected on
Android if we always buffer. So to support both platforms,
composition string buffering will only be turned on by default
for non-Android devices.

However, we have also added a `COMPOSITION_BUFFER_MODE` token
to make this configurable by the application. In some cases, apps
might might still want to receive intermediate values. For example,
some inputs begin searching based on Latin letters before a
character selection is made.

As a provider, this is fairly flexible. If you want to turn
composition buffering off, simply provide the token at the top
level:

```ts
providers: [
   {provide: COMPOSITION_BUFFER_MODE, useValue: false}
]
```

Or, if you want to change the mode  based on locale or platform,
you can use a factory:

```ts
import {shouldUseBuffering} from 'my/lib';

....
providers: [
   {provide: COMPOSITION_BUFFER_MODE, useFactory: shouldUseBuffering}
]
```

Closes #15079.

PR Close #15256
2017-03-21 16:47:18 -05:00
97149f9424 fix(core): update peer dep on zone.js to ^0.8.5 (#15365)
Closes #15185

PR Close #15365
2017-03-21 16:47:01 -05:00
bac265fdc2 refactor(core): misc cleanup (#15366)
PR Close #15366
2017-03-21 16:46:51 -05:00
e59e5e24b9 build(aio): upgrade to rxjs@5.2.0 2017-03-21 15:20:29 -05:00
9e5d4781cb refactor(aio): cleanup polyfill imports and realign them with the latest cli blueprint
This shouldn't change anything. But it's interesting that we used to have this  import
that seemed bogus, but there were no compilation or rutime errors.
2017-03-21 15:20:28 -05:00
fc1f6efe0d build(aio): fix paths to "index" pages
Content pages like `tutorial/index.md` were being mapped to `tutorial.index.json`,
which meant that they could only be rendered if you browsed to `/tutorial/index`.

This didn't sit well so now these pages are mapped to `tutorial.json`, which
means that you browser to them via `/tutorial/` or just `/tutorial`.

Fixed #15335
2017-03-21 15:20:28 -05:00
c9710d4fb5 build(aio): upgrade @angular/cli to 1.0.0-rc.4 2017-03-21 15:20:28 -05:00
cf16f3b0dd build(aio): update @angular to 4.0.0-rc.5 and zone.js to 0.8.4 2017-03-21 15:20:28 -05:00
a9e91115bf build(aio): add version into navigation.json
The navigation.json is now passed through the dgeni pipeline.
The source file has been moved to `aio/content/navigation.json`
but the generated file will now appear where the original source file
was found, `aio/src/content/navigation.json`.

Everything inside `aio/src/content` is now generated and ignored by git.

The `processNavigationMap` processor in this commit adds the current version
information to the navigation.json file and verifies the relative urls in
the file map to real documents.

The navigationService exposes the versionInfo as an observable, which the
AppComponent renders at the top of the sidenav.
2017-03-21 15:20:28 -05:00
90f699fdcf build(aio): ensure that internal document links work with base href 2017-03-21 15:20:28 -05:00
fd7b855cfc test(aio): fix test descriptions for ordering of processors 2017-03-21 15:20:28 -05:00
20aab64c65 test(aio): use MockLogger in AppComponent tests
This component sends a lot of messages to the console, which makes the
test run less easy to follow.
2017-03-21 15:20:28 -05:00
2eb027a793 build(aio): don't process unnecessary example files in doc-gen
This lowers the `yarn docs` processing time by about 40% (from 50 secs
to 30 secs).
2017-03-21 15:20:28 -05:00
b0a7bc77ee build(aio): remove cheatsheet processing and content
This will be replaced by a single file migrated from angular.io
2017-03-21 15:20:28 -05:00
4e10faf1eb build(aio): add version into navigation.json
The navigation.json is now passed through the dgeni pipeline.
The source file has been moved to `aio/content/navigation.json`
but the generated file will now appear where the original source file
was found, `aio/src/content/navigation.json`.

Everything inside `aio/src/content` is now generated and ignored by git.

The `processNavigationMap` processor in this commit adds the current version
information to the navigation.json file and verifies the relative urls in
the file map to real documents.

The navigationService exposes the versionInfo as an observable, which the
AppComponent renders at the top of the sidenav.
2017-03-21 15:20:28 -05:00
a0c6d44e18 build(aio): ensure that internal document links work with base href 2017-03-21 15:20:28 -05:00
9bc998c7a1 build(aio): use forked version of Rho rendering engine
The original Rho is too strict when it comes to markdown headings.
It requires that there be a blank line separating the heading and the
next paragraph.  The forked version here fixes that; but the Rho project
will not merge it as it goes against there basic rules.
2017-03-21 15:20:28 -05:00
1a0c6d89b1 build(aio): do not render ignored docs 2017-03-21 15:20:28 -05:00
8c12374c4c build(aio): do not render private classes and members 2017-03-21 15:20:28 -05:00
45e2126273 refactor(aio): removed dead code 2017-03-21 15:20:28 -05:00
41497b052d feat(aio): font change and toolbar shadow 2017-03-21 15:20:28 -05:00
068cad1c1b build: update to angular 4.0.0-rc.4 and service-worker@1.0.0-beta.7 2017-03-21 15:20:28 -05:00
31ef92fc36 build(aio): fix generated github links
The change from `modules` to `packages` needed to be applied to these links.
2017-03-21 15:20:28 -05:00
b4081e3713 build(aio): update to dgeni-packages 0.17.0
This new version fixes the problem with mistaking
content inside inline HTML blocks as tags

Related to #15199
2017-03-21 15:20:28 -05:00
c8b4a33a7f ci(aio): lower prerender browser instances
Hopefully this will reduce aio test flakes
2017-03-21 15:20:28 -05:00
a13ddf2e8a feat(aio): expand nav menu branch of select item on page load 2017-03-21 15:20:28 -05:00
64beae9527 fix(core): mark components for check when host events trigger. (#15359)
Fixes #15352

PR Close #15359
2017-03-21 14:27:01 -05:00
15a082c74e fix(platform-server): throw a better error message for relative URLs (#15357)
Unlike in the browser, on the server there is no concept of a document origin.
Thus, it is illegal to make requests for relative URLs against Http on platform-server.

Currently this fails with a vague error:

Error: Uncaught (in promise): Error at resolvePromise

This change adds explicit validation and a friendlier error message:

Error: URLs requested via Http on the server must be absolute. URL: /testing

Another option considered was to track the concept of an origin for the platform
and automatically prepend it to relative URLs. This would cause automatic "local
RPCs" to be made, though, which would be an unexpected and undesirable default
behavior.

Fixes #15349

PR Close #15357
2017-03-21 14:26:51 -05:00
fbccd5cd38 fix(animations): ensure empty animate() steps work at the end of a sequence (#15328)
Closes #15310
Closes #15328

PR Close #15328
2017-03-21 14:26:43 -05:00
1e8b132ade refactor(compiler): only produce log expressions for elements / text (#15350)
This change reduces the amount of generated code by only adding `log`
calls for elements and text nodes.

We need the `log` calls to allow users to jump to the right place
in the template via source maps. However, we only need it for element
and text nodes, but not for directives, queries, … as for them we 
first locate the corresponding element or text node.

Related to #15239

PR Close #15350
2017-03-21 14:26:30 -05:00
431eb309f3 fix(core): provide NgModuleRef in ViewContainerRef.createComponent. (#15350)
This is needed to support the corner cases:
- usage of a `ComponentFactory` that was created on the fly via `Compiler`
- overwriting of the `NgModuleRef` that is associated to a
  `ComponentFactory` by the `ComponentFactoryResolver` from
  which it was read.

Fixes #15241
2017-03-21 14:26:26 -05:00
8e6995c91e fix(core): stringify shouldn't throw when toString returns null/undefined (#14975)
Fixes #14948

PR Close #14975
2017-03-21 12:20:44 -05:00
0759911431 docs(compiler-cli): mention that .ngsummary.json files should be gitignore'd (#15047)
PR Close #15047
2017-03-21 12:20:38 -05:00
1d7693c1e1 fix(compiler): use attribute id to merge translations (#15302)
We extracted ids from i18n attributes but forgot to use them when merging the translations, resulting in an error about missing translations even when they were correctly defined.

Fixes #15234

PR Close #15302
2017-03-21 12:20:31 -05:00
16e0423085 revert: feat(compiler-cli): support metadata file aliases (#15331)
This reverts commit 0ab49d4cec.

PR Close #15331
2017-03-21 12:20:14 -05:00
c2ffb6bfcd build: remove use of alias metadata (#15331) 2017-03-21 12:20:06 -05:00
2489e4ba1b fix(animations): correct the main entry path in package.json (#15300)
PR Close #15300
2017-03-20 22:36:37 -05:00
94da80148e fix(animations): stringify boolean values as 1 and 0 (#15311)
Closes #15247
Closes #15311

PR Close #15311
2017-03-20 22:36:29 -05:00
764e90f9bb fix(compiler): don’t call check if we don’t need to (#15322)
If a directive has not bindings nor has a `ngDoCheck` / `ngOnInit`
lifecycle hook, don’t generate a `check` call.

This does not have an impact on the behavior, but produces
less code.

PR Close #15322
2017-03-20 22:36:20 -05:00
9bf2fb4a74 fix(animations): ensure enter/leave cancellations work (#15323)
Closes #15315
Closes #15323

PR Close #15323
2017-03-20 22:36:11 -05:00
de3d2eeeba fix(platform-server): interpret Native view encapsulation as Emulated on the server (#15155)
PR Close #15155
2017-03-20 17:14:09 -05:00
a805d00256 ci(forms): add tina as secondary for forms (#15262)
PR Close #15262
2017-03-20 17:13:52 -05:00
61135bc842 fix: remove left over debugger statement (#15309)
PR Close #15309
2017-03-20 17:13:44 -05:00
fa36ffda14 Revert "fix(compiler): shouldn't throw when Symbol is used as DI token (#13701)" (#15319)
This reverts commit 8b5c6b2732.

This feature is not compatible with the `Injector.get` which now only 
takes `Type` or `InjectableToken`. `Symbol` is not a valid type.

Closes #15183

PR Close #15319
2017-03-20 17:13:37 -05:00
d3eda7a5b5 feat(router): add ParamMap.keys to get a list of parameters 2017-03-20 09:19:32 -07:00
a755b715ed feat(router): introduce ParamMap to access parameters
The Router use the type `Params` for all of:
- position parameters,
- matrix parameters,
- query parameters.

`Params` is defined as follow `type Params = {[key: string]: any}`

Because parameters can either have single or multiple values, the type should
actually be `type Params = {[key: string]: string | string[]}`.

The client code often assumes that parameters have single values, as in the
following exemple:

```
class MyComponent {
sessionId: Observable<string>;

constructor(private route: ActivatedRoute) {}

ngOnInit() {
    this.sessionId = this.route
      .queryParams
      .map(params => params['session_id'] || 'None');
}
}

```

The problem here is that `params['session_id']` could be `string` or `string[]`
but the error is not caught at build time because of the `any` type.

Fixing the type as describe above would break the build because `sessionId`
would becomes an `Observable<string | string[]>`.

However the client code knows if it expects a single or multiple values. By
using the new `ParamMap` interface the user code can decide when it needs a
single value (calling `ParamMap.get(): string`) or multiple values (calling
`ParamMap.getAll(): string[]`).

The above exemple should be rewritten as:

```
class MyComponent {
sessionId: Observable<string>;

constructor(private route: ActivatedRoute) {}

ngOnInit() {
    this.sessionId = this.route
      .queryParamMap
      .map(paramMap => paramMap.get('session_id') || 'None');
}
}

```

Added APIs:
- `interface ParamMap`,
- `ActivatedRoute.paramMap: ParamMap`,
- `ActivatedRoute.queryParamMap: ParamMap`,
- `ActivatedRouteSnapshot.paramMap: ParamMap`,
- `ActivatedRouteSnapshot.queryParamMap: ParamMap`,
- `UrlSegment.parameterMap: ParamMap`
2017-03-20 09:19:32 -07:00
a9d5de0e56 refactor(router): misc minor updates 2017-03-20 09:19:32 -07:00
f634c62cb3 test: add systemjs+umd integration test (#14196)
This test ensures the `__esModule` is set on UMD bundles, thus making them compatible with SystemJS@^0.22.3.

Followup from https://github.com/frankwallis/plugin-typescript/issues/185.

PR Close #14196
2017-03-19 12:23:07 -05:00
f8c075ae27 fix(core): don’t create a comment for components with empty template. (#15260)
Fixes #15143

PR Close #15260
2017-03-19 12:22:26 -05:00
aeb99645bb test(animations): test various combinations of animations with host bindings (#15251)
PR Close #15251
2017-03-19 12:21:54 -05:00
0d3e314df0 fix(core): trigger host animations for elements that are removed. (#15251)
Fixes #14813
Fixes #15193
2017-03-19 12:21:42 -05:00
28ce68a13d refactor(core): view engine - change BindingType to BindingFlags (#15251) 2017-03-19 12:21:37 -05:00
80075afe8a fix(animations): only process element nodes through the animation engine (#15268)
Closes #15267
Closes #15268

PR Close #15268
2017-03-19 10:50:07 -05:00
bcc29ffdd1 build: update rollup (#15258)
PR Close #15258
2017-03-17 17:39:19 -05:00
0c43535ccc fix(core): only apply WrappedValue to the binding of the pipe (#15257)
Previously, a pipe that returned a `WrappedValue` would force the change
of the next bound property, independent of the binding in which the pipe
was used.

Now only the binding in which the `WrappedValue` is used will be assumed
as changed.

Fixes #15116

PR Close #15257
2017-03-17 17:38:36 -05:00
49829b4a4d build: add package names to secondary endpoint package.json files (#15253)
Fixes #14736

PR Close #15253
2017-03-17 16:52:55 -05:00
5c5c2ae405 fix(platform-server): setup NoopAnimationsModule in ServerModule by default (#15131)
This is so that server side rendering does not throw an exception when it encounters animations on the server side and does not need the user to explicitly setup NoopAnimationsModule in their app server module.

Fixes #15098, #14784.

PR Close #15131
2017-03-17 16:21:51 -05:00
6e9264a79c fix(tsc-wrapped): emit flat module format correctly on Windows (#15215)
File name needed to be normalized before comparison when using
synthetic file names.

Fixes #15192

PR Close #15215
2017-03-17 15:35:28 -05:00
a6fb78ee3c fix(animations): make sure non-transitioned leave operations cancel existing animations (#15254)
Closes #15213

PR Close #15254
2017-03-17 15:35:11 -05:00
d0bc83ca27 build(aio): implement prerendering
The current implementation is based on @igorminar's [angular-io-v42][1]. It is
using Protractor to request all docs URLs, let them fallback to `/index.html`
and save the rendered page.

[1]: https://github.com/IgorMinar/angular-io-v42/tree/05508ab3/tools/prerenderer

Fixes #15104
2017-03-17 15:31:22 -05:00
b5b2fed54d refactor(aio): re-arrange npm scripts 2017-03-17 15:31:22 -05:00
4cef5dddc6 build(aio): use own .gitignore file 2017-03-17 15:31:22 -05:00
f92591054b fix(animations): make sure easing values work with web-animations (#15195)
Closes #15115
Closes #15195

PR Close #15195
2017-03-17 13:53:19 -05:00
2a0e55ffb5 fix(core): allow tree shaking of component factories and styles (#15214)
Closure compiler is very sensitive to top level function calls.
This commit makes the function calls `createComponentFactory`
and `createRendererTypeV2` logic-less.

Fixes #15181

PR Close #15214
2017-03-17 13:52:57 -05:00
9429032da1 feat(upgrade): use ComponentFactory.inputs/outputs/ngContentSelectors (#15214)
DEPRECATION:
- the arguments `inputs` / `outputs` / `ngContentSelectors` of `downgradeComponent`
  are no longer used as Angular calculates these automatically now.
- Compiler.getNgContentSelectors is deprecated. Use
  ComponentFactory.ngContentSelectors instead.
2017-03-17 13:52:50 -05:00
791534f2f4 feat(core): expose inputs, outputs and ngContentSelectors on ComponentFactory. (#15214)
E.g. for a component like this:
```
@Component({
  template: ‘<ng-content select=“child”></ng-content>’
})
class MyComp {
  @Input(‘aInputName’)
  aInputProp: string;

  @Output(‘aEventName’)
  aOuputProp: EventEmitter<any>;
}
```

the `ComponentFactory` will now contain the following:
- `inputs = {aInputProp: ‘aInputName’}`
- `outputs = {aOutputProp: ‘aOutputName’}`
- `ngContentSelectors = [‘child’]`
2017-03-17 13:52:41 -05:00
604546c287 refactor(upgrade): don’t rely on compiler internals (#15214)
Uses `Element.matches` to match selectors, instead
of copying the code from our compiler.
2017-03-17 13:52:34 -05:00
c66437fc13 fix(animations): only treat view removals as void state transitions (#15245)
Closes #15223

PR Close #15245
2017-03-17 13:48:25 -05:00
8415910375 fix(compiler): add an empty content for source file of non mapped code. (#15246)
Before this when using ngc, tools tried to load `ng://…<component>.ts`
if `…<component>.ts` was the source file of a template.

PR Close #15246
2017-03-17 13:48:09 -05:00
5486e5417b feat(forms): allow to compile forms in strictNullChecks mode (#14679)
Closes #14667

PR Close #14679
2017-03-17 13:47:12 -05:00
2d78c8cc05 docs: add changelog for 4.0.0-rc.5 2017-03-17 10:18:16 -07:00
52bed7f9b3 release: cut the 4.0.0-rc.5 release 2017-03-17 10:16:53 -07:00
7fb45283df fix(compiler-cli): update the tsc-wrapped dependency version (#15226) 2017-03-17 09:41:28 -07:00
b7212f5afe docs: add changelog for 4.0.0-rc.4 2017-03-16 20:10:54 -07:00
4e25601c4d release: cut the 4.0.0-rc.4 release 2017-03-16 20:10:53 -07:00
add7829cb7 docs: add changelog for 2.4.10 2017-03-16 20:10:53 -07:00
a52184bdda fix(core): update peer dep on zone.js to ^0.8.4 (#15209) 2017-03-16 20:10:53 -07:00
410aa33005 build: fix paths to typings files so tsickle resolves imports correctly
Fixes #15080
2017-03-16 17:34:29 -07:00
0ab49d4cec feat(compiler-cli): support metadata file aliases 2017-03-16 17:34:29 -07:00
994089d36b fix(compiler): always use ng:// prefix for sourcemap urls (#15218)
Fixes:
- In G3, filePaths don’t start with a `/` and therefore became relative.
- Always using the `ng://` prefix groups angular resources in the same
  way for AOT and JIT.
2017-03-16 17:33:17 -07:00
d2fbbb44ae fix(core): update peer dep on zone.js to ^0.8.4 (#15209)
Closes #15180
Closes #15185
2017-03-16 13:23:09 -07:00
77fd91d615 fix(core): ErrorHandler should not rethrow an error by default (#15077) (#15208)
ErrorHandler can not throw errors because it will unsubscribe itself from
the error stream.

Zones captures errors and feed it into NgZone, which than has a Rx Observable
to feed it into ErrorHandler. If the ErroHandler throws, then Rx will teardown
the observable which in essence causes the ErrorHandler to be removed from the
error handling. This implies that the ErrorHandler can never throw errors.

Closes #14949
Closes #15182
Closes #14316
2017-03-16 12:58:41 -07:00
492153a986 fix(compiler): make sourcemaps work in AOT mode
Inlcuded fixes:
- include preamble in generated source map
- always add a mapping for line/col 0 so that the
  generated sourcemap is not sparse
- use a uniue sourceUrl for inline templates even
  in the AOT case
2017-03-16 12:56:56 -07:00
c0e05e6f03 fix(tsc-wrapped): emit js files in all cases 2017-03-16 12:56:56 -07:00
73a46205a0 build(tsc-wrapped): update tsickle to version 0.21.6
Update tsickle to version 0.21.6 which fixes a bug where input source maps which specified filenames differently than the names supplied to tsc didn't get composed with tsc's source maps.  Also adds a test that the bug was fixed.
2017-03-16 12:56:56 -07:00
992aa17361 build: add source maps into NPM distribution (#15159)
Previous to 2.x there were some source maps distrubted, but they didn't go
all the way back to the TypeScript sources and they weren't available for
all JavaScript distrubted to NPM.

With this change source maps will be available for FESM distributions as
well as UMD and will go all the way back to TypeScript sources.

Fixes #15184
2017-03-16 12:55:15 -07:00
a4076c70cc fix(platform-browser): prevent clobbered elements from freezing the browser
see
4f69d38f09
2017-03-16 10:16:39 -07:00
52bbc9baf4 refactor(platform-browser): cleanup HtmlSanitizer specs 2017-03-16 10:16:39 -07:00
26d4ce29e8 fix(forms): ensure observable validators are properly canceled (#15132)
Observable subscriptions from previous validation runs should be canceled
before a new subscription is created for the next validation run.
Currently the subscription that sets the errors is canceled properly,
but the source observable created by the validator is not. While this
does not affect validation status or error setting, the source
observables will incorrectly continue through the pipeline until they
complete. This change ensures that the whole stream is canceled.

AsyncValidatorFn previously had an "any" return type, but now it more
explicitly requires a Promise or Observable return type. We don't
anticipate this causing problems given that any other return type
would have caused a runtime error already.
2017-03-16 10:15:17 -07:00
41f61b0b5b test: integration test that Angular apps compile with TS 2.2 (#15153) 2017-03-16 09:13:48 -07:00
f92d644c95 ci: disable Android 4.1, 4.2 and 4.3 in SauceLabs (#14461)
These browsers are not supported anymore: https://wiki.saucelabs.com/pages/viewpage.action?pageId=67012495

Tried to switch them to BrowserStack, but the emulators are too slow to run the full campaign (time out in tests, and disconnect eventually).
2017-03-16 09:13:07 -07:00
923d0c56e7 fix: correct UMD resolutions for platform-browser_animations (#15190)
Fixes #15114
2017-03-15 17:14:20 -07:00
dd36d413ba build: update symlink scripts for Windows (#14987) 2017-03-15 17:13:54 -07:00
013d806b79 fix(platform-server): handle styles with extra ':'s correctly (#15189)
Previously, style values were parsed with a regex that split on /:+/.

This causes errors for CSS such as

div {
  background-url: url(http://server.com/img.png);
}

since the regex would split the background-url line into 3 values instead of 2.

Now, the : character is detected with indexOf, avoiding this error.

A test was added to verify the behavior is correct.
2017-03-15 17:13:31 -07:00
6e98757665 fix(core): use presence of .subscribe to detect observables rather then Symbol.observable (#15171)
Fixes #14298
Fixes #14473
Fixes #14926
2017-03-15 17:13:03 -07:00
313158132d Revert "fix(core): ErrorHandler should not rethrow an error by default (#15077)"
This reverts commit 6559425b07.
2017-03-15 16:29:24 -07:00
6772c913c7 fix(aio): scroll to hash fragment element on URL change 2017-03-15 16:11:30 -07:00
b11d0119ac feat(aio): top bar typography, sidenav size 2017-03-15 16:11:30 -07:00
637a489996 refactor(aio): indent and org cleanup 2017-03-15 16:11:30 -07:00
c2bd357825 feat(aio): initial design commit
Implemented style guide elements to top header bar, side hamburger nav menu, content and search. Consolidated SCSS files to styles folder. Fixed PWA test.
2017-03-15 16:11:30 -07:00
4870f910d6 feat(aio): hide search results when click outside search elements
Clicking anywhere except on the search results and the search box will
now hide the search results

Closes #15095
2017-03-15 16:11:30 -07:00
c95a3048ce fix(aio): SearchBoxComponent should send query on click
The input may still have focus when the user hits ESC,
causing the search to be hidden.

If the user then clicks on the input again, they would expect
it to reopen the results.
2017-03-15 16:11:30 -07:00
0e6eb6d719 test(aio): fill in missing API page unit tests 2017-03-15 16:11:30 -07:00
09574fc285 build(aio): yarn test --sourcemap=false for faster dev/test cycles 2017-03-15 16:11:29 -07:00
4347cb2119 ci(aio): whitelist 'aio-contributors' for deploying PR previews 2017-03-15 16:11:29 -07:00
f600d4e9e4 refactor(aio): move window:resize handler into component 2017-03-15 16:11:29 -07:00
eaa04354d5 fix(aio): intercept all clicks on anchors
Previously we had the `LinkDirective` which intercepted clicks on anchors
outside the doc viewer. Now we intercept "all" link clicks within the app.
2017-03-15 16:11:29 -07:00
3f7cfde476 fix(aio): ignore private exports
Closes #14992
2017-03-15 16:11:29 -07:00
62d5543b01 fix(animations): make sure easing values are applied to an empty animate() step (#15174)
Closes #15115
Closes #15174
2017-03-15 15:31:06 -07:00
f1b33ab7b1 fix(http): Make ResponseOptionsArgs an interface (#14607) (#14623)
closes #13708
2017-03-15 15:28:52 -07:00
029d0f25e5 fix(router): fix query parameters with multiple values (#15129)
fixes #14796
2017-03-15 15:27:19 -07:00
3f38c6fdcd fix(animations): always fire callbacks even for noop animations (#15170)
Closes #15170
2017-03-15 13:41:00 -07:00
80112a9ea1 fix: fix path locally to empty.js (#15073) 2017-03-15 13:40:24 -07:00
795638e18b ci: remove unused import from integration test (#15175) 2017-03-15 13:39:00 -07:00
322bf7a0d5 Revert "refactor(upgrade): don’t rely on compiler internals"
This reverts commit 8e2c8b3e4d.
2017-03-15 13:23:31 -07:00
cd981499f9 Revert "feat(core): expose inputs, outputs and ngContentSelectors on ComponentFactory."
This reverts commit 1171f91a80.
2017-03-15 13:23:10 -07:00
c439742a54 Revert "feat(upgrade): use ComponentFactory.inputs/outputs/ngContentSelectors"
This reverts commit a3e32fb7e1.
2017-03-15 13:22:54 -07:00
a3e32fb7e1 feat(upgrade): use ComponentFactory.inputs/outputs/ngContentSelectors
DEPRECATION:
- the arguments `inputs` / `outputs` / `ngContentSelectors` of `downgradeComponent`
  are no longer used as Angular calculates these automatically now.
- Compiler.getNgContentSelectors is deprecated. Use
  ComponentFactory.ngContentSelectors instead.
2017-03-15 11:42:12 -07:00
1171f91a80 feat(core): expose inputs, outputs and ngContentSelectors on ComponentFactory.
E.g. for a component like this:
```
@Component({
  template: ‘<ng-content select=“child”></ng-content>’
})
class MyComp {
  @Input(‘aInputName’)
  aInputProp: string;

  @Output(‘aEventName’)
  aOuputProp: EventEmitter<any>;
}
```

the `ComponentFactory` will now contain the following:
- `inputs = {aInputProp: ‘aInputName’}`
- `outputs = {aOutputProp: ‘aOutputName’}`
- `ngContentSelectors = [‘child’]`
2017-03-15 11:42:12 -07:00
8e2c8b3e4d refactor(upgrade): don’t rely on compiler internals
Uses `Element.matches` to match selectors, instead
of copying the code from our compiler.
2017-03-15 11:42:12 -07:00
b00fe20afd fix(compiler): support interface types in injectable constuctors (#14894)
Fixes #12631
2017-03-15 09:24:56 -07:00
36ce0afff6 fix(animations): support multiple state names per state() call (#15147)
Closes #14732
Closes #15147
2017-03-15 09:24:09 -07:00
5c0ea20bd0 build(tsc-wrapped): use tsickleCompilerHost for initial file load
In order for tsickle's new support for input source maps to work, the tsickleCompilerHost must be used for the initial load of source files, since that's when the inline source maps are read and stripped.
2017-03-15 09:23:36 -07:00
49764a5bff build(tsc-wrapped): update tsickle to version 0.21.5 2017-03-15 09:23:36 -07:00
4f7d62adac fix(platform-server): fix an exception when HostListener('window:scroll') is used on the server (#15019) 2017-03-14 20:48:01 -07:00
c10c060d20 feat(common): support as syntax in template/* bindings (#15025)
* feat(common): support `as` syntax in template/* bindings

Closes #15020

Showing the new and the equivalent old syntax.
- `*ngIf="exp as var1”`
   => `*ngIf="exp; let var1 = ngIf”`
- `*ngFor="var item of itemsStream |async as items”`
   => `*ngFor="var item of itemsStream |async; let items = ngForOf”`

* feat(common): convert ngIf to use `*ngIf="exp as local“` syntax

* feat(common): convert ngForOf to use `*ngFor=“let i of exp as local“` syntax

* feat(common): expose NgForOfContext and NgIfContext
2017-03-14 20:46:29 -07:00
5fe2d8fd80 fix(core): update peer dep on zone.js to ^0.8.2 (#15078) 2017-03-14 20:42:39 -07:00
5c34066058 fix(compiler): only warn for @Injectable classes with invalid args.
In v2.x, users had to annotate classes that they intended to use as tokens with `@Injectable`. This is no longer required in v4.x for tokens,
and we now require the constructor parameters of classes annotated
with `@Injectable` to be statically analyzable by ngc.

This commit reduces the error into a warning
if the constructor parameters do not meet this condition.

DEPRECATION:
- Classes annotated with `@Injectable` but whose constructor’s parameter types
  are not statically analyzable by ngc will produce a warning.

Closes #15003
2017-03-14 19:52:53 -07:00
50ab06e29d fix(compiler): generated code should pass noUnusedLocals check
Closes #14797
2017-03-14 19:52:53 -07:00
06fc42bc44 fix(core): don’t throw if queries change during change detection.
Throwing on query changes would be a breaking change compared to v2.
Also discovers a bug with querying manually projected content, see #15117.

Related to #14748
Closes #14925
2017-03-14 19:52:53 -07:00
959a03a61f fix(compiler): fix utf8encode, move to sharted utils, add tests (#15076) 2017-03-14 17:13:39 -07:00
3b1956bbf2 fix(compiler): warning prints "WARNING" instead of "ERROR" (#15125) 2017-03-14 17:12:44 -07:00
3c15916e17 fix(compiler): Improve error message for missing annotations (#14724)
Currently, it says:
Unexpected value 'FuzzyTimePipe in
javascript/angular2/example/search/fuzzy_time.ts' declared by the module
'SearchModule in javascript/angular2/example/search/search_module.ts'

The new error message also suggests: 'Please add a @Pipe/@Directive/@Component annotation.'
2017-03-14 17:12:18 -07:00
ff60c041f6 test(platform-server): add initial e2e tests for platform-server (#15061) 2017-03-14 17:11:39 -07:00
13686bb518 fix: element injector vs module injector (#15044)
fixes #12869
fixes #12889
fixes #13885
fixes #13870

Before this change there was a single injector tree.
Now we have 2 injector trees, one for the modules and one for the components.
This fixes lazy loading modules.

See the design docs for details:
https://docs.google.com/document/d/1OEUIwc-s69l1o97K0wBd_-Lth5BBxir1KuCRWklTlI4

BREAKING CHANGES

`ComponentFactory.create()` takes an extra optional `NgModuleRef` parameter.
No change should be required in user code as the correct module will be used
when none is provided

DEPRECATIONS

The following methods were used internally and are no more required:
- `RouterOutlet.locationFactoryResolver`
- `RouterOutlet.locationInjector`
2017-03-14 16:26:17 -07:00
f093501501 fix(platform-server): support svg elements with namespaced attributes (#15101) 2017-03-14 15:40:55 -07:00
80649ea03c fix(platform-server): correctly implement get href in parse5 adapter (#15022) 2017-03-14 15:38:24 -07:00
778f7d6f33 fix(forms): remove equalsTo validator (#15050)
This API was introduced only in a beta release, and is being removed because we found it to be incorrect prior to launch. For more information about why this is being removed, see https://github.com/angular/angular/pull/15050.
2017-03-14 15:37:51 -07:00
2c5a671341 fix: don't instantiate providers with ngOnDestroy eagerly. (#15070)
BREAKING CHANGE:

Perviously, any provider that had an ngOnDestroy lifecycle hook would be created eagerly.

Now, only classes that are annotated with @Component, @Directive, @Pipe, @NgModule are eager. Providers only become eager if they are either directly or transitively injected into one of the above.

This also makes all `useValue` providers eager, which
should have no observable impact other than code size.

EXPECTED IMPACT:
Making providers eager was an incorrect behavior and never documented.
Also, providers that are used by a directive / pipe / ngModule stay eager.
So the impact should be rather small.

Fixes #14552
2017-03-14 14:32:26 -07:00
0aad270267 refactor(platform-browser): move platform-browser/animations to animations/browser (#15130)
Closes: #15130
2017-03-14 11:55:49 -07:00
221899a930 ci: fix incorrect github credetials (#15146) 2017-03-14 11:49:33 -07:00
060a2d11e5 ci(aio): deploy previews from aio-master (#15137) 2017-03-14 10:28:52 -07:00
abbbb4d52c ci: do not use shell to expand secure tokens to prevent leaks 2017-03-14 10:27:56 -07:00
ccd38dd54d ci: changing deployment token 2017-03-14 10:27:56 -07:00
cdc882bd36 feat: introduce source maps for templates (#15011)
The main use case for the generated source maps is to give
errors a meaningful context in terms of the original source
that the user wrote.

Related changes that are included in this commit:

* renamed virtual folders used for jit:
  * ng://<module type>/module.ngfactory.js
  * ng://<module type>/<comp type>.ngfactory.js
  * ng://<module type>/<comp type>.html (for inline templates)
* error logging:
  * all errors that happen in templates are logged
    from the place of the nearest element.
  * instead of logging error messages and stacks separately,
    we log the actual error. This is needed so that browsers apply
    source maps to the stack correctly.
  * error type and error is logged as one log entry.

Note that long-stack-trace zone has a bug that 
disables source maps for stack traces,
see https://github.com/angular/zone.js/issues/661.

BREAKING CHANGE:

- DebugNode.source no more returns the source location of a node.  

Closes 14013
2017-03-14 09:16:15 -07:00
1c1085b140 feat(aio): add google analytics (#15081) 2017-03-13 18:08:23 -07:00
914797a8ff feat(upgrade): support multi-slot projection in upgrade/static (#14282)
Closes #14261
2017-03-13 17:34:53 -07:00
ab40fcb068 docs(aio): fix relative links
The migrator was updated to automatically fix these links.
See fca5fb0280
and 3927b7a038

The result of this is that, going forward, we should ask
authors to include the path from the base href to the thing
being linked. E.g. guide/architecture#intro
2017-03-13 16:56:23 -07:00
1847550ad1 docs(aio): move cookbooks into guide
Also update all docs with migrated versions from latest angular.io

Fixes #14941
2017-03-13 16:56:23 -07:00
6497633529 feat(aio): improve search results functionality
* Ensure that all indexed documents are displayed in the search results.
(Previously the guide documents were not appearing because we only showed
results that had a `name` property, rather than a `name` or `title`.)
* Group the results by their containing folder (e.g. api, guide, tutorial, etc).
* Hide the results when the user hits the ESC key.
* Hide the results when the user clicks on a search result

Closes #14852
2017-03-13 16:55:52 -07:00
8850098ea4 build(aio): ensure API docs are generated in the correct place (#15103)
The move of the source code from modules/@angular to packages, caused a
problem with the path to the API docs.
2017-03-13 16:53:47 -07:00
eedca09d73 build(aio): generate the api-list.json file from the API docs 2017-03-13 16:52:58 -07:00
7b6dbf0952 fix(aio): LocationService search should handle corner cases
Adds tests and fixes corners cases for both `search()` and `setSearc()`
for things like empty queries and param keys that need encoding.

This commit refactors the `LocationService` to rely upon the `PlatformLocation`
rather than using `window.history` directly. This makes testing easier but also
makes the code simpler since `PlatformLocation` deals with platforms that
do not support history for us.
2017-03-13 16:52:58 -07:00
e6c81d2a42 Revert "refactor(platform-browser): move platform-browser/animations to animations/browser (#15043)"
This reverts commit 195b863ea4.
2017-03-13 15:46:44 -07:00
498a95148b Revert "build(animations): adjust animations/browser source to new standard for automatic build (#15043)"
This reverts commit 21a18d6ceb.
2017-03-13 15:46:07 -07:00
21a18d6ceb build(animations): adjust animations/browser source to new standard for automatic build (#15043) 2017-03-13 14:39:42 -07:00
195b863ea4 refactor(platform-browser): move platform-browser/animations to animations/browser (#15043) 2017-03-13 14:39:19 -07:00
75147ff008 fix(aio): remove invalid brace from CSS (#15122) 2017-03-13 13:36:22 -07:00
018e5c979b fix(platform-server): fix get/set title in parse5 adapter (#14965) 2017-03-13 13:22:03 -07:00
e7dab7e6c1 feat(aio): hard code a "Home" image link into the toolbar
Closes #15017
2017-03-13 12:55:11 -07:00
26efa3a25c style(aio): tidy up SCSS files 2017-03-13 12:55:11 -07:00
893652a813 fix(aio): upgrade to zone.js 0.7.8 (#15099)
This new version supports empty jasmine it clauses.
2017-03-13 12:53:45 -07:00
6559425b07 fix(core): ErrorHandler should not rethrow an error by default (#15077) 2017-03-13 11:27:01 -07:00
df914ef4bf fix(core): don’t recreate TemplateRef when used as a reference. (#15066)
This was a regression introduced in v4 rc.0.

Fixes #14873
2017-03-13 10:44:12 -07:00
4e1cf5b41a build(aio): replace all occurrences of env vars on a line in aio-builds-setup 2017-03-13 10:30:49 -07:00
0c5f893f6e test(aio): improve test description and expectations 2017-03-13 10:30:49 -07:00
17f5f3b32c ci(aio): fix clean-up script on ngbuilds.io 2017-03-13 10:30:49 -07:00
3bb59902f7 docs(aio): add more docs about aio-builds-setup 2017-03-13 10:30:49 -07:00
b804a488c5 feat(aio): make it easy to keep relevant logs outside the docker container 2017-03-13 10:30:49 -07:00
cbde75e77b feat(aio): redirect HTTP to HTTPS 2017-03-13 10:30:49 -07:00
413e11fac2 build(aio): remove redundant arg 2017-03-13 10:30:49 -07:00
0e2dd76c3b build: remove obsolete ci-lite/ directory and files (#14994)
The directory was removed in 91fe3aadb but then accidentally re-introduced while
rebasing and merging ab0db66bf.
2017-03-13 10:29:45 -07:00
ff71eff157 refactor(core): use flags in Renderer2.setStyle instead of booleans (#15045)
BREAKING CHANGE: (since v4 rc.1)
- `Renderer2.setStyle` no longer takes booleans but rather a
  bit mask of flags.
2017-03-13 09:45:04 -07:00
fa1920a02b feat(aio): enable deep-linking on deployed apps (until prerendering is done) (#15049) 2017-03-13 09:35:16 -07:00
71cd2957f7 ci: add wardbell and gkalpak to aio approvers (#15030) 2017-03-13 09:34:40 -07:00
6c8638cf01 feat(core): allow to provide multiple default testing modules (#15054)
This can be used to e.g. add the NoopAnimationsModule by default:

```
TestBed.initTestEnvironment([
  BrowserDynamicTestingModule,
  NoopAnimationsModule
], platformBrowserDynamicTesting());
```
2017-03-10 16:55:32 -08:00
8b5c6b2732 fix(compiler): shouldn't throw when Symbol is used as DI token (#13701)
Closes #13314
2017-03-10 15:26:37 -08:00
601494734c Revert "fix(core): ErrorHandler should not rethrow an error by default (#13534)"
This reverts commit 1aebea52e1.
2017-03-10 15:05:48 -08:00
1aebea52e1 fix(core): ErrorHandler should not rethrow an error by default (#13534)
Closes #14949
Closes #13159
Closes #8993
2017-03-10 14:44:14 -08:00
920b3d259d feat(aio): support @angular/service-worker using the CLI generated service worker manifest (#15042) 2017-03-10 14:05:29 -08:00
fce55d87d2 feat(aio): add Kapunahele animations v2 (#15023) 2017-03-10 14:05:00 -08:00
53d62fa7d0 ci: use angular-builds credentials for publishing artifacts (#15072) 2017-03-10 14:03:40 -08:00
a69afeb614 ci: skip root pullapproval for packages directory (#15071) 2017-03-10 13:54:28 -08:00
5f9fb911f7 fix(compiler): improve error msg for unexpected closing tags (#14747)
Closes #6652
2017-03-10 13:05:17 -08:00
d17d4a3b54 docs: add changelog for 4.0.0-rc.3 2017-03-10 12:26:19 -08:00
814dc107d9 release: cut the 4.0.0-rc.3 release 2017-03-10 12:21:52 -08:00
bebedfed24 build: build modules and examples for karma 2017-03-08 17:35:20 -08:00
2b44854885 build: fix paths for examples tests 2017-03-08 16:29:28 -08:00
b74ab83d2c refactor: update paths from modules/@angular to packages 2017-03-08 16:29:28 -08:00
da8ea350b2 refactor: More generic build.sh file 2017-03-08 16:29:28 -08:00
1efd508217 build: update package.json to remove babel. 2017-03-08 16:29:28 -08:00
58dd4673cd refactor: remove babel config files 2017-03-08 16:29:28 -08:00
72563b61fb build: fix rollup config file paths 2017-03-08 16:29:28 -08:00
5aed1e36b8 refactor: move secondary entry point rollup configs. 2017-03-08 16:29:28 -08:00
8573e36574 build: fix file paths after moving modules/@angular/* to packages/* 2017-03-08 16:29:28 -08:00
3e51a19983 refactor: move angular source to /packages rather than modules/@angular 2017-03-08 16:29:27 -08:00
5ad5301a3e feat(aio): add API list page (#14899) 2017-03-08 13:08:19 -08:00
174d4c8ef7 ci(aio): do not deploy PR if preconditions not met
This avoids incorrectly failing the build if the PR author is not a member of one of the whitelisted GitHub teams.
2017-03-07 18:24:45 -08:00
4ca772eea3 refactor(aio): move scripts into scripts/ directory 2017-03-07 18:24:45 -08:00
093d69f0f0 ci(aio): only deploy a preview if the PR touched relevant files 2017-03-07 18:24:45 -08:00
b4ec80b21d docs(aio): document arg for auto-restarting the docker container on boot 2017-03-07 18:24:45 -08:00
a4476654aa refactor(aio): use more intuitive log filepath 2017-03-07 18:24:45 -08:00
4210d2b4b1 test(aio): fix e2e tests 2017-03-07 18:24:45 -08:00
aa30c50144 feat(aio): make it easier to create a docker image (less variables to overwrite) 2017-03-07 18:24:45 -08:00
fd34a58e13 fix(aio): ensure NGBUILDS_IO_KEY is not printed
Gaining access to another PR's JWT, would allow faking that PR's author wrt to
GitHub team membership verification for as long as the JWT is valid (currently
90 mins).
2017-03-07 18:24:45 -08:00
e40f81b564 ci(aio): fail the build if preview deployment fails 2017-03-07 18:24:45 -08:00
5ab2e28703 ci(aio): bringing in #14888 (because no patience) 2017-03-07 18:24:45 -08:00
a3a7cf2090 build(aio): allow overwriting env vars at build time 2017-03-07 18:24:45 -08:00
d9d9d9de6f build(aio): allow passing parameters to docker build 2017-03-07 18:24:45 -08:00
7e67f37fc4 ci(aio): test aio-builds-setup as part of the aio job 2017-03-07 18:24:45 -08:00
d275667da0 build(aio): upgrade to latest and pin major pm2 version 2017-03-07 18:24:45 -08:00
03a5fd01c9 fix(aio): do not hardcode the domain in preview link comments 2017-03-07 18:24:45 -08:00
d28ea80db8 build(aio): always compile before running tests 2017-03-07 18:24:45 -08:00
0e9277b4c3 style(aio): change mock value for consistency 2017-03-07 18:24:45 -08:00
2796790c7d feat(aio): verify uploaded builds based on JWT from Travis 2017-03-07 18:24:45 -08:00
028b274750 feat(aio): support passing secrets as files to the docker container 2017-03-07 18:24:45 -08:00
3ed1f64d43 feat(aio): implement BuildVerifier 2017-03-07 18:24:45 -08:00
96f11dad18 feat(aio): implement GithubTeams 2017-03-07 18:24:45 -08:00
060d02eb82 fix(aio): remove unnecessary repoSlug parameter from GithubApi 2017-03-07 18:24:45 -08:00
951e653b0c feat(aio): implement GithubApi.getPaginated() 2017-03-07 18:24:45 -08:00
37348989f0 feat(aio): make githubToken mandatory for GithubApi 2017-03-07 18:24:45 -08:00
c5644e5a0d refactor(aio): add assertNotMissingOrEmpty() helper 2017-03-07 18:24:45 -08:00
c8d87a936b feat(aio): add support for HTTPS (certificates provided by host - fallback to self-signed) 2017-03-07 18:24:45 -08:00
6b8413f7b3 build(aio): update TypeScript (and other dependencies) 2017-03-07 18:24:45 -08:00
bc831ff4a4 feat(aio): check resolution of external URLs in HEALTHCHECK 2017-03-07 18:24:45 -08:00
8a8d4fe24f test(aio): fix typos (repoSlag --> repoSlug) 2017-03-07 18:24:45 -08:00
9df9bdc0f5 style(aio): correctly type tuple 2017-03-07 18:24:45 -08:00
c8ead9bcd0 refactor(aio): use types intead of interfaces for functions 2017-03-07 18:24:45 -08:00
115164033b ci(aio): add initial implementation for aio-builds setup 2017-03-07 18:24:45 -08:00
794f8f4e6a build(aio): update .gitignore 2017-03-07 18:24:45 -08:00
ad3b44aef7 RendererV2 -> Renderer2 rename (#14998)
* refactor: rename `RendererV2` into `Renderer2`

BREAKING CHANGE (since 4.0 rc.1):
- rename `RendererV2` to `Renderer2`
- rename `RendererTypeV2` to `RendererType2`
- rename `RendererFactoryV2` to `RendererFactory2`
2017-03-07 16:36:12 -08:00
5df998d086 fix(router): do not finish bootstrap until all the routes are resolved (#14762)
DEPRECATION:

Use `RouterModule.forRoot(routes, {initialNavigation: 'enabled'})` instead of
`RouterModule.forRoot(routes, {initialNavigtaion: true})`.

Before doing this, move the initialization logic affecting the router
from the bootstrapped component to the boostrapped module.

Similarly, use `RouterModule.forRoot(routes, {initialNavigation: 'disabled'})`
instead of `RouterModule.forRoot(routes, {initialNavigation: false})`.

Deprecated options: 'legacy_enabled', `true` (same as 'legacy_enabled'),
'legacy_disabled', `false` (same as 'legacy_disabled').

The "Router Initial Navigation" design document covers this change.
Read more here:
https://docs.google.com/document/d/1Hlw1fPaVs-PCj5KPeJRKhrQGAvFOxdvTlwAcnZosu5A/edit?usp=sharing
2017-03-07 14:27:20 -08:00
1cff1250ba refactor: remove old compiler options (#14891)
DEPRECATION:
- `CompilerOptions.debug` has no effect any more, as the compiler
  always produces the same code independent of debug mode.
2017-03-07 11:16:27 -08:00
07122f0ad9 fix(upgrade): populate upgraded component's view before creating the controller (#14289)
Previously, the relative order of the AngularJS compiling/linking operations was
not similar to AngularJS's, resulting in inconsistent behavior for upgraded
components (which made upgrading to Angular less straight forward).

This commit fixes it, by following the compiling/linking process of AngularJS
more closely.

Main differences:

- The components view is already populated when the controller is instantiated
  (and subsequent hooks are called).
- The correct DOM content is available when running the `$onChanges`, `$onInit`,
  `$doCheck` hooks. Previously, the "content children" were still present, not
  the "view children".
- The same for pre-linking.
- The template is compiled in the correct DOM context (e.g. has access to
  ancestors). Previously, it was compiled in isolation, inside a dummy element.

For reference, here is the order of operations:

**Before**

1. Compile template
2. Instantiate controller
3. Hook: $onChanges
4. Hook: $onInit
5. Hook: $doCheck
6. Pre-linking
7. Collect content children
8. Insert compiled template
9. Linking
10. Post-linking
11. Hook: $postLink

**After**

1. Collect content children
2. Insert template
3. Compile template
4. Instantiate controller
5. Hook: $onChanges
6. Hook: $onInit
7. Hook: $doCheck
8. Pre-linking
9. Linking
10. Post-linking
11. Hook: $postLink

Fixes #13912
2017-03-07 09:32:52 -08:00
ebd446397a fix(aio): reimplement the top menu dropping into the side bar
I couldn't find the CSS for the `.small` class to make it look nice.
2017-03-06 22:27:32 -08:00
55189b1b85 refactor(aio): simplify and document the NavigationService 2017-03-06 22:27:32 -08:00
b017fbe48e style(aio): rename local variable 2017-03-06 22:27:32 -08:00
0d6aa0caed style(aio): rename const to camelCase 2017-03-06 22:27:32 -08:00
b70c881c00 docs(aio): fix paths to images 2017-03-06 22:27:32 -08:00
0d63e2a586 docs(aio): copy devguide images from angular.io 2017-03-06 22:27:32 -08:00
d83b7ba4c0 refactor(aio): rename activeNodes to selectedNodes
This keeps the naming consistent with the nav-menu
and nav-item component properties
2017-03-06 22:27:32 -08:00
46d6e8d191 fix(aio): reimplement side-nav 2017-03-06 22:27:32 -08:00
61ef756ef2 fix(aio): ensure that only one request for navigation.json is made 2017-03-06 22:27:32 -08:00
3529813ca0 fix(aio): move interfaces into their own files to workaround compile weirdness
For some reason the tree-shaker is not picking up these interfaces
(perhaps TS is not passing it through) when they are in the same file
as their related services. This results in a distracting warning message.
2017-03-06 22:27:32 -08:00
b81693b30c fix(aio): no need for path and url properties in nav 2017-03-06 22:27:32 -08:00
5815983178 fix(aio): correctly intercept links with LinkDirective 2017-03-06 22:27:32 -08:00
fe962f6de7 fix(aio): LocationService urls should never start with a slash
There is a weirdness in the Angular Location service.
If the `baseHref` is only a single slash (`'/'`) then it
changes it to be an empty string (`''`). The effect of this
is that `Location.normaliseUrl(url)` does not strip off the
leading slash from url paths.

The problem is that the leading slash only appears on the
initial Location path, and not on urls that arrive from subscribing
to the Location service.

This commit is a workaround this problem.
2017-03-06 22:27:32 -08:00
04e14589c4 docs(aio): make home doc the default home page
Now the `/` url will display the home page.

Closes #14849
2017-03-06 22:27:32 -08:00
d6c1ccaf14 fix(aio): emit a hard-coded doc if file-not-found fails
Closes #14848
2017-03-06 22:27:32 -08:00
4abd6f333c fix(aio): map the empty url to the correct doc path 2017-03-06 22:27:32 -08:00
dd50922747 refactor(aio): move document path computation
Do the path computation before doing the cache look up.
2017-03-06 22:27:32 -08:00
66cc88c8a8 fix(aio): ensure that only one request per document is made 2017-03-06 22:27:32 -08:00
b44bc9c022 test(aio): add mock services 2017-03-06 22:27:32 -08:00
b7e76cc2e1 ci: re-enable publishing master artifacts (#14973) 2017-03-06 18:03:38 -08:00
3651d8d673 fix: throw for synthetic properties / listeners by default (#14880)
This allows to detect the case that the animation module is not loaded.
2017-03-06 17:15:08 -08:00
ba4b6f58d9 fix(core): allow to use the Renderer outside of views. (#14882)
Fixes #14872
2017-03-06 17:08:22 -08:00
6cd3326b55 fix(compiler): don’t throw for empty array literal in assignments (#14878)
Closes #14782
2017-03-06 17:01:45 -08:00
6bc6482765 fix(compiler): improve error message when a module imports itself (#14646)
Closes #14644
2017-03-06 17:00:25 -08:00
f2adb2900d refactor: remove the symlink hack from tsc-wrapped (#14837) 2017-03-06 15:22:33 -08:00
8343fb7740 refactor: remove lang.ts (#14837) 2017-03-06 15:22:29 -08:00
84a65cf788 refactor: remove global from facade/lang (#14837) 2017-03-06 15:22:22 -08:00
b0e0839075 refactor: remove facade/collection (#14837) 2017-03-06 15:22:14 -08:00
4fe0b90948 refactor: remove facade/browser (#14837) 2017-03-06 15:22:06 -08:00
928c5657c8 refactor: remove EventEmitter from facade (#14837)
As of right now each module has its own copy of the EventEmitter
contributing to bloat.
2017-03-06 15:21:39 -08:00
728fe472f8 ci: replace '| head -n1' with 'git log -1' due to broken pipe breaking -o pipefail 2017-03-05 11:53:45 -08:00
32990307fe ci: turn on bash xtrace in publish-build-artifacts.sh to debug deploy failure on master 2017-03-05 11:43:25 -08:00
00fdcf4e58 build: allow check-environment.js to be run directly 2017-03-05 08:41:26 -08:00
ab0db66bf7 build(aio): move gulp tasks to package.json
Generate the docs with `yarn docs`.
Test the doc generation code with `yarn docs-test`

The docs are automatically built as part of the `yarn build` task,
so there is no need to rebuild them in the test_aio.sh file
2017-03-05 08:41:26 -08:00
8757656508 build(aio): upgrade to angular 4.0.0-rc.2 2017-03-05 08:41:26 -08:00
01ff427685 test(aio): reimplemented all the commented-out unit tests 2017-03-05 08:41:26 -08:00
2ebfa2ff31 test(aio): add placeholder test descriptions 2017-03-05 08:41:26 -08:00
b09ee424bf refactor(aio): reorganize search service for easier testing 2017-03-05 08:41:26 -08:00
b8321e2f7d ci: add comment and remove debugging printenv statement 2017-03-05 08:12:55 -08:00
b8f0c3dc7b ci: redo how env variables are set and shared in ci to prevent collisions 2017-03-05 08:03:38 -08:00
91fe3aadbc ci: delete obsolete files and move scripts/ci-lite/ to scripts/ci/ 2017-03-05 08:03:38 -08:00
a24e652f2b ci: clean up CI logging, folding, add build time logging, and improve error handling (#14425) 2017-03-02 00:22:24 -08:00
207298cd3a build(aio): generate marketing docs JSON files 2017-03-02 00:21:06 -08:00
62eafa4eec test(aio): fix up links in app e2e test 2017-03-02 00:21:06 -08:00
4626ca2bff build(aio): add file-not-found doc 2017-03-02 00:21:06 -08:00
38bb744008 fix(aio): remove external HTML and SCSS from app component 2017-03-02 00:21:06 -08:00
dca83ec738 feat(aio): add search service and search UI 2017-03-02 00:21:06 -08:00
71e22b8d11 feat(aio): implement doc and navigation UI 2017-03-02 00:21:06 -08:00
371dc4744c feat(aio): add navigation service 2017-03-02 00:21:06 -08:00
4767f107fb feat(aio): add logger service 2017-03-02 00:21:06 -08:00
5ae4b77d8b feat(aio): add document service 2017-03-02 00:21:06 -08:00
887d32a9bf feat(aio): add location service 2017-03-02 00:21:06 -08:00
3883b736c0 build(aio): content documents should have a contents field 2017-03-02 00:21:06 -08:00
c2e672cd1c docs(aio): add NOTES 2017-03-02 00:21:06 -08:00
93c0ab7131 fix(aio): remove previous files 2017-03-02 00:21:06 -08:00
e6e8123bdd build: update to yarn 0.21.3 (#14805) 2017-03-01 23:58:05 -08:00
49aa50886a docs: add 2.4.9 release notes 2017-03-01 23:22:56 -08:00
180b705227 release: cut the 4.0.0-rc.2 release 2017-03-01 22:46:11 -08:00
0f94b93c81 docs: add changelog for 4.0.0-rc.2 2017-03-01 22:44:18 -08:00
de57b2d9fd docs: update 2.4.9 release schedule 2017-03-01 22:43:38 -08:00
9560ad81b9 fix(animations): make animations work in AOT (#14775) 2017-03-01 17:13:06 -08:00
3168ef75da build: fix secondary entry point es5 output and core Rx references (#14820)
Secondary entry points (testing, static, etc) are rolled up into a
single ESM/ES2015 file, then downleveled to ESM/ES2015. This downleveling
was not working and was producing ESM/ES2015. Also, the @angular/core
package's .babelrc file was missing reference to Rx Observable which
broke the UMD bundle.

Fixes #14730
2017-03-01 15:59:09 -08:00
b6e6fc1724 docs: fix changelog to list npm command for Windows (#14812)
Closes #14809
2017-03-01 15:37:16 -08:00
dd499010be ci: add NGBUILDS_IO_KEY to .travis.yaml for ngbuilds.io integration 2017-03-01 15:28:10 -08:00
d2803da8b6 ci: add AlexE and Jason to the build-and-ci pullapprove group 2017-03-01 15:28:10 -08:00
a6996a9cdd fix(tsc-wrapped): validate metadata in static members of a class (#14772)
Fixes #13481
2017-03-01 14:45:00 -08:00
6bae7378b1 fix(language-service): tolerate errors in decorators (#14634)
Fixes #14631
2017-03-01 13:23:34 -08:00
7a66a4115b test(language-service): test @angular/language-service can be loaded by tsserver.js (#14721) 2017-03-01 13:22:46 -08:00
79fc1e3959 fix(compiler): apply element bindings before host bindings (#14823) 2017-03-01 12:17:43 -08:00
9402df92de test(platform-server): enable meta tag and title service specs for server (#14574)
Fix CSS selector syntax to allow single and double quotes. Needed for meta tag service selector to work properly on parse5.

Fixes #14565.
2017-03-01 11:19:22 -08:00
47bdc2b0b7 fix(platform-server): don't setup Testability and TestabilityRegistry on the server (#14510) 2017-03-01 11:16:56 -08:00
49b462e815 test: closure integration test for change detection (#14691) 2017-03-01 08:05:22 -08:00
5753de50f0 fix(core): fix isComponentView() and isEmbeddedView() tests (#14789)
fixes #14778
2017-03-01 08:03:14 -08:00
d1182af1a4 refactor: change remaining <template> to <ng-template> (#14706) 2017-03-01 08:02:37 -08:00
bc9e1debf2 docs: changelog - fix else example (#14766)
The else syntax needs a `ng-template`.
2017-03-01 08:02:02 -08:00
fc9e6b2a0a docs: add details about OpaqueToken deprecation in the changelog (#14790) 2017-03-01 08:01:08 -08:00
968995a4c6 fix(upgrade): fix upgrade component Closure optimization. (#14801)
$scope and $element are passed to AngularJS, which is not optimized
together with the app. That means properties must be quoted to prevent
renaming.
2017-03-01 06:49:25 -08:00
5ba55b0e04 fix(compiler): fix identifier names of EMPTY_MAP / EMPTY_ARRAY (#14806) 2017-02-28 14:16:26 -08:00
126fda2613 perf: delete pre-view-engine core, compiler, platform-browser, etc code (#14788)
After the introduction of the view engine, we can drop a lot of code that is not used any more.

This should reduce the size of the app bundles because a lot of this code was not being properly tree-shaken by today's tools even though it was dead code.
2017-02-27 23:08:19 -08:00
e58cb7ba08 docs: fix animations module instructions in the changelog (#14729)
Closes #14728
2017-02-27 16:17:14 -08:00
5caab71f7d refactor: make identifiers for generated code small to improve dev size 2017-02-27 14:50:41 -08:00
d2e42567a6 fix(core): call lifecycle hooks for siblings in declaration order 2017-02-27 14:50:41 -08:00
14d37fe052 refactor(core): move NodeType and ProviderType into NodeFlags
Also removes `ng-reflect` attributes for element properties.
2017-02-27 14:50:41 -08:00
f356041f09 docs: update version in changelog to reflect RC.1 (not 0) (#14740) 2017-02-27 11:26:16 -08:00
77682a3397 fix(compiler): quote animation in RendererTypeV2 and skip if empty (#14773) 2017-02-27 11:23:37 -08:00
213e210a93 docs: changelog - add anchor to the FESM section 2017-02-25 20:17:02 -08:00
c868457e21 docs: improve Flat ES Modules docs 2017-02-25 20:15:50 -08:00
f32f4de95c docs: more changelog fixes 2017-02-24 18:24:18 -08:00
26ed2621a3 docs: fix typo in the changelog 2017-02-24 18:22:30 -08:00
c7d1b3664b docs: update release schedule due to bad 4.0.0-rc.0 release 2017-02-24 18:08:28 -08:00
1920 changed files with 36355 additions and 33427 deletions

8
.gitignore vendored
View File

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

View File

@ -10,6 +10,7 @@
# chuckjaz - Chuck Jazdzewski
# gkalpak - George Kalpakas
# IgorMinar - Igor Minar
# jasonaden - Jason Aden
# kara - Kara Erickson
# matsko - Matias Niemelä
# mhevery - Misko Hevery
@ -19,6 +20,8 @@
# tbosch - Tobias Bosch
# vicb - Victor Berchet
# vikerman - Vikram Subramanian
# wardbell - Ward Bell
# tinayuangao - Tina Gao
version: 2
@ -39,6 +42,7 @@ groups:
- "aio/*"
- "integration/*"
- "modules/*"
- "packages/*"
- "tools/*"
users:
- IgorMinar
@ -67,7 +71,9 @@ groups:
- "aio/*"
users:
- IgorMinar #primary
- mhevery
- alexeagle
- jasonaden
- mhevery #fallback
integration:
conditions:
@ -84,7 +90,7 @@ groups:
core:
conditions:
files:
- "modules/@angular/core/*"
- "packages/core/*"
users:
- tbosch #primary
- mhevery
@ -94,7 +100,7 @@ groups:
compiler/animations:
conditions:
files:
- "modules/@angular/compiler/src/animation/*"
- "packages/compiler/src/animation/*"
users:
- matsko #primary
- tbosch
@ -104,7 +110,7 @@ groups:
compiler/i18n:
conditions:
files:
- "modules/@angular/compiler/src/i18n/*"
- "packages/compiler/src/i18n/*"
users:
- vicb #primary
- tbosch
@ -114,7 +120,7 @@ groups:
compiler:
conditions:
files:
- "modules/@angular/compiler/*"
- "packages/compiler/*"
users:
- tbosch #primary
- vicb
@ -126,7 +132,7 @@ groups:
conditions:
files:
- "tools/@angular/tsc-wrapped/*"
- "modules/@angular/compiler-cli/*"
- "packages/compiler-cli/*"
users:
- alexeagle
- chuckjaz
@ -137,7 +143,7 @@ groups:
common:
conditions:
files:
- "modules/@angular/common/*"
- "packages/common/*"
users:
- pkozlowski-opensource #primary
- vicb
@ -147,17 +153,17 @@ groups:
forms:
conditions:
files:
- "modules/@angular/forms/*"
- "packages/forms/*"
users:
- kara #primary
# needs secondary
- tinayuangao #secondary
- IgorMinar #fallback
- mhevery #fallback
http:
conditions:
files:
- "modules/@angular/http/*"
- "packages/http/*"
users:
- vikerman #primary
- alxhub
@ -167,27 +173,28 @@ groups:
language-service:
conditions:
files:
- "modules/@angular/language-service/*"
- "packages/language-service/*"
users:
- chuckjaz #primary
# needs secondary
- tbosch #secondary
- vicb
- IgorMinar #fallback
- mhevery #fallback
router:
conditions:
files:
- "modules/@angular/router/*"
- "packages/router/*"
users:
- vicb #primary
# needs secondary
- jasonaden
- vicb
- IgorMinar #fallback
- mhevery #fallback
upgrade:
conditions:
files:
- "modules/@angular/upgrade/*"
- "packages/upgrade/*"
users:
- petebacondarwin #primary
- gkalpak
@ -197,7 +204,7 @@ groups:
platform-browser:
conditions:
files:
- "modules/@angular/platform-browser/*"
- "packages/platform-browser/*"
users:
- tbosch #primary
- vicb #secondary
@ -207,7 +214,7 @@ groups:
platform-server:
conditions:
files:
- "modules/@angular/platform-server/*"
- "packages/platform-server/*"
users:
- vikerman #primary
- alxhub
@ -219,7 +226,7 @@ groups:
platform-webworker:
conditions:
files:
- "modules/@angular/platform-webworker/*"
- "packages/platform-webworker/*"
users:
- vicb #primary
- tbosch #secondary
@ -231,7 +238,7 @@ groups:
benchpress:
conditions:
files:
- "modules/@angular/benchpress/*"
- "packages/benchpress/*"
users:
- tbosch #primary
# needs secondary
@ -243,7 +250,8 @@ groups:
files:
- "aio/*"
users:
- IgorMinar
- robwormald
- petebacondarwin
- IgorMinar #primary
- petebacondarwin #secondary
- gkalpak
- wardbell
- mhevery #fallback

View File

@ -10,11 +10,16 @@ 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
- g3
cache:
yarn: true
@ -25,9 +30,9 @@ cache:
env:
global:
# GITHUB_TOKEN_ANGULAR
# GITHUB_TOKEN_ANGULAR=<github token, a personal access token of the angular-builds account, account access in valentine>
# This is needed for the e2e Travis matrix task to publish packages to github for continuous packages delivery.
- secure: "fq/U7VDMWO8O8SnAQkdbkoSe2X92PVqg4d044HmRYVmcf6YbO48+xeGJ8yOk0pCBwl3ISO4Q2ot0x546kxfiYBuHkZetlngZxZCtQiFT9kyId8ZKcYdXaIW9OVdw3Gh3tQyUwDucfkVhqcs52D6NZjyE2aWZ4/d1V4kWRO/LMgo="
- secure: "rNqXoy2gqjbF5tBXlRBy+oiYntO3BtzcxZuEtlLMzNaTNzC4dyMOFub0GkzIPWwOzkARoEU9Kv+bC97fDVbCBUKeyzzEqxqddUKhzRxeaYjsefJ6XeTvBvDxwo7wDwyxZSuWdBeGAe4eARVHm7ypsd+AlvqxtzjyS27TK2BzdL4="
# 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.
@ -35,6 +40,7 @@ env:
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=e2e_2
- CI_MODE=js
- CI_MODE=saucelabs_required
- CI_MODE=browserstack_required
@ -49,14 +55,19 @@ 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-lite/install.sh
- ./scripts/ci/install.sh
script:
- ./scripts/ci-lite/build.sh && ./scripts/ci-lite/test.sh
after_success:
- ./scripts/ci-lite/deploy_aio_staging.sh
after_script:
- ./scripts/ci-lite/cleanup.sh
- ./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

View File

@ -1,7 +1,275 @@
<a name="4.0.0-rc.0"></a>
<a name="4.0.2"></a>
## [4.0.2](https://github.com/angular/angular/compare/4.0.1...4.0.2) (2017-04-11)
### Bug Fixes
* **compiler:** fix inheritance for AOT with summaries ([#15583](https://github.com/angular/angular/issues/15583)) ([1864ccb](https://github.com/angular/angular/commit/1864ccb))
* **language-service:** avoid throwing exceptions when reporting metadata errors ([0861fda](https://github.com/angular/angular/commit/0861fda))
* **language-service:** detect when there isn't a tsconfig.json ([168a2eb](https://github.com/angular/angular/commit/168a2eb)), closes [#15874](https://github.com/angular/angular/issues/15874)
* **language-service:** improve resilience to incomplete information ([e4277a0](https://github.com/angular/angular/commit/e4277a0))
* **language-service:** initialize static reflector correctly ([5b99533](https://github.com/angular/angular/commit/5b99533)), closes [#15768](https://github.com/angular/angular/issues/15768)
* **language-service:** parse extended i18n forms ([c9c7acd](https://github.com/angular/angular/commit/c9c7acd))
* **language-service:** resolve any parameter types to any result ([feae7b6](https://github.com/angular/angular/commit/feae7b6))
* **router:** fix query param parsing ([2f41b52](https://github.com/angular/angular/commit/2f41b52))
* **router:** the preloader use the module from the loaded config ([978f809](https://github.com/angular/angular/commit/978f809))
* **tsc-wrapped:** ensure valid path separators in metadata ([c10e50c](https://github.com/angular/angular/commit/c10e50c))
<a name="4.0.1"></a>
## [4.0.1](https://github.com/angular/angular/compare/4.0.0...4.0.1) (2017-03-29)
### Bug Fixes
* **animations:** make sure style calculations are not computed too early ([#15540](https://github.com/angular/angular/issues/15540)) ([c828511](https://github.com/angular/angular/commit/c828511)), closes [#15507](https://github.com/angular/angular/issues/15507)
* **compiler:** allow single quotes into named interpolations ([#15461](https://github.com/angular/angular/issues/15461)) ([a654875](https://github.com/angular/angular/commit/a654875)), closes [#15318](https://github.com/angular/angular/issues/15318)
* **compiler:** ignore errors when evaluating base classes ([#15560](https://github.com/angular/angular/issues/15560)) ([a88413f](https://github.com/angular/angular/commit/a88413f)), closes [#15536](https://github.com/angular/angular/issues/15536)
* **compiler:** throw when a component defines both template and templateUrl ([#15572](https://github.com/angular/angular/issues/15572)) ([902bb2f](https://github.com/angular/angular/commit/902bb2f)), closes [#15566](https://github.com/angular/angular/issues/15566)
* **core:** check for undefined on normalizeDebugBindingValue ([#15503](https://github.com/angular/angular/issues/15503)) ([b8c0a97](https://github.com/angular/angular/commit/b8c0a97)), closes [#15494](https://github.com/angular/angular/issues/15494)
* **core:** fix inheritance in JIT mode for TS 2.1 ([#15599](https://github.com/angular/angular/issues/15599)) ([ca66530](https://github.com/angular/angular/commit/ca66530)), closes [#15502](https://github.com/angular/angular/issues/15502)
* **core:** fix the key/value differ ([#15539](https://github.com/angular/angular/issues/15539)) ([e72124c](https://github.com/angular/angular/commit/e72124c)), closes [#15457](https://github.com/angular/angular/issues/15457)
* **core:** improve error msg for invalid KeyValueDiffer.diff arg ([#15489](https://github.com/angular/angular/issues/15489)) ([d74e4d0](https://github.com/angular/angular/commit/d74e4d0)), closes [#15402](https://github.com/angular/angular/issues/15402)
* **core:** Update types for TypeScript nullability support ([#15472](https://github.com/angular/angular/issues/15472)) ([8c4b963](https://github.com/angular/angular/commit/8c4b963))
* **language-service:** be resilient to invalidate ordering ([#15470](https://github.com/angular/angular/issues/15470)) ([a2c2b87](https://github.com/angular/angular/commit/a2c2b87)), closes [#15466](https://github.com/angular/angular/issues/15466)
* **language-service:** correctly determine base members of types ([#15600](https://github.com/angular/angular/issues/15600)) ([0fe4985](https://github.com/angular/angular/commit/0fe4985)), closes [#15460](https://github.com/angular/angular/issues/15460)
* **language-service:** don't require `reflect-metadata` module to be provided ([#15569](https://github.com/angular/angular/issues/15569)) ([bfa4f70](https://github.com/angular/angular/commit/bfa4f70)), closes [#15568](https://github.com/angular/angular/issues/15568)
* **language-service:** guard access to `Symbol.members` ([#15529](https://github.com/angular/angular/issues/15529)) ([bf25e94](https://github.com/angular/angular/commit/bf25e94)), closes [#15528](https://github.com/angular/angular/issues/15528)
* **language-service:** improve performance of `updateModuleAnalysis()` ([#15543](https://github.com/angular/angular/issues/15543)) ([5597fd3](https://github.com/angular/angular/commit/5597fd3))
* **router:** should run CanActivate after CanDeactivate guards ([75478b2](https://github.com/angular/angular/commit/75478b2)), closes [#14059](https://github.com/angular/angular/issues/14059) [#15467](https://github.com/angular/angular/issues/15467)
* **router:** shouldn't execute CanLoad when a route has been loaded ([2360676](https://github.com/angular/angular/commit/2360676)), closes [#14475](https://github.com/angular/angular/issues/14475) [#15438](https://github.com/angular/angular/issues/15438)
### Performance Improvements
* **router:** don't create new serializer every time UrlTree.toString is called ([#15565](https://github.com/angular/angular/issues/15565)) ([fd61145](https://github.com/angular/angular/commit/fd61145))
<a name="4.0.0"></a>
# [4.0.0](https://github.com/angular/angular/compare/4.0.0-rc.6...4.0.0) invisible-makeover (2017-03-23)
### Bug Fixes
* **compiler:** assume queries with no matches as static ([#15429](https://github.com/angular/angular/issues/15429)) ([c8ab5cb](https://github.com/angular/angular/commit/c8ab5cb)), closes [#15417](https://github.com/angular/angular/issues/15417)
* **compiler:** correctly handle when `toString` is exported ([#15430](https://github.com/angular/angular/issues/15430)) ([0dda01e](https://github.com/angular/angular/commit/0dda01e)), closes [#15420](https://github.com/angular/angular/issues/15420)
* **platform-browser:** setAttribute should work with xmlns namespace ([#14874](https://github.com/angular/angular/issues/14874)) ([92084f2](https://github.com/angular/angular/commit/92084f2)), closes [#14865](https://github.com/angular/angular/issues/14865)
* **router:** should pass new data to Observable when query params change ([#15387](https://github.com/angular/angular/issues/15387)) ([08f2f08](https://github.com/angular/angular/commit/08f2f08)), closes [#15290](https://github.com/angular/angular/issues/15290)
* prevent strictNullChecks support until [#15432](https://github.com/angular/angular/issues/15432) is fixed ([#15434](https://github.com/angular/angular/issues/15434)) ([b800a0c](https://github.com/angular/angular/commit/b800a0c))
<a name="4.0.0-rc.6"></a>
# [4.0.0-rc.6](https://github.com/angular/angular/compare/4.0.0-rc.5...4.0.0-rc.6) (2017-03-23)
### Bug Fixes
* **animations:** correct the main entry path in package.json ([#15300](https://github.com/angular/angular/issues/15300)) ([2489e4b](https://github.com/angular/angular/commit/2489e4b))
* **animations:** ensure empty animate() steps work at the end of a sequence ([#15328](https://github.com/angular/angular/issues/15328)) ([fbccd5c](https://github.com/angular/angular/commit/fbccd5c)), closes [#15310](https://github.com/angular/angular/issues/15310)
* **animations:** ensure enter/leave cancellations work ([#15323](https://github.com/angular/angular/issues/15323)) ([9bf2fb4](https://github.com/angular/angular/commit/9bf2fb4)), closes [#15315](https://github.com/angular/angular/issues/15315)
* **animations:** make sure easing values work with web-animations ([#15195](https://github.com/angular/angular/issues/15195)) ([f925910](https://github.com/angular/angular/commit/f925910)), closes [#15115](https://github.com/angular/angular/issues/15115)
* **animations:** make sure non-transitioned leave operations cancel existing animations ([#15254](https://github.com/angular/angular/issues/15254)) ([a6fb78e](https://github.com/angular/angular/commit/a6fb78e)), closes [#15213](https://github.com/angular/angular/issues/15213)
* **animations:** only process element nodes through the animation engine ([#15268](https://github.com/angular/angular/issues/15268)) ([80075af](https://github.com/angular/angular/commit/80075af)), closes [#15267](https://github.com/angular/angular/issues/15267)
* **animations:** only treat view removals as `void` state transitions ([#15245](https://github.com/angular/angular/issues/15245)) ([c66437f](https://github.com/angular/angular/commit/c66437f)), closes [#15223](https://github.com/angular/angular/issues/15223)
* **animations:** stringify boolean values as `1` and `0` ([#15311](https://github.com/angular/angular/issues/15311)) ([94da801](https://github.com/angular/angular/commit/94da801)), closes [#15247](https://github.com/angular/angular/issues/15247)
* **compiler:** add an empty content for source file of non mapped code. ([#15246](https://github.com/angular/angular/issues/15246)) ([8415910](https://github.com/angular/angular/commit/8415910))
* **compiler:** dont call `check` if we dont need to ([#15322](https://github.com/angular/angular/issues/15322)) ([764e90f](https://github.com/angular/angular/commit/764e90f))
* **compiler:** look for flat module resources using declaration module path ([#15367](https://github.com/angular/angular/issues/15367)) ([90d2518](https://github.com/angular/angular/commit/90d2518)), closes [#15221](https://github.com/angular/angular/issues/15221)
* **compiler:** only log template deprecation warning once ([#15364](https://github.com/angular/angular/issues/15364)) ([08d8675](https://github.com/angular/angular/commit/08d8675))
* **compiler:** use attribute id to merge translations ([#15302](https://github.com/angular/angular/issues/15302)) ([1d7693c](https://github.com/angular/angular/commit/1d7693c)), closes [#15234](https://github.com/angular/angular/issues/15234)
* **compiler-cli:** adding missing format xliff for the extractor ([#15386](https://github.com/angular/angular/issues/15386)) ([a50d79d](https://github.com/angular/angular/commit/a50d79d))
* **core:** allow tree shaking of component factories and styles ([#15214](https://github.com/angular/angular/issues/15214)) ([2a0e55f](https://github.com/angular/angular/commit/2a0e55f)), closes [#15181](https://github.com/angular/angular/issues/15181)
* **core:** dont create a comment for components with empty template. ([#15260](https://github.com/angular/angular/issues/15260)) ([f8c075a](https://github.com/angular/angular/commit/f8c075a)), closes [#15143](https://github.com/angular/angular/issues/15143)
* **core:** mark components for check when host events trigger. ([#15359](https://github.com/angular/angular/issues/15359)) ([64beae9](https://github.com/angular/angular/commit/64beae9)), closes [#15352](https://github.com/angular/angular/issues/15352)
* **core:** only apply `WrappedValue` to the binding of the pipe ([#15257](https://github.com/angular/angular/issues/15257)) ([0c43535](https://github.com/angular/angular/commit/0c43535)), closes [#15116](https://github.com/angular/angular/issues/15116)
* **core:** provide `NgModuleRef` in `ViewContainerRef.createComponent`. ([#15350](https://github.com/angular/angular/issues/15350)) ([431eb30](https://github.com/angular/angular/commit/431eb30)), closes [#15241](https://github.com/angular/angular/issues/15241)
* **core:** stringify shouldn't throw when toString returns null/undefined ([#14975](https://github.com/angular/angular/issues/14975)) ([8e6995c](https://github.com/angular/angular/commit/8e6995c)), closes [#14948](https://github.com/angular/angular/issues/14948)
* **core:** trigger host animations for elements that are removed. ([#15251](https://github.com/angular/angular/issues/15251)) ([0d3e314](https://github.com/angular/angular/commit/0d3e314)), closes [#14813](https://github.com/angular/angular/issues/14813) [#15193](https://github.com/angular/angular/issues/15193)
* **core:** update peer dep on zone.js to ^0.8.5 ([#15365](https://github.com/angular/angular/issues/15365)) ([97149f9](https://github.com/angular/angular/commit/97149f9)), closes [#15185](https://github.com/angular/angular/issues/15185)
* **forms:** make composition event buffering configurable ([#15256](https://github.com/angular/angular/issues/15256)) ([5efc860](https://github.com/angular/angular/commit/5efc860)), closes [#15079](https://github.com/angular/angular/issues/15079)
* **platform-server:** interpret Native view encapsulation as Emulated on the server ([#15155](https://github.com/angular/angular/issues/15155)) ([de3d2ee](https://github.com/angular/angular/commit/de3d2ee))
* **platform-server:** setup NoopAnimationsModule in ServerModule by default ([#15131](https://github.com/angular/angular/issues/15131)) ([5c5c2ae](https://github.com/angular/angular/commit/5c5c2ae)), closes [#15098](https://github.com/angular/angular/issues/15098) [#14784](https://github.com/angular/angular/issues/14784)
* **platform-server:** throw a better error message for relative URLs ([#15357](https://github.com/angular/angular/issues/15357)) ([15a082c](https://github.com/angular/angular/commit/15a082c)), closes [#15349](https://github.com/angular/angular/issues/15349)
* **tsc-wrapped:** emit flat module format correctly on Windows ([#15215](https://github.com/angular/angular/issues/15215)) ([6e9264a](https://github.com/angular/angular/commit/6e9264a)), closes [#15192](https://github.com/angular/angular/issues/15192)
* **tsc-wrapped:** use windows friendly path normalization in bundler ([#15374](https://github.com/angular/angular/issues/15374)) ([c584997](https://github.com/angular/angular/commit/c584997)), closes [#15289](https://github.com/angular/angular/issues/15289)
* **upgrade:** component injectors should not link the module injector tree ([#15385](https://github.com/angular/angular/issues/15385)) ([ea49a95](https://github.com/angular/angular/commit/ea49a95))
### Features
* **core:** expose `inputs`, `outputs` and `ngContentSelectors` on `ComponentFactory`. ([#15214](https://github.com/angular/angular/issues/15214)) ([791534f](https://github.com/angular/angular/commit/791534f))
* **router:** add `ParamMap.keys` to get a list of parameters ([d3eda7a](https://github.com/angular/angular/commit/d3eda7a))
* **router:** introduce `ParamMap` to access parameters ([a755b71](https://github.com/angular/angular/commit/a755b71))
* **tsc-wrapped:** record original location of flattened symbols ([#15367](https://github.com/angular/angular/issues/15367)) ([7354949](https://github.com/angular/angular/commit/7354949))
* **upgrade:** use `ComponentFactory.inputs/outputs/ngContentSelectors` ([#15214](https://github.com/angular/angular/issues/15214)) ([9429032](https://github.com/angular/angular/commit/9429032))
<a name="4.0.0-rc.5"></a>
# [4.0.0-rc.5](https://github.com/angular/angular/compare/4.0.0-rc.4...4.0.0-rc.5) (2017-03-17)
### Bug Fixes
* **compiler-cli:** update the tsc-wrapped dependency version ([#15226](https://github.com/angular/angular/issues/15226)) ([7fb4528](https://github.com/angular/angular/commit/7fb4528))
<a name="4.0.0-rc.4"></a>
# [4.0.0-rc.4](https://github.com/angular/angular/compare/4.0.0-rc.3...4.0.0-rc.4) (2017-03-17)
### Bug Fixes
* **animations:** always fire callbacks even for noop animations ([#15170](https://github.com/angular/angular/issues/15170)) ([3f38c6f](https://github.com/angular/angular/commit/3f38c6f))
* **animations:** make sure easing values are applied to an empty animate() step ([#15174](https://github.com/angular/angular/issues/15174)) ([62d5543](https://github.com/angular/angular/commit/62d5543)), closes [#15115](https://github.com/angular/angular/issues/15115)
* **animations:** support multiple state names per state() call ([#15147](https://github.com/angular/angular/issues/15147)) ([36ce0af](https://github.com/angular/angular/commit/36ce0af)), closes [#14732](https://github.com/angular/angular/issues/14732)
* **compiler:** always use `ng://` prefix for sourcemap urls ([#15218](https://github.com/angular/angular/issues/15218)) ([994089d](https://github.com/angular/angular/commit/994089d))
* **compiler:** fix utf8encode, move to sharted utils, add tests ([#15076](https://github.com/angular/angular/issues/15076)) ([959a03a](https://github.com/angular/angular/commit/959a03a))
* **compiler:** generated code should pass `noUnusedLocals` check ([50ab06e](https://github.com/angular/angular/commit/50ab06e)), closes [#14797](https://github.com/angular/angular/issues/14797)
* **compiler:** Improve error message for missing annotations ([#14724](https://github.com/angular/angular/issues/14724)) ([3c15916](https://github.com/angular/angular/commit/3c15916))
* **compiler:** improve error msg for unexpected closing tags ([#14747](https://github.com/angular/angular/issues/14747)) ([5f9fb91](https://github.com/angular/angular/commit/5f9fb91)), closes [#6652](https://github.com/angular/angular/issues/6652)
* **compiler:** make sourcemaps work in AOT mode ([492153a](https://github.com/angular/angular/commit/492153a))
* **compiler:** only warn for `[@Injectable](https://github.com/Injectable)` classes with invalid args. ([5c34066](https://github.com/angular/angular/commit/5c34066)), closes [#15003](https://github.com/angular/angular/issues/15003)
* **compiler:** shouldn't throw when Symbol is used as DI token ([#13701](https://github.com/angular/angular/issues/13701)) ([8b5c6b2](https://github.com/angular/angular/commit/8b5c6b2)), closes [#13314](https://github.com/angular/angular/issues/13314)
* **compiler:** support interface types in injectable constuctors ([#14894](https://github.com/angular/angular/issues/14894)) ([b00fe20](https://github.com/angular/angular/commit/b00fe20)), closes [#12631](https://github.com/angular/angular/issues/12631)
* **compiler:** warning prints "WARNING" instead of "ERROR" ([#15125](https://github.com/angular/angular/issues/15125)) ([3b1956b](https://github.com/angular/angular/commit/3b1956b))
* **core:** dont recreate `TemplateRef` when used as a reference. ([#15066](https://github.com/angular/angular/issues/15066)) ([df914ef](https://github.com/angular/angular/commit/df914ef)), closes [#14873](https://github.com/angular/angular/issues/14873)
* **core:** dont throw if queries change during change detection. ([06fc42b](https://github.com/angular/angular/commit/06fc42b)), closes [#14925](https://github.com/angular/angular/issues/14925)
* **core:** ErrorHandler should not rethrow an error by default ([#15077](https://github.com/angular/angular/issues/15077)) ([#15208](https://github.com/angular/angular/issues/15208)) ([77fd91d](https://github.com/angular/angular/commit/77fd91d)), closes [#14949](https://github.com/angular/angular/issues/14949) [#15182](https://github.com/angular/angular/issues/15182) [#14316](https://github.com/angular/angular/issues/14316)
* **core:** update peer dep on zone.js to ^0.8.4 ([#15209](https://github.com/angular/angular/issues/15209)) ([d2fbbb4](https://github.com/angular/angular/commit/d2fbbb4)), closes [#15180](https://github.com/angular/angular/issues/15180) [#15185](https://github.com/angular/angular/issues/15185)
* **core:** use presence of .subscribe to detect observables rather then Symbol.observable ([#15171](https://github.com/angular/angular/issues/15171)) ([6e98757](https://github.com/angular/angular/commit/6e98757)), closes [#14298](https://github.com/angular/angular/issues/14298) [#14473](https://github.com/angular/angular/issues/14473) [#14926](https://github.com/angular/angular/issues/14926)
* **forms:** ensure observable validators are properly canceled ([#15132](https://github.com/angular/angular/issues/15132)) ([26d4ce2](https://github.com/angular/angular/commit/26d4ce2))
* **forms:** remove equalsTo validator ([#15050](https://github.com/angular/angular/issues/15050)) ([778f7d6](https://github.com/angular/angular/commit/778f7d6))
* element injector vs module injector ([#15044](https://github.com/angular/angular/issues/15044)) ([13686bb](https://github.com/angular/angular/commit/13686bb)), closes [#12869](https://github.com/angular/angular/issues/12869) [#12889](https://github.com/angular/angular/issues/12889) [#13885](https://github.com/angular/angular/issues/13885) [#13870](https://github.com/angular/angular/issues/13870)
* **http:** Make ResponseOptionsArgs an interface ([#14607](https://github.com/angular/angular/issues/14607)) ([#14623](https://github.com/angular/angular/issues/14623)) ([f1b33ab](https://github.com/angular/angular/commit/f1b33ab)), closes [#13708](https://github.com/angular/angular/issues/13708)
* **platform-browser:** prevent clobbered elements from freezing the browser ([a4076c7](https://github.com/angular/angular/commit/a4076c7))
* **platform-server:** correctly implement get href in parse5 adapter ([#15022](https://github.com/angular/angular/issues/15022)) ([80649ea](https://github.com/angular/angular/commit/80649ea))
* **platform-server:** fix an exception when HostListener('window:scroll') is used on the server ([#15019](https://github.com/angular/angular/issues/15019)) ([4f7d62a](https://github.com/angular/angular/commit/4f7d62a))
* correct UMD resolutions for platform-browser_animations ([#15190](https://github.com/angular/angular/issues/15190)) ([923d0c5](https://github.com/angular/angular/commit/923d0c5)), closes [#15114](https://github.com/angular/angular/issues/15114)
* don't instantiate providers with ngOnDestroy eagerly. ([#15070](https://github.com/angular/angular/issues/15070)) ([2c5a671](https://github.com/angular/angular/commit/2c5a671)), closes [#14552](https://github.com/angular/angular/issues/14552)
* fix path locally to empty.js ([#15073](https://github.com/angular/angular/issues/15073)) ([80112a9](https://github.com/angular/angular/commit/80112a9))
* **platform-server:** fix get/set title in parse5 adapter ([#14965](https://github.com/angular/angular/issues/14965)) ([018e5c9](https://github.com/angular/angular/commit/018e5c9))
* **platform-server:** handle styles with extra ':'s correctly ([#15189](https://github.com/angular/angular/issues/15189)) ([013d806](https://github.com/angular/angular/commit/013d806))
* **platform-server:** support svg elements with namespaced attributes ([#15101](https://github.com/angular/angular/issues/15101)) ([f093501](https://github.com/angular/angular/commit/f093501))
* **router:** fix query parameters with multiple values ([#15129](https://github.com/angular/angular/issues/15129)) ([029d0f2](https://github.com/angular/angular/commit/029d0f2)), closes [#14796](https://github.com/angular/angular/issues/14796)
* **tsc-wrapped:** emit js files in all cases ([c0e05e6](https://github.com/angular/angular/commit/c0e05e6))
### Code Refactoring
* **core:** use flags in `Renderer2.setStyle` instead of booleans ([#15045](https://github.com/angular/angular/issues/15045)) ([ff71eff](https://github.com/angular/angular/commit/ff71eff))
### Features
* **common:** support `as` syntax in template/* bindings ([#15025](https://github.com/angular/angular/issues/15025)) ([c10c060](https://github.com/angular/angular/commit/c10c060)), closes [#15020](https://github.com/angular/angular/issues/15020)
* **compiler-cli:** support metadata file aliases ([0ab49d4](https://github.com/angular/angular/commit/0ab49d4))
* **core:** allow to provide multiple default testing modules ([#15054](https://github.com/angular/angular/issues/15054)) ([6c8638c](https://github.com/angular/angular/commit/6c8638c))
* **core:** expose `inputs`, `outputs` and `ngContentSelectors` on `ComponentFactory`. ([1171f91](https://github.com/angular/angular/commit/1171f91))
* **upgrade:** support multi-slot projection in upgrade/static ([#14282](https://github.com/angular/angular/issues/14282)) ([914797a](https://github.com/angular/angular/commit/914797a)), closes [#14261](https://github.com/angular/angular/issues/14261)
* **upgrade:** use `ComponentFactory.inputs/outputs/ngContentSelectors` ([a3e32fb](https://github.com/angular/angular/commit/a3e32fb))
* introduce source maps for templates ([#15011](https://github.com/angular/angular/issues/15011)) ([cdc882b](https://github.com/angular/angular/commit/cdc882b))
### BREAKING CHANGES
* Perviously, any provider that had an ngOnDestroy lifecycle hook would be created eagerly.
Now, only classes that are annotated with @Component, @Directive, @Pipe, @NgModule are eager. Providers only become eager if they are either directly or transitively injected into one of the above.
This also makes all `useValue` providers eager, which
should have no observable impact other than code size.
**EXPECTED IMPACT**:
Making providers eager was an incorrect behavior and never documented.
Also, providers that are used by a directive / pipe / ngModule stay eager.
So the impact should be rather small.
* DebugNode.source no longer returns the source location of a node.
Closes 14013
* core: (since v4 rc.1)
- `Renderer2.setStyle` no longer takes booleans but rather a
bit mask of flags.
<a name="2.4.10"></a>
## [2.4.10](https://github.com/angular/angular/compare/2.4.9...2.4.10) (2017-03-17)
### Bug Fixes
* **compiler:** fix decoding surrogate pairs ([#15154](https://github.com/angular/angular/issues/15154)) ([e5c9bbc](https://github.com/angular/angular/commit/e5c9bbc))
* **router:** do not finish bootstrap until all the routes are resolved ([#15121](https://github.com/angular/angular/issues/15121)) ([34403cd](https://github.com/angular/angular/commit/34403cd))
<a name="4.0.0-rc.3"></a>
# [4.0.0-rc.3](https://github.com/angular/angular/compare/4.0.0-rc.2...4.0.0-rc.3) (2017-03-10)
### Bug Fixes
* **compiler:** dont throw for empty array literal in assignments ([#14878](https://github.com/angular/angular/issues/14878)) ([6cd3326](https://github.com/angular/angular/commit/6cd3326)), closes [#14782](https://github.com/angular/angular/issues/14782)
* **compiler:** improve error message when a module imports itself ([#14646](https://github.com/angular/angular/issues/14646)) ([6bc6482](https://github.com/angular/angular/commit/6bc6482)), closes [#14644](https://github.com/angular/angular/issues/14644)
* **core:** allow to use the `Renderer` outside of views. ([#14882](https://github.com/angular/angular/issues/14882)) ([ba4b6f5](https://github.com/angular/angular/commit/ba4b6f5)), closes [#14872](https://github.com/angular/angular/issues/14872)
* **router:** do not finish bootstrap until all the routes are resolved ([#14762](https://github.com/angular/angular/issues/14762)) ([5df998d](https://github.com/angular/angular/commit/5df998d))
* **upgrade:** populate upgraded component's view before creating the controller ([#14289](https://github.com/angular/angular/issues/14289)) ([07122f0](https://github.com/angular/angular/commit/07122f0)), closes [#13912](https://github.com/angular/angular/issues/13912)
* throw for synthetic properties / listeners by default ([#14880](https://github.com/angular/angular/issues/14880)) ([3651d8d](https://github.com/angular/angular/commit/3651d8d))
### BREAKING CHANGES
#### since 4.0 rc.1:
- rename `RendererV2` to `Renderer2`
- rename `RendererTypeV2` to `RendererType2`
- rename `RendererFactoryV2` to `RendererFactory2`
<a name="2.4.9"></a>
## [2.4.9](https://github.com/angular/angular/compare/2.4.8...2.4.9) (2017-03-02)
### Bug Fixes
* **http:** Make ResponseOptionsArgs an interface ([b658fa9](https://github.com/angular/angular/commit/b658fa9)), closes [#13708](https://github.com/angular/angular/issues/13708)
* **router:** improve robustness ([#14602](https://github.com/angular/angular/issues/14602)) ([2a12346](https://github.com/angular/angular/commit/2a12346))
### Reverts
* fix(router): do not finish bootstrap until all the routes are resolved ([#14327](https://github.com/angular/angular/issues/14327)) ([de36f8a](https://github.com/angular/angular/commit/de36f8a)), closes [#14681](https://github.com/angular/angular/issues/14681) [#14588](https://github.com/angular/angular/issues/14588)
<a name="4.0.0-rc.2"></a>
## [4.0.0-rc.2](https://github.com/angular/angular/compare/4.0.0-rc.1...4.0.0-rc.2) (2017-03-02)
### Bug Fixes
* **animations:** make animations work in AOT ([#14775](https://github.com/angular/angular/issues/14775)) ([9560ad8](https://github.com/angular/angular/commit/9560ad8))
* **compiler:** apply element bindings before host bindings ([#14823](https://github.com/angular/angular/issues/14823)) ([79fc1e3](https://github.com/angular/angular/commit/79fc1e3))
* **compiler:** fix identifier names of `EMPTY_MAP` / `EMPTY_ARRAY` ([#14806](https://github.com/angular/angular/issues/14806)) ([5ba55b0](https://github.com/angular/angular/commit/5ba55b0))
* **compiler:** quote `animation` in `RendererTypeV2` and skip if empty ([#14773](https://github.com/angular/angular/issues/14773)) ([77682a3](https://github.com/angular/angular/commit/77682a3))
* **core:** call lifecycle hooks for siblings in declaration order ([d2e4256](https://github.com/angular/angular/commit/d2e4256))
* **core:** fix `isComponentView()` and `isEmbeddedView()` tests ([#14789](https://github.com/angular/angular/issues/14789)) ([5753de5](https://github.com/angular/angular/commit/5753de5)), closes [#14778](https://github.com/angular/angular/issues/14778)
* **language-service:** tolerate errors in decorators ([#14634](https://github.com/angular/angular/issues/14634)) ([6bae737](https://github.com/angular/angular/commit/6bae737)), closes [#14631](https://github.com/angular/angular/issues/14631)
* **platform-server:** don't setup Testability and TestabilityRegistry on the server ([#14510](https://github.com/angular/angular/issues/14510)) ([47bdc2b](https://github.com/angular/angular/commit/47bdc2b))
* **tsc-wrapped:** validate metadata in static members of a class ([#14772](https://github.com/angular/angular/issues/14772)) ([a6996a9](https://github.com/angular/angular/commit/a6996a9)), closes [#13481](https://github.com/angular/angular/issues/13481)
* **upgrade:** fix upgrade component Closure optimization. ([#14801](https://github.com/angular/angular/issues/14801)) ([968995a](https://github.com/angular/angular/commit/968995a))
### Performance Improvements
* delete pre-view-engine core, compiler, platform-browser, etc code ([#14788](https://github.com/angular/angular/issues/14788)) ([126fda2](https://github.com/angular/angular/commit/126fda2))
* **compiler:** make identifiers for generated code small to improve dev size ([5caab71](https://github.com/angular/angular/commit/5caab71f7dc64b10f3544b2a3b2650e1666adbf1))
<a name="4.0.0-rc.1"></a>
# [4.0.0-rc.1](https://github.com/angular/angular/compare/4.0.0-beta.8...4.0.0-rc.1) (2017-02-24)
We are excited to share RC0 with the community. This is a feature-complete pre-release of 4.0.0. Upgrade to get to know the new features to be released in 4.0.0, and to help us validate the release.
We are excited to share 4.0.0-RC.1 with the community. This is a feature-complete pre-release of 4.0.0. Upgrade to get to know the new features to be released in 4.0.0, and to help us validate the release.
Its important to note that this release has been tested extensively. All Angular applications within Google use the master branch of Angular, so every commit is tested and validated at scale prior to any release.
@ -18,7 +286,7 @@ Weve made changes under to hood to what AOT generated code looks like. These
Our template binding syntax now supports a couple helpful changes. You can now use an if/else style syntax, and assign local variables such as when unrolling an observable.
```
<div #loading>Loading...</div>
<ng-template #loading>Loading...</ng-template>
<div *ngIf="userObservable | async; else loading; let user">
{{ user.name }}
</div>
@ -41,8 +309,9 @@ Angular is now compliant with [TypeScripts StrictNullChecks](https://www.type
Universal, the project that allows developers to run Angular on a server, is now up to date with Angular again, and has been adopted by the Angular team. This release now includes the results of the work from the Universal team over the last few months. The majority of the Universal code is now in platform-server. To learn more about this change, take a look the new [`renderModuleFactory`](https://github.com/angular/angular/blob/56f232cdd70a352cb9151bc7cfe8981bc2710ea6/modules/%40angular/platform-server/src/utils.ts#L63-L72) method, or Rob Wormalds [Demo Repository](https://github.com/robwormald/ng-universal-demo/). More documentation is forthcoming.
### Flat ES Modules (ESM)
We now ship flattened versions of our modules, this should help tree-shaking and help reduce the size of your generated bundles.
<a name="flat-es-modules-esm"></a><!-- legacy anchor link, keep it here -->
### Flat ES Modules (Flat ESM / FESM)
We now ship flattened versions of our modules ("rolled up" version of our code in the EcmaScript Module format, see [example file](https://github.com/angular/core-builds/blob/85cbe3f8d6107af033b0f8b56456c181cbcb5eb7/%40angular/core.js)). This format should help tree-shaking, help reduce the size of your generated bundles, and speed up build, transpilation, and loading in the browser in certain scenarios.
### ES2015 Builds
@ -59,18 +328,19 @@ The following is a list of known issues that will be fixed in the next rcs.
- legacy UMD bundles don't have correct RxJS mappings when running in ES5 mode without a module system
## Installing RC.0
## Installing RC.1
We have two main ways to update. If you have an existing project, you should be able to run:
`npm install @angular/{common,compiler,compiler-cli,core,forms,http,platform-browser,platform-browser-dynamic,platform-server,router,animations}@next --save`
On Linux/Mac: `npm install @angular/{common,compiler,compiler-cli,core,forms,http,platform-browser,platform-browser-dynamic,platform-server,router,animations}@next --save`
On Windows: `npm install @angular/common@next @angular/compiler@next @angular/compiler-cli@next @angular/core@next @angular/forms@next @angular/http@next @angular/platform-browser@next @angular/platform-browser-dynamic@next @angular/platform-server@next @angular/router@next @angular/animations@next --save`
Then run whatever `ng serve` or `npm start` command you normally use, and everything should work.
*Please ensure that you are using Typescript v2.1.6 or higher.*
*If you rely on Animations* youll also need to install the animations package and import the new `AnimationsModule` from `@angular/animations` in your root NgModule. Without this, your code will compile and run, but animations wont activate.
*If you rely on Animations* youll also need to install the animations package `@angular/animations` and import the new `BrowserAnimationsModule` from `@angular/platform-browser/animations` in your root NgModule. Without this, your code will compile and run, but animations wont activate.
Imports from `@angular/core` were deprecated, use imports from the new package `import { trigger, state, style, transition, animate } from '@angular/animations';`.
## What's next?
We have [three more release candidates scheduled](https://github.com/angular/angular/blob/master/docs/RELEASE_SCHEDULE.md) before our planned GA the week of March 22. In the meantime we'll be looking for your feedback, fixing bugs and working on docs.
@ -103,7 +373,7 @@ We have [three more release candidates scheduled](https://github.com/angular/ang
* **packaging:** properly build the core-testing bundle ([4301dce](https://github.com/angular/angular/commit/4301dce))
* **platform-webworker:** integrate review feedback ([601fd3e](https://github.com/angular/angular/commit/601fd3e)), closes [#14581](https://github.com/angular/angular/issues/14581)
* **router:** do not finish bootstrap until all the routes are resolved ([#14608](https://github.com/angular/angular/issues/14608)) ([2a191ca](https://github.com/angular/angular/commit/2a191ca)), closes [#12162](https://github.com/angular/angular/issues/12162) [#14155](https://github.com/angular/angular/issues/14155)
* app ids for better <style> management for ssr ([#14632](https://github.com/angular/angular/issues/14632)) ([88bc143](https://github.com/angular/angular/commit/88bc143))
* app ids for better style tag management for ssr ([#14632](https://github.com/angular/angular/issues/14632)) ([88bc143](https://github.com/angular/angular/commit/88bc143))
### Code Refactoring
@ -408,6 +678,12 @@ returned value being an array.
* **tsc-wrapped:** Support of vinyl like config file was added ([#13987](https://github.com/angular/angular/issues/13987)) ([0c7726d](https://github.com/angular/angular/commit/0c7726d))
* **upgrade:** Support ng-model in downgraded components ([#10578](https://github.com/angular/angular/issues/10578)) ([e21e9c5](https://github.com/angular/angular/commit/e21e9c5))
### DEPRECATIONS
* `OpaqueToken` is now deprecated, use `InjectionToken<T>` instead.
* `Injector.get(token: any, notFoundValue?: any): any` is now deprecated
use the same method which is now overloaded as
`Injector.get<T>(token: Type<T>|InjectionToken<T>, notFoundValue?: T): T;`
### BREAKING CHANGES
@ -1971,7 +2247,7 @@ prefix using `animate-` must now be preixed using `bind-animate-`.
### ROUTER CHANGE LOG
[You can find the router changelog here.](https://github.com/angular/angular/blob/master/modules/@angular/router/CHANGELOG.md)
[You can find the router changelog here.](https://github.com/angular/angular/blob/master/packages/router/CHANGELOG.md)
<a name="2.0.0-rc.4"></a>
# [2.0.0-rc.4](https://github.com/angular/angular/compare/2.0.0-rc.3...2.0.0-rc.4) (2016-06-30)

View File

@ -147,7 +147,7 @@ To ensure consistency throughout the source code, keep these rules in mind as yo
* All public API methods **must be documented**. (Details TBC).
* We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at
**100 characters**. An automated formatter is available, see
[DEVELOPER.md](DEVELOPER.md#clang-format).
[DEVELOPER.md](docs/DEVELOPER.md#clang-format).
## <a name="commit"></a> Commit Message Guidelines
@ -263,7 +263,7 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
[corporate-cla]: http://code.google.com/legal/corporate-cla-v1.0.html
[dev-doc]: https://github.com/angular/angular/blob/master/DEVELOPER.md
[dev-doc]: https://github.com/angular/angular/blob/master/docs/DEVELOPER.md
[github]: https://github.com/angular/angular
[gitter]: https://gitter.im/angular/angular
[individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html

View File

@ -10,6 +10,7 @@
"assets": [
"assets",
"content",
"app/search/search-worker.js",
"favicon.ico"
],
"index": "index.html",
@ -18,6 +19,7 @@
"test": "test.ts",
"tsconfig": "tsconfig.json",
"prefix": "aio",
"serviceWorker": true,
"styles": [
"styles.scss"
],

11
aio/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# Ignore node_modules
node_modules
# Ignore npm/yarn debug log
npm-debug.log
yarn-error.log
# Ignore generated content
/dist
/src/content
/.sass-cache

View File

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

View File

@ -0,0 +1,158 @@
# 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,aio-contributors
ARG TEST_AIO_GITHUB_TEAM_SLUGS=angular-core,aio-contributors
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_LOGS_DIR=/var/log/aio/nginx TEST_AIO_NGINX_LOGS_DIR=/var/log/aio/nginx-test \
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
RUN printenv | grep AIO_ >> /etc/environment
# Set up dnsmasq
COPY dnsmasq/dnsmasq.conf /etc/
RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/dnsmasq.conf
RUN sed -i "s|{{\$TEST_AIO_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
RUN sed -i "s|{{\$TEST_AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /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|g" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$AIO_DOMAIN_NAME|g" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|g" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$AIO_NGINX_LOGS_DIR|g" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|g" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/sites-available/aio-builds-prod.conf
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|g" /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|g" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$TEST_AIO_DOMAIN_NAME|g" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|g" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$TEST_AIO_NGINX_LOGS_DIR|g" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|g" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/sites-available/aio-builds-test.conf
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|g" /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

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

View File

@ -0,0 +1,16 @@
# 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

@ -0,0 +1,87 @@
# Redirect all HTTP traffic to HTTPS
server {
server_name _;
listen {{$AIO_NGINX_PORT_HTTP}} default_server;
listen [::]:{{$AIO_NGINX_PORT_HTTP}};
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
# Ideally we want 308 (permanent + keep original method),
# but it is relatively new and not supported by some clients (e.g. cURL).
return 307 https://$host:{{$AIO_NGINX_PORT_HTTPS}}$request_uri;
}
# Serve PR-preview requests
server {
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{40})\.";
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;
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
location "~/[^/]+\.[^/]+$" {
try_files $uri $uri/ =404;
}
location / {
try_files $uri $uri/ /index.html =404;
}
}
# Handle all other requests
server {
server_name _;
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;
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
# 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

@ -0,0 +1,20 @@
#!/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

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

View File

@ -0,0 +1,71 @@
// 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

@ -0,0 +1,23 @@
// 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() {
console.log(`[${new Date()}] - Cleaning up builds...`);
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

@ -0,0 +1,110 @@
// 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

@ -0,0 +1,44 @@
// 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

@ -0,0 +1,45 @@
// 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

@ -0,0 +1,23 @@
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

@ -0,0 +1,17 @@
// 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

@ -0,0 +1,81 @@
// 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

@ -0,0 +1,15 @@
// 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

@ -0,0 +1,78 @@
// 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

@ -0,0 +1,39 @@
// 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

@ -0,0 +1,10 @@
// 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

@ -0,0 +1,35 @@
// 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

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

View File

@ -0,0 +1,117 @@
// 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

@ -0,0 +1,191 @@
// 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

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

View File

@ -0,0 +1,271 @@
// Imports
import * as path from 'path';
import {helper as h} from './helper';
// Tests
describe(`nginx`, () => {
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
afterEach(() => h.cleanUp());
it('should redirect HTTP to HTTPS', done => {
const httpHost = `${h.nginxHostname}:${h.nginxPortHttp}`;
const httpsHost = `${h.nginxHostname}:${h.nginxPortHttps}`;
const urlMap = {
[`http://${httpHost}/`]: `https://${httpsHost}/`,
[`http://${httpHost}/foo`]: `https://${httpsHost}/foo`,
[`http://foo.${httpHost}/`]: `https://foo.${httpsHost}/`,
};
const verifyRedirection = (httpUrl: string) => h.runCmd(`curl -i ${httpUrl}`).then(result => {
h.verifyResponse(307)(result);
const headers = result.stdout.split(/(?:\r?\n){2,}/)[0];
expect(headers).toContain(`Location: ${urlMap[httpUrl]}`);
});
Promise.
all(Object.keys(urlMap).map(verifyRedirection)).
then(done);
});
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);
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 bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
Promise.all([
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
]).then(done);
});
it('should return /foo/bar.js', done => {
const bodyRegex = 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, bodyRegex)).
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 to files', done => {
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz.css`).
then(h.verifyResponse(404)).
then(done);
});
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => {
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
Promise.all([
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz`).then(h.verifyResponse(200, bodyRegex)),
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)),
]).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 trim the zeros)', done => {
const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
const bodyRegex0 = 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}-${sha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)),
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)),
]).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 trim the zeros)', 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

@ -0,0 +1,84 @@
// 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

@ -0,0 +1,266 @@
// 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 trim the zeros)', 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}/${sha9}`).then(h.verifyResponse(500)),
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

@ -0,0 +1,43 @@
{
"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

@ -0,0 +1,318 @@
// 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

@ -0,0 +1,410 @@
// 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

@ -0,0 +1,117 @@
// 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

@ -0,0 +1,232 @@
// 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

@ -0,0 +1,81 @@
// 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

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

View File

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

View File

@ -0,0 +1,320 @@
// 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

@ -0,0 +1,61 @@
// 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

@ -0,0 +1,261 @@
// 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', done => {
const promise = bv.verify(pr, createAuthHeader());
promise.then(done); // Do not complete the test (and release the spies) synchronously
// to avoid running the actual `bvGetPrAuthorTeamMembership()`.
expect(promise).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', done => {
const promise = bv.getPrAuthorTeamMembership(pr);
promise.then(done); // Do not complete the test (and release the spies) synchronously
// to avoid running the actual `GithubTeams#isMemberBySlug()`.
expect(promise).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

@ -0,0 +1,11 @@
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

@ -0,0 +1,39 @@
// 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

@ -0,0 +1,403 @@
// 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 trim the zeros)', done => {
const sha40 = '0'.repeat(40);
const sha41 = `0${sha40}`;
const request40 = agent.get(`/create-build/${pr}/${sha40}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
const request41 = agent.get(`/create-build/${pr}/${sha41}`).set('AUTHORIZATION', 'baz').set('X-FILE', 'qux');
Promise.all([
promisifyRequest(request40.expect(201)),
promisifyRequest(request41.expect(404)),
]).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

@ -0,0 +1,28 @@
{
"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

@ -0,0 +1,15 @@
{
"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

@ -0,0 +1,8 @@
#!/bin/bash
set -e -o pipefail
# Set up env variables
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
# Run the clean-up
node $AIO_SCRIPTS_JS_DIR/dist/lib/clean-up >> /var/log/aio/clean-up.log 2>&1

View File

@ -0,0 +1,53 @@
#!/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

@ -0,0 +1,18 @@
#!/bin/bash
set -e -o pipefail
exec >> /var/log/aio/init.log
exec 2>&1
# Start the services
echo [`date`] - Starting services...
mkdir -p $AIO_NGINX_LOGS_DIR
mkdir -p $TEST_AIO_NGINX_LOGS_DIR
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

@ -0,0 +1,15 @@
#!/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

@ -0,0 +1,29 @@
#!/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

@ -0,0 +1,40 @@
#!/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

@ -0,0 +1,28 @@
# VM Setup Instructions
## Overview
- [General overview](overview--general.md)
- [Security model](overview--security-model.md)
- [Available Commands](overview--scripts-and-commands.md)
## Setting up the VM
- [Set up secrets](vm-setup--set-up-secrets.md)
- [Set up docker](vm-setup--set-up-docker.md)
- [Attach persistent disk](vm-setup--attach-persistent-disk.md)
- [Create host directories and files](vm-setup--create-host-dirs-and-files.md)
- [Create docker image](vm-setup--create-docker-image.md)
## Configuring the docker image
- [Available environment variables](image-config--environment-variables.md)
## Starting the docker container
- [Create docker image](vm-setup--start-docker-container.md)
## Miscellaneous
- [Debug docker container](misc--debug-docker-container.md)
- [Integrate with CI](misc--integrate-with-ci.md)

View File

@ -0,0 +1,52 @@
# Image config - Environment variables
Below is a list of environment variables that can be configured when creating the docker image (as
described [here](vm-setup--create-docker-image.md)). An up-to-date list of the configurable
environment variables and their default values can be found in the
[Dockerfile](../dockerbuild/Dockerfile).
**Note:**
Each variable has a `TEST_` prefixed counterpart, which is used for testing purposes. In most cases
you don't need to specify values for those.
- `AIO_BUILDS_DIR`:
The directory (inside the container) where the uploaded build artifacts are kept.
- `AIO_DOMAIN_NAME`:
The domain name of the server.
- `AIO_GITHUB_ORGANIZATION`:
The GitHub organization whose teams arew whitelisted for accepting uploads.
See also `AIO_GITHUB_TEAM_SLUGS`.
- `AIO_GITHUB_TEAM_SLUGS`:
A comma-separated list of teams, whose authors are allowed to upload PRs.
See also `AIO_GITHUB_ORGANIZATION`.
- `AIO_NGINX_HOSTNAME`:
The internal hostname for accessing the nginx server. This is mostly used for performing a
periodic health-check.
- `AIO_NGINX_PORT_HTTP`:
The port number on which nginx listens for HTTP connections. This should be mapped to the
corresponding port on the host VM (as described [here](vm-setup--start-docker-container.md)).
- `AIO_NGINX_PORT_HTTPS`:
The port number on which nginx listens for HTTPS connections. This should be mapped to the
corresponding port on the host VM (as described [here](vm-setup--start-docker-container.md)).
- `AIO_REPO_SLUG`:
The repository slug (in the form `<user>/<repo>`) for which PRs will be uploaded.
- `AIO_UPLOAD_HOSTNAME`:
The internal hostname for accessing the Node.js upload-server. This is used by nginx for
delegating upload requests and also for performing a periodic health-check.
- `AIO_UPLOAD_MAX_SIZE`:
The maximum allowed size for the uploaded gzip archive containing the build artifacts. Files
larger than this will be rejected.
- `AIO_UPLOAD_PORT`:
The port number on which the Node.js upload-server listens for HTTP connections. This is used by
nginx for delegating upload requests and also for performing a periodic health-check.

View File

@ -0,0 +1,12 @@
# Miscellaneous - Debug docker container
TODO (gkalpak): Add docs. Mention:
- `aio-health-check`
- `aio-verify-setup`
- Test nginx accessible at:
- `http://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTP`
- `https://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTPS`
- Test upload-server accessible at:
- `http://$TEST_AIO_UPLOAD_HOTNAME:$TEST_AIO_UPLOAD_PORT`
- Local DNS (via dnsmasq) maps the above hostnames to 127.0.0.1

View File

@ -0,0 +1,12 @@
# Miscellaneous - Integrate with CI
TODO (gkalpak): Add docs. Mention:
- Travis' JWT addon (+ limitations).
Relevant files: `.travis.yml`
- Testing on CI.
Relevant files: `ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
- Preverifying on CI.
Relevant files: `ci/deploy.sh`, `aio/aio-builds-setup/scripts/travis-preverify-pr.sh`
- Deploying from CI.
Relevant files: `ci/deploy.sh`, `aio/scripts/deploy-preview.sh`

View File

@ -0,0 +1,84 @@
# Overview - General
## Objective
Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to
a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview
the changes without having to checkout and build the app locally.
## Source code
In order to make it easier to administer the server and version-control the setup, we are using
[docker](https://www.docker.com) to run a container on a VM. The Dockerfile and all other files
necessary for creating the docker container are stored (and versioned) along with the angular.io
project's source code (currently part of the angular/angular repo) in the `aio-builds-setup/`
directory.
## Setup
The VM is hosted on [Google Compute Engine](https://cloud.google.com/compute/). The host OS is
debian:jessie. For more info how to set up the host VM take a look at the "Setting up the VM"
section in [TOC](_TOC.md).
## Security model
Since we are managing a public server, it is important to take appropriate measures in order to
prevent abuse. For more details on the challenges and the chosen approach take a look at the
[security model](overview--security-model.md).
## The 10000 feet view
This section gives a brief summary of the several operations performed on CI and by the docker
container:
### On CI (Travis)
- Build job completes successfully (i.e. build succeeds and tests pass).
- The CI script checks whether the build job was initiated by a PR against the angular/angular
master branch.
- The CI script checks whether the PR has touched any files inside the angular.io project directory
(currently `aio/`).
- The CI script checks whether the author of the PR is a member of one of the whitelisted GitHub
teams (and therefore allowed to upload).
**Note:**
For security reasons, the same checks will be performed on the server as well. This is an optional
step with the purpose of:
1. Avoiding the wasted overhead associated with uploads that are going to be rejected (e.g.
building the artifacts, sending them to the server, running checks on the server, etc).
2. Avoiding failing the build (due to an error response from the server) or requiring additional
logic for detecting the reasons of the failure.
- The CI script gzip and upload the build artifacts to the server.
More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md).
### Uploading build artifacts
- nginx receives upload request.
- nginx checks that the uploaded gzip archive does not exceed the specified max file size, stores it
in a temporary location and passes the filepath to the Node.js upload-server.
- The upload-server verifies that the uploaded file is not trying to overwrite an existing build,
and runs several checks to determine whether the request should be accepted (more details can be
found [here](overview--security-model.md)).
- The upload-server deploys the artifacts to a sub-directory named after the PR number and SHA:
`<PR>/<SHA>/`
- The upload-server posts a comment on the corresponding PR on GitHub mentioning the SHA and the
the link where the preview can be found.
### Serving build artifacts
- nginx receives a request for an uploaded resource on a subdomain corresponding to the PR and SHA.
E.g.: `pr<PR>-<SHA>.ngbuilds.io/path/to/resurce`
- nginx maps the subdomain to the correct sub-direcory and serves the resource.
E.g.: `/<PR>/<SHA>/path/to/resource`
### Removing obsolete artifacts
In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a
clean-up tasks once a day. The task retrieves all open PRs from GitHub and removes all directories
that do not correspond with an open PR.
### Health-check
The docker service runs a periodic health-check that verifies the running conditions of the
container. This includes verifying the status of specific system services, the responsiveness of
nginx and the upload-server and internet connectivity.

View File

@ -0,0 +1,55 @@
# Overview - Scripts and Commands
This is an overview of the available scripts and commands.
## Scripts
The scripts are located inside `<aio-builds-setup-dir>/scripts/`. The following scripts are
available:
- `build.sh`:
Can be used for creating a preconfigured docker image.
See [here](vm-setup--create-docker-image.md) for more info.
- `test.sh`
Can be used for running the tests for `<aio-builds-setup-dir>/dockerbuild/scripts-js/`. This is
useful for CI integration. See [here](misc--integrate-with-ci.md) for more info.
- `travis-preverify-pr.sh`
Can be used for "preverifying" a PR before uploading the artifacts to the server. It checks that
the author of the PR a member of one of the specified GitHub teams and therefore allowed to upload
build artifacts. This is useful for CI integration. See [here](misc--integrate-with-ci.md) for
more info.
## Commands
The following commands are available globally from inside the docker container. They are either used
by the container to perform its various operations or can be used ad-hoc, mainly for testing
purposes. Each command is backed by a corresponding script inside
`<aio-builds-setup-dir>/dockerbuild/scripts-sh/`.
- `aio-clean-up`:
Cleans up the builds directory by removing the artifacts that do not correspond to an open PR.
_It is run as a daily cronjob._
- `aio-health-check`:
Runs a basic health-check, verifying that the necessary services are running, the servers are
responding and there is a working internet connection.
_It is used periodically by docker for determining the container's health status._
- `aio-init`:
Initializes the container (mainly by starting the necessary services).
_It is run (by default) when starting the container._
- `aio-upload-server-prod`:
Spins up a Node.js upload-server instance.
_It is used in `aio-init` (see above) during initialization._
- `aio-upload-server-test`:
Spins up a Node.js upload-server instance for tests.
_It is used in `aio-verify-setup` (see below) for running tests._
- `aio-verify-setup`:
Runs a suite of e2e-like tests, mainly verifying the correct (inter)operation of nginx and the
Node.js upload-server.

View File

@ -0,0 +1,116 @@
# Overview - Security model
Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to
a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview
the changes without having to checkout and build the app locally.
This document discusses the security considerations associated with uploading build artifacts as
part of the CI setup and serving them publicly.
## Security objectives
- **Prevent uploading arbitrary content to our servers.**
Since there is no restriction on who can submit a PR, we cannot allow any PR's build artifacts to
be uploaded.
- **Prevent overwriting other peoples uploaded content.**
There needs to be a mechanism in place to ensure that the uploaded content does indeed correspond
to the PR indicated by its URL.
- **Prevent arbitrary access on the server.**
Since the PR author has full access over the build artifacts that would be uploaded, we must
ensure that the uploaded files will not enable arbitrary access to the server or expose sensitive
info.
## Issues / Caveats
- Because the PR author can change the scripts run on CI, any security mechanisms must be immune to
such changes.
- For security reasons, encrypted Travis variables are not available to PRs, so we can't rely on
them to implement security.
## Implemented approach
### In a nutshell
The implemented approach can be broken up to the following sub-tasks:
1. Verify which PR the uploaded artifacts correspond to.
2. Determine the author of the PR.
3. Check whether the PR author is a member of some whitelisted GitHub team.
4. Deploy the artifacts to the corresponding PR's directory.
5. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
during deployment will remain valid until the artifacts are removed).
6. Prevent uploaded files from accessing anything outside their directory.
### Implementation details
This section describes how each of the aforementioned sub-tasks is accomplished:
1. **Verify which PR the uploaded artifacts correspond to.**
We are taking advantage of Travis' [JWT addon](https://docs.travis-ci.com/user/jwt). By sharing
a secret between Travis (which keeps it private but uses it to sign a JWT) and the server (which
uses it to verify the authenticity of the JWT), we can accomplish the following:
a. Verify that the upload request comes from Travis.
b. Determine the PR that these artifacts correspond to (since Travis puts that information into
the JWT, without the PR author being able to modify it).
_Note:_
_There are currently certain limitation in the implementation of the JWT addon._
_See the next section for more details._
2. **Determine the author of the PR.**
Once we have securely associated the uploaded artifaacts to a PR, we retrieve the PR's metadata -
including the author's username - using the [GitHub API](https://developer.github.com/v3/).
To avoid rate-limit restrictions, we use a Personal Access Token (issued by
[@mary-poppins](https://github.com/mary-poppins)).
3. **Check whether the PR author is a member of some whitelisted GitHub team.**
Again using the GitHub API, we can verify the author's membership in one of the
whitelisted/trusted GitHub teams. For this operation, we need a PErsonal Access Token with the
`read:org` scope issued by a user that can "see" the specified GitHub organization.
Here too, we use token by @mary-poppins.
4. **Deploy the artifacts to the corresponding PR's directory.**
With the preceeding steps, we have verified that the uploaded artifacts have been uploaded by
Travis and correspond to a PR whose author is a member of a trusted team. Essentially, as long as
sub-tasks 1, 2 and 3 can be securely accomplished, it is possible to "project" the trust we have
in a team's members through the PR and Travis to the build artifacts.
5. **Prevent overwriting previously deployed artifacts**.
In order to enforce this restriction (and ensure that the deployed artifacts validity is
preserved throughout their "lifetime"), the server that handles the upload (currently a Node.js
Express server) rejects uploads that target an existing directory.
_Note: A PR can contain multiple uploads; one for each SHA that was built on Travis._
6. **Prevent uploaded files from accessing anything outside their directory.**
Nginx (which is used to serve the uploaded artifacts) has been configured to not follow symlinks
outside of the directory where the build artifacts are stored.
## Assumptions / Things to keep in mind
- Each trusted PR author has full control over the content that is uploaded for their PRs. Part of
the security model relies on the trustworthiness of these authors.
- If anyone gets access to the `PREVIEW_DEPLOYMENT_TOKEN` (a.k.a. `NGBUILDS_IO_KEY` on
angular/angular) variable generated for each Travis job, they will be able to impersonate the
corresponding PR's author on the preview server for as long as the token is valid (currently 90
mins). Because of this, the value of the `PREVIEW_DEPLOYMENT_TOKEN` should not be made publicly
accessible (e.g. by printing it on the Travis job log).
- Travis does only allow specific whitelisted property names to be used with the JWT addon. The only
known such property at the time is `SAUCE_ACCESS_KEY` (used for integration with SauceLabs). In
order to be able to actually use the JWT addon we had to name the encrypted variable
`SAUCE_ACCESS_KEY` (which we later re-assign to `NGBUILDS_IO_KEY`).

View File

@ -0,0 +1,20 @@
# 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
- Run:
```
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

@ -0,0 +1,32 @@
# VM setup - Create docker image
## Checkout repository
- `git clone <repo-url>`
## Build docker image
- `<aio-builds-setup-dir>/scripts/build.sh [<name>[:<tag>] [--build-arg <NAME>=<value> ...]]`
- You can overwrite the default environment variables inside the image, by passing new values using
`--build-arg`.
**Note:** The build script has to execute docker commands with `sudo`.
## Example
The following commands would create a docker image from GitHub repo `foo/bar` to be deployed on the
`foobar-builds.io` domain and accepting PR deployments from authors that are members of the
`bar-core` and `bar-docs-authors` teams of organization `foo`:
- `git clone https://github.com/foo/bar.git foobar`
- Run:
```
./foobar/aio-builds-setup/scripts/build.sh foobar-builds \
--build-arg AIO_REPO_SLUG=foo/bar \
--build-arg AIO_DOMAIN_NAME=foobar-builds.io \
--build-arg AIO_GITHUB_ORGANIZATION=foo \
--build-arg AIO_GITHUB_TEMA_SLUGS=bar-core,bar-docs-authors
```
A full list of the available environment variables can be found
[here](image-config--environment-variables.md).

View File

@ -0,0 +1,74 @@
# VM setup - Create host directories and files
## Create directory with secrets
For security reasons, sensitive info (such as tokens and passwords) are not hardcoded into the
docker image, nor passed as environment variables at runtime. They are passed to the docker
container from the host VM as files inside a directory. Each file's name is the name of the variable
and the file content is the value. These are read from inside the running container when necessary.
More info on how to create `secrets` directory and files can be found
[here](vm-setup--set-up-secrets.md).
## Create directory for build artifacts
The uploaded build artifacts should be kept on a directory outside the docker container, so it is
easier to replace the container without losing the uploaded builds. For portability across VMs a
persistent disk can be used (as described [here](vm-setup--attach-persistent-disk.md)).
**Note:** The directories created inside that directory will be owned by user `www-data`.
## Create SSL certificates (Optional for dev)
The host VM can attach a directory containing the SSL certificate and key to be used by the nginx
server for serving the uploaded build artifacts. More info on how to attach the directory when
starting the container can be found [here](vm-setup--start-docker-container.md).
In order for the container to be able to find the certificate and key, they should be named
`<DOMAIN_NAME>.crt` and `<DOMAIN_NAME>.key` respectively. For example, for a domain name
`ngbuild.io`, nginx will look for files `ngbuilds.io.crt` and `ngbuilds.io.key`. More info on how to
specify the domain name see [here](vm-setup--create-docker-image.md).
If no directory is attached, nginx will use an internal self-signed certificate. This is convenient
during development, but is not suitable for production.
**Note:**
Since nginx needs to be able to serve requests for both the main domain as well as any subdomain
(e.g. `ngbuilds.io/` and `foo-bar.ngbuilds.io/`), the provided certificate needs to be a wildcard
certificate covering both the domain and subdomains.
## Create directory for logs (Optional)
Optionally, a logs directory can pe passed to the docker container for storing non-system-related
logs. If not provided, the logs are kept locally on the container and will be lost whenever the
container is replaced (e.g. when updating to use a newer version of the docker image).
The following files log files are kept in this directory:
- `clean-up.log`:
Output of the `aio-clean-up` command, run as a cronjob for cleaning up the build artifacts of
closed PRs.
- `init.log`:
Output of the `aio-init` command, run (by default) when starting the container.
- `nginx/{access,error}.log`:
The access and error logs produced by the nginx server while serving "production" files.
- `nginx-test/{access,error}.log`:
The access and error logs produced by the nginx server while serving "test" files. This is only
used when running tests locally from inside the container, e.g. with the `aio-verify-setup`
command. (See [here](overview--scripts-and-commands.md) for more info.)
- `upload-server-{prod,test,verify-setup}-*.log`:
The logs produced by the Node.js upload-server while serving either:
- `-prod`: "Production" files (g.g during normal operation).
- `-test`: "Test" files (e.g. when a test instance is started with the `aio-upload-server-test`
command).
- `-verify-setup`: "Test" files, but while running `aio-verify-setup`.
(See [here](overview--scripts-and-commands.md) for more info the commands mentioned above.)
- `verify-setup.log`:
The output of the `aio-verify-setup` command (e.g. Jasmine output), except for upload-server
output which is logged to `upload-server-verify-setup-*.log` (see above).

View File

@ -0,0 +1,35 @@
# 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

@ -0,0 +1,52 @@
# 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

@ -0,0 +1,92 @@
# VM setup - Start docker container
## The `docker run` command
Once everything has been setup and configured, a docker container can be started with the following
command:
```
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 \
[-v <host-logs-dir>:/var/log/aio] \
<name>[:<tag>]
```
Below is the same command with inline comments explaining each option. The aPI docs for `docker run`
can be found [here](https://docs.docker.com/engine/reference/run/).
```
sudo docker run \
# Start as a daemon.
-d \
# Use the local DNS server.
# (This is necessary for mapping internal URLs, e.g. for the Node.js upload-server.)
--dns 127.0.0.1 \
# USe `<instance-name>` as an alias for the container.
# Useful for running `docker` commands, e.g.: `docker stop <instance-name>`
--name <instance-name> \
# Map ports of the hosr VM (left) to ports of the docker container (right)
-p 80:80 \
-p 443:443 \
# Automatically restart the container (unless it was explicitly stopped by the user).
# (This ensures that the container will be automatically started on boot.)
--restart unless-stopped \
# The directory the contains the SSL certificates.
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
# If not provided, the container will use self-signed certificates.
[-v <host-cert-dir>:/etc/ssl/localcerts:ro] \
# The directory the contains the secrets (e.g. GitHub token, JWT secret, etc).
# (See [here](vm-setup--set-up-secrets.md) for more info.)
-v <host-secrets-dir>:/aio-secrets:ro \
# The uploaded build artifacts will stored to and served from this directory.
# (If you are using a persistent disk - as described [here](vm-setup--attach-persistent-disk.md) -
# this will be a directory inside the disk.)
-v <host-builds-dir>:/var/www/aio-builds \
# The directory where the logs are being kept.
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
# If not provided, the logs will be kept inside the container, which means they will be lost
# whenever a new container is created.
[-v <host-logs-dir>:/var/log/aio] \
# The name of the docker image to use (and an optional tag; defaults to `latest`).
# (See [here](vm-setup--create-docker-image.md) for instructions on how to create the iamge.)
<name>[:<tag>]
```
## Example
The following command would start a docker container based on the previously created `foobar-builds`
docker image, alias it as 'foobar-builds-1' and map predefined directories on the host VM to be used
by the container for accesing secrets and SSL certificates and keeping the build artifacts and logs.
```
sudo docker run \
-d \
--dns 127.0.0.1 \
--name foobar-builds-1 \
-p 80:80 \
-p 443:443 \
--restart unless-stopped \
-v /etc/ssl/localcerts:/etc/ssl/localcerts:ro \
-v /foobar-secrets:/aio-secrets:ro \
-v /mnt/disks/foobar-builds:/var/www/aio-builds \
-v /foobar-logs:/var/log/aio \
foobar-builds
```

View File

@ -0,0 +1,16 @@
#!/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

@ -0,0 +1,5 @@
#!/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

@ -0,0 +1,11 @@
#!/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

@ -0,0 +1,13 @@
#!/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-contributors" \
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,3 +0,0 @@
module.exports = (gulp) => () => {
// TODO:(petebd): hook up with whatever builds need doing for the webapp
};

View File

@ -1,24 +0,0 @@
/**
* @license
* Copyright 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 https://angular.io/license
*/
module.exports = {
generate: (gulp) => () => {
const path = require('path');
const Dgeni = require('dgeni');
const angularDocsPackage = require(path.resolve(__dirname, '../transforms/angular.io-package'));
const dgeni = new Dgeni([angularDocsPackage]);
return dgeni.generate();
},
test: (gulp) => () => {
const execSync = require('child_process').execSync;
execSync(
'node ../dist/tools/cjs-jasmine/index-tools ../../transforms/**/*.spec.js',
{stdio: ['inherit', 'inherit', 'inherit']});
}
};

View File

@ -1,18 +0,0 @@
@cheatsheetSection
Bootstrapping
@cheatsheetIndex 0
@description
{@target ts}`import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';`{@endtarget}
{@target js}Available from the `ng.platformBrowserDynamic` namespace{@endtarget}
@cheatsheetItem
syntax(ts):
`platformBrowserDynamic().bootstrapModule(AppModule);`|`platformBrowserDynamic().bootstrapModule`
syntax(js):
`document.addEventListener('DOMContentLoaded', function() {
ng.platformBrowserDynamic
.platformBrowserDynamic()
.bootstrapModule(app.AppModule);
});`|`platformBrowserDynamic().bootstrapModule`
description:
Bootstraps the app, using the root component from the specified `NgModule`. {@target js}Must be wrapped in the event listener to fire when the page loads.{@endtarget}

View File

@ -1,34 +0,0 @@
@cheatsheetSection
Built-in directives
@cheatsheetIndex 3
@description
{@target ts}`import { CommonModule } from '@angular/common';`{@endtarget}
{@target js}Available using the `ng.common.CommonModule` module{@endtarget}
@cheatsheetItem
syntax:
`<section *ngIf="showSection">`|`*ngIf`
description:
Removes or recreates a portion of the DOM tree based on the `showSection` expression.
@cheatsheetItem
syntax:
`<li *ngFor="let item of list">`|`*ngFor`
description:
Turns the li element and its contents into a template, and uses that to instantiate a view for each item in list.
@cheatsheetItem
syntax:
`<div [ngSwitch]="conditionExpression">
<ng-template [ngSwitchCase]="case1Exp">...</ng-template>
<ng-template ngSwitchCase="case2LiteralString">...</ng-template>
<ng-template ngSwitchDefault>...</ng-template>
</div>`|`[ngSwitch]`|`[ngSwitchCase]`|`ngSwitchCase`|`ngSwitchDefault`
description:
Conditionally swaps the contents of the div by selecting one of the embedded templates based on the current value of `conditionExpression`.
@cheatsheetItem
syntax:
`<div [ngClass]="{active: isActive, disabled: isDisabled}">`|`[ngClass]`
description:
Binds the presence of CSS classes on the element to the truthiness of the associated map values. The right-hand expression should return {class-name: true/false} map.

View File

@ -1,49 +0,0 @@
@cheatsheetSection
Class decorators
@cheatsheetIndex 5
@description
{@target ts}`import { Directive, ... } from '@angular/core';`{@endtarget}
{@target js}Available from the `ng.core` namespace{@endtarget}
@cheatsheetItem
syntax(ts):
`@Component({...})
class MyComponent() {}`|`@Component({...})`
syntax(js):
`var MyComponent = ng.core.Component({...}).Class({...})`|`ng.core.Component({...})`
description:
Declares that a class is a component and provides metadata about the component.
@cheatsheetItem
syntax(ts):
`@Directive({...})
class MyDirective() {}`|`@Directive({...})`
syntax(js):
`var MyDirective = ng.core.Directive({...}).Class({...})`|`ng.core.Directive({...})`
description:
Declares that a class is a directive and provides metadata about the directive.
@cheatsheetItem
syntax(ts):
`@Pipe({...})
class MyPipe() {}`|`@Pipe({...})`
syntax(js):
`var MyPipe = ng.core.Pipe({...}).Class({...})`|`ng.core.Pipe({...})`
description:
Declares that a class is a pipe and provides metadata about the pipe.
@cheatsheetItem
syntax(ts):
`@Injectable()
class MyService() {}`|`@Injectable()`
syntax(js):
`var OtherService = ng.core.Class(
{constructor: function() { }});
var MyService = ng.core.Class(
{constructor: [OtherService, function(otherService) { }]});`|`var MyService = ng.core.Class({constructor: [OtherService, function(otherService) { }]});`
description:
{@target ts}Declares that a class has dependencies that should be injected into the constructor when the dependency injector is creating an instance of this class.
{@endtarget}
{@target js}
Declares a service to inject into a class by providing an array with the services, with the final item being the function to receive the injected services.
{@endtarget}

View File

@ -1,38 +0,0 @@
@cheatsheetSection
Component configuration
@cheatsheetIndex 7
@description
{@target js}`ng.core.Component` extends `ng.core.Directive`,
so the `ng.core.Directive` configuration applies to components as well{@endtarget}
{@target ts}`@Component` extends `@Directive`,
so the `@Directive` configuration applies to components as well{@endtarget}
@cheatsheetItem
syntax:
`moduleId: module.id`|`moduleId:`
description:
If set, the `templateUrl` and `styleUrl` are resolved relative to the component.
@cheatsheetItem
syntax(ts):
`viewProviders: [MyService, { provide: ... }]`|`viewProviders:`
syntax(js):
`viewProviders: [MyService, { provide: ... }]`|`viewProviders:`
description:
List of dependency injection providers scoped to this component's view.
@cheatsheetItem
syntax:
`template: 'Hello {{name}}'
templateUrl: 'my-component.html'`|`template:`|`templateUrl:`
description:
Inline template or external template URL of the component's view.
@cheatsheetItem
syntax:
`styles: ['.primary {color: red}']
styleUrls: ['my-component.css']`|`styles:`|`styleUrls:`
description:
List of inline CSS styles or external stylesheet URLs for styling the components view.

View File

@ -1,30 +0,0 @@
@cheatsheetSection
Dependency injection configuration
@cheatsheetIndex 10
@description
@cheatsheetItem
syntax(ts):
`{ provide: MyService, useClass: MyMockService }`|`provide`|`useClass`
syntax(js):
`{ provide: MyService, useClass: MyMockService }`|`provide`|`useClass`
description:
Sets or overrides the provider for `MyService` to the `MyMockService` class.
@cheatsheetItem
syntax(ts):
`{ provide: MyService, useFactory: myFactory }`|`provide`|`useFactory`
syntax(js):
`{ provide: MyService, useFactory: myFactory }`|`provide`|`useFactory`
description:
Sets or overrides the provider for `MyService` to the `myFactory` factory function.
@cheatsheetItem
syntax(ts):
`{ provide: MyValue, useValue: 41 }`|`provide`|`useValue`
syntax(js):
`{ provide: MyValue, useValue: 41 }`|`provide`|`useValue`
description:
Sets or overrides the provider for `MyValue` to the value `41`.

View File

@ -1,86 +0,0 @@
@cheatsheetSection
Class field decorators for directives and components
@cheatsheetIndex 8
@description
{@target ts}`import { Input, ... } from '@angular/core';`{@endtarget}
{@target js}Available from the `ng.core` namespace{@endtarget}
@cheatsheetItem
syntax(ts):
`@Input() myProperty;`|`@Input()`
syntax(js):
`ng.core.Input(myProperty, myComponent);`|`ng.core.Input(`|`);`
description:
Declares an input property that you can update via property binding (example:
`<my-cmp [myProperty]="someExpression">`).
@cheatsheetItem
syntax(ts):
`@Output() myEvent = new EventEmitter();`|`@Output()`
syntax(js):
`myEvent = new ng.core.EventEmitter();
ng.core.Output(myEvent, myComponent);`|`ng.core.Output(`|`);`
description:
Declares an output property that fires events that you can subscribe to with an event binding (example: `<my-cmp (myEvent)="doSomething()">`).
@cheatsheetItem
syntax(ts):
`@HostBinding('class.valid') isValid;`|`@HostBinding('class.valid')`
syntax(js):
`ng.core.HostBinding('class.valid',
'isValid', myComponent);`|`ng.core.HostBinding('class.valid', 'isValid'`|`);`
description:
Binds a host element property (here, the CSS class `valid`) to a directive/component property (`isValid`).
@cheatsheetItem
syntax(ts):
`@HostListener('click', ['$event']) onClick(e) {...}`|`@HostListener('click', ['$event'])`
syntax(js):
`ng.core.HostListener('click',
['$event'], onClick(e) {...}, myComponent);`|`ng.core.HostListener('click', ['$event'], onClick(e)`|`);`
description:
Subscribes to a host element event (`click`) with a directive/component method (`onClick`), optionally passing an argument (`$event`).
@cheatsheetItem
syntax(ts):
`@ContentChild(myPredicate) myChildComponent;`|`@ContentChild(myPredicate)`
syntax(js):
`ng.core.ContentChild(myPredicate,
'myChildComponent', myComponent);`|`ng.core.ContentChild(myPredicate,`|`);`
description:
Binds the first result of the component content query (`myPredicate`) to a property (`myChildComponent`) of the class.
@cheatsheetItem
syntax(ts):
`@ContentChildren(myPredicate) myChildComponents;`|`@ContentChildren(myPredicate)`
syntax(js):
`ng.core.ContentChildren(myPredicate,
'myChildComponents', myComponent);`|`ng.core.ContentChildren(myPredicate,`|`);`
description:
Binds the results of the component content query (`myPredicate`) to a property (`myChildComponents`) of the class.
@cheatsheetItem
syntax(ts):
`@ViewChild(myPredicate) myChildComponent;`|`@ViewChild(myPredicate)`
syntax(js):
`ng.core.ViewChild(myPredicate,
'myChildComponent', myComponent);`|`ng.core.ViewChild(myPredicate,`|`);`
description:
Binds the first result of the component view query (`myPredicate`) to a property (`myChildComponent`) of the class. Not available for directives.
@cheatsheetItem
syntax(ts):
`@ViewChildren(myPredicate) myChildComponents;`|`@ViewChildren(myPredicate)`
syntax(js):
`ng.core.ViewChildren(myPredicate,
'myChildComponents', myComponent);`|`ng.core.ViewChildren(myPredicate,`|`);`
description:
Binds the results of the component view query (`myPredicate`) to a property (`myChildComponents`) of the class. Not available for directives.

View File

@ -1,23 +0,0 @@
@cheatsheetSection
Directive configuration
@cheatsheetIndex 6
@description
{@target ts}`@Directive({ property1: value1, ... })`{@endtarget}
{@target js}`ng.core.Directive({ property1: value1, ... }).Class({...})`{@endtarget}
@cheatsheetItem
syntax:
`selector: '.cool-button:not(a)'`|`selector:`
description:
Specifies a CSS selector that identifies this directive within a template. Supported selectors include `element`,
`[attribute]`, `.class`, and `:not()`.
Does not support parent-child relationship selectors.
@cheatsheetItem
syntax(ts):
`providers: [MyService, { provide: ... }]`|`providers:`
syntax(js):
`providers: [MyService, { provide: ... }]`|`providers:`
description:
List of dependency injection providers for this directive and its children.

View File

@ -1,12 +0,0 @@
@cheatsheetSection
Forms
@cheatsheetIndex 4
@description
{@target ts}`import { FormsModule } from '@angular/forms';`{@endtarget}
{@target js}Available using the `ng.forms.FormsModule` module{@endtarget}
@cheatsheetItem
syntax:
`<input [(ngModel)]="userName">`|`[(ngModel)]`
description:
Provides two-way data-binding, parsing, and validation for form controls.

View File

@ -1,86 +0,0 @@
@cheatsheetSection
Directive and component change detection and lifecycle hooks
@cheatsheetIndex 9
@description
{@target ts}(implemented as class methods){@endtarget}
{@target js}(implemented as component properties){@endtarget}
@cheatsheetItem
syntax(ts):
`constructor(myService: MyService, ...) { ... }`|`constructor(myService: MyService, ...)`
syntax(js):
`constructor: function(MyService, ...) { ... }`|`constructor: function(MyService, ...)`
description:
Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here.
@cheatsheetItem
syntax(ts):
`ngOnChanges(changeRecord) { ... }`|`ngOnChanges(changeRecord)`
syntax(js):
`ngOnChanges: function(changeRecord) { ... }`|`ngOnChanges: function(changeRecord)`
description:
Called after every change to input properties and before processing content or child views.
@cheatsheetItem
syntax(ts):
`ngOnInit() { ... }`|`ngOnInit()`
syntax(js):
`ngOnInit: function() { ... }`|`ngOnInit: function()`
description:
Called after the constructor, initializing input properties, and the first call to `ngOnChanges`.
@cheatsheetItem
syntax(ts):
`ngDoCheck() { ... }`|`ngDoCheck()`
syntax(js):
`ngDoCheck: function() { ... }`|`ngDoCheck: function()`
description:
Called every time that the input properties of a component or a directive are checked. Use it to extend change detection by performing a custom check.
@cheatsheetItem
syntax(ts):
`ngAfterContentInit() { ... }`|`ngAfterContentInit()`
syntax(js):
`ngAfterContentInit: function() { ... }`|`ngAfterContentInit: function()`
description:
Called after `ngOnInit` when the component's or directive's content has been initialized.
@cheatsheetItem
syntax(ts):
`ngAfterContentChecked() { ... }`|`ngAfterContentChecked()`
syntax(js):
`ngAfterContentChecked: function() { ... }`|`ngAfterContentChecked: function()`
description:
Called after every check of the component's or directive's content.
@cheatsheetItem
syntax(ts):
`ngAfterViewInit() { ... }`|`ngAfterViewInit()`
syntax(js):
`ngAfterViewInit: function() { ... }`|`ngAfterViewInit: function()`
description:
Called after `ngAfterContentInit` when the component's view has been initialized. Applies to components only.
@cheatsheetItem
syntax(ts):
`ngAfterViewChecked() { ... }`|`ngAfterViewChecked()`
syntax(js):
`ngAfterViewChecked: function() { ... }`|`ngAfterViewChecked: function()`
description:
Called after every check of the component's view. Applies to components only.
@cheatsheetItem
syntax(ts):
`ngOnDestroy() { ... }`|`ngOnDestroy()`
syntax(js):
`ngOnDestroy: function() { ... }`|`ngOnDestroy: function()`
description:
Called once, before the instance is destroyed.

View File

@ -1,58 +0,0 @@
@cheatsheetSection
NgModules
@cheatsheetIndex 1
@description
{@target ts}`import { NgModule } from '@angular/core';`{@endtarget}
{@target js}Available from the `ng.core` namespace{@endtarget}
@cheatsheetItem
syntax(ts):
`@NgModule({ declarations: ..., imports: ...,
exports: ..., providers: ..., bootstrap: ...})
class MyModule {}`|`NgModule`
description:
Defines a module that contains components, directives, pipes, and providers.
syntax(js):
`ng.core.NgModule({declarations: ..., imports: ...,
exports: ..., providers: ..., bootstrap: ...}).
Class({ constructor: function() {}})`
description:
Defines a module that contains components, directives, pipes, and providers.
@cheatsheetItem
syntax:
`declarations: [MyRedComponent, MyBlueComponent, MyDatePipe]`|`declarations:`
description:
List of components, directives, and pipes that belong to this module.
@cheatsheetItem
syntax(ts):
`imports: [BrowserModule, SomeOtherModule]`|`imports:`
description:
List of modules to import into this module. Everything from the imported modules
is available to `declarations` of this module.
syntax(js):
`imports: [ng.platformBrowser.BrowserModule, SomeOtherModule]`|`imports:`
description:
List of modules to import into this module. Everything from the imported modules
is available to `declarations` of this module.
@cheatsheetItem
syntax:
`exports: [MyRedComponent, MyDatePipe]`|`exports:`
description:
List of components, directives, and pipes visible to modules that import this module.
@cheatsheetItem
syntax:
`providers: [MyService, { provide: ... }]`|`providers:`
description:
List of dependency injection providers visible both to the contents of this module and to importers of this module.
@cheatsheetItem
syntax:
`bootstrap: [MyAppComponent]`|`bootstrap:`
description:
List of components to bootstrap when this module is bootstrapped.

View File

@ -1,170 +0,0 @@
@cheatsheetSection
Routing and navigation
@cheatsheetIndex 11
@description
{@target ts}`import { Routes, RouterModule, ... } from '@angular/router';`{@endtarget}
{@target js}Available from the `ng.router` namespace{@endtarget}
@cheatsheetItem
syntax(ts):
`const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'path/:routeParam', component: MyComponent },
{ path: 'staticPath', component: ... },
{ path: '**', component: ... },
{ path: 'oldPath', redirectTo: '/staticPath' },
{ path: ..., component: ..., data: { message: 'Custom' } }
]);
const routing = RouterModule.forRoot(routes);`|`Routes`
syntax(js):
`var routes = [
{ path: '', component: HomeComponent },
{ path: ':routeParam', component: MyComponent },
{ path: 'staticPath', component: ... },
{ path: '**', component: ... },
{ path: 'oldPath', redirectTo: '/staticPath' },
{ path: ..., component: ..., data: { message: 'Custom' } }
]);
var routing = ng.router.RouterModule.forRoot(routes);`|`ng.router.Routes`
description:
Configures routes for the application. Supports static, parameterized, redirect, and wildcard routes. Also supports custom route data and resolve.
@cheatsheetItem
syntax:
`
<router-outlet></router-outlet>
<router-outlet name="aux"></router-outlet>
`|`router-outlet`
description:
Marks the location to load the component of the active route.
@cheatsheetItem
syntax:
`
<a routerLink="/path">
<a [routerLink]="[ '/path', routeParam ]">
<a [routerLink]="[ '/path', { matrixParam: 'value' } ]">
<a [routerLink]="[ '/path' ]" [queryParams]="{ page: 1 }">
<a [routerLink]="[ '/path' ]" fragment="anchor">
`|`[routerLink]`
description:
Creates a link to a different view based on a route instruction consisting of a route path, required and optional parameters, query parameters, and a fragment. To navigate to a root route, use the `/` prefix; for a child route, use the `./`prefix; for a sibling or parent, use the `../` prefix.
@cheatsheetItem
syntax:
`<a [routerLink]="[ '/path' ]" routerLinkActive="active">`
description:
The provided classes are added to the element when the `routerLink` becomes the current active route.
@cheatsheetItem
syntax(ts):
`class CanActivateGuard implements CanActivate {
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean>|Promise<boolean>|boolean { ... }
}
{ path: ..., canActivate: [CanActivateGuard] }`|`CanActivate`
syntax(js):
`var CanActivateGuard = ng.core.Class({
canActivate: function(route, state) {
// return Observable/Promise boolean or boolean
}
});
{ path: ..., canActivate: [CanActivateGuard] }`|`CanActivate`
description:
An interface for defining a class that the router should call first to determine if it should activate this component. Should return a boolean or an Observable/Promise that resolves to a boolean.
@cheatsheetItem
syntax(ts):
`class CanDeactivateGuard implements CanDeactivate<T> {
canDeactivate(
component: T,
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean>|Promise<boolean>|boolean { ... }
}
{ path: ..., canDeactivate: [CanDeactivateGuard] }`|`CanDeactivate`
syntax(js):
`var CanDeactivateGuard = ng.core.Class({
canDeactivate: function(component, route, state) {
// return Observable/Promise boolean or boolean
}
});
{ path: ..., canDeactivate: [CanDeactivateGuard] }`|`CanDeactivate`
description:
An interface for defining a class that the router should call first to determine if it should deactivate this component after a navigation. Should return a boolean or an Observable/Promise that resolves to a boolean.
@cheatsheetItem
syntax(ts):
`class CanActivateChildGuard implements CanActivateChild {
canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean>|Promise<boolean>|boolean { ... }
}
{ path: ..., canActivateChild: [CanActivateGuard],
children: ... }`|`CanActivateChild`
syntax(js):
`var CanActivateChildGuard = ng.core.Class({
canActivateChild: function(route, state) {
// return Observable/Promise boolean or boolean
}
});
{ path: ..., canActivateChild: [CanActivateChildGuard],
children: ... }`|`CanActivateChild`
description:
An interface for defining a class that the router should call first to determine if it should activate the child route. Should return a boolean or an Observable/Promise that resolves to a boolean.
@cheatsheetItem
syntax(ts):
`class ResolveGuard implements Resolve<T> {
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any>|Promise<any>|any { ... }
}
{ path: ..., resolve: [ResolveGuard] }`|`Resolve`
syntax(js):
`var ResolveGuard = ng.core.Class({
resolve: function(route, state) {
// return Observable/Promise value or value
}
});
{ path: ..., resolve: [ResolveGuard] }`|`Resolve`
description:
An interface for defining a class that the router should call first to resolve route data before rendering the route. Should return a value or an Observable/Promise that resolves to a value.
@cheatsheetItem
syntax(ts):
`class CanLoadGuard implements CanLoad {
canLoad(
route: Route
): Observable<boolean>|Promise<boolean>|boolean { ... }
}
{ path: ..., canLoad: [CanLoadGuard], loadChildren: ... }`|`CanLoad`
syntax(js):
`var CanLoadGuard = ng.core.Class({
canLoad: function(route) {
// return Observable/Promise boolean or boolean
}
});
{ path: ..., canLoad: [CanLoadGuard], loadChildren: ... }`|`CanLoad`
description:
An interface for defining a class that the router should call first to check if the lazy loaded module should be loaded. Should return a boolean or an Observable/Promise that resolves to a boolean.

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.

View File

@ -416,14 +416,14 @@ can be found in the LICENSE file at http://angular.io/license
<router-outlet></router-outlet>
<!--
<!--
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
-->"><input type="hidden" name="files[app/banner.component.html]" value="<h1>{{title}}</h1>
<!--
<!--
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
@ -432,7 +432,7 @@ can be found in the LICENSE file at http://angular.io/license
</div>
<!--
<!--
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
@ -445,7 +445,7 @@ can be found in the LICENSE file at http://angular.io/license
</div>
<!--
<!--
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
@ -462,7 +462,7 @@ can be found in the LICENSE file at http://angular.io/license
</div>
<!--
<!--
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
@ -476,7 +476,7 @@ can be found in the LICENSE file at http://angular.io/license
</ul>
<!--
<!--
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
@ -548,7 +548,7 @@ 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
*/"><input type="hidden" name="files[app/app.component.router.spec.ts]" value="// For more examples:
// https://github.com/angular/angular/blob/master/modules/@angular/router/test/integration.spec.ts
// https://github.com/angular/angular/blob/master/packages/router/test/integration.spec.ts
import { async, ComponentFixture, fakeAsync, TestBed, tick,
} from '@angular/core/testing';
@ -3296,7 +3296,7 @@ can be found in the LICENSE file at http://angular.io/license
</html>
<!--
<!--
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
@ -3375,4 +3375,4 @@ 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
*/
"></form><script>document.getElementById("mainForm").submit();</script></body></html>
"></form><script>document.getElementById("mainForm").submit();</script></body></html>

View File

@ -1,5 +1,5 @@
// For more examples:
// https://github.com/angular/angular/blob/master/modules/@angular/router/test/integration.spec.ts
// https://github.com/angular/angular/blob/master/packages/router/test/integration.spec.ts
import { async, ComponentFixture, fakeAsync, TestBed, tick,
} from '@angular/core/testing';

View File

@ -0,0 +1,3 @@
@title Document not found
@description
<h3>Document not found</h3>

View File

@ -2,7 +2,7 @@
AngularJS to Angular Quick Reference
@intro
Learn how AngularJS concepts and techniques map to Angular
Learn how AngularJS concepts and techniques map to Angular.
@description
@ -16,16 +16,16 @@ by mapping AngularJS syntax to the equivalent Angular syntax.
**See the Angular syntax in this <live-example name="cb-ajs-quick-reference"></live-example>**.
## Contents
This page covers:
* [Template basics](#template-basics) - binding and local variables.
* [Template directives](#template-directives) - built-in directives `ngIf` and `ngClass`.
* [Template basics](guide/ajs-quick-reference#template-basics)&mdash;binding and local variables.
* [Filters/pipes](#filters-pipes) - built-in *filters*, known as *pipes* in Angular.
* [Template directives](guide/ajs-quick-reference#template-directives)&mdash;built-in directives `ngIf` and `ngClass`.
* [Modules/controllers/components](#controllers-components) - *modules* in Angular are slightly different from *modules* in AngularJS, and *controllers* are *components* in Angular.
* [Filters/pipes](guide/ajs-quick-reference#filters-pipes)&mdash;built-in *filters*, known as *pipes* in Angular.
* [Style sheets](#style-sheets) - more options for CSS than in AngularJS.
* [Modules/controllers/components](guide/ajs-quick-reference#controllers-components)&mdash;*modules* in Angular are slightly different from *modules* in AngularJS, and *controllers* are *components* in Angular.
* [Style sheets](guide/ajs-quick-reference#style-sheets)&mdash;more options for CSS than in AngularJS.
## Template basics
Templates are the user-facing part of an Angular application and are written in HTML.
@ -86,7 +86,8 @@ The following table lists some of the key AngularJS template features with their
The context of the binding is implied and is always the
associated component, so it needs no reference variable.
For more information, see the [Interpolation](../guide/template-syntax.html#interpolation) section of the Template Syntax page.
For more information, see the [Interpolation](guide/template-syntax)
section of the [Template Syntax](guide/template-syntax) page.
</td>
@ -116,7 +117,7 @@ The following table lists some of the key AngularJS template features with their
Many (but not all) of the built-in filters from AngularJS are
built-in pipes in Angular.
For more information, see the heading [Filters/pipes](#filters-pipes) below.
For more information, see [Filters/pipes](guide/ajs-quick-reference#filters-pipes) below.
</td>
@ -144,7 +145,8 @@ The following table lists some of the key AngularJS template features with their
Angular has true template input variables that are explicitly defined using the `let` keyword.
For more information, see the [ngFor micro-syntax](../guide/template-syntax.html#ngForMicrosyntax) section of the Template Syntax page.
For more information, see the [ngFor micro-syntax](guide/template-syntax)
section of the [Template Syntax](guide/template-syntax) page.
</td>
@ -153,7 +155,7 @@ The following table lists some of the key AngularJS template features with their
</table>
[Back to top](#top)
[Back to top](guide/ajs-quick-reference#top)
## Template directives
AngularJS provides more than seventy built-in directives for templates.
@ -217,7 +219,7 @@ The following are some of the key AngularJS built-in directives and their equiva
in `main.ts`
and the application's root component (`AppComponent`) in `app.module.ts`.
For more information see the [Setup](../guide/setup.html) page.
For more information see the [Setup](guide/setup) page.
</td>
@ -260,7 +262,8 @@ The following are some of the key AngularJS built-in directives and their equiva
Angular also has **class binding**, which is a good way to add or remove a single class,
as shown in the third example.
For more information see the [Attribute, Class, and Style Bindings](../guide/template-syntax.html#other-bindings) section of the Template Syntax page.
For more information see the [Attribute, class, and style bindings](guide/template-syntax)
section of the [Template Syntax](guide/template-syntax) page.
</td>
@ -287,7 +290,7 @@ The following are some of the key AngularJS built-in directives and their equiva
<td>
### bind to the `click` event
### Bind to the `click` event
{@example 'cb-ajs-quick-reference/ts/src/app/app.component.html' region='event-binding'}
@ -306,7 +309,8 @@ The following are some of the key AngularJS built-in directives and their equiva
For a list of DOM events, see: https://developer.mozilla.org/en-US/docs/Web/Events.
For more information, see the [Event Binding](../guide/template-syntax.html#event-binding) section of the Template Syntax page.
For more information, see the [Event binding](guide/template-syntax)
section of the [Template Syntax](guide/template-syntax) page.
</td>
@ -336,7 +340,7 @@ The following are some of the key AngularJS built-in directives and their equiva
In Angular, the template no longer specifies its associated controller.
Rather, the component specifies its associated template as part of the component class decorator.
For more information, see [Architecture Overview](../guide/architecture.html#component).
For more information, see [Architecture Overview](guide/architecture).
</td>
@ -349,14 +353,14 @@ The following are some of the key AngularJS built-in directives and their equiva
<td>
### ng-hide
In AngularJS, the `ng-hide` directive shows or hides the associated HTML element based on
an expression. For more information, see [ng-show](#ng-show).
an expression. For more information, see [ng-show](guide/ajs-quick-reference#ng-show).
</td>
<td>
### bind to the `hidden` property
### Bind to the `hidden` property
In Angular, you use property binding; there is no built-in *hide* directive.
For more information, see [ng-show](#ng-show).
For more information, see [ng-show](guide/ajs-quick-reference#ng-show).
</td>
@ -385,20 +389,22 @@ The following are some of the key AngularJS built-in directives and their equiva
<td>
### bind to the `href` property
### Bind to the `href` property
{@example 'cb-ajs-quick-reference/ts/src/app/app.component.html' region='href'}
Angular, uses property binding; there is no built-in *href* directive.
Angular uses property binding; there is no built-in *href* directive.
Place the element's `href` property in square brackets and set it to a quoted template expression.
For more information on property binding, see [Template Syntax](../guide/template-syntax.html#property-binding).
For more information see the [Property binding](guide/template-syntax)
section of the [Template Syntax](guide/template-syntax) page.
In Angular, `href` is no longer used for routing. Routing uses `routerLink`, as shown in the third example.
In Angular, `href` is no longer used for routing. Routing uses `routerLink`, as shown in the following example.
{@example 'cb-ajs-quick-reference/ts/src/app/app.component.html' region='router-link'}
For more information on routing, see [Routing & Navigation](../guide/router.html#router-link).
For more information on routing, see the [RouterLink binding](guide/router)
section of the [Routing & Navigation](guide/router) page.
</td>
@ -417,7 +423,7 @@ The following are some of the key AngularJS built-in directives and their equiva
In AngularJS, the `ng-if` directive removes or recreates a portion of the DOM,
based on an expression. If the expression is false, the element is removed from the DOM.
In this example, the `table` element is removed from the DOM unless the `movies` array has a length greater than zero.
In this example, the `<table>` element is removed from the DOM unless the `movies` array has a length greater than zero.
</td>
@ -426,12 +432,13 @@ The following are some of the key AngularJS built-in directives and their equiva
{@example 'cb-ajs-quick-reference/ts/src/app/movie-list.component.html' region='ngIf'}
The `*ngIf` directive in Angular works the same as the `ng-if` directive in AngularJS. It removes or recreates a portion of the DOM based on an expression.
The `*ngIf` directive in Angular works the same as the `ng-if` directive in AngularJS. It removes
or recreates a portion of the DOM based on an expression.
In this example, the `table` element is removed from the DOM unless the `movies` array has a length.
In this example, the `<table>` element is removed from the DOM unless the `movies` array has a length.
The (*) before `ngIf` is required in this example.
For more information, see [Structural Directives](../guide/structural-directives.html).
For more information, see [Structural Directives](guide/structural-directives).
</td>
@ -459,7 +466,9 @@ The following are some of the key AngularJS built-in directives and their equiva
In Angular, **two-way binding** is denoted by `[()]`, descriptively referred to as a "banana in a box". This syntax is a shortcut for defining both property binding (from the component to the view)
and event binding (from the view to the component), thereby providing two-way binding.
For more information on two-way binding with ngModel, see [Template Syntax](../guide/template-syntax.html#ngModel).
For more information on two-way binding with `ngModel`, see the [NgModel&mdash;Two-way binding to
form elements with `[(ngModel)]`](../guide/template-syntax.html#ngModel)
section of the [Template Syntax](guide/template-syntax) page.
</td>
@ -477,7 +486,7 @@ The following are some of the key AngularJS built-in directives and their equiva
In AngularJS, the `ng-repeat` directive repeats the associated DOM element
for each item in the specified collection.
In this example, the table row (`tr`) element repeats for each movie object in the collection of movies.
In this example, the table row (`<tr>`) element repeats for each movie object in the collection of movies.
</td>
@ -486,8 +495,9 @@ The following are some of the key AngularJS built-in directives and their equiva
{@example 'cb-ajs-quick-reference/ts/src/app/movie-list.component.html' region='ngFor'}
The `*ngFor` directive in Angular is similar to the `ng-repeat` directive in AngularJS. It repeats the associated DOM element for each item in the specified collection.
More accurately, it turns the defined element (`tr` in this example) and its contents into a template and
The `*ngFor` directive in Angular is similar to the `ng-repeat` directive in AngularJS. It repeats
the associated DOM element for each item in the specified collection.
More accurately, it turns the defined element (`<tr>` in this example) and its contents into a template and
uses that template to instantiate a view for each item in the list.
Notice the other syntax differences:
@ -495,7 +505,7 @@ The following are some of the key AngularJS built-in directives and their equiva
the `let` keyword identifies `movie` as an input variable;
the list preposition is `of`, not `in`.
For more information, see [Structural Directives](../guide/structural-directives.html).
For more information, see [Structural Directives](guide/structural-directives).
</td>
@ -515,24 +525,25 @@ The following are some of the key AngularJS built-in directives and their equiva
In AngularJS, the `ng-show` directive shows or hides the associated DOM element, based on
an expression.
In this example, the `div` element is shown if the `favoriteHero` variable is truthy.
In this example, the `<div>` element is shown if the `favoriteHero` variable is truthy.
</td>
<td>
### bind to the `hidden` property
### Bind to the `hidden` property
{@example 'cb-ajs-quick-reference/ts/src/app/movie-list.component.html' region='hidden'}
Angular, uses property binding; there is no built-in *show* directive.
Angular uses property binding; there is no built-in *show* directive.
For hiding and showing elements, bind to the HTML `hidden` property.
To conditionally display an element, place the element's `hidden` property in square brackets and
set it to a quoted template expression that evaluates to the *opposite* of *show*.
In this example, the `div` element is hidden if the `favoriteHero` variable is not truthy.
In this example, the `<div>` element is hidden if the `favoriteHero` variable is not truthy.
For more information on property binding, see [Template Syntax](../guide/template-syntax.html#property-binding).
For more information on property binding, see the [Property binding](guide/template-syntax)
section of the [Template Syntax](guide/template-syntax) page.
</td>
@ -554,14 +565,15 @@ The following are some of the key AngularJS built-in directives and their equiva
<td>
### bind to the `src` property
### Bind to the `src` property
{@example 'cb-ajs-quick-reference/ts/src/app/app.component.html' region='src'}
Angular, uses property binding; there is no built-in *src* directive.
Angular uses property binding; there is no built-in *src* directive.
Place the `src` property in square brackets and set it to a quoted template expression.
For more information on property binding, see [Template Syntax](../guide/template-syntax.html#property-binding).
For more information on property binding, see the [Property binding](guide/template-syntax)
section of the [Template Syntax](guide/template-syntax) page.
</td>
@ -578,7 +590,7 @@ The following are some of the key AngularJS built-in directives and their equiva
In AngularJS, the `ng-style` directive sets a CSS style on an HTML element
based on an expression. That expression is often a key-value control object with each
key of the object defined as a CSS style name, and each value defined as an expression
key of the object defined as a CSS property, and each value defined as an expression
that evaluates to a value appropriate for the style.
In the example, the `color` style is set to the current value of the `colorPreference` variable.
@ -596,9 +608,11 @@ The following are some of the key AngularJS built-in directives and their equiva
Angular also has **style binding**, which is good way to set a single style. This is shown in the second example.
For more information on style binding, see [Template Syntax](../guide/template-syntax.html#style-binding).
For more information on style binding, see the [Style binding](guide/template-syntax) section of the
[Template Syntax](guide/template-syntax) page.
For more information on the ngStyle directive, see [Template Syntax](../guide/template-syntax.html#ngStyle).
For more information on the `ngStyle` directive, see [NgStyle](guide/template-syntax)
section of the [Template Syntax](guide/template-syntax) page.
</td>
@ -650,7 +664,8 @@ The following are some of the key AngularJS built-in directives and their equiva
The (*) before `ngSwitchCase` and `ngSwitchDefault` is required in this example.
For more information on the ngSwitch directive, see [Template Syntax](../guide/template-syntax.html#ngSwitch).
For more information, see [The NgSwitch directives](guide/template-syntax)
section of the [Template Syntax](guide/template-syntax) page.
</td>
@ -659,15 +674,15 @@ The following are some of the key AngularJS built-in directives and their equiva
</table>
[Back to top](#top)
[Back to top](guide/ajs-quick-reference#top)
{@a filters-pipes}
## Filters/pipes
Angular **pipes** provide formatting and transformation for data in our template, similar to AngularJS **filters**.
Angular **pipes** provide formatting and transformation for data in the template, similar to AngularJS **filters**.
Many of the built-in filters in AngularJS have corresponding pipes in Angular.
For more information on pipes, see [Pipes](../guide/pipes.html).
For more information on pipes, see [Pipes](guide/pipes).
<table width="100%">
@ -704,7 +719,7 @@ For more information on pipes, see [Pipes](../guide/pipes.html).
&lt;td>{{movie.price | currency}}&lt;/td>
</code-example>
Formats a number as a currency.
Formats a number as currency.
</td>
@ -881,7 +896,7 @@ For more information on pipes, see [Pipes](../guide/pipes.html).
</code-example>
Displays the collection in the order specified by the expression.
In this example, the movie title orders the movieList.
In this example, the movie title orders the `movieList`.
</td>
@ -898,7 +913,7 @@ For more information on pipes, see [Pipes](../guide/pipes.html).
</table>
[Back to top](#top)
[Back to top](guide/ajs-quick-reference#top)
{@a controllers-components}
@ -949,17 +964,19 @@ The Angular code is shown using TypeScript.
}());
</code-example>
In AngularJS, you often defined an immediately invoked function expression (or IIFE) around your controller code.
This kept your controller code out of the global namespace.
In AngularJS, an immediately invoked function expression (or IIFE) around controller code
keeps it out of the global namespace.
</td>
<td>
### none
You don't need to worry about this in Angular because you use ES 2015 modules
and modules handle the namespacing for you.
This is a nonissue in Angular because ES 2015 modules
handle the namespacing for you.
For more information on modules, see [Architecture Overview](../guide/architecture.html#module).
For more information on modules, see the [Modules](guide/architecture) section of the
[Architecture Overview](guide/architecture).
</td>
@ -974,7 +991,8 @@ The Angular code is shown using TypeScript.
angular.module("movieHunter", ["ngRoute"]);
</code-example>
In AngularJS, an Angular module keeps track of controllers, services, and other code. The second argument defines the list of other modules that this module depends upon.
In AngularJS, an Angular module keeps track of controllers, services, and other code.
The second argument defines the list of other modules that this module depends upon.
</td>
@ -987,7 +1005,7 @@ The Angular code is shown using TypeScript.
- `imports`: specifies the list of other modules that this module depends upon
- `declaration`: keeps track of your components, pipes, and directives.
For more information on modules, see [Angular Modules](../guide/ngmodule.html).
For more information on modules, see [Angular Modules (NgModule)](guide/ngmodule).
</td>
@ -1006,7 +1024,7 @@ The Angular code is shown using TypeScript.
MovieListCtrl]);
</code-example>
AngularJS, has code in each controller that looks up an appropriate Angular module
AngularJS has code in each controller that looks up an appropriate Angular module
and registers the controller with that module.
The first argument is the controller name. The second argument defines the string names of
@ -1015,17 +1033,18 @@ The Angular code is shown using TypeScript.
<td>
### Component Decorator
### Component decorator
{@example 'cb-ajs-quick-reference/ts/src/app/movie-list.component.ts' region='component'}
Angular, adds a decorator to the component class to provide any required metadata.
The Component decorator declares that the class is a component and provides metadata about
Angular adds a decorator to the component class to provide any required metadata.
The `@Component` decorator declares that the class is a component and provides metadata about
that component such as its selector (or tag) and its template.
This is how you associate a template with code, which is defined in the component class.
This is how you associate a template with logic, which is defined in the component class.
For more information, see the [Components](../guide/architecture.html#components) section of the Architecture Overview page.
For more information, see the [Components](guide/architecture)
section of the [Architecture Overview](guide/architecture) page.
</td>
@ -1054,7 +1073,8 @@ The Angular code is shown using TypeScript.
NOTE: If you are using TypeScript with AngularJS, you must use the `export` keyword to export the component class.
For more information, see the [Components](../guide/architecture.html#components) section of the Architecture Overview page.
For more information, see the [Components](guide/architecture)
section of the [Architecture Overview](guide/architecture) page.
</td>
@ -1088,7 +1108,8 @@ The Angular code is shown using TypeScript.
This example injects a `MovieService`.
The first parameter's TypeScript type tells Angular what to inject, even after minification.
For more information, see the [Dependency Injection](../guide/architecture.html#dependency-injection) section of the Architecture Overview.
For more information, see the [Dependency injection](guide/architecture)
section of the [Architecture Overview](guide/architecture).
</td>
@ -1097,7 +1118,7 @@ The Angular code is shown using TypeScript.
</table>
[Back to top](#top)
[Back to top](guide/ajs-quick-reference#top)
{@a style-sheets}
@ -1171,4 +1192,4 @@ also encapsulate a style sheet within a specific component.
</table>
[Back to top](#top)
[Back to top](guide/ajs-quick-reference#top)

View File

@ -30,16 +30,16 @@ add it to your page.
# Contents
* [Example: Transitioning between two states](#example-transitioning-between-states).
* [States and transitions](#states-and-transitions).
* [Example: Entering and leaving](#example-entering-and-leaving).
* [Example: Entering and leaving from different states](#example-entering-and-leaving-from-different-states).
* [Animatable properties and units](#animatable-properties-and-units).
* [Automatic property calculation](#automatic-property-calculation).
* [Animation timing](#animation-timing).
* [Multi-step animations with keyframes](#multi-step-animations-with-keyframes).
* [Parallel animation groups](#parallel-animation-groups).
* [Animation callbacks](#animation-callbacks).
* [Example: Transitioning between two states](guide/animations#example-transitioning-between-states).
* [States and transitions](guide/animations#states-and-transitions).
* [Example: Entering and leaving](guide/animations#example-entering-and-leaving).
* [Example: Entering and leaving from different states](guide/animations#example-entering-and-leaving-from-different-states).
* [Animatable properties and units](guide/animations#animatable-properties-and-units).
* [Automatic property calculation](guide/animations#automatic-property-calculation).
* [Animation timing](guide/animations#animation-timing).
* [Multi-step animations with keyframes](guide/animations#multi-step-animations-with-keyframes).
* [Parallel animation groups](guide/animations#parallel-animation-groups).
* [Animation callbacks](guide/animations#animation-callbacks).
The examples in this page are available as a <live-example></live-example>.
@ -48,7 +48,7 @@ The examples in this page are available as a <live-example></live-example>.
## Quickstart example: Transitioning between two states
<figure>
<img src="/resources/images/devguide/animations/animation_basic_click.gif" alt="A simple transition animation" align="right" style="width:220px;margin-left:20px"> </img>
<img src="assets/images/devguide/animations/animation_basic_click.gif" alt="A simple transition animation" align="right" style="width:220px;margin-left:20px"> </img>
</figure>
You can build a simple animation that transitions an element between two states
@ -121,7 +121,7 @@ controls the timing of switching between one set of styles and the next:
<figure class='image-display'>
<img src="/resources/images/devguide/animations/ng_animate_transitions_inactive_active.png" alt="In Angular animations you define states and transitions between states" width="400"> </img>
<img src="assets/images/devguide/animations/ng_animate_transitions_inactive_active.png" alt="In Angular animations you define states and transitions between states" width="400"> </img>
</figure>
If several transitions have the same timing configuration, you can combine
@ -154,7 +154,7 @@ transitions that apply regardless of which state the animation is in. For exampl
* The `* => *` transition applies when *any* change between two states takes place.
<figure class='image-display'>
<img src="/resources/images/devguide/animations/ng_animate_transitions_inactive_active_wildcards.png" alt="The wildcard state can be used to match many different transitions at once" width="400"> </img>
<img src="assets/images/devguide/animations/ng_animate_transitions_inactive_active_wildcards.png" alt="The wildcard state can be used to match many different transitions at once" width="400"> </img>
</figure>
### The `void` state
@ -168,14 +168,14 @@ For example the `* => void` transition applies when the element leaves the view,
regardless of what state it was in before it left.
<figure class='image-display'>
<img src="/resources/images/devguide/animations/ng_animate_transitions_void_in.png" alt="The void state can be used for enter and leave transitions" width="400"> </img>
<img src="assets/images/devguide/animations/ng_animate_transitions_void_in.png" alt="The void state can be used for enter and leave transitions" width="400"> </img>
</figure>
The wildcard state `*` also matches `void`.
## Example: Entering and leaving
<figure>
<img src="/resources/images/devguide/animations/animation_enter_leave.gif" alt="Enter and leave animations" align="right" style="width:250px;"> </img>
<img src="assets/images/devguide/animations/animation_enter_leave.gif" alt="Enter and leave animations" align="right" style="width:250px;"> </img>
</figure>
Using the `void` and `*` states you can define transitions that animate the
@ -184,6 +184,8 @@ entering and leaving of elements:
* Enter: `void => *`
* Leave: `* => void`
For example, in the `animations` !{_array} below there are two transitions that use
the `void => *` and `* => void` syntax to animate the element in and out of the view.
{@example 'animations/ts/src/app/hero-list-enter-leave.component.ts' region='animationdef'}
@ -201,7 +203,7 @@ These two common animations have their own aliases:
## Example: Entering and leaving from different states
<figure>
<img src="/resources/images/devguide/animations/animation_enter_leave_states.gif" alt="Enter and leave animations combined with state animations" align="right" style="width:200px"> </img>
<img src="assets/images/devguide/animations/animation_enter_leave_states.gif" alt="Enter and leave animations combined with state animations" align="right" style="width:200px"> </img>
</figure>
You can also combine this animation with the earlier state transition animation by
@ -217,7 +219,7 @@ is:
This gives you fine-grained control over each transition:
<figure class='image-display'>
<img src="/resources/images/devguide/animations/ng_animate_transitions_inactive_active_void.png" alt="This example transitions between active, inactive, and void states" width="400"> </img>
<img src="assets/images/devguide/animations/ng_animate_transitions_inactive_active_void.png" alt="This example transitions between active, inactive, and void states" width="400"> </img>
</figure>
@ -245,7 +247,7 @@ If you don't provide a unit when specifying dimension, Angular assumes the defau
## Automatic property calculation
<figure>
<img src="/resources/images/devguide/animations/animation_auto.gif" alt="Animation with automated height calculation" align="right" style="width:220px;margin-left:20px"> </img>
<img src="assets/images/devguide/animations/animation_auto.gif" alt="Animation with automated height calculation" align="right" style="width:220px;margin-left:20px"> </img>
</figure>
Sometimes you don't know the value of a dimensional style property until runtime.
@ -297,21 +299,22 @@ and the delay (or as the *second* value when there is no delay):
* Run for 200ms, with easing: `'0.2s ease-in-out'`
<figure>
<img src="/resources/images/devguide/animations/animation_timings.gif" alt="Animations with specific timings" align="right" style="width:220px;margin-left:20px"> </img>
<img src="assets/images/devguide/animations/animation_timings.gif" alt="Animations with specific timings" align="right" style="width:220px;margin-left:20px"> </img>
</figure>
### Example
Here are a couple of custom timings in action. Both enter and leave last for
200 milliseconds but they have different easings. The leave begins after a
slight delay:
200 milliseconds, that is `0.2s`, but they have different easings. The leave begins after a
slight delay of 10 milliseconds as specified in `'0.2s 10 ease-out'`:
{@example 'animations/ts/src/app/hero-list-timings.component.ts' region='animationdef'}
## Multi-step animations with keyframes
<figure>
<img src="/resources/images/devguide/animations/animation_multistep.gif" alt="Animations with some bounce implemented with keyframes" align="right" style="width:220px;margin-left:20px"> </img>
<img src="assets/images/devguide/animations/animation_multistep.gif" alt="Animations with some bounce implemented with keyframes" align="right" style="width:220px;margin-left:20px"> </img>
</figure>
Animation *keyframes* go beyond a simple transition to a more intricate animation
@ -336,7 +339,7 @@ spacing are automatically assigned. For example, three keyframes without predefi
offsets receive offsets `0`, `0.5`, and `1`.
## Parallel animation groups
<figure>
<img src="/resources/images/devguide/animations/animation_groups.gif" alt="Parallel animations with different timings, implemented with groups" align="right" style="width:220px;margin-left:20px"> </img>
<img src="assets/images/devguide/animations/animation_groups.gif" alt="Parallel animations with different timings, implemented with groups" align="right" style="width:220px;margin-left:20px"> </img>
</figure>
You've seen how to animate multiple style properties at the same time:
@ -358,7 +361,7 @@ One group animates the element transform and width; the other group animates the
A callback is fired when an animation is started and also when it is done.
In the keyframes example, you have a `trigger` called `@flyInOut`. There you can hook
In the keyframes example, you have a `trigger` called `@flyInOut`. You can hook
those callbacks like this:

View File

@ -2,7 +2,7 @@
Ahead-of-Time Compilation
@intro
Learn how to use Ahead-of-time compilation
Learn how to use Ahead-of-time compilation.
@description
This cookbook describes how to radically improve performance by compiling _Ahead of Time_ (AOT)
@ -11,16 +11,16 @@ 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)
* [Overview](guide/aot-compiler#overview)
* [_Ahead-of-Time_ vs _Just-in-Time_](guide/aot-compiler#aot-jit)
* [Compile with AOT](guide/aot-compiler#compile)
* [Bootstrap](guide/aot-compiler#bootstrap)
* [Tree Shaking](guide/aot-compiler#tree-shaking)
* [Load the bundle](guide/aot-compiler#load)
* [Serve the app](guide/aot-compiler#serve)
* [Workflow and convenience script](guide/aot-compiler#workflow)
* [Source Code](guide/aot-compiler#source-code)
* [Tour of Heroes](guide/aot-compiler#toh)
{@a overview}
@ -128,7 +128,7 @@ then modify it to look as follows.
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.
This is important as explained later in the [Tree Shaking](guide/aot-compiler#tree-shaking) section.
What's really new is the `ngc` section at the bottom called `angularCompilerOptions`.
Its `"genDir"` property tells the compiler
@ -198,7 +198,7 @@ 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").
This is the JIT version; set it aside as you may need it [later](guide/aot-compiler#run-jit "Running with JIT").
Open `main.ts` and convert it to AOT compilation.
Switch from the `platformBrowserDynamic.bootstrap` used in JIT compilation to
@ -408,7 +408,7 @@ The same source code can be built both ways. Here's one way to do that.
{@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)
That's the JIT version of the bootstrap file that we preserved [above](guide/aot-compiler#bootstrap)
Open a _different_ terminal window and enter.
<code-example language="none" class="code-shell">
npm start
@ -434,7 +434,7 @@ Now you can develop JIT and AOT, side-by-side.
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).
to an app with more substance, the tutorial [_Tour of Heroes_](tutorial/toh-pt6).
### JIT in development, AOT in production
@ -472,7 +472,7 @@ 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,
The key differences, covered in the [Bootstrap](guide/aot-compiler#bootstrap) section above,
are evident in these `main` files which can and should reside in the same folder:
<md-tab-group>
@ -615,9 +615,9 @@ The `source-map-explorer` analyzes the source map generated with the bundle and
showing exactly which application and Angular modules and classes are included in the bundle.
Here's the map for _Tour of Heroes_.
<a href="/resources/images/cookbooks/aot-compiler/toh6-bundle.png" target="_blank" title="View larger image">
<a href="assets/images/cookbooks/aot-compiler/toh6-bundle.png" target="_blank" title="View larger image">
<figure class='image-display'>
<img src="/resources/images/cookbooks/aot-compiler/toh6-bundle.png" alt="TOH-6-bundle"> </img>
<img src="assets/images/cookbooks/aot-compiler/toh6-bundle.png" alt="TOH-6-bundle"> </img>
</figure>
</a>

View File

@ -7,17 +7,17 @@ Tell Angular how to construct and bootstrap the app in the root "AppModule".
@description
An Angular module class describes how the application parts fit together.
Every application has at least one Angular module, the _root_ module
that you [bootstrap](#main) to launch the application.
that you [bootstrap](guide/appmodule#main) to launch the application.
You can call it anything you want. The conventional name is `AppModule`.
The [setup](setup.html) instructions produce a new project with the following minimal `AppModule`.
The [setup](guide/setup) instructions produce a new project with the following minimal `AppModule`.
You'll evolve this module as your application grows.
{@example 'setup/ts/src/app/app.module.ts'}
After the `import` statements, you come to a class adorned with the
**`@NgModule`** [_decorator_](glossary.html#decorator '"Decorator" explained').
**`@NgModule`** [_decorator_](guide/glossary).
The `@NgModule` decorator identifies `AppModule` as an Angular module class (also called an `NgModule` class).
`@NgModule` takes a _metadata_ object that tells Angular how to compile and launch the application.
@ -26,7 +26,7 @@ The `@NgModule` decorator identifies `AppModule` as an Angular module class (als
* **_declarations_** &mdash; the application's lone component, which is also ...
* **_bootstrap_** &mdash; the _root_ component that Angular creates and inserts into the `index.html` host web page.
The [Angular Modules (NgModule)](ngmodule.html) guide dives deeply into the details of Angular modules.
The [Angular Modules (NgModule)](guide/ngmodule) guide dives deeply into the details of Angular modules.
All you need to know at the moment is a few basics about these three properties.
@ -76,7 +76,7 @@ You must declare _every_ component in an `NgModule` class.
If you use a component without declaring it, you'll see a clear error message in the browser console.
You'll learn to create two other kinds of classes &mdash;
[directives](attribute-directives.html) and [pipes](pipes.html) &mdash;
[directives](guide/attribute-directives) and [pipes](guide/pipes) &mdash;
that you must also add to the `declarations` array.
@ -93,7 +93,7 @@ Do not put any other kind of class in `declarations`; _not_ `NgModule` classes,
{@a bootstrap-array}
### The _bootstrap_ array
You launch the application by [_bootstrapping_](#main) the root `AppModule`.
You launch the application by [_bootstrapping_](guide/appmodule#main) the root `AppModule`.
Among other things, the _bootstrapping_ process creates the component(s) listed in the `bootstrap` array
and inserts each one into the browser DOM.
@ -151,7 +151,7 @@ This file is very stable. Once you've set it up, you may never change it again.
Your initial app has only a single module, the _root_ module.
As your app grows, you'll consider subdividing it into multiple "feature" modules,
so of which can be loaded later ("lazy loaded") if and when the user chooses
some of which can be loaded later ("lazy loaded") if and when the user chooses
to visit those features.
When you're ready to explore these possibilities, visit the [Angular Modules (NgModule)](ngmodule.html) guide.
When you're ready to explore these possibilities, visit the [Angular Modules (NgModule)](guide/ngmodule) guide.

View File

@ -2,7 +2,7 @@
Architecture Overview
@intro
The basic building blocks of Angular applications
The basic building blocks of Angular applications.
@description
You write Angular applications by composing HTML *templates* with Angularized markup,
@ -17,19 +17,19 @@ Of course, there is more to it than this.
You'll learn the details in the pages that follow. For now, focus on the big picture.
<figure>
<img src="/resources/images/devguide/architecture/overview2.png" alt="overview" style="margin-left:-40px;" width="700"> </img>
<img src="assets/images/devguide/architecture/overview2.png" alt="overview" style="margin-left:-40px;" width="700"> </img>
</figure>
The architecture diagram identifies the eight main building blocks of an Angular application:
* [Modules](#modules)
* [Components](#components)
* [Templates](#templates)
* [Metadata](#metadata)
* [Data binding](#data-binding)
* [Directives](#directives)
* [Services](#services)
* [Dependency injection](#dependency-injection)
* [Modules](guide/architecture#modules)
* [Components](guide/architecture#components)
* [Templates](guide/architecture#templates)
* [Metadata](guide/architecture#metadata)
* [Data binding](guide/architecture#data-binding)
* [Directives](guide/architecture#directives)
* [Services](guide/architecture#services)
* [Dependency injection](guide/architecture#dependency-injection)
Learn these building blocks, and you're on your way.
@ -41,13 +41,13 @@ Learn these building blocks, and you're on your way.
## Modules
<figure>
<img src="/resources/images/devguide/architecture/module.png" alt="Component" align="left" style="width:240px; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/module.png" alt="Component" align="left" style="width:240px; margin-left:-40px;margin-right:10px"> </img>
</figure>
### Angular libraries
<figure>
<img src="/resources/images/devguide/architecture/library-module.png" alt="Component" align="left" style="width:240px; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/library-module.png" alt="Component" align="left" style="width:240px; margin-left:-40px;margin-right:10px"> </img>
</figure>
@ -59,7 +59,7 @@ Learn these building blocks, and you're on your way.
## Components
<figure>
<img src="/resources/images/devguide/architecture/hero-component.png" alt="Component" align="left" style="width:200px; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/hero-component.png" alt="Component" align="left" style="width:200px; margin-left:-40px;margin-right:10px"> </img>
</figure>
A _component_ controls a patch of screen called a *view*.
@ -78,7 +78,7 @@ For example, this `HeroListComponent` has a `heroes` property that returns !{_an
that it acquires from a service.
`HeroListComponent` also has a `selectHero()` method that sets a `selectedHero` property when the user clicks to choose a hero from that list.
Angular creates, updates, and destroys components as the user moves through the application.
Your app can take action at each moment in this lifecycle through optional [lifecycle hooks](lifecycle-hooks.html), like `ngOnInit()` declared above.
Your app can take action at each moment in this lifecycle through optional [lifecycle hooks](guide/lifecycle-hooks), like `ngOnInit()` declared above.
<div class='l-hr'>
@ -87,7 +87,7 @@ Your app can take action at each moment in this lifecycle through optional [life
## Templates
<figure>
<img src="/resources/images/devguide/architecture/template.png" alt="Template" align="left" style="width:200px; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/template.png" alt="Template" align="left" style="width:200px; margin-left:-40px;margin-right:10px"> </img>
</figure>
You define a component's view with its companion **template**. A template is a form of HTML
@ -99,7 +99,7 @@ template for our `HeroListComponent`:
{@example 'architecture/ts/src/app/hero-list.component.html'}
Although this template uses typical HTML elements like `<h2>` and `<p>`, it also has some differences. Code like `*ngFor`, `{{hero.name}}`, `(click)`, `[hero]`, and `<hero-detail>` uses Angular's [template syntax](template-syntax.html).
Although this template uses typical HTML elements like `<h2>` and `<p>`, it also has some differences. Code like `*ngFor`, `{{hero.name}}`, `(click)`, `[hero]`, and `<hero-detail>` uses Angular's [template syntax](guide/template-syntax).
In the last line of the template, the `<hero-detail>` tag is a custom element that represents a new component, `HeroDetailComponent`.
@ -110,7 +110,7 @@ hero that the user selects from the list presented by the `HeroListComponent`.
The `HeroDetailComponent` is a **child** of the `HeroListComponent`.
<figure>
<img src="/resources/images/devguide/architecture/component-tree.png" alt="Metadata" align="left" style="width:300px; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/component-tree.png" alt="Metadata" align="left" style="width:300px; margin-left:-40px;margin-right:10px"> </img>
</figure>
Notice how `<hero-detail>` rests comfortably among native HTML elements. Custom components mix seamlessly with native HTML in the same layouts.
@ -122,11 +122,11 @@ Notice how `<hero-detail>` rests comfortably among native HTML elements. Custom
## Metadata
<figure>
<img src="/resources/images/devguide/architecture/metadata.png" alt="Metadata" align="left" style="width:150px; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/metadata.png" alt="Metadata" align="left" style="width:150px; margin-left:-40px;margin-right:10px"> </img>
</figure>
<p style="padding-top:10px">Metadata tells Angular how to process a class.</p>
<br class="l-clear-both">[Looking back at the code](#component-code) for `HeroListComponent`, you can see that it's just a class.
<br class="l-clear-both">[Looking back at the code](guide/architecture#component-code) for `HeroListComponent`, you can see that it's just a class.
There is no evidence of a framework, no "Angular" in it at all.
In fact, `HeroListComponent` really is *just a class*. It's not a component until you *tell Angular about it*.
@ -144,13 +144,13 @@ where it finds a `<hero-list>` tag in *parent* HTML.
For example, if an app's HTML contains `<hero-list></hero-list>`, then
Angular inserts an instance of the `HeroListComponent` view between those tags.
- `templateUrl`: module-relative address of this component's HTML template, shown [above](#templates).
- `templateUrl`: module-relative address of this component's HTML template, shown [above](guide/architecture#templates).
- `providers`: !{_array} of **dependency injection providers** for services that the component requires.
This is one way to tell Angular that the component's constructor requires a `HeroService`
so it can get the list of heroes to display.
<figure>
<img src="/resources/images/devguide/architecture/template-metadata-component.png" alt="Metadata" align="left" style="height:200px; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/template-metadata-component.png" alt="Metadata" align="left" style="height:200px; margin-left:-40px;margin-right:10px"> </img>
</figure>
The metadata in the `@Component` tells Angular where to get the major building blocks you specify for the component.
@ -171,21 +171,21 @@ Without a framework, you would be responsible for pushing data values into the H
into actions and value updates. Writing such push/pull logic by hand is tedious, error-prone, and a nightmare to
read as any experienced jQuery programmer can attest.
<figure>
<img src="/resources/images/devguide/architecture/databinding.png" alt="Data Binding" style="width:220px; float:left; margin-left:-40px;margin-right:20px"> </img>
<img src="assets/images/devguide/architecture/databinding.png" alt="Data Binding" style="width:220px; float:left; margin-left:-40px;margin-right:20px"> </img>
</figure>
Angular supports **data binding**,
a mechanism for coordinating parts of a template with parts of a component.
Add binding markup to the template HTML to tell Angular how to connect both sides.
As the diagram shows, there are four forms of data binding syntax. Each form has a direction &mdash; to the DOM, from the DOM, or in both directions.<br class="l-clear-both">The `HeroListComponent` [example](#templates) template has three forms:
* The `{{hero.name}}` [*interpolation*](displaying-data.html#interpolation)
As the diagram shows, there are four forms of data binding syntax. Each form has a direction &mdash; to the DOM, from the DOM, or in both directions.<br class="l-clear-both">The `HeroListComponent` [example](guide/architecture#templates) template has three forms:
* The `{{hero.name}}` [*interpolation*](guide/displaying-data)
displays the component's `hero.name` property value within the `<li>` element.
* The `[hero]` [*property binding*](template-syntax.html#property-binding) passes the value of `selectedHero` from
* The `[hero]` [*property binding*](guide/template-syntax) passes the value of `selectedHero` from
the parent `HeroListComponent` to the `hero` property of the child `HeroDetailComponent`.
* The `(click)` [*event binding*](user-input.html#click) calls the component's `selectHero` method when the user clicks a hero's name.
* The `(click)` [*event binding*](guide/user-input) calls the component's `selectHero` method when the user clicks a hero's name.
**Two-way data binding** is an important fourth form
that combines property and event binding in a single notation, using the `ngModel` directive.
@ -198,13 +198,13 @@ Angular processes *all* data bindings once per JavaScript event cycle,
from the root of the application component tree through all child components.
<figure>
<img src="/resources/images/devguide/architecture/component-databinding.png" alt="Data Binding" style="float:left; width:300px; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/component-databinding.png" alt="Data Binding" style="float:left; width:300px; margin-left:-40px;margin-right:10px"> </img>
</figure>
Data binding plays an important role in communication
between a template and its component.<br class="l-clear-both">
<figure>
<img src="/resources/images/devguide/architecture/parent-child-binding.png" alt="Parent/Child binding" style="float:left; width:300px; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/parent-child-binding.png" alt="Parent/Child binding" style="float:left; width:300px; margin-left:-40px;margin-right:10px"> </img>
</figure>
Data binding is also important for communication between parent and child components.<br class="l-clear-both">
@ -215,7 +215,7 @@ Data binding is also important for communication between parent and child compon
## Directives
<figure>
<img src="/resources/images/devguide/architecture/directive.png" alt="Parent child" style="float:left; width:150px; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/directive.png" alt="Parent child" style="float:left; width:150px; margin-left:-40px;margin-right:10px"> </img>
</figure>
Angular templates are *dynamic*. When Angular renders them, it transforms the DOM
@ -234,9 +234,9 @@ sometimes by name but more often as the target of an assignment or a binding.
**Structural** directives alter layout by adding, removing, and replacing elements in DOM.
The [example template](#templates) uses two built-in structural directives:
* [`*ngFor`](displaying-data.html#ngFor) tells Angular to stamp out one `<li>` per hero in the `heroes` list.
* [`*ngIf`](displaying-data.html#ngIf) includes the `HeroDetail` component only if a selected hero exists.
The [example template](guide/architecture#templates) uses two built-in structural directives:
* [`*ngFor`](guide/displaying-data) tells Angular to stamp out one `<li>` per hero in the `heroes` list.
* [`*ngIf`](guide/displaying-data) includes the `HeroDetail` component only if a selected hero exists.
**Attribute** directives alter the appearance or behavior of an existing element.
In templates they look like regular HTML attributes, hence the name.
@ -245,9 +245,9 @@ an example of an attribute directive. `ngModel` modifies the behavior of
an existing element (typically an `<input>`)
by setting its display value property and responding to change events.
Angular has a few more directives that either alter the layout structure
(for example, [ngSwitch](template-syntax.html#ngSwitch))
(for example, [ngSwitch](guide/template-syntax))
or modify aspects of DOM elements and components
(for example, [ngStyle](template-syntax.html#ngStyle) and [ngClass](template-syntax.html#ngClass)).
(for example, [ngStyle](guide/template-syntax) and [ngClass](guide/template-syntax)).
Of course, you can also write your own directives. Components such as
`HeroListComponent` are one kind of custom directive.
@ -260,7 +260,7 @@ Of course, you can also write your own directives. Components such as
## Services
<figure>
<img src="/resources/images/devguide/architecture/service.png" alt="Service" style="float:left; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/service.png" alt="Service" style="float:left; margin-left:-40px;margin-right:10px"> </img>
</figure>
_Service_ is a broad category encompassing any value, function, or feature that your application needs.
@ -305,7 +305,7 @@ application logic into services and make those services available to components
## Dependency injection
<figure>
<img src="/resources/images/devguide/architecture/dependency-injection.png" alt="Service" style="float:left; width:200px; margin-left:-40px;margin-right:10px"> </img>
<img src="assets/images/devguide/architecture/dependency-injection.png" alt="Service" style="float:left; width:200px; margin-left:-40px;margin-right:10px"> </img>
</figure>
_Dependency injection_ is a way to supply a new instance of a class
@ -324,7 +324,7 @@ This is *dependency injection*.
The process of `HeroService` injection looks a bit like this:
<figure>
<img src="/resources/images/devguide/architecture/injector-injects.png" alt="Service"> </img>
<img src="assets/images/devguide/architecture/injector-injects.png" alt="Service"> </img>
</figure>
If the injector doesn't have a `HeroService`, how does it know how to make one?
@ -336,7 +336,7 @@ Registering at a component level means you get a new instance of the
service with each new instance of that component.
<!-- We've vastly oversimplified dependency injection for this overview.
The full story is in the [dependency injection](dependency-injection.html) page. -->
The full story is in the [dependency injection](guide/dependency-injection) page. -->
Points to remember about dependency injection:
@ -359,14 +359,14 @@ Points to remember about dependency injection:
You've learned the basics about the eight main building blocks of an Angular application:
* [Modules](#modules)
* [Components](#components)
* [Templates](#templates)
* [Metadata](#metadata)
* [Data binding](#data-binding)
* [Directives](#directives)
* [Services](#services)
* [Dependency injection](#dependency-injection)
* [Modules](guide/architecture#modules)
* [Components](guide/architecture#components)
* [Templates](guide/architecture#templates)
* [Metadata](guide/architecture#metadata)
* [Data binding](guide/architecture#data-binding)
* [Directives](guide/architecture#directives)
* [Services](guide/architecture#services)
* [Dependency injection](guide/architecture#dependency-injection)
That's a foundation for everything else in an Angular application,
and it's more than enough to get going.
@ -375,7 +375,7 @@ But it doesn't include everything you need to know.
Here is a brief, alphabetical list of other important Angular features and services.
Most of them are covered in this documentation (or soon will be).
> [**Animations**](animations.html): Animate component behavior
> [**Animations**](guide/animations): Animate component behavior
without deep knowledge of animation techniques or CSS with Angular's animation library.
> **Change detection**: The change detection documentation will cover how Angular decides that a component property value has changed,
@ -384,18 +384,18 @@ when to update the screen, and how it uses **zones** to intercept asynchronous a
> **Events**: The events documentation will cover how to use components and services to raise events with mechanisms for
publishing and subscribing to events.
> [**Forms**](forms.html): Support complex data entry scenarios with HTML-based validation and dirty checking.
> [**Forms**](guide/forms): Support complex data entry scenarios with HTML-based validation and dirty checking.
> [**HTTP**](server-communication.html): Communicate with a server to get data, save data, and invoke server-side actions with an HTTP client.
> [**HTTP**](guide/server-communication): Communicate with a server to get data, save data, and invoke server-side actions with an HTTP client.
> [**Lifecycle hooks**](lifecycle-hooks.html): Tap into key moments in the lifetime of a component, from its creation to its destruction,
> [**Lifecycle hooks**](guide/lifecycle-hooks): Tap into key moments in the lifetime of a component, from its creation to its destruction,
by implementing the lifecycle hook interfaces.
> [**Pipes**](pipes.html): Use pipes in your templates to improve the user experience by transforming values for display. Consider this `currency` pipe expression:
> [**Pipes**](guide/pipes): Use pipes in your templates to improve the user experience by transforming values for display. Consider this `currency` pipe expression:
>
> > `price | currency:'USD':true`
>
> It displays a price of 42.33 as `$42.33`.
> [**Router**](router.html): Navigate from page to page within the client
> [**Router**](guide/router): Navigate from page to page within the client
application and never leave the browser.

View File

@ -9,19 +9,15 @@ An **Attribute** directive changes the appearance or behavior of a DOM element.
# Contents
* [Directives overview](#directive-overview)
* [Build a simple attribute directive](#write-directive)
* [Apply the attribute directive to an element in a template](#apply-directive)
* [Respond to user-initiated events](#respond-to-user)
* [Pass values into the directive with an _@Input_ data binding](#bindings)
* [Bind to a second property](#second-property)
* [Directives overview](guide/attribute-directives#directive-overview)
* [Build a simple attribute directive](guide/attribute-directives#write-directive)
* [Apply the attribute directive to an element in a template](guide/attribute-directives#apply-directive)
* [Respond to user-initiated events](guide/attribute-directives#respond-to-user)
* [Pass values into the directive with an _@Input_ data binding](guide/attribute-directives#bindings)
* [Bind to a second property](guide/attribute-directives#second-property)
Try the <live-example title="Attribute Directive example"></live-example>.
{@a directive-overview}
## Directives overview
There are three kinds of directives in Angular:
@ -31,20 +27,19 @@ There are three kinds of directives in Angular:
1. Attribute directives&mdash;change the appearance or behavior of an element, component, or another directive.
*Components* are the most common of the three directives.
You saw a component for the first time in the [QuickStart](../quickstart.html) guide.
You saw a component for the first time in the [QuickStart](quickstart) guide.
*Structural Directives* change the structure of the view.
Two examples are [NgFor](template-syntax.html#ngFor) and [NgIf](template-syntax.html#ngIf).
Learn about them in the [Structural Directives](structural-directives.html) guide.
Two examples are [NgFor](guide/template-syntax) and [NgIf](guide/template-syntax).
Learn about them in the [Structural Directives](guide/structural-directives) guide.
*Attribute directives* are used as attributes of elements.
The built-in [NgStyle](template-syntax.html#ngStyle) directive in the [Template Syntax](template-syntax.html) guide, for example,
The built-in [NgStyle](guide/template-syntax) directive in the
[Template Syntax](guide/template-syntax) guide, for example,
can change several element styles at the same time.
{@a write-directive}
## Build a simple attribute directive
An attribute directive minimally requires building a controller class annotated with
`@Directive`, which specifies the selector that identifies
the attribute.
@ -53,35 +48,45 @@ The controller class implements the desired directive behavior.
This page demonstrates building a simple _myHighlight_ attribute
directive to set an element's background color
when the user hovers over that element. You can apply it like this:
{@example 'attribute-directives/ts/src/app/app.component.1.html' region='applied'}
### Write the directive code
Follow the [setup](setup.html) instructions for creating a new local project
Follow the [setup](guide/setup) instructions for creating a new local project
named <span ngio-ex>attribute-directives</span>.
Create the following source file in `src/app` with the following code:
Create the following source file in the indicated folder:
{@example 'attribute-directives/ts/src/app/highlight.directive.1.ts'}
The `import` statement specifies symbols from the Angular `core`:
1. `Directive` provides the functionality of the `@Directive` decorator.
1. `ElementRef` [injects](guide/dependency-injection) into the directive's constructor
so the code can access the DOM element.
1. `Input` allows data to flow from the binding expression into the directive.
Next, the `@Directive` decorator function contains the directive metadata in a configuration object
as an argument.
`@Directive` requires a CSS selector to identify
the HTML in the template that is associated with the directive.
The [CSS selector for an attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors)
is the attribute name in square brackets.
Here, the directive's selector is `[myHighlight]`.
Angular locates all elements in the template that have an attribute named `myHighlight`.
### Why not call it "highlight"?
Though *highlight* is a more concise name than *myHighlight* and would work,
a best practice is to prefix selector names to ensure
they don't conflict with standard HTML attributes.
they don't conflict with standard HTML attributes.
This also reduces the risk of colliding with third-party directive names.
Make sure you do **not** prefix the `highlight` directive name with **`ng`** because
that prefix is reserved for Angular and using it could cause bugs that are difficult to diagnose. For a simple demo, the short prefix, `my`, helps distinguish your custom directive.
<p>
After the <code> @Directive </code> metadata comes the directive's controller class, called <code> HighlightDirective </code> , which contains the logic for the directive.
</p>
that prefix is reserved for Angular and using it could cause bugs that are difficult to diagnose.
For a simple demo, the short prefix, `my`, helps distinguish your custom directive.
After the `@Directive` metadata comes the directive's controller class,
called `HighlightDirective`, which contains the logic for the directive.
<span if-docs="ts">Exporting `HighlightDirective` makes it accessible to other components.</span>
Angular creates a new instance of the directive's controller class for
each matching element, injecting an Angular `ElementRef`
@ -89,42 +94,43 @@ into the constructor.
`ElementRef` is a service that grants direct access to the DOM element
through its `nativeElement` property.
{@a apply-directive}
## Apply the attribute directive
To use the new `HighlightDirective`, create a template that
applies the directive as an attribute to a paragraph (`<p>`) element.
In Angular terms, the `<p>` element is the attribute **host**.
<p>
Put the template in its own <code> </code> file that looks like this:
</p>
Put the template in its own <span ngio-ex>app.component.html</span>
file that looks like this:
{@example 'attribute-directives/ts/src/app/app.component.1.html'}
Now reference this template in the `AppComponent`:
{@example 'attribute-directives/ts/src/app/app.component.ts'}
Next, add an `import` statement to fetch the `Highlight` directive and
add that class to the `declarations` NgModule metadata. This way Angular
recognizes the directive when it encounters `myHighlight` in the template.
{@example 'attribute-directives/ts/src/app/app.module.ts'}
Now when the app runs, the `myHighlight` directive highlights the paragraph text.
<figure class='image-display'>
<img src="/resources/images/devguide/attribute-directives/first-highlight.png" alt="First Highlight"> </img>
<img src="assets/images/devguide/attribute-directives/first-highlight.png" alt="First Highlight"> </img>
</figure>
### Your directive isn't working?
Did you remember to add the directive to the `declarations` attribute of `@NgModule`? It is easy to forget!
Did you remember to add the directive to the `declarations` attribute of `@NgModule`?
It is easy to forget!
Open the console in the browser tools and look for an error like this:
<code-example format="nocode">
EXCEPTION: Template parse errors:
Can't bind to 'myHighlight' since it isn't a known property of 'p'.
@ -140,9 +146,6 @@ It created an instance of the `HighlightDirective` class and
injected a reference to the `<p>` element into the directive's constructor
which sets the `<p>` element's background style to yellow.
{@a respond-to-user}
## Respond to user-initiated events
Currently, `myHighlight` simply sets an element color.
@ -152,14 +155,10 @@ and respond by setting or clearing the highlight color.
Begin by adding `HostListener` to the list of imported symbols;
add the `Input` symbol as well because you'll need it soon.
{@example 'attribute-directives/ts/src/app/highlight.directive.ts' region='imports'}
Then add two eventhandlers that respond when the mouse enters or leaves, each adorned by the `HostListener` !{_decorator}.
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts' region='mouse-methods'}
The `@HostListener` !{_decorator} lets you subscribe to events of the DOM element that hosts an attribute directive, the `<p>` in this case.
Then add two eventhandlers that respond when the mouse enters or leaves,
each adorned by the `HostListener` !{_decorator}.
The `@HostListener` !{_decorator} lets you subscribe to events of the DOM
element that hosts an attribute directive, the `<p>` in this case.
Of course you could reach into the DOM with standard JavaScript and and attach event listeners manually.
There are at least three problems with _that_ approach:
@ -169,24 +168,19 @@ There are at least three problems with _that_ approach:
1. Talking to DOM API directly isn't a best practice.
The handlers delegate to a helper method that sets the color on the DOM element, `#{_priv}el`,
which you declare and initialize in the constructor.
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts' region='ctor'}
Here's the updated directive in full:
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts'}
Run the app and confirm that the background color appears when the mouse hovers over the `p` and
disappears as it moves out.
Run the app and confirm that the background color appears when
the mouse hovers over the `p` and disappears as it moves out.
<figure class='image-display'>
<img src="/resources/images/devguide/attribute-directives/highlight-directive-anim.gif" alt="Second Highlight"> </img>
<img src="assets/images/devguide/attribute-directives/highlight-directive-anim.gif" alt="Second Highlight"> </img>
</figure>
{@a bindings}
## Pass values into the directive with an _@Input_ data binding
Currently the highlight color is hard-coded _within_ the directive. That's inflexible.
@ -194,9 +188,6 @@ In this section, you give the developer the power to set the highlight color whi
Start by adding a `highlightColor` property to the directive class like this:
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts' region='color'}
{@a input}
### Binding to an _@Input_ property
@ -204,33 +195,18 @@ Start by adding a `highlightColor` property to the directive class like this:
Notice the `@Input` !{_decorator}. It adds metadata to the class that makes the directive's `highlightColor` property available for binding.
It's called an *input* property because data flows from the binding expression _into_ the directive.
Without that input metadata, Angular rejects the binding; see [below](#why-input "Why add @Input?") for more about that.
Without that input metadata, Angular rejects the binding; see [below](guide/attribute-directives#why-input "Why add @Input?") for more about that.
Try it by adding the following directive binding variations to the `AppComponent` template:
{@example 'attribute-directives/ts/src/app/app.component.1.html' region='color-1'}
Add a `color` property to the `AppComponent`.
{@example 'attribute-directives/ts/src/app/app.component.1.ts' region='class'}
Let it control the highlight color with a property binding.
{@example 'attribute-directives/ts/src/app/app.component.1.html' region='color-2'}
That's good, but it would be nice to _simultaneously_ apply the directive and set the color _in the same attribute_ like this.
{@example 'attribute-directives/ts/src/app/app.component.html' region='color'}
The `[myHighlight]` attribute binding both applies the highlighting directive to the `<p>` element
and sets the directive's highlight color with a property binding.
You're re-using the directive's attribute selector (`[myHighlight]`) to do both jobs.
That's a crisp, compact syntax.
You'll have to rename the directive's `highlightColor` property to `myHighlight` because that's now the color property binding name.
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts' region='color-2'}
This is disagreeable. The word, `myHighlight`, is a terrible property name and it doesn't convey the property's intent.
@ -244,64 +220,58 @@ _Inside_ the directive the property is known as `highlightColor`.
_Outside_ the directive, where you bind to it, it's known as `myHighlight`.
You get the best of both worlds: the property name you want and the binding syntax you want:
{@example 'attribute-directives/ts/src/app/app.component.html' region='color'}
Now that you're binding to `highlightColor`, modify the `onMouseEnter()` method to use it.
If someone neglects to bind to `highlightColor`, highlight in "red" by default.
If someone neglects to bind to `highlightColor`, highlight in red:
Here's the latest version of the directive class.
## Write a harness to try it
{@example 'attribute-directives/ts/src/app/highlight.directive.3.ts' region='mouse-enter'}
Here's the latest version of the directive class.## Write a harness to try itIt may be difficult to imagine how this directive actually works.
It may be difficult to imagine how this directive actually works.
In this section, you'll turn `AppComponent` into a harness that
lets you pick the highlight color with a radio button and bind your color choice to the directive.
Update `app.component.html` as follows:
Revise the `AppComponent.color` so that it has no initial value.Here is the harness and directive in action.
Update <span ngio-ex>app.component.html</span> as follows:
Revise the `AppComponent.color` so that it has no initial value.
Here are the harness and directive in action.
<figure class='image-display'>
<img src="/resources/images/devguide/attribute-directives/highlight-directive-v2-anim.gif" alt="Highlight v.2"> </img>
<img src="assets/images/devguide/attribute-directives/highlight-directive-v2-anim.gif" alt="Highlight v.2"> </img>
</figure>
{@a second-property}
## Bind to a second property
This highlight directive has a single customizable property. In a real app, it may need more.
At the moment, the default color&mdash;the color that prevails until
the user picks a highlight color&mdash;is hard-coded as "red".
At the moment, the default color&mdash;the color that prevails until
the user picks a highlight color&mdash;is hard-coded as "red".
Let the template developer set the default color.
Add a second **input** property to `HighlightDirective` called `defaultColor`:Revise the directive's `onMouseEnter` so that it first tries to highlight with the `highlightColor`,
Add a second **input** property to `HighlightDirective` called `defaultColor`:
Revise the directive's `onMouseEnter` so that it first tries to highlight with the `highlightColor`,
then with the `defaultColor`, and falls back to "red" if both properties are undefined.
{@example 'attribute-directives/ts/src/app/highlight.directive.ts' region='mouse-enter'}
How do you bind to a second property when you're already binding to the `myHighlight` attribute name?
As with components, you can add as many directive property bindings as you need by stringing them along in the template.
The developer should be able to write the following template HTML to both bind to the `AppComponent.color`
and fall back to "violet" as the default color.
{@example 'attribute-directives/ts/src/app/app.component.html' region='defaultColor'}
Angular knows that the `defaultColor` binding belongs to the `HighlightDirective`
because you made it _public_ with the `@Input` !{_decorator}.
Here's how the harness should work when you're done coding.
<figure class='image-display'>
<img src="/resources/images/devguide/attribute-directives/highlight-directive-final-anim.gif" alt="Final Highlight"> </img>
<img src="assets/images/devguide/attribute-directives/highlight-directive-final-anim.gif" alt="Final Highlight"> </img>
</figure>
## Summary
This page covered how to:
- [Build an **attribute directive**](#write-directive) that modifies the behavior of an element.
- [Apply the directive](#apply-directive) to an element in a template.
- [Respond to **events**](#respond-to-user) that change the directive's behavior.
- [**Bind** values to the directive](#bindings).
- [Build an **attribute directive**](guide/attribute-directives#write-directive) that modifies the behavior of an element.
- [Apply the directive](guide/attribute-directives#apply-directive) to an element in a template.
- [Respond to **events**](guide/attribute-directives#respond-to-user) that change the directive's behavior.
- [**Bind** values to the directive](guide/attribute-directives#bindings).
The final source code follows:
@ -341,20 +311,11 @@ The final source code follows:
You can also experience and download the <live-example title="Attribute Directive example"></live-example>.
{@a why-input}
### Appendix: Why add _@Input_?
In this demo, the `hightlightColor` property is an ***input*** property of
the `HighlightDirective`. You've seen it applied without an alias:
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts' region='color'}
You've seen it with an alias:
{@example 'attribute-directives/ts/src/app/highlight.directive.ts' region='color'}
Either way, the `@Input` !{_decorator} tells Angular that this property is
_public_ and available for binding by a parent component.
Without `@Input`, Angular refuses to bind to the property.
@ -377,20 +338,17 @@ Only then can it be bound by some other component or directive.
You can tell if `@Input` is needed by the position of the property name in a binding.
* When it appears in the template expression to the ***right*** of the equals (=),
it belongs to the template's component and does not require the `@Input` !{_decorator}.
it belongs to the template's component and does not require the `@Input` !{_decorator}.
* When it appears in **square brackets** ([ ]) to the **left** of the equals (=),
the property belongs to some _other_ component or directive;
that property must be adorned with the `@Input` !{_decorator}.
the property belongs to some _other_ component or directive;
that property must be adorned with the `@Input` !{_decorator}.
Now apply that reasoning to the following example:
{@example 'attribute-directives/ts/src/app/app.component.html' region='color'}
* The `color` property in the expression on the right belongs to the template's component.
The template and its component trust each other.
The `color` property doesn't require the `@Input` !{_decorator}.
The template and its component trust each other.
The `color` property doesn't require the `@Input` !{_decorator}.
* The `myHighlight` property on the left refers to an _aliased_ property of the `MyHighlightDirective`,
not a property of the template's component. There are trust issues.
Therefore, the directive property must carry the `@Input` !{_decorator}.
* The `myHighlight` property on the left refers to an _aliased_ property of the `HighlightDirective`,
not a property of the template's component. There are trust issues.
Therefore, the directive property must carry the `@Input` !{_decorator}.

View File

@ -244,7 +244,7 @@ using <a href="https://saucelabs.com/" target="_blank">SauceLabs</a> and
Angular is built on the latest standards of the web platform.
Targeting such a wide range of browsers is challenging because they do not support all features of modern browsers.
You compensate by loading polyfill scripts ("polyfills") on the host web page (`index.html`)
You can compensate by loading polyfill scripts ("polyfills") on the host web page (`index.html`)
that implement missing features in JavaScript.
{@example 'quickstart/ts/src/index.html' region='polyfills'}
@ -252,12 +252,12 @@ that implement missing features in JavaScript.
A particular browser may require at least one polyfill to run _any_ Angular application.
You may need additional polyfills for specific features.
The tables below will help you determine which polyfills to load, depending on the browsers you target and the features you use.
The tables below can help you determine which polyfills to load, depending on the browsers you target and the features you use.
~~~ {.alert.is-important}
The suggested polyfills are the ones we know will run full Angular applications.
The suggested polyfills are the ones that run full Angular applications.
You may need additional polyfills to support features not covered by this list.
Note that polyfills cannot magically transform an old, slow browser into a modern, fast one.
@ -307,7 +307,7 @@ These are the polyfills required to run an Angular application on each supported
<td>
[ES6](#core-es6)
[ES6](guide/browser-support#core-es6)
</td>
@ -322,7 +322,7 @@ These are the polyfills required to run an Angular application on each supported
<td>
[ES6<br>classList](#classlist)
[ES6<br>classList](guide/browser-support#classlist)
</td>
@ -370,7 +370,7 @@ Here are the features which may require additional polyfills:
<td>
[Web Animations](#web-animations)
[Web Animations](guide/browser-support#web-animations)
</td>
@ -390,7 +390,7 @@ Here are the features which may require additional polyfills:
<td>
[Intl API](#intl)
[Intl API](guide/browser-support#intl)
</td>
@ -410,7 +410,7 @@ Here are the features which may require additional polyfills:
<td>
[classList](#classlist)
[classList](guide/browser-support#classlist)
</td>
@ -430,7 +430,7 @@ Here are the features which may require additional polyfills:
<td>
[Typed&nbsp;Array](#typedarray) <br>[Blob](#blob)<br>[FormData](#formdata)
[Typed&nbsp;Array](guide/browser-support#typedarray) <br>[Blob](guide/browser-support#blob)<br>[FormData](guide/browser-support#formdata)
</td>
@ -457,7 +457,7 @@ Below are the polyfills which are used to test the framework itself. They are a
<th>
Licence
License
</th>
@ -517,7 +517,7 @@ Below are the polyfills which are used to test the framework itself. They are a
<td>
MIT / Unicode licence
MIT / Unicode license
</td>
@ -611,4 +611,5 @@ Below are the polyfills which are used to test the framework itself. They are a
</table>
\* Figures are for minified and gzipped code, computed with the <a href="http://closure-compiler.appspot.com/home" target="_blank">closure compiler</a>
\* Figures are for minified and gzipped code,
computed with the <a href="http://closure-compiler.appspot.com/home" target="_blank">closure compiler</a>.

View File

@ -2,48 +2,48 @@
Dependency Injection
@intro
Techniques for Dependency Injection
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)
[Application-wide dependencies](guide/cb-dependency-injection#app-wide-dependencies)
[External module configuration](#external-module-configuration)
[External module configuration](guide/cb-dependency-injection#external-module-configuration)
[*@Injectable* and nested service dependencies](#nested-dependencies)
[*@Injectable* and nested service dependencies](guide/cb-dependency-injection#nested-dependencies)
[Limit service scope to a component subtree](#service-scope)
[Limit service scope to a component subtree](guide/cb-dependency-injection#service-scope)
[Multiple service instances (sandboxing)](#multiple-service-instances)
[Multiple service instances (sandboxing)](guide/cb-dependency-injection#multiple-service-instances)
[Qualify dependency lookup with *@Optional* and *@Host*](#qualify-dependency-lookup)
[Qualify dependency lookup with *@Optional* and *@Host*](guide/cb-dependency-injection#qualify-dependency-lookup)
[Inject the component's DOM element](#component-element)
[Inject the component's DOM element](guide/cb-dependency-injection#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)
[Define dependencies with providers](guide/cb-dependency-injection#providers)
* [The *provide* object literal](guide/cb-dependency-injection#provide)
* [useValue - the *value provider*](guide/cb-dependency-injection#usevalue)
* [useClass - the *class provider*](guide/cb-dependency-injection#useclass)
* [useExisting - the *alias provider*](guide/cb-dependency-injection#useexisting)
* [useFactory - the *factory provider*](guide/cb-dependency-injection#usefactory)
[Provider token alternatives](#tokens)
* [class-interface](#class-interface)
* [OpaqueToken](#opaque-token)
[Provider token alternatives](guide/cb-dependency-injection#tokens)
* [class-interface](guide/cb-dependency-injection#class-interface)
* [OpaqueToken](guide/cb-dependency-injection#opaque-token)
[Inject into a derived class](#di-inheritance)
[Inject into a derived class](guide/cb-dependency-injection#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)
[Find a parent component by injection](guide/cb-dependency-injection#find-parent)
* [Find parent with a known component type](guide/cb-dependency-injection#known-parent)
* [Cannot find a parent by its base class](guide/cb-dependency-injection#base-parent)
* [Find a parent by its class-interface](guide/cb-dependency-injection#class-interface-parent)
* [Find a parent in a tree of parents (*@SkipSelf*)](guide/cb-dependency-injection#parent-tree)
* [A *provideParent* helper function](guide/cb-dependency-injection#provideparent)
[Break circularities with a forward class reference (*forwardRef*)](#forwardref)
[Break circularities with a forward class reference (*forwardRef*)](guide/cb-dependency-injection#forwardref)
**See the <live-example name="cb-dependency-injection"></live-example>**
of the code supporting this cookbook.
@ -62,7 +62,7 @@ Service classes can act as their own providers which is why listing them in the
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,
Learn more about providers [below](guide/cb-dependency-injection#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'}
@ -79,7 +79,7 @@ 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
[location strategy](guide/router) by listing its provider
in the `providers` list of the `AppModule`.
@ -122,7 +122,7 @@ The author simply declared what was needed in the constructor (`LoggerService` a
Once all the dependencies are in place, the `AppComponent` displays the user information:
<figure class='image-display'>
<img src="/resources/images/cookbooks/dependency-injection/logged-in-user.png" alt="Logged In User"> </img>
<img src="assets/images/cookbooks/dependency-injection/logged-in-user.png" alt="Logged In User"> </img>
</figure>
### *@Injectable()*
@ -136,7 +136,7 @@ Technically, the `@Injectable()`decorator is only _required_ for a service class
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
But the service would break the moment we gave it a dependency and we'd have to go back
and add `@Injectable()` to fix it. We add `@Injectable()` from the start for the sake of consistency and to avoid future pain.
@ -233,7 +233,7 @@ 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="/resources/images/cookbooks/dependency-injection/hero-bios.png" alt="Bios"> </img>
<img src="assets/images/cookbooks/dependency-injection/hero-bios.png" alt="Bios"> </img>
</figure>
@ -264,7 +264,7 @@ But when this component is projected into a *parent* component, that parent comp
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).
The `HeroBiosAndContactsComponent` is a revision of the `HeroBiosComponent` that we looked at [above](guide/cb-dependency-injection#hero-bios-component).
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='hero-bios-and-contacts'}
@ -280,7 +280,7 @@ placing it in the `<ng-content>` slot of the `HeroBioComponent` template:
It looks like this, with the hero's telephone number from `HeroContactComponent` projected above the hero description:
<figure class='image-display'>
<img src="/resources/images/cookbooks/dependency-injection/hero-bio-and-content.png" alt="bio and contact"> </img>
<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:
@ -304,14 +304,14 @@ Thanks to `@Optional()`, Angular sets the `loggerService` to null and the rest o
We'll come back to the `elementRef` property shortly.Here's the `HeroBiosAndContactsComponent` in action.
<figure class='image-display'>
<img src="/resources/images/cookbooks/dependency-injection/hero-bios-and-contacts.png" alt="Bios with contact into"> </img>
<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="/resources/images/cookbooks/dependency-injection/hero-bio-contact-no-host.png" alt="Without @Host"> </img>
<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`,
@ -325,7 +325,7 @@ Although we strive to avoid it, many visual effects and 3rd party tools (such as
require DOM access.
To illustrate, we've written a simplified version of the `HighlightDirective` from
the [Attribute Directives](../guide/attribute-directives.html) chapter.
the [Attribute Directives](guide/attribute-directives) chapter.
{@example 'cb-dependency-injection/ts/src/app/highlight.directive.ts'}
@ -343,7 +343,7 @@ first without a value (yielding the default color) and then with an assigned col
The following image shows the effect of mousing over the `<hero-bios-and-contacts>` tag.
<figure class='image-display'>
<img src="/resources/images/cookbooks/dependency-injection/highlight.png" alt="Highlighted bios"> </img>
<img src="assets/images/cookbooks/dependency-injection/highlight.png" alt="Highlighted bios"> </img>
</figure>
<a id="providers"></a>
@ -362,7 +362,7 @@ 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
Angular asks the injector for the service associated with the `LoggerService`
and assigns the returned value to the `logger` parameter.
Where did the injector get that value?
@ -371,7 +371,7 @@ 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).
If the search is futile, the injector throws an error ... unless the request was [optional](guide/cb-dependency-injection#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.
@ -394,7 +394,7 @@ We need other ways to deliver dependency values and that means we need other way
The `HeroOfTheMonthComponent` example demonstrates many of the alternatives and why we need them.
<figure class='image-display'>
<img src="/resources/images/cookbooks/dependency-injection/hero-of-month.png" alt="Hero of the month" width="300px"> </img>
<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.
@ -408,7 +408,7 @@ It's visually simple: a few properties and the output of a logger. The code behi
#### 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 *token* is usually a class but [it doesn't have to be](guide/cb-dependency-injection#tokens).
The *definition* object has one main property, (e.g. `useValue`) that indicates how the provider
should create or return the provided value.
@ -433,7 +433,7 @@ The `Hero` provider token is a class which makes sense because the value is a `H
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).
It's a special kind of provider lookup key called an [OpaqueToken](guide/cb-dependency-injection#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.
@ -486,21 +486,21 @@ creating ***two ways to access the same service object***.
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):
We want to shrink that API surface to just the two members exposed by the `MinimalLogger` [*class-interface*](guide/cb-dependency-injection#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="/resources/images/cookbooks/dependency-injection/minimal-logger-intellisense.png" alt="MinimalLogger restricted API"> </img>
<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="/resources/images/cookbooks/dependency-injection/date-logger-entry.png" alt="DateLoggerService entry" width="300px"> </img>
<img src="assets/images/cookbooks/dependency-injection/date-logger-entry.png" alt="DateLoggerService entry" width="300px"> </img>
</figure>
@ -641,7 +641,7 @@ In this contrived example, `SortedHeroesComponent` inherits from `HeroesBaseComp
to display a *sorted* list of heroes.
<figure class='image-display'>
<img src="/resources/images/cookbooks/dependency-injection/sorted-heroes.png" alt="Sorted Heroes"> </img>
<img src="assets/images/cookbooks/dependency-injection/sorted-heroes.png" alt="Sorted Heroes"> </img>
</figure>
The `HeroesBaseComponent` could stand on its own.
@ -718,7 +718,7 @@ 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
We added the [@Optional](guide/cb-dependency-injection#optional) qualifier for safety but
the <live-example name="cb-dependency-injection"></live-example>
confirms that the `alex` parameter is set.
@ -741,7 +741,7 @@ That's not possible because TypeScript interfaces disappear from the transpiled
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)
The sample's `CraigComponent` explores this question. [Looking back](guide/cb-dependency-injection#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'}
@ -758,14 +758,14 @@ confirms that the `alex` parameter is null.
<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).
We can find a parent component with a [class-interface](guide/cb-dependency-injection#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).
that's why we could inject *Alex* into *Cathy* [earlier](guide/cb-dependency-injection#known-parent).
We write an [*alias provider*](#useexisting) &mdash; a `provide` object literal with a `useExisting` definition &mdash;
We write an [*alias provider*](guide/cb-dependency-injection#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`:
@ -774,8 +774,8 @@ and add that provider to the `providers` array of the `@Component` metadata for
{@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.
[Parent](guide/cb-dependency-injection#parent-token) is the provider's *class-interface* token.
The [*forwardRef*](guide/cb-dependency-injection#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:
@ -783,7 +783,7 @@ The [*forwardRef*](#forwardref) breaks the circular reference we just created by
Here's *Alex* and family in action:
<figure class='image-display'>
<img src="/resources/images/cookbooks/dependency-injection/alex.png" alt="Alex in action"> </img>
<img src="assets/images/cookbooks/dependency-injection/alex.png" alt="Alex in action"> </img>
</figure>
@ -802,8 +802,8 @@ 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).
*Barry*'s `providers` array looks just like [*Alex*'s](guide/cb-dependency-injection#alex-providers).
If we're going to keep writing [*alias providers*](guide/cb-dependency-injection#useexisting) like this we should create a [helper function](guide/cb-dependency-injection#provideparent).
For now, focus on *Barry*'s constructor:
<md-tab-group>
@ -834,14 +834,14 @@ which *is* what parent means.
Here's *Alice*, *Barry* and family in action:
<figure class='image-display'>
<img src="/resources/images/cookbooks/dependency-injection/alice.png" alt="Alice in action"> </img>
<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.
We [learned earlier](guide/cb-dependency-injection#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* .
@ -870,7 +870,7 @@ It doesn't in this example *only* to demonstrate that the code will compile and
### A *provideParent* helper function
Writing variations of the same parent *alias provider* gets old quickly,
especially this awful mouthful with a [*forwardRef*](#forwardref):
especially this awful mouthful with a [*forwardRef*](guide/cb-dependency-injection#forwardref):
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-providers'}

View File

@ -2,7 +2,7 @@
Cookbook
@intro
A collection of recipes for common Angular application scenarios
A collection of recipes for common Angular application scenarios.
@description
The *Cookbook* offers answers to common implementation questions.

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