Compare commits

...

63 Commits

Author SHA1 Message Date
c2dbc55f11 release: cut the 5.1.0 release 2017-12-06 12:17:04 -08:00
9ee2703824 docs: add changelog for 5.1.0 2017-12-06 12:17:04 -08:00
b78ada198a fix(animations): ensure DOM is cleaned up after multiple @trigger leave animations finish (#20740)
Closes #20541

PR Close #20740
2017-12-06 07:02:42 -08:00
6790e02a13 ci: upgrade to node 8 and Bazel 0.8 (#20807)
Closes #19648

PR Close #20807
2017-12-06 06:58:33 -08:00
7cabaa0ae7 fix(service-worker): ensure initialised in browser only (#20782)
closes #20360

PR Close #20782
2017-12-06 06:55:33 -08:00
da3563ce19 fix(service-worker): esm2015 points to wrong path (#20800)
The package.json esm2015 points to the wrong path.
"esm15" should be "esm2015"

Service Worker can't be compiled with use of Closure Compiler
PR Close #20800
2017-12-06 06:53:30 -08:00
9bbec42a6c docs(aio): add service worker guide content and update nav (#20736)
PR Close #20736
2017-12-06 06:52:20 -08:00
be994496cd docs(core): add docs for i18n tokens (#17920)
PR Close #17920
2017-12-05 21:56:43 -08:00
77ef527993 docs(aio): update myUnless references to appUnless (#20658)
fixes Issue Number: #20447

PR Close #20658
2017-12-05 21:54:39 -08:00
f092a7c824 docs(aio): fix numbering of the three ways to access hero details (#20647)
Fixes #20628

PR Close #20647
2017-12-05 21:54:34 -08:00
5e25d3986d docs: component-styles guide - inline styles must be CSS (#20701)
Cannot write them in less, sass, or stylus
See CLI issue https://github.com/angular/angular-cli/issues/8472

PR Close #20701
2017-12-05 21:54:23 -08:00
35977e3830 fix(forms): Broken link NG_VALIDATORS replace by an example (#15480)
PR Close #15480
2017-12-05 21:53:12 -08:00
f7328c69b3 docs(aio): architecture review for a11y (#17848)
PR Close #17848
2017-12-05 21:49:27 -08:00
25f2211726 docs: add We Are One Sàrl as onsite training (#20714)
Adding We Are One Sàrl as an onsite training company in Switzerland.
PR Close #20714
2017-12-05 21:48:41 -08:00
18793c896b docs(aio): Fix typo (#20732)
Remove duplicate word 'to'

PR Close #20732
2017-12-05 21:46:56 -08:00
e7cdb9f660 ci(core): Improve the payload size message (#20786)
PR Close #20786
2017-12-05 21:46:10 -08:00
6911a250ef build: set preserveWhitespaces to false by default on Bazel (#20783)
`preserveWhitespaces: false` will be the default in Angular 6+

You can opt-out at component or element level.

Docs: https://angular.io/api/core/Component#preserveWhitespaces

PR Close #20783
2017-12-04 16:16:01 -08:00
7e7ff2e0aa ci: fix the payload-size checking scripts (#20683)
The scripts were accidentally broken in #20524. More specifically, when a limit
was exceeded the script would break while trying to log an error message due to
a missing `commit` variable.
This commit fixes it and also does some minor clean-up (improve docs, use more
descriptive variable names, remove dead code, etc).

PR Close #20683
2017-12-04 14:52:15 -08:00
d34f0bf573 docs: add changelog for 5.1.0-rc.1 2017-12-01 14:55:51 -08:00
1f5fa25583 release: cut the 5.1.0-rc.1 release 2017-12-01 14:55:39 -08:00
d507057476 docs: add changelog for 5.0.5 2017-12-01 14:41:48 -08:00
f582620d5b fix(service-worker): use relative path for ngsw.json
Not every application is served from the domain root. The Service
Worker made a bad assumption that it would be, and so requested
/ngsw.json from the domain root.

This change corrects this assumption, and requests ngsw.json without
the leading slash. This causes the request to be interpreted
relative to the SW origin, which will be the application root.
2017-12-01 14:21:07 -08:00
3fbcde9048 fix(service-worker): send initialization signal from the application
The Service Worker contains a mechanism by which it will postMessage
itself a signal to initialize its caches. Through this mechanism,
initialization happens asynchronously while keeping the SW process
alive.

Unfortunately in Firefox, the SW does not have the ability to
postMessage itself during the activation event. This prevents the
above mechanism from working, and the SW initializes on the next
fetch event, which is often too late.

Therefore, this change has the application wait for SW changes and
tells each new SW to initialize itself. This happens in addition to
the self-signal that the SW attempts to send (as self-signaling is
more reliable). That way even on browsers such as Firefox,
initialization happens eagerly.
2017-12-01 14:21:07 -08:00
f841fbe60f fix(compiler-cli): propagate ts.SourceFile moduleName into metadata 2017-12-01 14:19:06 -08:00
b9a91a5e74 fix(service-worker): don't crash if SW not supported
Currently a bug exists where attempting to inject SwPush crashes the
application if Service Workers are unsupported. This happens because
SwPush doesn't properly detect that navigator.serviceWorker isn't
set.

This change ensures that all passive observation of SwPush and
SwUpdate doesn't cause crashes, and that calling methods to perform
actions on them results in rejected Promises. It's up to applications
to detect when those services are not available, and refrain from
attempting to use them.

To that end, this change also adds an `isSupported` getter to both
services, so users don't have to rely on feature detection directly
with browser APIs. Currently this simply detects whether the SW API
is present, but in the future it will be expanded to detect whether
a particular browser supports specific APIs (such as push
notifications, for example).
2017-12-01 14:18:16 -08:00
65f4fad801 fix(service-worker): allow disabling SW while still using services
Currently, the way to not use the SW is to not install its module.
However, this means that you can't inject any of its services.

This change adds a ServiceWorkerModule.disabled() MWP, that still
registers all of the right providers but acts as if the browser does
not support Service Workers.
2017-12-01 14:18:16 -08:00
60a30818ef docs: add changelog for 5.1.0-rc.0 2017-11-30 21:38:52 -08:00
b967cbfc66 release: cut the 5.1.0-rc.0 release 2017-11-30 21:37:42 -08:00
8826a8235b docs: add changelog for 5.0.4 2017-11-30 21:34:44 -08:00
47addd169d revert: docs(aio): add service worker guide content and update nav (#20021) (#20716)
* revert: style: broken build due to missing new lines

This reverts commit ba6af2a6dd.
The commit that introduced these files (48300067f) will also get
reverted.

* revert: docs(aio): add service worker guide content and update nav (#20021)

This reverts commit 48300067fb.
This commit has some issues (e.g. breaks some e2e tests, adds images
to the wrong directories, breaks linting, etc).
Reverting in order to investigate and fix.
2017-11-30 09:53:07 -08:00
ba6af2a6dd style: broken build due to missing new lines 2017-11-29 20:27:25 -08:00
b9e4d62d5a docs(aio): remove services plurality (#20696)
remove services plurality for the sentence formation to be proper

PR Close #20696
2017-11-29 16:53:49 -06:00
71e5de646b fix(compiler-cli): fix memory leak in program creation (#20692)
Saving `oldProgram` in `AngularCompilerProgram` instances is causing a memory leak for unemitted programs.

It's not actually used so simply not saving it fixes the memory leak.

Fix #20691

PR Close #20692
2017-11-29 16:53:11 -06:00
3def2cc552 test(aio): cleaner approach to reliable Google Analytics e2e tests (#20661)
PR Close #20661
2017-11-29 16:53:04 -06:00
4ec4a99f16 test(aio): fix e2e API test due to #20607 (#20661)
PR Close #20661
2017-11-29 16:53:04 -06:00
3203069d6c fix(language-service): Allow empty templates (#20651)
Fixes the bug where templates with empty strings show up as error in the editor.

PR Close #19406

PR Close #20651
2017-11-29 16:52:55 -06:00
54bfe14313 fix(language-service): Fix crash when no script files are found (#20550)
Fixes the crash in typescript host when getScriptFileNames() returns an
empty array.

PR Close #19325

PR Close #20550
2017-11-29 16:52:48 -06:00
ba850b36de Revert "fix(core): should use native addEventListener in ngZone (#20672)"
This reverts commit 65a2cb8307.
2017-11-29 14:56:29 -06:00
f3c5481181 docs(aio): Updating with Ignite UI for Angular (#20663)
PR Close #20663
2017-11-29 12:15:34 -06:00
48300067fb docs(aio): add service worker guide content and update nav (#20021)
PR Close #20021
2017-11-29 12:15:27 -06:00
b841e0d530 docs(aio): add class attribute to example referenced in Structural Directive guide (#19446)
See https://github.com/angular/angular/blob/master/aio/content/guide/structural-directives.md

From the structural-directives.md:
The rest of the <div>, including its class attribute, moved inside the <ng-template> element.

Maybe this made sense at one time but it has become out of sync.

PR Close #19446
2017-11-29 12:15:21 -06:00
65a2cb8307 fix(core): should use native addEventListener in ngZone (#20672)
PR Close #20672
2017-11-28 22:27:25 -06:00
0bef021321 build(aio): upgrade codelyzer to 4.0.x and angular/cli to 1.5.4 (#20392)
PR Close #20392
2017-11-28 22:27:18 -06:00
aafa75da84 fix(common): don't strip XSSI prefix for if error isn't JSON (#19958)
This changes XhrBackend to not strip the XSSI prefix from error text
if such a prefix is present but the remaining body does not parse as
JSON.

PR Close #19958
2017-11-28 22:27:10 -06:00
503be69af6 fix(common): treat an empty body as null when parsing JSON in HttpClient (#19958)
Previously, XhrBackend would call JSON.parse('') if the response body was
empty (a 200 status code with content-length 0). This changes the XhrBackend
to attempt the JSON parse only if the response body is non-empty. Otherwise,
the body is left as null.

Fixes #18680.
Fixes #19413.
Fixes #19502.
Fixes #19555.

PR Close #19958
2017-11-28 22:27:10 -06:00
eb01ad583f fix(common): remove useless guard in HttpClient (#19958)
An invalid "if" condition is always true, and is thus useless. This
change removes it. No behavior changes.

Fixes #19223.

PR Close #19958
2017-11-28 22:27:10 -06:00
15a54df7d3 fix(common): accept falsy values as HTTP bodies (#19958)
Previously, HttpClient used the overly clever test "body || null"
to determine when a body parameter was provided. This breaks when
the valid bodies '0' or 'false' are provided.

This change tests directly against 'undefined' to detect the presence
of the body parameter, and thus correctly allows falsy values through.

Fixes #19825.
Fixes #19195.

PR Close #19958
2017-11-28 22:27:10 -06:00
eaaae2edf4 docs: fix grammar and wording (#18530)
PR Close #18530
2017-11-28 22:27:02 -06:00
c2b3792a3b fix(animations): ensure multi-level leave animations work (#19455)
PR Close #19455
2017-11-28 18:24:41 -06:00
b2a586cee1 fix(animations): ensure multi-level enter animations work (#19455)
PR Close #19455
2017-11-28 18:24:40 -06:00
8bb42df47e fix(compiler): correctly detect when to serialze summary metadata (#20668)
The change to improve error messages broke the summary serialization
of summaries.

PR Close #20668
2017-11-28 16:43:35 -06:00
add5953aa1 Revert "fix(animations): ensure multi-level enter animations work (#19455)"
This reverts commit dd6237ecd9.
2017-11-28 15:08:44 -06:00
6b4c24020d Revert "fix(animations): ensure multi-level leave animations work (#19455)"
This reverts commit 1366762d12.
2017-11-28 15:08:31 -06:00
24bf3e2a25 feat(common): add locale id parameter to registerLocaleData (#20623)
PR Close #20623
2017-11-27 17:00:06 -06:00
8ecda94899 feat(compiler-cli): improve error messages produced during structural errors (#20459)
The errors produced when error were encountered while interpreting the
content of a directive was often incomprehencible. With this change
these kind of error messages should be easier to understand and diagnose.

PR Close #20459
2017-11-27 16:59:57 -06:00
1366762d12 fix(animations): ensure multi-level leave animations work (#19455)
PR Close #19455
2017-11-27 16:59:47 -06:00
dd6237ecd9 fix(animations): ensure multi-level enter animations work (#19455)
PR Close #19455
2017-11-27 16:59:46 -06:00
6e83204238 fix(bazel): produce named AMD modules for codegen (#20547)
fixes #19422

Signed-off-by: Alex Eagle <alexeagle@google.com>

PR Close #20547
2017-11-27 13:44:41 -06:00
a53a040071 build(aio): prevent comments in code from leaking into doc-gen code snippets (#20607)
The new version of dgeni-packages (0.22.1) does a better job of rendering
code nodes, which do not include comments.

Fixes #19751

PR Close #20607
2017-11-27 12:16:28 -06:00
5bef070e16 docs(aio): update homepage tooling image (#20593)
Fix #19831

PR Close #20593
2017-11-27 12:16:20 -06:00
89de98b25e build(aio): ensure downloadable zip filenames are unique (#20586)
Fixes #16227

PR Close #20586
2017-11-27 12:16:14 -06:00
de78307928 fix(compiler-cli): normalize sourcepaths for i18n extracted files (#20417)
Fixes #20416
PR Close #20417
2017-11-27 12:16:07 -06:00
6293ca23c3 docs(core): fix broken NgZone code example (#19291)
The current code example was broken as there were a couple of syntax errors. This commit fixes the demo.

PR Close #19291
2017-11-27 12:14:47 -06:00
133 changed files with 3457 additions and 711 deletions

View File

@ -11,7 +11,7 @@
anchor_1: &job_defaults
working_directory: ~/ng
docker:
- image: angular/ngcontainer:0.0.2
- image: angular/ngcontainer:0.0.6
# After checkout, rebase on top of master.
# Similar to travis behavior, but not quite the same.
@ -39,7 +39,7 @@ jobs:
<<: *post_checkout
- restore_cache:
key: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
- run: bazel info release
- run: bazel run @yarn//:yarn
- run: bazel build packages/...
- run: bazel test @angular//...

View File

@ -2,7 +2,7 @@ language: node_js
sudo: false
dist: trusty
node_js:
- '6.9.5'
- '8.9.1'
addons:
# firefox: "38.0"

View File

@ -1,3 +1,90 @@
<a name="5.1.0"></a>
# [5.1.0](https://github.com/angular/angular/compare/5.1.0-rc.1...5.1.0) (2017-12-06)
### Bug Fixes
* **animations:** ensure DOM is cleaned up after multiple [@trigger](https://github.com/trigger) leave animations finish ([#20740](https://github.com/angular/angular/issues/20740)) ([b78ada1](https://github.com/angular/angular/commit/b78ada1)), closes [#20541](https://github.com/angular/angular/issues/20541)
* **service-worker:** initialize in browser only ([#20782](https://github.com/angular/angular/issues/20782)) ([7cabaa0](https://github.com/angular/angular/commit/7cabaa0)), closes [#20360](https://github.com/angular/angular/issues/20360)
* **service-worker:** esm2015 points to wrong path ([#20800](https://github.com/angular/angular/issues/20800)) ([da3563c](https://github.com/angular/angular/commit/da3563c))
<a name="5.1.0-rc.1"></a>
# [5.1.0-rc.1](https://github.com/angular/angular/compare/5.1.0-rc.0...5.1.0-rc.1) (2017-12-01)
### Bug Fixes
* **compiler-cli:** propagate ts.SourceFile moduleName into metadata ([f841fbe](https://github.com/angular/angular/commit/f841fbe))
* **service-worker:** allow disabling SW while still using services ([65f4fad](https://github.com/angular/angular/commit/65f4fad))
* **service-worker:** don't crash if SW not supported ([b9a91a5](https://github.com/angular/angular/commit/b9a91a5))
* **service-worker:** send initialization signal from the application ([3fbcde9](https://github.com/angular/angular/commit/3fbcde9))
* **service-worker:** use relative path for ngsw.json ([f582620](https://github.com/angular/angular/commit/f582620))
<a name="5.0.5"></a>
## [5.0.5](https://github.com/angular/angular/compare/5.0.4...5.0.5) (2017-12-01)
### Bug Fixes
* **compiler-cli:** propagate ts.SourceFile moduleName into metadata ([a2ff4ab](https://github.com/angular/angular/commit/a2ff4ab))
* **service-worker:** allow disabling SW while still using services ([f99335b](https://github.com/angular/angular/commit/f99335b))
* **service-worker:** don't crash if SW not supported ([ee37d4b](https://github.com/angular/angular/commit/ee37d4b))
* **service-worker:** send initialization signal from the application ([6bf07b4](https://github.com/angular/angular/commit/6bf07b4))
* **service-worker:** use relative path for ngsw.json ([56c98f7](https://github.com/angular/angular/commit/56c98f7))
<a name="5.1.0-rc.0"></a>
# [5.1.0-rc.0](https://github.com/angular/angular/compare/5.1.0-beta.2...5.1.0-rc.0) (2017-12-01)
### Bug Fixes
* **animations:** ensure multi-level enter animations work ([#19455](https://github.com/angular/angular/issues/19455)) ([dd6237e](https://github.com/angular/angular/commit/dd6237e))
* **animations:** ensure multi-level enter animations work ([#19455](https://github.com/angular/angular/issues/19455)) ([b2a586c](https://github.com/angular/angular/commit/b2a586c))
* **animations:** ensure multi-level leave animations work ([#19455](https://github.com/angular/angular/issues/19455)) ([1366762](https://github.com/angular/angular/commit/1366762))
* **animations:** ensure multi-level leave animations work ([#19455](https://github.com/angular/angular/issues/19455)) ([c2b3792](https://github.com/angular/angular/commit/c2b3792))
* **bazel:** produce named AMD modules for codegen ([#20547](https://github.com/angular/angular/issues/20547)) ([6e83204](https://github.com/angular/angular/commit/6e83204)), closes [#19422](https://github.com/angular/angular/issues/19422)
* **common:** accept falsy values as HTTP bodies ([#19958](https://github.com/angular/angular/issues/19958)) ([15a54df](https://github.com/angular/angular/commit/15a54df)), closes [#19825](https://github.com/angular/angular/issues/19825) [#19195](https://github.com/angular/angular/issues/19195)
* **common:** don't strip XSSI prefix for if error isn't JSON ([#19958](https://github.com/angular/angular/issues/19958)) ([aafa75d](https://github.com/angular/angular/commit/aafa75d))
* **common:** remove useless guard in HttpClient ([#19958](https://github.com/angular/angular/issues/19958)) ([eb01ad5](https://github.com/angular/angular/commit/eb01ad5)), closes [#19223](https://github.com/angular/angular/issues/19223)
* **common:** treat an empty body as null when parsing JSON in HttpClient ([#19958](https://github.com/angular/angular/issues/19958)) ([503be69](https://github.com/angular/angular/commit/503be69)), closes [#18680](https://github.com/angular/angular/issues/18680) [#19413](https://github.com/angular/angular/issues/19413) [#19502](https://github.com/angular/angular/issues/19502) [#19555](https://github.com/angular/angular/issues/19555)
* **compiler:** correctly detect when to serialze summary metadata ([#20668](https://github.com/angular/angular/issues/20668)) ([8bb42df](https://github.com/angular/angular/commit/8bb42df))
* **compiler-cli:** fix memory leak in program creation ([#20692](https://github.com/angular/angular/issues/20692)) ([71e5de6](https://github.com/angular/angular/commit/71e5de6)), closes [#20691](https://github.com/angular/angular/issues/20691)
* **compiler-cli:** normalize sourcepaths for i18n extracted files ([#20417](https://github.com/angular/angular/issues/20417)) ([de78307](https://github.com/angular/angular/commit/de78307)), closes [#20416](https://github.com/angular/angular/issues/20416)
* **core:** should use native addEventListener in ngZone ([#20672](https://github.com/angular/angular/issues/20672)) ([65a2cb8](https://github.com/angular/angular/commit/65a2cb8))
* **language-service:** Allow empty templates ([#20651](https://github.com/angular/angular/issues/20651)) ([3203069](https://github.com/angular/angular/commit/3203069)), closes [#19406](https://github.com/angular/angular/issues/19406)
* **language-service:** Fix crash when no script files are found ([#20550](https://github.com/angular/angular/issues/20550)) ([54bfe14](https://github.com/angular/angular/commit/54bfe14)), closes [#19325](https://github.com/angular/angular/issues/19325)
### Features
* **common:** add locale id parameter to `registerLocaleData` ([#20623](https://github.com/angular/angular/issues/20623)) ([24bf3e2](https://github.com/angular/angular/commit/24bf3e2))
* **compiler-cli:** improve error messages produced during structural errors ([#20459](https://github.com/angular/angular/issues/20459)) ([8ecda94](https://github.com/angular/angular/commit/8ecda94))
<a name="5.0.4"></a>
## [5.0.4](https://github.com/angular/angular/compare/5.0.3...5.0.4) (2017-12-01)
### Bug Fixes
* **animations:** ensure multi-level enter animations work ([#19455](https://github.com/angular/angular/issues/19455)) ([22bbd6e](https://github.com/angular/angular/commit/22bbd6e))
* **animations:** ensure multi-level leave animations work ([#19455](https://github.com/angular/angular/issues/19455)) ([c7b211c](https://github.com/angular/angular/commit/c7b211c))
* **common:** accept falsy values as HTTP bodies ([#19958](https://github.com/angular/angular/issues/19958)) ([66fd1f8](https://github.com/angular/angular/commit/66fd1f8)), closes [#19825](https://github.com/angular/angular/issues/19825) [#19195](https://github.com/angular/angular/issues/19195)
* **common:** don't strip XSSI prefix for if error isn't JSON ([#19958](https://github.com/angular/angular/issues/19958)) ([ead7596](https://github.com/angular/angular/commit/ead7596))
* **common:** remove useless guard in HttpClient ([#19958](https://github.com/angular/angular/issues/19958)) ([e099911](https://github.com/angular/angular/commit/e099911)), closes [#19223](https://github.com/angular/angular/issues/19223)
* **common:** treat an empty body as null when parsing JSON in HttpClient ([#19958](https://github.com/angular/angular/issues/19958)) ([bdaee50](https://github.com/angular/angular/commit/bdaee50)), closes [#18680](https://github.com/angular/angular/issues/18680) [#19413](https://github.com/angular/angular/issues/19413) [#19502](https://github.com/angular/angular/issues/19502) [#19555](https://github.com/angular/angular/issues/19555)
* **compiler-cli:** fix memory leak in program creation ([#20692](https://github.com/angular/angular/issues/20692)) ([38be44d](https://github.com/angular/angular/commit/38be44d)), closes [#20691](https://github.com/angular/angular/issues/20691)
* **compiler-cli:** normalize sourcepaths for i18n extracted files ([#20417](https://github.com/angular/angular/issues/20417)) ([2b0c896](https://github.com/angular/angular/commit/2b0c896)), closes [#20416](https://github.com/angular/angular/issues/20416)
<a name="5.1.0-beta.2"></a>
# [5.1.0-beta.2](https://github.com/angular/angular/compare/5.1.0-beta.1...5.1.0-beta.2) (2017-11-22)

View File

@ -1,9 +1,10 @@
<hr>
<h4>{{hero.name}} Detail</h4>
<div>Id: {{hero.id}}</div>
<div>Name:
<label>Name:
<!-- #docregion ngModel -->
<input [(ngModel)]="hero.name">
<!-- #enddocregion ngModel -->
</div>
<div>Power:<input [(ngModel)]="hero.power"></div>
</label>
<br />
<label>Power: <input [(ngModel)]="hero.power"></label>

View File

@ -7,7 +7,7 @@ import { TaxRateService } from './tax-rate.service';
selector: 'app-sales-tax',
template: `
<h2>Sales Tax Calculator</h2>
Amount: <input #amountBox (change)="0">
<label>Amount: <input #amountBox (change)="0"></label>
<div *ngIf="amountBox.value">
The sales tax is

View File

@ -2,5 +2,6 @@
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
registerLocaleData(localeFr);
// the second parameter 'fr' is optional
registerLocaleData(localeFr, 'fr');
// #enddocregion import-locale

View File

@ -1,7 +1,7 @@
// #docregion import-locale-extra
import { registerLocaleData } from '@angular/common';
import localeFrCa from '@angular/common/locales/fr-CA';
import localeFrCaExtra from '@angular/common/locales/extra/fr-CA';
import localeFr from '@angular/common/locales/fr';
import localeFrExtra from '@angular/common/locales/extra/fr';
registerLocaleData(localeFrCa, localeFrCaExtra);
registerLocaleData(localeFr, 'fr-FR', localeFrExtra);
// #enddocregion import-locale-extra

View File

@ -0,0 +1,37 @@
import { AppPage } from './app.po';
import { browser, element, by } from 'protractor';
describe('sw-example App', () => {
let page: AppPage;
let logo = element(by.css('img'));
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to Service Workers!');
});
it('should display the Angular logo', () => {
page.navigateTo();
expect(logo.isPresent()).toBe(true);
});
it('should show a header for the list of links', function () {
const listHeader = element(by.css('app-root > h2'));
expect(listHeader.getText()).toEqual('Here are some links to help you start:');
});
it('should show a list of links', function () {
element.all(by.css('ul > li > h2 > a')).then(function(items) {
expect(items.length).toBe(4);
expect(items[0].getText()).toBe('Angular Service Worker Intro');
expect(items[1].getText()).toBe('Tour of Heroes');
expect(items[2].getText()).toBe('CLI Documentation');
expect(items[3].getText()).toBe('Angular blog');
});
});
});

View File

@ -0,0 +1,23 @@
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
<h1>
Welcome to {{title}}!
</h1>
<img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<h2>Here are some links to help you start: </h2>
<ul>
<li>
<h2><a target="_blank" rel="noopener" href="https://angular.io/service-worker-intro">Angular Service Worker Intro</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
</li>
</ul>

View File

@ -0,0 +1,27 @@
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});

View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Service Workers';
}

View File

@ -0,0 +1,31 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
// #docregion sw-import
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
// #enddocregion sw-import
import { CheckForUpdateService } from './check-for-update.service';
import { LogUpdateService } from './log-update.service';
import { PromptUpdateService } from './prompt-update.service';
// #docregion sw-module
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production})
],
providers: [
CheckForUpdateService,
LogUpdateService,
PromptUpdateService,
],
bootstrap: [AppComponent]
})
export class AppModule { }
// #enddocregion sw-module

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';
function promptUser(event): boolean {
return true;
}
// #docregion sw-check-update
@Injectable()
export class CheckForUpdateService {
constructor(updates: SwUpdate) {
Observable.interval(6 * 60 * 60).subscribe(() => updates.checkForUpdate());
}
}
// #enddocregion sw-check-update

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
// #docregion sw-update
@Injectable()
export class LogUpdateService {
constructor(updates: SwUpdate) {
updates.available.subscribe(event => {
console.log('current version is', event.current);
console.log('available version is', event.available);
});
updates.activated.subscribe(event => {
console.log('old version was', event.previous);
console.log('new version is', event.current);
});
}
}
// #enddocregion sw-update

View File

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
function promptUser(event): boolean {
return true;
}
// #docregion sw-activate
@Injectable()
export class PromptUpdateService {
constructor(updates: SwUpdate) {
updates.available.subscribe(event => {
if (promptUser(event)) {
updates.activateUpdate().then(() => document.location.reload());
}
});
}
}
// #enddocregion sw-activate

View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>SwExample</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));

View File

@ -0,0 +1,28 @@
{
"index": "/index.html",
"assetGroups": [{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html"
],
"versionedFiles": [
"/*.bundle.css",
"/*.bundle.js",
"/*.chunk.js"
]
}
}, {
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**"
]
}
}]
}

View File

@ -37,7 +37,7 @@ describe('Structural Directives', function () {
expect(paragraph.count()).toEqual(1);
});
it('myUnless should show 3 paragraph (A)s and (B)s at the start', function () {
it('appUnless should show 3 paragraph (A)s and (B)s at the start', function () {
const paragraph = element.all(by.css('p.unless'));
expect(paragraph.count()).toEqual(3);
for (let i = 0; i < 3; i++) {
@ -45,7 +45,7 @@ describe('Structural Directives', function () {
}
});
it('myUnless should show 1 paragraph (B) after toggling condition', function () {
it('appUnless should show 1 paragraph (B) after toggling condition', function () {
const toggleConditionButton = element.all(by.cssContainingText('button', 'Toggle condition')).get(0);
const paragraph = element.all(by.css('p.unless'));

View File

@ -6,7 +6,7 @@
<blockquote>
<!-- #docregion built-in, asterisk, ngif -->
<div *ngIf="hero" >{{hero.name}}</div>
<div *ngIf="hero" class="name">{{hero.name}}</div>
<!-- #enddocregion built-in, asterisk, ngif -->
</blockquote>
@ -51,7 +51,7 @@
<p>&lt;ng-template&gt; element</p>
<!-- #docregion ngif-template -->
<ng-template [ngIf]="hero">
<div>{{hero.name}}</div>
<div class="name">{{hero.name}}</div>
</ng-template>
<!-- #enddocregion ngif-template -->
@ -188,7 +188,7 @@
<hr>
<h2 id="myUnless">UnlessDirective</h2>
<h2 id="appUnless">UnlessDirective</h2>
<p>
The condition is currently
<span [ngClass]="{ 'a': !condition, 'b': condition, 'unless': true }">{{condition}}</span>.
@ -198,31 +198,31 @@
Toggle condition to {{condition ? 'false' : 'true'}}
</button>
</p>
<!-- #docregion myUnless-->
<!-- #docregion appUnless-->
<p *appUnless="condition" class="unless a">
(A) This paragraph is displayed because the condition is false.
</p>
<p *appUnless="!condition" class="unless b">
(B) Although the condition is true,
this paragraph is displayed because myUnless is set to false.
this paragraph is displayed because appUnless is set to false.
</p>
<!-- #enddocregion myUnless-->
<!-- #enddocregion appUnless-->
<h4>UnlessDirective with template</h4>
<!-- #docregion myUnless-1 -->
<!-- #docregion appUnless-1 -->
<p *appUnless="condition">Show this sentence unless the condition is true.</p>
<!-- #enddocregion myUnless-1 -->
<!-- #enddocregion appUnless-1 -->
<p *appUnless="condition" class="code unless">
(A) &lt;p *myUnless="condition" class="code unless"&gt;
(A) &lt;p *appUnless="condition" class="code unless"&gt;
</p>
<ng-template [appUnless]="condition">
<p class="code unless">
(A) &lt;ng-template [myUnless]="condition"&gt;
(A) &lt;ng-template [appUnless]="condition"&gt;
</p>
</ng-template>

View File

@ -8,7 +8,7 @@ import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
* Add the template content to the DOM unless the condition is true.
// #enddocregion no-docs
*
* If the expression assigned to `myUnless` evaluates to a truthy value
* If the expression assigned to `appUnless` evaluates to a truthy value
* then the templated elements are removed removed from the DOM,
* the templated elements are (re)inserted into the DOM.
*
@ -18,8 +18,8 @@ import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
*
* ### Syntax
*
* - `<div *myUnless="condition">...</div>`
* - `<ng-template [myUnless]="condition"><div>...</div></ng-template>`
* - `<div *appUnless="condition">...</div>`
* - `<ng-template [appUnless]="condition"><div>...</div></ng-template>`
*
// #docregion no-docs
*/

View File

@ -2,7 +2,7 @@
The Angular Ahead-of-Time (AOT) compiler converts your Angular HTML and TypeScript code into efficient JavaScript code during the build phase _before_ the browser downloads and runs that code.
This guide explains how to to build with the AOT compiler and how to write Angular metadata that AOT can compile.
This guide explains how to build with the AOT compiler and how to write Angular metadata that AOT can compile.
<div class="l-sub-section">

View File

@ -179,7 +179,7 @@ to a component's `@Component` decorator:
<code-tabs>
<code-pane title="src/app/hero-app.component.ts (CSS in file)" path="component-styles/src/app/hero-app.component.1.ts"></code-pane>
<code-pane title="src/app/hero-app.component.css" path="component-styles/src/app/hero-app.component.1.css"></code-pane>
</code-tabs>
</code-tabs>
<div class="alert is-critical">
@ -245,7 +245,8 @@ See the [CLI documentation](https://github.com/angular/angular-cli/wiki/stories-
### Non-CSS style files
You can write style files in [sass](http://sass-lang.com/), [less](http://lesscss.org/), or [stylus](http://stylus-lang.com/) and specify those files in the `styleUrls` metadata, e.g.,
If you're building with the CLI,
you can write style files in [sass](http://sass-lang.com/), [less](http://lesscss.org/), or [stylus](http://stylus-lang.com/) and specify those files in the `@Component.styleUrls` metadata with the appropriate extensions (`.scss`, `.less`, `.styl`) as in the following example:
<code-example>
@Component({
@ -256,10 +257,18 @@ You can write style files in [sass](http://sass-lang.com/), [less](http://lesscs
...
</code-example>
The CLI build process runs the corresponding CSS pre-processors.
The CLI build process runs the pertinent CSS preprocessor.
You can also configure the CLI to default to your preferred CSS pre-processer
as explained in the [CLI documentation](https://github.com/angular/angular-cli/wiki/stories-css-preprocessors).
When generating a component file with `ng generate component`, the CLI emits an empty CSS styles file (`.css`) by default.
You can configure the CLI to default to your preferred CSS preprocessor
as explained in the [CLI documentation](https://github.com/angular/angular-cli/wiki/stories-css-preprocessors
"CSS Preprocessor integration").
<div class="alert is-important">
Style strings added to the `@Component.styles` array _must be written in CSS_ because the CLI cannot apply a preprocessor to inline styles.
</div>
{@a view-encapsulation}

View File

@ -8,7 +8,6 @@ See the <live-example downloadOnly name="i18n">i18n Example</live-example> for a
an AOT-compiled app, translated into French.
{@a angular-i18n}
## Angular and i18n
Angular simplifies the following aspects of internationalization:
@ -62,6 +61,23 @@ For more information about Unicode locale identifiers, see the
For a complete list of locales supported by Angular, see
[the Angular repository](https://github.com/angular/angular/tree/master/packages/common/locales).
The locale identifiers used by CLDR and Angular are based on [BCP47](http://www.rfc-editor.org/rfc/bcp/bcp47.txt).
These specifications change over time; the following table maps previous identifiers to current ones at
time of writing:
| Locale name | Old locale id | New locale id |
|-------------------------------|-------------------|---------------|
| Indonesian | in | id |
| Hebrew | iw | he |
| Romanian Moldova | mo | ro-MD |
| Norwegian Bokmål | no, no-NO | nb |
| Serbian Latin | sh | sr-Latn |
| Filipino | tl | fil |
| Portuguese Brazil | pt-BR | pt |
| Chinese Simplified | zh-cn, zh-Hans-CN | zh-Hans |
| Chinese Traditional | zh-tw, zh-Hant-TW | zh-Hant |
| Chinese Traditional Hong Kong | zh-hk | zh-Hant-HK |
## i18n pipes
@ -78,6 +94,14 @@ If you want to import locale data for other languages, you can do it manually:
<code-example path="i18n/doc-files/app.locale_data.ts" region="import-locale" title="src/app/app.module.ts" linenums="false">
</code-example>
The first parameter is an object containing the locale data imported from `@angular/common/locales`.
By default, the imported locale data is registered with the locale id that is defined in the Angular
locale data itself.
If you want to register the imported locale data with another locale id, use the second parameter to
specify a custom locale id. For example, Angular's locale data defines the locale id for French as
"fr". You can use the second parameter to associate the imported French locale data with the custom
locale id "fr-FR instead of "fr".
The files in `@angular/common/locales` contain most of the locale data that you
need, but some advanced formatting options might only be available in the extra dataset that you can
import from `@angular/common/locales/extra`. An error message informs you when this is the case.
@ -118,7 +142,6 @@ in the target language.
You need to build and deploy a separate version of the app for each supported language.
{@a i18n-attribute}
### Mark text with the i18n attribute
The Angular `i18n` attribute marks translatable content. Place it on every element tag whose fixed
@ -144,7 +167,6 @@ To mark the greeting for translation, add the `i18n` attribute to the `<h1>` tag
{@a help-translator}
### Help the translator with a description and meaning
To translate a text message accurately, the translator may need additional information or context.
@ -175,7 +197,6 @@ text messages with different descriptions (not different meanings), then they ar
{@a custom-id}
### Set a custom id for persistence and maintenance
The angular i18n extractor tool generates a file with a translation unit entry for each `i18n`
@ -250,7 +271,6 @@ the same text, `Bonjour`:
{@a no-element}
### Translate text without creating an element
If there is a section of text that you would like to translate, you can wrap it in a `<span>` tag.
@ -262,7 +282,6 @@ The `<ng-container>` is transformed into an html comment:
</code-example>
{@a translate-attributes}
## Add i18n translation attributes
You also can translate attributes.
@ -286,7 +305,6 @@ You also can assign a meaning, description, and id with the `i18n-x="<meaning>|<
syntax.
{@a plural-ICU}
## Translate singular and plural
Different languages have different pluralization rules.
@ -342,7 +360,6 @@ for two, three, or any other number if the pluralization rules were different. F
</div>
{@a select-ICU}
## Select among alternative text messages
If your template needs to display different text messages depending on the value of a variable, you
@ -360,7 +377,6 @@ The message maps those values to the appropriate translations:
</code-example>
{@a nesting-ICUS}
## Nesting plural and select ICU expressions
You can also nest different ICU expressions together, as shown in this example:
@ -369,7 +385,6 @@ You can also nest different ICU expressions together, as shown in this example:
</code-example>
{@a ng-xi18n}
## Create a translation source file with _ng xi18n_
Use the `ng xi18n` command provided by the CLI to extract the text messages marked with `i18n` into
@ -394,7 +409,6 @@ package, or you can manually use the CLI Webpack plugin `ExtractI18nPlugin` from
</div>
{@a other-formats}
### Other translation formats
Angular i18n tooling supports three translation formats:
@ -422,7 +436,6 @@ The sample in this guide uses the default XLIFF 1.2 format.
</div>
{@a ng-xi18n-options}
### Other options
You can specify the output path used by the CLI to extract your translation source file with
@ -456,7 +469,6 @@ file. This information is not used by Angular, but external translation tools ma
{@a translate}
## Translate text messages
The `ng xi18n` command generates a translation source file named `messages.xlf` in the project `src`
@ -466,7 +478,6 @@ The next step is to translate this source file into the specific language
translation files. The example in this guide creates a French translation file.
{@a localization-folder}
### Create a localization folder
Most apps are translated into more than one other language. For this reason, it is standard practice
@ -500,7 +511,6 @@ For this example:
If you were translating to other languages, you would repeat these steps for each target language.
{@a translate-text-nodes}
### Translate text nodes
In a large translation project, you would send the `messages.fr.xlf` file to a French translator who
@ -544,7 +554,6 @@ This sample file is easy to translate without a special editor or knowledge of F
</div>
{@a translate-plural-select}
## Translate plural and select expressions
_Plural_ and _select_ ICU expressions are extracted separately, so they require special attention
@ -555,7 +564,6 @@ elsewhere in the source template. In this example, you know the translation unit
must be just below the translation unit for the logo.
{@a translate-plural}
### Translate _plural_
To translate a `plural`, translate its ICU format match values:
@ -567,7 +575,6 @@ You can add or remove plural cases, with each language having its own cardinalit
[CLDR plural rules](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html).)
{@a translate-select}
### Translate _select_
Below is the content of our example `select` ICU expression in the component template:
@ -598,7 +605,6 @@ Here they are together, after translation:
</code-example>
{@a translate-nested}
### Translate a nested expression
A nested expression is similar to the previous examples. As in the previous example, there are
@ -621,7 +627,6 @@ The entire template translation is complete. The next section describes how to l
into the app.
{@a app-pre-translation}
### The app and its translation file
The sample app and its translation file are now as follows:
@ -640,7 +645,6 @@ The sample app and its translation file are now as follows:
</code-tabs>
{@a merge}
## Merge the completed translation file into the app
To merge the translated text into component templates, compile the app with the completed
@ -656,12 +660,11 @@ format that Angular understands, such as `.xtb`.
How you provide this information depends upon whether you compile with
the JIT compiler or the AOT compiler.
* With [AOT](guide/i18n#aot), you pass the information as a CLI parameter.
* With [JIT](guide/i18n#jit), you provide the information at bootstrap time.
* With [AOT](guide/i18n#merge-aot), you pass the information as a CLI parameter.
* With [JIT](guide/i18n#merge-jit), you provide the information at bootstrap time.
{@a aot}
{@a merge-aot}
### Merge with the AOT compiler
The AOT (_Ahead-of-Time_) compiler is part of a build process that produces a small, fast,
@ -685,8 +688,7 @@ guide:
ng serve --aot --i18nFile=src/locale/messages.fr.xlf --i18nFormat=xlf --locale=fr
</code-example>
{@a jit}
{@a merge-jit}
### Merge with the JIT compiler
The JIT compiler compiles the app in the browser as the app loads.
@ -713,3 +715,29 @@ Then provide the `LOCALE_ID` in the main module:
<code-example path="i18n/doc-files/app.module.ts" title="src/app/app.module.ts" linenums="false">
</code-example>
{@a missing-translation}
### Report missing translations
By default, when a translation is missing, the build succeeds but generates a warning such as
`Missing translation for message "foo"`. You can configure the level of warning that is generated by
the Angular compiler:
* Error: throw an error. If you are using AOT compilation, the build will fail. If you are using JIT
compilation, the app will fail to load.
* Warning (default): show a 'Missing translation' warning in the console or shell.
* Ignore: do nothing.
If you use the AOT compiler, specify the warning level by using the CLI parameter
`--missingTranslation`. The example below shows how to set the warning level to error:
<code-example language="sh" class="code-shell">
ng serve --aot --missingTranslation=error
</code-example>
If you use the JIT compiler, specify the warning level in the compiler config at bootstrap by adding
the 'MissingTranslationStrategy' property. The example below shows how to set the warning level to
error:
<code-example path="i18n/doc-files/main.3.ts" title="src/main.ts">
</code-example>

View File

@ -74,8 +74,8 @@ Generate a new project and skeleton application by running the following command
Patience please.
It takes time to set up a new project, most of it spent installing npm packages.
Patience, please.
It takes time to set up a new project; most of it is spent installing npm packages.
</div>

View File

@ -0,0 +1,41 @@
# Communicating with service workers
Importing `ServiceWorkerModule` into your `AppModule` doesn't just register the service worker, it also provides a few services you can use to interact with the service worker and control the caching of your app.
## `SwUpdate` service
The `SwUpdate` service gives you access to events that indicate when the service worker has discovered an available update for your app or when it has activated such an update&mdash;meaning it is now serving content from that update to your app.
The `SwUpdate` service supports four separate operations:
* Getting notified of *available* updates. These are new versions of the app to be loaded if the page is refreshed.
* Getting notified of update *activation*. This is when the service worker starts serving a new version of the app immediately.
* Asking the service worker to check the server for new updates.
* Asking the service worker to activate the latest version of the app for the current tab.
### Available and activated updates
The two update events, `available` and `activated`, are `Observable` properties of `SwUpdate`:
<code-example path="service-worker-getstart/src/app/log-update.service.ts" linenums="false" title="log-update.service.ts" region="sw-update"> </code-example>
You can use these events to notify the user of a pending update or to refresh their pages when the code they are running is out of date.
### Checking for updates
It's possible to ask the service worker to check if any updates have been deployed to the server. You might choose to do this if you have a site that changes frequently or want updates to happen on a schedule.
Do this with the `checkForUpdate()` method:
<code-example path="service-worker-getstart/src/app/check-for-update.service.ts" linenums="false" title="check-for-update.service.ts" region="sw-check-update"> </code-example>
This method returns a `Promise` which indicates that the update check has completed successfully, though it does not indicate whether an update was discovered as a result of the check. Even if one is found, the service worker must still successfully download the changed files, which can fail. If successful, the `available` event will indicate availability of a new version of the app.
### Forcing update activation
If the current tab needs to be updated to the latest app version immediately, it can ask to do so with the `activateUpdate()` method:
<code-example path="service-worker-getstart/src/app/prompt-update.service.ts" linenums="false" title="prompt-update.service.ts" region="sw-activate"> </code-example>
Doing this could break lazy-loading into currently running apps, especially if the lazy-loaded chunks use filenames with hashes, which change every version.

View File

@ -0,0 +1,161 @@
{@a glob}
# Reference: Configuration file
The `src/ngsw-config.json` configuration file specifies which files and data URLs the Angular
service worker should cache and how it should update the cached files and data. The
CLI processes the configuration file during `ng build --prod`. Manually, you can process
it with the `ngsw-config` tool:
```sh
ngsw-config dist src/ngswn-config.json /base/href
```
The configuration file uses the JSON format. All file paths must begin with `/`, which is the deployment directory&mdash;usually `dist` in CLI projects.
Patterns use a limited glob format:
* `**` matches 0 or more path segments.
* `*` matches exactly one path segment or filename segment.
* The `!` prefix marks the pattern as being negative, meaning that only files that don't match the pattern will be included.
Example patterns:
* `/**/*.html` specifies all HTML files.
* `/*.html` specifies only HTML files in the root.
* `!/**/*.map` exclude all sourcemaps.
Each section of the configuration file is described below.
## `appData`
This section enables you to pass any data you want that describes this particular version of the app.
The `SwUpdate` service includes that data in the update notifications. Many apps use this section to provide additional information for the display of UI popups, notifying users of the available update.
## `index`
Specifies the file that serves as the index page to satisfy navigation requests. Usually this is `/index.html`.
## `assetGroups`
*Assets* are resources that are part of the app version that update along with the app. They can include resources loaded from the page's origin as well as third-party resources loaded from CDNs and other external URLs. As not all such external URLs may be known at build time, URL patterns can be matched.
This field contains an array of asset groups, each of which defines a set of asset resources and the policy by which they are cached.
```json
{
"assetGroups": [{
...
}, {
...
}]
}
```
Each asset group specifies both a group of resources and a policy that governs them. This policy determines when the resources are fetched and what happens when changes are detected.
Asset groups follow the Typescript interface shown here:
```typescript
interface AssetGroup {
name: string;
installMode?: 'prefetch' | 'lazy';
updateMode?: 'prefetch' | 'lazy';
resources: {
files?: string[];
versionedFiles?: string[];
urls?: string[];
};
}
```
### `name`
A `name` is mandatory. It identifies this particular group of assets between versions of the configuration.
### `installMode`
The `installMode` determines how these resources are initially cached. The `installMode` can be either of two values:
* `prefetch` tells the Angular service worker to fetch every single listed resource while it's caching the current version of the app. This is bandwidth-intensive but ensures resources are available whenever they're requested, even if the browser is currently offline.
* `lazy` does not cache any of the resources up front. Instead, the Angular service worker only caches resources for which it receives requests. This is an on-demand caching mode. Resources that are never requested will not be cached. This is useful for things like images at different resolutions, so the service worker only caches the correct assets for the particular screen and orientation.
### `updateMode`
For resources already in the cache, the `updateMode` determines the caching behavior when a new version of the app is discovered. Any resources in the group that have changed since the previous version are updated in accordance with `updateMode`.
* `prefetch` tells the service worker to download and cache the changed resources immediately.
* `lazy` tells the service worker to not cache those resources. Instead, it treats them as unrequested and waits until they're requested again before updating them. An `updateMode` of `lazy` is only valid if the `installMode` is also `lazy`.
### `resources`
This section describes the resources to cache, broken up into three groups.
* `files` lists patterns that match files in the distribution directory. These can be single files or glob-like patterns that match a number of files.
* `versionedFiles` is like `files` but should be used for build artifacts that already include a hash in the filename, which is used for cache busting. The Angular service worker can optimize some aspects of its operation if it can assume file contents are immutable.
* `urls` includes both URLs and URL patterns that will be matched at runtime. These resources are not fetched directly and do not have content hashes, but they will be cached according to their HTTP headers. This is most useful for CDNs such as the Google Fonts service.
## `dataGroups`
Unlike asset resources, data requests are not versioned along with the app. They're cached according to manually-configured policies that are more useful for situations such as API requests and other data dependencies.
Data groups follow this Typescript interface:
```typescript
export interface DataGroup {
name: string;
urls: string[];
version?: number;
cacheConfig: {
maxSize: number;
maxAge: string;
timeout?: string;
strategy?: 'freshness' | 'performance';
};
}
```
### `name`
Similar to `assetGroups`, every data group has a `name` which uniquely identifies it.
### `urls`
A list of URL patterns. URLs that match these patterns will be cached according to this data group's policy.
### `version`
Occasionally APIs change formats in a way that is not backward-compatible. A new version of the app may not be compatible with the old API format and thus may not be compatible with existing cached resources from that API.
`version` provides a mechanism to indicate that the resources being cached have been updated in a backwards-incompatible way, and that the old cache entries&mdash;those from previous versions&mdash;should be discarded.
`version` is an integer field and defaults to `0`.
### `cacheConfig`
This section defines the policy by which matching requests will be cached.
#### `maxSize`
(required) The maximum number of entries, or responses, in the cache. Open-ended caches can grow in unbounded ways and eventually exceed storage quotas, calling for eviction.
#### `maxAge`
(required) The `maxAge` parameter indicates how long responses are allowed to remain in the cache before being considered invalid and evicted. `maxAge` is a duration string, using the following unit suffixes:
* `d`: days
* `h`: hours
* `m`: minutes
* `s`: seconds
* `u`: milliseconds
For example, the string `3d12h` will cache content for up to three and a half days.
#### `timeout`
This duration string specifies the network timeout. The network timeout is how long the Angular service worker will wait for the network to respond before using a cached response, if configured to do so.
#### `strategy`
The Angular service worker can use either of two caching strategies for data resources.
* `performance`, the default, optimizes for responses that are as fast as possible. If a resource exists in the cache, the cached version is used. This allows for some staleness, depending on the `maxAge`, in exchange for better performance. This is suitable for resources that don't change often; for example, user avatar images.
* `freshness` optimizes for currency of data, preferentially fetching requested data from the network. Only if the network times out, according to `timeout`, does the request fall back to the cache. This is useful for resources that change frequently; for example, account balances.

View File

@ -0,0 +1,302 @@
# DevOps: Angular service worker in production
This page is a reference for deploying and supporting production apps that use the Angular service worker. It explains how the Angular service worker fits into the larger production environment, the service worker's behavior under various conditions, and available recourses and fail-safes.
## Service worker and caching of app resources
Conceptually, you can imagine the Angular service worker as a forward cache or a CDN edge that is installed in the end user's web browser. The service worker's job is to satisfy requests made by the Angular app for resources or data from a local cache, without needing to wait for the network. Like any cache, it has rules for how content is expired and updated.
{@a versions}
### App versions
In the context of an Angular service worker, a "version" is a collection of resources that represent a specific build of the Angular app. Whenever a new build of the app is deployed, the service worker treats that build as a new version of the app. This is true even if only a single file is updated. At any given time, the service worker may have multiple versions of the app in its cache and it may be serving them simultaneously. For more information, see the [App tabs](guide/service-worker-devops#tabs) section below.
To preserve app integrity, the Angular service worker groups all files into a version together. The files grouped into a version usually include HTML, JS, and CSS files. Grouping of these files is essential for integrity because HTML, JS, and CSS files frequently refer to each other and depend on specific content. For example, an `index.html` file might have a `<script>` tag that references `bundle.js` and it might attempt to call a function `startApp()` from within that script. Any time this version of `index.html` is served, the corresponding `bundle.js` must be served with it. For example, assume that the `startApp()` function is renamed to `runApp()` in both files. In this scenario, it is not valid to serve the old `index.html`, which calls `startApp()`, along with the new bundle, which defines `runApp()`.
This file integrity is especially important when lazy loading modules.
A JS bundle may reference many lazy chunks, and the filenames of the
lazy chunks are unique to the particular build of the app. If a running
app at version `X` attempts to load a lazy chunk, but the server has
updated to version `X + 1` already, the lazy loading operation will fail.
The version identifier of the app is determined by the contents of all
resources, and it changes if any of them change. In practice, the version
is determined by the contents of the `ngsw.json` file, which includes
hashes for all known content. If any of the cached files change, the file's
hash will change in `ngsw.json`, causing the Angular service worker to
treat the active set of files as a new version.
With the versioning behavior of the Angular service worker, an application
server can ensure that the Angular app always has a consistent set of files.
#### Update checks
Every time the Angular service worker starts, it checks for updates to the
app by looking for updates to the `ngsw.json` manifest.
Note that the service worker starts periodically throughout the usage of
the app because the web browser terminates the service worker if the page
is idle beyond a given timeout.
### Resource integrity
One of the potential side effects of long caching is inadvertently
caching an invalid resource. In a normal HTTP cache, a hard refresh
or cache expiration limits the negative effects of caching an invalid
file. A service worker ignores such constraints and effectively long
caches the entire app. Consequently, it is essential that the service worker
get the correct content.
To ensure resource integrity, the Angular service worker validates
the hashes of all resources for which it has a hash. Typically for
a CLI app, this is everything in the `dist` directory covered by
the user's `src/ngsw-config.json` configuration.
If a particular file fails validation, the Angular service worker
attempts to re-fetch the content using a "cache-busting" URL
parameter to eliminate the effects of browser or intermediate
caching. If that content also fails validation, the service worker
considers the entire version of the app to be invalid and it stops
serving the app. If necessary, the service worker enters a safe mode
where requests fall back on the network, opting not to use its cache
if the risk of serving invalid, broken, or outdated content is high.
Hash mismatches can occur for a variety of reasons:
* Caching layers in between the origin server and the end user could serve stale content.
* A non-atomic deployment could result in the Angular service worker having visibility of partially updated content.
* Errors during the build process could result in updated resources without `ngsw.json` being updated. The reverse could also happen resulting in an updated `ngsw.json` without updated resources.
#### Unhashed content
The only resources that have hashes in the `ngsw.json`
manifest are resources that were present in the `dist`
directory at the time the manifest was built. Other
resources, especially those loaded from CDNs, have
content that is unknown at build time or are updated
more frequently than the app is deployed.
If the Angular service worker does not have a hash to validate
a given resource, it still caches its contents but it honors
the HTTP caching headers by using a policy of "stale while
revalidate." That is, when HTTP caching headers for a cached
resource indicate that the resource has expired, the Angular
service worker continues to serve the content and it attempts
to refresh the resource in the background. This way, broken
unhashed resources do not remain in the cache beyond their
configured lifetimes.
{@a tabs}
### App tabs
It can be problematic for an app if the version of resources
it's receiving changes suddenly or without warning. See the
[Versions](guide/service-worker-devops#versions) section above
for a description of such issues.
The Angular service worker provides a guarantee: a running app
will continue to run the same version of the app. If another
instance of the app is opened in a new web browser tab, then
the most current version of the app is served. As a result,
that new tab can be running a different version of the app
than the original tab.
It's important to note that this guarantee is **stronger**
than that provided by the normal web deployment model. Without
a service worker, there is no guarantee that code lazily loaded
later in a running app is from the same version as the initial
code for the app.
There are a few limited reasons why the Angular service worker
might change the version of a running app. Some of them are
error conditions:
* The current version becomes invalid due to a failed hash.
* An unrelated error causes the service worker to enter safe mode; that is, temporary deactivation.
The Angular service worker is aware of which versions are in
use at any given moment and it cleans up versions when
no tab is using them.
Other reasons the Angular service worker might change the version
of a running app are normal events:
* The page is reloaded/refreshed.
* The page requests an update be immediately activated via the `SwUpdate` service.
### Service worker updates
The Angular service worker is a small script that runs in web browsers.
From time to time, the service worker will be updated with bug
fixes and feature improvements.
The Angular service worker is downloaded when the app is first opened
and when the app is accessed after a period of inactivity. If the
service worker has changed, the service worker will be updated in the background.
Most updates to the Angular service worker are transparent to the
app&mdash;the old caches are still valid and content is still served
normally. However, occasionally a bugfix or feature in the Angular
service worker requires the invalidation of old caches. In this case,
the app will be refreshed transparently from the network.
## Debugging the Angular service worker
Occasionally, it may be necessary to examine the Angular service
worker in a running state to investigate issues or to ensure that
it is operating as designed. Browsers provide built-in tools for
debugging service workers and the Angular service worker itself
includes useful debugging features.
### Locating and analyzing debugging information
The Angular service worker exposes debugging information under
the `ngsw/` virtual directory. Currently, the single exposed URL
is `ngsw/state`. Here is an example of this debug page's contents:
```
NGSW Debug Info:
Driver state: NORMAL ((nominal))
Latest manifest hash: eea7f5f464f90789b621170af5a569d6be077e5c
Last update check: never
=== Version eea7f5f464f90789b621170af5a569d6be077e5c ===
Clients: 7b79a015-69af-4d3d-9ae6-95ba90c79486, 5bc08295-aaf2-42f3-a4cc-9e4ef9100f65
=== Idle Task Queue ===
Last update tick: 1s496u
Last update run: never
Task queue:
* init post-load (update, cleanup)
Debug log:
```
#### Driver state
The first line indicates the driver state:
```
Driver state: NORMAL ((nominal))
```
`NORMAL` indicates that the service worker is operating normally and is not in a degraded state.
There are two possible degraded states:
* `EXISTING_CLIENTS_ONLY`: the service worker does not have a
clean copy of the latest known version of the app. Older cached
versions are safe to use, so existing tabs continue to run from
cache, but new loads of the app will be served from the network.
* `SAFE_MODE`: the service worker cannot guarantee the safety of
using cached data. Either an unexpected error occurred or all c
ached versions are invalid. All traffic will be served from the
network, running as little service worker code as possible.
In both cases, the parenthetical annotation provides the
error that caused the service worker to enter the degraded state.
#### Latest manifest hash
```
Latest manifest hash: eea7f5f464f90789b621170af5a569d6be077e5c
```
This is the SHA1 hash of the most up-to-date version of the app that the service worker knows about.
#### Last update check
```
Last update check: never
```
This indicates the last time the service worker checked for a new version, or update, of the app. `never` indicates that the service worker has never checked for an update.
In this example debug file, the update check is currently scheduled, as explained the next section.
#### Version
```
=== Version eea7f5f464f90789b621170af5a569d6be077e5c ===
Clients: 7b79a015-69af-4d3d-9ae6-95ba90c79486, 5bc08295-aaf2-42f3-a4cc-9e4ef9100f65
```
In this example, the service worker has one version of the app cached and
being used to serve two different tabs. Note that this version hash
is the "latest manifest hash" listed above. Both clients are on the
latest version. Each client is listed by its ID from the `Clients`
API in the browser.
#### Idle task queue
```
=== Idle Task Queue ===
Last update tick: 1s496u
Last update run: never
Task queue:
* init post-load (update, cleanup)
```
The Idle Task Queue is the queue of all pending tasks that happen
in the background in the service worker. If there are any tasks
in the queue, they are listed with a description. In this example,
the service worker has one such task scheduled, a post-initialization
operation involving an update check and cleanup of stale caches.
The last update tick/run counters give the time since specific
events happened related to the idle queue. The "Last update run"
counter shows the last time idle tasks were actually executed.
"Last update tick" shows the time since the last event after
which the queue might be processed.
#### Debug log
```
Debug log:
```
Errors that occur within the service worker will be logged here.
### Developer Tools
Browsers such as Chrome provide developer tools for interacting
with service workers. Such tools can be powerful when used properly,
but there are a few things to keep in mind.
* When using developer tools, the service worker is kept running
in the background and never restarts. For the Angular service
worker, this means that update checks to the app will generally not happen.
* If you look in the Cache Storage viewer, the cache is frequently
out of date. Right click the Cache Storage title and refresh the caches.
Stopping and starting the service worker in the Service Worker
pane triggers a check for updates.
## Fail-safe
Like any complex system, bugs or broken configurations can cause
the Angular service worker to act in unforeseen ways. While its
design attempts to minimize the impact of such problems, the
Angular service worker contains a failsafe mechanism in case
an administrator ever needs to deactivate the service worker quickly.
To deactivate the service worker, remove or rename the
`ngsw-config.json` file. When the service worker's request
for `ngsw.json` returns a `404`, then the service worker
removes all of its caches and de-registers itself,
essentially self-destructing.

View File

@ -0,0 +1,191 @@
# Getting started
Beginning in Angular 5.0.0, you can easily enable Angular service worker support in any CLI project. This document explains how to enable Angular service worker support in new and existing projects. It then uses a simple example to show you a service worker in action, demonstrating loading and basic caching.
See the <live-example></live-example>.
## Adding a service worker to a new application
If you're generating a new CLI project, you can use the CLI to set up the Angular service worker as part of creating the project. To do so, add the `--service-worker` flag to the `ng new` command:
```sh
ng new my-project --service-worker
```
The `--service-worker` flag takes care of configuring your app to
use service workers by adding the `service-worker` package along
with setting up the necessary files to support service workers.
For information on the details, see the following section
which covers the process in detail as it shows you how to add a
service worker manually to an existing app.
## Adding a service worker to an existing app
To add a service worker to an existing app:
1. Add the service worker package.
2. Enable service worker build support in the CLI.
3. Import and register the service worker.
4. Create the service worker configuration file, which specifies the caching behaviors and other settings.
5. Build the project.
### Step 1: Add the service worker package
Add the package `@angular/service-worker`, using the yarn utility as shown here:
```sh
yarn add @angular/service-worker
```
### Step 2: Enable service worker build support in the CLI
To enable the Angular service worker, the CLI must generate an Angular service worker manifest at build time. To cause the CLI to generate the manifest for an existing project, set the `serviceWorker` flag to `true` in the project's `.angular-cli.json` file as shown here:
```sh
ng set apps.0.serviceWorker=true
```
### Step 3: Import and register the service worker
To import and register the Angular service worker:
At the top of the root module, `src/app/app.module.ts`, import `ServiceWorkerModule` and `environment`.
<code-example path="service-worker-getstart/src/app/app.module.ts" linenums="false" title="src/app/app.module.ts" region="sw-import"> </code-example>
Add `ServiceWorkerModule` to the `@NgModule` `imports` array. Use the `register()` helper to take care of registering the service worker, taking care to disable the service worker when not running in production mode.
<code-example path="service-worker-getstart/src/app/app.module.ts" linenums="false" title="src/app/app.module.ts" region="sw-module"> </code-example>
The file `ngsw-worker.js` is the name of the prebuilt service worker script, which the CLI copies into `dist/` to deploy along with your server.
### Step 4: Create the configuration file, `ngsw-config.json`
The Angular CLI needs a service worker configuration file, called `ngsw-config.json`. The configuration file controls how the service worker caches files and data
resources.
You can begin with the boilerplate version from the CLI, which configures sensible defaults for most applications.
Alternately, save the following as `src/ngsw-config.json`:
<code-example path="service-worker-getstart/src/ngsw-config.json" linenums="false" title="src/ngsw-config.json"> </code-example>
### Step 5: Build the project
Finally, build the project:
```sh
ng build --prod
```
The CLI project is now set up to use the Angular service worker.
## Service worker in action: a tour
This section demonstrates a service worker in action,
using an example application.
### Serving with `http-server`
As `ng serve` does not work with service workers, you must use a real HTTP server to test your project locally. It's a good idea to test on a dedicated port.
```sh
cd dist
http-server -p 8080
```
### Initial load
With the server running, you can point your browser at http://localhost:8080/. Your application should load normally.
**Tip:** When testing Angular service workers, it's a good idea to use an incognito or private window in your browser to ensure the service worker doesn't end up reading from a previous leftover state, which can cause unexpected behavior.
### Simulating a network issue
To simulate a network issue, disable network interaction for your application. In Chrome:
1. Select **Tools** > **Developer Tools** (from the Chrome menu located at the top right corner).
2. Go to the **Network tab**.
3. Check the **Offline box**.
<figure>
<img src="generated/images/guide/service-worker/offline-checkbox.png" alt="The offline checkbox in the Network tab is checked">
</figure>
Now the app has no access to network interaction.
For applications that do not use the Angular service worker, refreshing now would display Chrome's Internet disconnected page that says "There is no Internet connection".
With the addition of an Angular service worker, the application behavior changes. On a refresh, the page loads normally.
If you look at the Network tab, you can verify that the service worker is active.
<figure>
<img src="generated/images/guide/service-worker/sw-active.png" alt="Requests are marked as from ServiceWorker">
</figure>
Notice that under the "Size" column, the requests state is `(from ServiceWorker)`. This means that the resources are not being loaded from the network. Instead, they are being loaded from the service worker's cache.
### What's being cached?
Notice that all of the files the browser needs to render this application are cached. The `ngsw-config.json` boilerplate configuration is set up to cache the specific resources used by the CLI:
* `index.html`.
* `favicon.ico`.
* Build artifacts (JS and CSS bundles).
* Anything under `assets`.
### Making changes to your application
Now that you've seen how service workers cache your application, the
next step is understanding how updates work.
1. If you're testing in an incognito window, open a second blank tab. This will keep the incognito and the cache state alive during your test.
2. Close the application tab, but not the window. This should also close the Developer Tools.
3. Shut down `http-server`.
4. Next, make a change to the application, and watch the service worker install the update.
5. Open `src/app/app.component.html` for editing.
6. Change the text `Welcome to {{title}}!` to `Bienvenue à {{title}}!`.
7. Build and run the server again:
```sh
ng build --prod
cd dist
http-server -p 8080
```
### Updating your application in the browser
Now look at how the browser and service worker handle the updated application.
1. Open http://localhost:8080 again in the same window. What happens?
<figure>
<img src="generated/images/guide/service-worker/welcome-msg-en.png" alt="It still says Welcome to Service Workers!">
</figure>
What went wrong? Nothing, actually. The Angular service worker is doing its job and serving the version of the application that it has **installed**, even though there is an update available. In the interest of speed, the service worker doesn't wait to check for updates before it serves the application that it has cached.
If you look at the `http-server` logs, you can see the service worker requesting `/ngsw.json`. This is how the service worker checks for updates.
2. Refresh the page.
![The text has changed to say "Bienvenue à app!"](generated/images/guide/service-worker/welcome-msg-fr.png)
<figure>
<img src="generated/images/guide/service-worker/welcome-msg-fr.png" alt="The text has changed to say Bienvenue à app!">
</figure>
The service worker installed the updated version of your app *in the background*, and the next time the page is loaded or reloaded, the service worker switches to the latest version.

View File

@ -0,0 +1,48 @@
# Introduction to Angular service workers
Service workers augment the traditional web deployment model and empower applications to deliver a user experience with the reliability and performance on par with natively-installed code.
At its simplest, a service worker is a script that runs in the web browser and manages caching for an application.
Service workers function as a network proxy. They intercept all outgoing HTTP requests made by the application and can choose how to respond to them. For example, they can query a local cache and deliver a cached response if one is available. Proxying isn't limited to requests made through programmatic APIs, such as `fetch`; it also includes resources referenced in HTML and even the initial request to `index.html`. Service worker-based caching is thus completely programmable and doesn't rely on server-specified caching headers.
Unlike the other scripts that make up an application, such as the Angular app bundle, the service worker is preserved after the user closes the tab. The next time that browser loads the application, the service worker loads first, and can intercept every request for resources to load the application. If the service worker is designed to do so, it can *completely satisfy the loading of the application, without the need for the network*.
Even across a fast reliable network, round-trip delays can introduce significant latency when loading the application. Using a service worker to reduce dependency on the network can significantly improve the user experience.
## Service workers in Angular
Angular applications, as single-page applications, are in a prime position to benefit from the advantages of service workers. Starting with version 5.0.0, Angular ships with a service worker implementation. Angular developers can take advantage of this service worker and benefit from the increased reliability and performance it provides, without needing to code against low-level APIs.
Angular's service worker is designed to optimize the end user experience of using an application over a slow or unreliable network connection, while also minimizing the risks of serving outdated content.
The Angular service worker's behavior follows that design goal:
* Caching an application is like installing a native application. The application is cached as one unit, and all files update together.
* A running application continues to run with the same version of all files. It does not suddenly start receiving cached files from a newer version, which are likely incompatible.
* When users refresh the application, they see the latest fully cached version. New tabs load the latest cached code.
* Updates happen in the background, relatively quickly after changes are published. The previous version of the application is served until an update is installed and ready.
* The service worker conserves bandwidth when possible. Resources are only downloaded if they've changed.
To support these behaviors, the Angular service worker loads a *manifest* file from the server. The manifest describes the resources to cache and includes hashes of every file's contents. When an update to the application is deployed, the contents of the manifest change, informing the service worker that a new version of the application should be downloaded and cached. This manifest is generated from a user-provided configuration file called `ngsw-config.json`, by using a build tool such as the Angular CLI.
Installing the Angular service worker is as simple as including an `NgModule`. In addition to registering the Angular service worker with the browser, this also makes a few services available for injection which interact with the service worker and can be used to control it. For example, an application can ask to be notified when a new update becomes available, or an application can ask the service worker to check the server for available updates.
## Prerequisites
To use Angular service workers, you must have the following Angular and CLI versions:
* Angular 5.0.0 or later.
* Angular CLI 1.6.0 or later.
Your application must run in a web browser that supports service workers. Currently, the latest versions of Chrome and Firefox are supported. To learn about other browsers that are service worker ready, see the [Can I Use](http://caniuse.com/#feat=serviceworkers) page.
## Related resources
For more information about service workers in general, see [Service Workers: an Introduction](https://developers.google.com/web/fundamentals/primers/service-workers/).
For more information about browser support, see the [browser support](https://developers.google.com/web/fundamentals/primers/service-workers/#browser_support) section of [Service Workers: an Introduction](https://developers.google.com/web/fundamentals/primers/service-workers/), Jake Archibald's [Is Serviceworker ready?](https://jakearchibald.github.io/isserviceworkerready/), and
[Can I Use](http://caniuse.com/#feat=serviceworkers).
The remainder of this Angular documentation specifically addresses the Angular implementation of service workers.

View File

@ -625,7 +625,7 @@ that does the opposite of `NgIf`.
`UnlessDirective` displays the content when the condition is ***false***.
<code-example path="structural-directives/src/app/app.component.html" linenums="false" title="src/app/app.component.html (myUnless-1)" region="myUnless-1">
<code-example path="structural-directives/src/app/app.component.html" linenums="false" title="src/app/app.component.html (appUnless-1)" region="appUnless-1">
</code-example>
@ -650,14 +650,14 @@ Here's how you might begin:
The directive's _selector_ is typically the directive's **attribute name** in square brackets, `[myUnless]`.
The directive's _selector_ is typically the directive's **attribute name** in square brackets, `[appUnless]`.
The brackets define a CSS
<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors" title="MDN: Attribute selectors">attribute selector</a>.
The directive _attribute name_ should be spelled in _lowerCamelCase_ and begin with a prefix.
Don't use `ng`. That prefix belongs to Angular.
Pick something short that fits you or your company.
In this example, the prefix is `my`.
In this example, the prefix is `app`.
The directive _class_ name ends in `Directive` per the [style guide](guide/styleguide#02-03 "Angular Style Guide").
@ -685,10 +685,10 @@ You inject both in the directive constructor as private variables of the class.
### The _myUnless_ property
### The _appUnless_ property
The directive consumer expects to bind a true/false condition to `[myUnless]`.
That means the directive needs a `myUnless` property, decorated with `@Input`
The directive consumer expects to bind a true/false condition to `[appUnless]`.
That means the directive needs an `appUnless` property, decorated with `@Input`
<div class="l-sub-section">
@ -708,8 +708,8 @@ Read about `@Input` in the [_Template Syntax_](guide/template-syntax#inputs-outp
Angular sets the `myUnless` property whenever the value of the condition changes.
Because the `myUnless` property does work, it needs a setter.
Angular sets the `appUnless` property whenever the value of the condition changes.
Because the `appUnless` property does work, it needs a setter.
* If the condition is falsy and the view hasn't been created previously,
tell the _view container_ to create the _embedded view_ from the template.
@ -717,7 +717,7 @@ tell the _view container_ to create the _embedded view_ from the template.
* If the condition is truthy and the view is currently displayed,
clear the container which also destroys the view.
Nobody reads the `myUnless` property so it doesn't need a getter.
Nobody reads the `appUnless` property so it doesn't need a getter.
The completed directive code looks like this:
@ -733,7 +733,7 @@ Add this directive to the `declarations` array of the AppModule.
Then create some HTML to try it.
<code-example path="structural-directives/src/app/app.component.html" linenums="false" title="src/app/app.component.html (myUnless)" region="myUnless">
<code-example path="structural-directives/src/app/app.component.html" linenums="false" title="src/app/app.component.html (appUnless)" region="appUnless">
</code-example>

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -241,6 +241,12 @@
"UI Components": {
"order": 4,
"resources": {
"IgniteUIforAngular": {
"desc": "Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps.",
"rev": true,
"title": "Ignite UI for Angular",
"url": "https://www.infragistics.com/products/ignite-ui-angular"
},
"DevExtreme": {
"desc": "50+ UI components including data grid, pivot grid, scheduler, charts, editors, maps and other multi-purpose controls for creating highly responsive web applications for touch devices and traditional desktops.",
"rev": true,
@ -633,6 +639,13 @@
"rev": true,
"title": "Formation JavaScript (French)",
"url": "https://formationjavascript.com/formation-angular/"
},
"wao": {
"desc": "Onsite Angular Training delivered by We Are One Sàrl in Switzerland",
"logo": "https://weareone.ch/wordpress/wao-content/uploads/2014/12/logo_200_2x.png",
"rev": true,
"title": "We Are One Sàrl",
"url": "https://weareone.ch/courses/angular/"
}
}
}

View File

@ -276,6 +276,38 @@
"title": "Routing & Navigation",
"tooltip": "Discover the basics of screen navigation with the Angular Router."
},
{
"title": "Service Workers",
"tooltip": "Angular service workers: Controlling caching of application resources.",
"children": [
{
"url": "guide/service-worker-intro",
"title": "Introduction",
"tooltip": "Angular's implementation of service workers improves user experience with slow or unreliable network connectivity."
},
{
"url": "guide/service-worker-getstart",
"title": "Getting Started",
"tooltip": "Enabling the service worker in a CLI project and observing behavior in the browser."
},
{
"url": "guide/service-worker-comm",
"title": "Communication",
"tooltip": "Services that enable you to interact with an Angular service worker."
},
{
"url": "guide/service-worker-devops",
"title": "Service Workers in Production",
"tooltip": "Information about running applications with service workers, including application update management, debugging, and killing applications."
},
{
"url": "guide/service-worker-configref",
"title": "Reference: Configuration File",
"tooltip": "The ngsw-config.json configuration file controls service worker caching behavior."
}
]
},
{
"url": "guide/testing",
"title": "Testing",

View File

@ -3,7 +3,7 @@
The Tour of Heroes `HeroesComponent` is currently getting and displaying fake data.
After the refactoring in this tutorial, `HeroesComponent` will be lean and focused on supporting the view.
It will also be easier to unit-test with a mock services.
It will also be easier to unit-test with a mock service.
## Why services

View File

@ -275,6 +275,7 @@ The `HeroDetailsComponent` displays details of a selected hero.
At the moment the `HeroDetailsComponent` is only visible at the bottom of the `HeroesComponent`
The user should be able to get to these details in three ways.
1. By clicking a hero in the dashboard.
1. By clicking a hero in the heroes list.
1. By pasting a "deep link" URL into the browser address bar that identifies the hero to display.

View File

@ -28,7 +28,7 @@ describe('Api pages', function() {
it('should show readonly properties as getters', () => {
const page = new ApiPage('api/common/http/HttpRequest');
expect(page.getOverview('class').getText()).toContain('get body: T|null');
expect(page.getOverview('class').getText()).toContain('get body: T | null');
});
it('should not show parenthesis for getters', () => {

View File

@ -80,8 +80,6 @@ describe('site App', function() {
});
});
// TODO(https://github.com/angular/angular/issues/19785): Activate this again
// once it is no more flaky.
describe('google analytics', () => {
it('should call ga with initial URL', done => {

View File

@ -29,10 +29,7 @@ export class SitePage {
locationPath() { return browser.executeScript('return document.location.pathname') as promise.Promise<string>; }
navigateTo(pageUrl = '') {
return browser.get('/' + pageUrl)
// We need to tell the index.html not to load the real analytics library
// See the GA snippet in index.html
.then(() => browser.executeScript('sessionStorage.setItem("__e2e__", true);'));
return browser.get('/' + pageUrl);
}
getDocViewerText() {

View File

@ -59,7 +59,7 @@
"~~update-webdriver": "webdriver-manager update --standalone false --gecko false"
},
"engines": {
"node": ">=6.9.5 <7.0.0",
"node": ">=8.9.1 <9.0.0",
"yarn": ">=1.0.2 <2.0.0"
},
"private": true,
@ -100,7 +100,7 @@
"cross-spawn": "^5.1.0",
"css-selector-parser": "^1.3.0",
"dgeni": "^0.4.7",
"dgeni-packages": "0.22.0",
"dgeni-packages": "0.22.1",
"entities": "^1.1.1",
"eslint": "^3.19.0",
"eslint-plugin-jasmine": "^2.2.0",

View File

@ -1,3 +1,21 @@
{
"aio":{"master":{"change":"application","gzip7":{"inline":925,"main":119519,"polyfills":11863},"gzip9":{"inline":925,"main":119301,"polyfills":11861},"uncompressed":{"inline":1533,"main":486493,"polyfills":37068}}}
"aio": {
"master": {
"gzip7": {
"inline": 925,
"main": 119519,
"polyfills": 11863
},
"gzip9": {
"inline": 925,
"main": 119301,
"polyfills": 11861
},
"uncompressed": {
"inline": 1533,
"main": 486493,
"polyfills": 37068
}
}
}
}

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
set -eu -o pipefail

View File

@ -34,11 +34,13 @@
<!-- Google Analytics -->
<script>
// Note this is a customised version of the GA tracking snippet to aid e2e testing
// See the bit between /**/.../**/
// Note this is a customised version of the GA tracking snippet
// See the comments below for more info
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;/**/i.sessionStorage.__e2e__||/**/m.parentNode.insertBefore(a,m)
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;
~i.name.indexOf('NG_DEFER_BOOTSTRAP')|| // only load library if not running e2e tests
m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
</script>
<!-- End Google Analytics -->

View File

@ -62,7 +62,7 @@ class ExampleZipper {
let exampleZipName;
const exampleType = this._getExampleType(path.join(sourceDirName, relativeDirName));
if (relativeDirName.indexOf('/') !== -1) { // Special example
exampleZipName = relativeDirName.split('/')[0];
exampleZipName = relativeDirName.split('/').join('-');
} else {
exampleZipName = jsonFileName.replace(/(plnkr|zipper).json/, relativeDirName);
}

View File

@ -26,13 +26,13 @@
"zone.js": "^0.8.14"
},
"devDependencies": {
"@angular/cli": "1.5.0",
"@angular/cli": "1.5.4",
"@angular/compiler-cli": "^5.0.0",
"@angular/language-service": "^5.0.0",
"@types/jasmine": "~2.5.53",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~6.0.60",
"codelyzer": "~3.2.0",
"codelyzer": "^4.0.1",
"jasmine-core": "~2.6.2",
"jasmine-spec-reporter": "~4.1.0",
"karma": "~1.7.0",

View File

@ -24,6 +24,7 @@
"@angular/platform-browser-dynamic": "~5.0.0",
"@angular/platform-server": "~5.0.0",
"@angular/router": "~5.0.0",
"@angular/service-worker": "~5.0.0",
"@angular/upgrade": "~5.0.0",
"@nguniversal/express-engine": "^1.0.0-beta.3",
"@nguniversal/module-map-ngfactory-loader": "^1.0.0-beta.3",

View File

@ -160,6 +160,12 @@
dependencies:
tslib "^1.7.1"
"@angular/service-worker@~5.0.0":
version "5.0.5"
resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-5.0.5.tgz#221d6ae41309c6660609c29aea3de12f1fce474a"
dependencies:
tslib "^1.7.1"
"@angular/upgrade@~5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@angular/upgrade/-/upgrade-5.0.0.tgz#d2c4bc83e7dcbe4ad3b1b7e845c6cba7379d779f"

View File

@ -2287,9 +2287,9 @@ devtools-timeline-model@1.1.6:
chrome-devtools-frontend "1.0.401423"
resolve "1.1.7"
dgeni-packages@0.22.0:
version "0.22.0"
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.22.0.tgz#7ed07af9074f6547847256c1a65b488a5a17ad03"
dgeni-packages@0.22.1:
version "0.22.1"
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.22.1.tgz#c4587a765689c4c9d48ed661517ed2249403bfb2"
dependencies:
canonical-path "0.0.2"
catharsis "^0.8.1"

View File

@ -22,7 +22,7 @@ local_repository(
git_repository(
name = "io_bazel_rules_sass",
remote = "https://github.com/bazelbuild/rules_sass.git",
tag = "0.0.2",
tag = "0.0.3",
)
load("@io_bazel_rules_sass//sass:sass.bzl", "sass_repositories")

View File

@ -20,6 +20,6 @@
},
"scripts": {
"postinstall": "ngc -p angular.tsconfig.json",
"test": "bazel build ..."
"test": "bazel build ... --noshow_progress"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "5.1.0-beta.2",
"version": "5.1.0",
"private": true,
"branchPattern": "2.0.*",
"description": "Angular - a web framework for modern web apps",
@ -8,8 +8,7 @@
"bugs": "https://github.com/angular/angular/issues",
"license": "MIT",
"engines": {
"node": ">=6.9.5 <7.0.0",
"npm": ">=3.10.7 <4.0.0",
"node": ">=8.9.1 <9.0.0",
"yarn": ">=1.0.2 <2.0.0"
},
"repository": {
@ -32,7 +31,7 @@
"fsevents": "1.1.2"
},
"devDependencies": {
"@bazel/typescript": "0.3.1",
"@bazel/typescript": "0.3.2",
"@types/angularjs": "1.5.14-alpha",
"@types/base64-js": "1.2.5",
"@types/chokidar": "1.7.3",

View File

@ -8,7 +8,7 @@
import {AnimationMetadata, AnimationMetadataType, AnimationOptions, ɵStyleData} from '@angular/animations';
import {AnimationDriver} from '../render/animation_driver';
import {normalizeStyles} from '../util';
import {ENTER_CLASSNAME, LEAVE_CLASSNAME, normalizeStyles} from '../util';
import {Ast} from './animation_ast';
import {buildAnimationAst} from './animation_ast_builder';
@ -39,7 +39,8 @@ export class Animation {
const errors: any = [];
subInstructions = subInstructions || new ElementInstructionMap();
const result = buildAnimationTimelines(
this._driver, element, this._animationAst, start, dest, options, subInstructions, errors);
this._driver, element, this._animationAst, ENTER_CLASSNAME, LEAVE_CLASSNAME, start, dest,
options, subInstructions, errors);
if (errors.length) {
const errorMessage = `animation building failed:\n${errors.join("\n")}`;
throw new Error(errorMessage);

View File

@ -60,10 +60,6 @@ export function buildAnimationAst(
return new AnimationAstBuilderVisitor(driver).build(metadata, errors);
}
const LEAVE_TOKEN = ':leave';
const LEAVE_TOKEN_REGEX = new RegExp(LEAVE_TOKEN, 'g');
const ENTER_TOKEN = ':enter';
const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
const ROOT_SELECTOR = '';
export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
@ -478,9 +474,8 @@ function normalizeSelector(selector: string): [string, boolean] {
selector = selector.replace(SELF_TOKEN_REGEX, '');
}
selector = selector.replace(ENTER_TOKEN_REGEX, ENTER_SELECTOR)
.replace(LEAVE_TOKEN_REGEX, LEAVE_SELECTOR)
.replace(/@\*/g, NG_TRIGGER_SELECTOR)
// the :enter and :leave selectors are filled in at runtime during timeline building
selector = selector.replace(/@\*/g, NG_TRIGGER_SELECTOR)
.replace(/@\w+/g, match => NG_TRIGGER_SELECTOR + '-' + match.substr(1))
.replace(/:animating/g, NG_ANIMATING_SELECTOR);

View File

@ -15,6 +15,10 @@ import {AnimationTimelineInstruction, createTimelineInstruction} from './animati
import {ElementInstructionMap} from './element_instruction_map';
const ONE_FRAME_IN_MILLISECONDS = 1;
const ENTER_TOKEN = ':enter';
const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
const LEAVE_TOKEN = ':leave';
const LEAVE_TOKEN_REGEX = new RegExp(LEAVE_TOKEN, 'g');
/*
* The code within this file aims to generate web-animations-compatible keyframes from Angular's
@ -102,19 +106,23 @@ const ONE_FRAME_IN_MILLISECONDS = 1;
*/
export function buildAnimationTimelines(
driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>,
startingStyles: ɵStyleData = {}, finalStyles: ɵStyleData = {}, options: AnimationOptions,
enterClassName: string, leaveClassName: string, startingStyles: ɵStyleData = {},
finalStyles: ɵStyleData = {}, options: AnimationOptions,
subInstructions?: ElementInstructionMap, errors: any[] = []): AnimationTimelineInstruction[] {
return new AnimationTimelineBuilderVisitor().buildKeyframes(
driver, rootElement, ast, startingStyles, finalStyles, options, subInstructions, errors);
driver, rootElement, ast, enterClassName, leaveClassName, startingStyles, finalStyles,
options, subInstructions, errors);
}
export class AnimationTimelineBuilderVisitor implements AstVisitor {
buildKeyframes(
driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>,
startingStyles: ɵStyleData, finalStyles: ɵStyleData, options: AnimationOptions,
subInstructions?: ElementInstructionMap, errors: any[] = []): AnimationTimelineInstruction[] {
enterClassName: string, leaveClassName: string, startingStyles: ɵStyleData,
finalStyles: ɵStyleData, options: AnimationOptions, subInstructions?: ElementInstructionMap,
errors: any[] = []): AnimationTimelineInstruction[] {
subInstructions = subInstructions || new ElementInstructionMap();
const context = new AnimationTimelineContext(driver, rootElement, subInstructions, errors, []);
const context = new AnimationTimelineContext(
driver, rootElement, subInstructions, enterClassName, leaveClassName, errors, []);
context.options = options;
context.currentTimeline.setStyles([startingStyles], null, context.errors, options);
@ -445,8 +453,9 @@ export class AnimationTimelineContext {
constructor(
private _driver: AnimationDriver, public element: any,
public subInstructions: ElementInstructionMap, public errors: any[],
public timelines: TimelineBuilder[], initialTimeline?: TimelineBuilder) {
public subInstructions: ElementInstructionMap, private _enterClassName: string,
private _leaveClassName: string, public errors: any[], public timelines: TimelineBuilder[],
initialTimeline?: TimelineBuilder) {
this.currentTimeline = initialTimeline || new TimelineBuilder(this._driver, element, 0);
timelines.push(this.currentTimeline);
}
@ -499,8 +508,8 @@ export class AnimationTimelineContext {
AnimationTimelineContext {
const target = element || this.element;
const context = new AnimationTimelineContext(
this._driver, target, this.subInstructions, this.errors, this.timelines,
this.currentTimeline.fork(target, newTime || 0));
this._driver, target, this.subInstructions, this._enterClassName, this._leaveClassName,
this.errors, this.timelines, this.currentTimeline.fork(target, newTime || 0));
context.previousNode = this.previousNode;
context.currentAnimateTimings = this.currentAnimateTimings;
@ -555,6 +564,8 @@ export class AnimationTimelineContext {
results.push(this.element);
}
if (selector.length > 0) { // if :self is only used then the selector is empty
selector = selector.replace(ENTER_TOKEN_REGEX, '.' + this._enterClassName);
selector = selector.replace(LEAVE_TOKEN_REGEX, '.' + this._leaveClassName);
const multi = limit != 1;
let elements = this._driver.query(this.element, selector, multi);
if (limit !== 0) {

View File

@ -37,7 +37,8 @@ export class AnimationTransitionFactory {
build(
driver: AnimationDriver, element: any, currentState: any, nextState: any,
currentOptions?: AnimationOptions, nextOptions?: AnimationOptions,
enterClassName: string, leaveClassName: string, currentOptions?: AnimationOptions,
nextOptions?: AnimationOptions,
subInstructions?: ElementInstructionMap): AnimationTransitionInstruction {
const errors: any[] = [];
@ -55,8 +56,8 @@ export class AnimationTransitionFactory {
const animationOptions = {params: {...transitionAnimationParams, ...nextAnimationParams}};
const timelines = buildAnimationTimelines(
driver, element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions,
subInstructions, errors);
driver, element, this.ast.animation, enterClassName, leaveClassName, currentStateStyles,
nextStateStyles, animationOptions, subInstructions, errors);
if (errors.length) {
return createTransitionInstruction(

View File

@ -13,6 +13,7 @@ import {buildAnimationTimelines} from '../dsl/animation_timeline_builder';
import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instruction';
import {ElementInstructionMap} from '../dsl/element_instruction_map';
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
import {ENTER_CLASSNAME, LEAVE_CLASSNAME} from '../util';
import {AnimationDriver} from './animation_driver';
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
@ -55,7 +56,8 @@ export class TimelineAnimationEngine {
if (ast) {
instructions = buildAnimationTimelines(
this._driver, element, ast, {}, {}, options, EMPTY_INSTRUCTION_MAP, errors);
this._driver, element, ast, ENTER_CLASSNAME, LEAVE_CLASSNAME, {}, {}, options,
EMPTY_INSTRUCTION_MAP, errors);
instructions.forEach(inst => {
const styles = getOrSetAsInMap(autoStylesMap, inst.element, {});
inst.postStyleProps.forEach(prop => styles[prop] = null);

View File

@ -13,7 +13,7 @@ import {AnimationTransitionInstruction} from '../dsl/animation_transition_instru
import {AnimationTrigger} from '../dsl/animation_trigger';
import {ElementInstructionMap} from '../dsl/element_instruction_map';
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_ANIMATING_SELECTOR, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util';
import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_ANIMATING_SELECTOR, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, iteratorToArray, setStyles} from '../util';
import {AnimationDriver} from './animation_driver';
import {getBodyNode, getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
@ -22,6 +22,8 @@ const QUEUED_CLASSNAME = 'ng-animate-queued';
const QUEUED_SELECTOR = '.ng-animate-queued';
const DISABLED_CLASSNAME = 'ng-animate-disabled';
const DISABLED_SELECTOR = '.ng-animate-disabled';
const STAR_CLASSNAME = 'ng-star-inserted';
const STAR_SELECTOR = '.ng-star-inserted';
const EMPTY_PLAYER_ARRAY: TransitionAnimationPlayer[] = [];
const NULL_REMOVAL_STATE: ElementAnimationState = {
@ -714,10 +716,12 @@ export class TransitionAnimationEngine {
return () => {};
}
private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) {
private _buildInstruction(
entry: QueueInstruction, subTimelines: ElementInstructionMap, enterClassName: string,
leaveClassName: string) {
return entry.transition.build(
this.driver, entry.element, entry.fromState.value, entry.toState.value,
entry.fromState.options, entry.toState.options, subTimelines);
this.driver, entry.element, entry.fromState.value, entry.toState.value, enterClassName,
leaveClassName, entry.fromState.options, entry.toState.options, subTimelines);
}
destroyInnerAnimations(containerElement: any) {
@ -798,6 +802,13 @@ export class TransitionAnimationEngine {
this.newHostElements.clear();
}
if (this.totalAnimations && this.collectedEnterElements.length) {
for (let i = 0; i < this.collectedEnterElements.length; i++) {
const elm = this.collectedEnterElements[i];
addClass(elm, STAR_CLASSNAME);
}
}
if (this._namespaceList.length &&
(this.totalQueuedPlayers || this.collectedLeaveElements.length)) {
const cleanupFns: Function[] = [];
@ -862,37 +873,57 @@ export class TransitionAnimationEngine {
});
const bodyNode = getBodyNode();
const allEnterNodes: any[] = this.collectedEnterElements.length ?
this.collectedEnterElements.filter(createIsRootFilterFn(this.collectedEnterElements)) :
[];
const allTriggerElements = Array.from(this.statesByElement.keys());
const enterNodeMap = buildRootMap(allTriggerElements, this.collectedEnterElements);
// this must occur before the instructions are built below such that
// the :enter queries match the elements (since the timeline queries
// are fired during instruction building).
for (let i = 0; i < allEnterNodes.length; i++) {
addClass(allEnterNodes[i], ENTER_CLASSNAME);
}
const enterNodeMapIds = new Map<any, string>();
let i = 0;
enterNodeMap.forEach((nodes, root) => {
const className = ENTER_CLASSNAME + i++;
enterNodeMapIds.set(root, className);
nodes.forEach(node => addClass(node, className));
});
const allLeaveNodes: any[] = [];
const mergedLeaveNodes = new Set<any>();
const leaveNodesWithoutAnimations = new Set<any>();
for (let i = 0; i < this.collectedLeaveElements.length; i++) {
const element = this.collectedLeaveElements[i];
const details = element[REMOVAL_FLAG] as ElementAnimationState;
if (details && details.setForRemoval) {
addClass(element, LEAVE_CLASSNAME);
allLeaveNodes.push(element);
if (!details.hasAnimation) {
mergedLeaveNodes.add(element);
if (details.hasAnimation) {
this.driver.query(element, STAR_SELECTOR, true).forEach(elm => mergedLeaveNodes.add(elm));
} else {
leaveNodesWithoutAnimations.add(element);
}
}
}
const leaveNodeMapIds = new Map<any, string>();
const leaveNodeMap = buildRootMap(allTriggerElements, Array.from(mergedLeaveNodes));
leaveNodeMap.forEach((nodes, root) => {
const className = LEAVE_CLASSNAME + i++;
leaveNodeMapIds.set(root, className);
nodes.forEach(node => addClass(node, className));
});
cleanupFns.push(() => {
allEnterNodes.forEach(element => removeClass(element, ENTER_CLASSNAME));
allLeaveNodes.forEach(element => {
removeClass(element, LEAVE_CLASSNAME);
this.processLeaveNode(element);
enterNodeMap.forEach((nodes, root) => {
const className = enterNodeMapIds.get(root) !;
nodes.forEach(node => removeClass(node, className));
});
leaveNodeMap.forEach((nodes, root) => {
const className = leaveNodeMapIds.get(root) !;
nodes.forEach(node => removeClass(node, className));
});
allLeaveNodes.forEach(element => { this.processLeaveNode(element); });
});
const allPlayers: TransitionAnimationPlayer[] = [];
@ -909,7 +940,10 @@ export class TransitionAnimationEngine {
return;
}
const instruction = this._buildInstruction(entry, subTimelines) !;
const leaveClassName = leaveNodeMapIds.get(element) !;
const enterClassName = enterNodeMapIds.get(element) !;
const instruction =
this._buildInstruction(entry, subTimelines, enterClassName, leaveClassName) !;
if (instruction.errors && instruction.errors.length) {
erroneousTransitions.push(instruction);
return;
@ -973,18 +1007,6 @@ export class TransitionAnimationEngine {
this.reportError(errors);
}
// these can only be detected here since we have a map of all the elements
// that have animations attached to them... We use a set here in the event
// multiple enter captures on the same element were caught in different
// renderer namespaces (e.g. when a @trigger was on a host binding that had *ngIf)
const enterNodesWithoutAnimations = new Set<any>();
for (let i = 0; i < allEnterNodes.length; i++) {
const element = allEnterNodes[i];
if (!subTimelines.has(element)) {
enterNodesWithoutAnimations.add(element);
}
}
const allPreviousPlayersMap = new Map<any, TransitionAnimationPlayer[]>();
// this map works to tell which element in the DOM tree is contained by
// which animation. Further down below this map will get populated once
@ -1022,8 +1044,9 @@ export class TransitionAnimationEngine {
});
// POST STAGE: fill the * styles
const [postStylesMap, allLeaveQueriedNodes] = cloakAndComputeStyles(
this.driver, leaveNodesWithoutAnimations, allPostStyleElements, AUTO_STYLE);
const postStylesMap = new Map<any, ɵStyleData>();
const allLeaveQueriedNodes = cloakAndComputeStyles(
postStylesMap, this.driver, leaveNodesWithoutAnimations, allPostStyleElements, AUTO_STYLE);
allLeaveQueriedNodes.forEach(node => {
if (replacePostStylesAsPre(node, allPreStyleElements, allPostStyleElements)) {
@ -1032,10 +1055,11 @@ export class TransitionAnimationEngine {
});
// PRE STAGE: fill the ! styles
const [preStylesMap] = allPreStyleElements.size ?
cloakAndComputeStyles(
this.driver, enterNodesWithoutAnimations, allPreStyleElements, PRE_STYLE) :
[new Map<any, ɵStyleData>()];
const preStylesMap = new Map<any, ɵStyleData>();
enterNodeMap.forEach((nodes, root) => {
cloakAndComputeStyles(
preStylesMap, this.driver, new Set(nodes), allPreStyleElements, PRE_STYLE);
});
replaceNodes.forEach(node => {
const post = postStylesMap.get(node);
@ -1505,12 +1529,11 @@ function cloakElement(element: any, value?: string) {
}
function cloakAndComputeStyles(
driver: AnimationDriver, elements: Set<any>, elementPropsMap: Map<any, Set<string>>,
defaultStyle: string): [Map<any, ɵStyleData>, any[]] {
valuesMap: Map<any, ɵStyleData>, driver: AnimationDriver, elements: Set<any>,
elementPropsMap: Map<any, Set<string>>, defaultStyle: string): any[] {
const cloakVals: string[] = [];
elements.forEach(element => cloakVals.push(cloakElement(element)));
const valuesMap = new Map<any, ɵStyleData>();
const failedElements: any[] = [];
elementPropsMap.forEach((props: Set<string>, element: any) => {
@ -1532,39 +1555,57 @@ function cloakAndComputeStyles(
// an index value for the closure (but instead just the value)
let i = 0;
elements.forEach(element => cloakElement(element, cloakVals[i++]));
return [valuesMap, failedElements];
return failedElements;
}
/*
Since the Angular renderer code will return a collection of inserted
nodes in all areas of a DOM tree, it's up to this algorithm to figure
out which nodes are roots.
out which nodes are roots for each animation @trigger.
By placing all nodes into a set and traversing upwards to the edge,
the recursive code can figure out if a clean path from the DOM node
to the edge container is clear. If no other node is detected in the
set then it is a root element.
This algorithm also keeps track of all nodes along the path so that
if other sibling nodes are also tracked then the lookup process can
skip a lot of steps in between and avoid traversing the entire tree
multiple times to the edge.
By placing each inserted node into a Set and traversing upwards, it
is possible to find the @trigger elements and well any direct *star
insertion nodes, if a @trigger root is found then the enter element
is placed into the Map[@trigger] spot.
*/
function createIsRootFilterFn(nodes: any): (node: any) => boolean {
function buildRootMap(roots: any[], nodes: any[]): Map<any, any[]> {
const rootMap = new Map<any, any[]>();
roots.forEach(root => rootMap.set(root, []));
if (nodes.length == 0) return rootMap;
const NULL_NODE = 1;
const nodeSet = new Set(nodes);
const knownRootContainer = new Set();
let isRoot: (node: any) => boolean;
isRoot = node => {
if (!node) return true;
if (nodeSet.has(node.parentNode)) return false;
if (knownRootContainer.has(node.parentNode)) return true;
if (isRoot(node.parentNode)) {
knownRootContainer.add(node);
return true;
const localRootMap = new Map<any, any>();
function getRoot(node: any): any {
if (!node) return NULL_NODE;
let root = localRootMap.get(node);
if (root) return root;
const parent = node.parentNode;
if (rootMap.has(parent)) { // ngIf inside @trigger
root = parent;
} else if (nodeSet.has(parent)) { // ngIf inside ngIf
root = NULL_NODE;
} else { // recurse upwards
root = getRoot(parent);
}
return false;
};
return isRoot;
localRootMap.set(node, root);
return root;
}
nodes.forEach(node => {
const root = getRoot(node);
if (root !== NULL_NODE) {
rootMap.get(root) !.push(node);
}
});
return rootMap;
}
const CLASSES_CACHE_KEY = '$$classes';

View File

@ -10,6 +10,7 @@ import {AnimationOptions, animate, state, style, transition} from '@angular/anim
import {AnimationTransitionInstruction} from '@angular/animations/browser/src/dsl/animation_transition_instruction';
import {AnimationTrigger} from '@angular/animations/browser/src/dsl/animation_trigger';
import {ENTER_CLASSNAME, LEAVE_CLASSNAME} from '../../src/util';
import {MockAnimationDriver} from '../../testing';
import {makeTrigger} from '../shared';
@ -228,7 +229,9 @@ function buildTransition(
const trans = trigger.matchTransition(fromState, toState) !;
if (trans) {
const driver = new MockAnimationDriver();
return trans.build(driver, element, fromState, toState, fromOptions, toOptions) !;
return trans.build(
driver, element, fromState, toState, ENTER_CLASSNAME, LEAVE_CLASSNAME, fromOptions,
toOptions) !;
}
return null;
}

View File

@ -32,7 +32,6 @@ export class AnimationGroupPlayer implements AnimationPlayer {
scheduleMicroTask(() => this._onFinish());
} else {
this.players.forEach(player => {
player.parentPlayer = this;
player.onDone(() => {
if (++doneCount >= total) {
this._onFinish();

View File

@ -9,7 +9,7 @@
"typescript": ">=2.4.2 <2.6"
},
"dependencies": {
"@bazel/typescript": "0.3.x",
"@bazel/typescript": "0.3.2",
"@types/node": "6.0.84"
},
"repository": {

View File

@ -73,7 +73,8 @@ def _ngc_tsconfig(ctx, files, srcs, **kwargs):
"allowEmptyCodegenFiles": True,
"enableSummariesForJit": True,
# FIXME: wrong place to de-dupe
"expectedOut": depset([o.path for o in expected_outs]).to_list()
"expectedOut": depset([o.path for o in expected_outs]).to_list(),
"preserveWhitespaces": False,
}
})

View File

@ -164,11 +164,19 @@ export function compile({allowNonHermeticReads, allDepsCompiledWithBazel = true,
}
return origBazelHostFileExist.call(bazelHost, fileName);
};
const origBazelHostShouldNameModule = bazelHost.shouldNameModule.bind(bazelHost);
bazelHost.shouldNameModule = (fileName: string) =>
origBazelHostShouldNameModule(fileName) || NGC_GEN_FILES.test(fileName);
const ngHost = ng.createCompilerHost({options: compilerOpts, tsHost: bazelHost});
ngHost.fileNameToModuleName = (importedFilePath: string, containingFilePath: string) =>
relativeToRootDirs(importedFilePath, compilerOpts.rootDirs).replace(EXT, '');
ngHost.fileNameToModuleName = (importedFilePath: string, containingFilePath: string) => {
if ((compilerOpts.module === ts.ModuleKind.UMD || compilerOpts.module === ts.ModuleKind.AMD) &&
ngHost.amdModuleName) {
return ngHost.amdModuleName({ fileName: importedFilePath } as ts.SourceFile);
}
return relativeToRootDirs(importedFilePath, compilerOpts.rootDirs).replace(EXT, '');
};
ngHost.toSummaryFileName = (fileName: string, referringSrcFileName: string) =>
ngHost.fileNameToModuleName(fileName, referringSrcFileName);
if (allDepsCompiledWithBazel) {

View File

@ -352,12 +352,10 @@ export class HttpClient {
// Figure out the headers.
let headers: HttpHeaders|undefined = undefined;
if (!!options.headers !== undefined) {
if (options.headers instanceof HttpHeaders) {
headers = options.headers;
} else {
headers = new HttpHeaders(options.headers);
}
if (options.headers instanceof HttpHeaders) {
headers = options.headers;
} else {
headers = new HttpHeaders(options.headers);
}
// Sort out parameters.
@ -371,7 +369,7 @@ export class HttpClient {
}
// Construct the request.
req = new HttpRequest(first, url !, options.body || null, {
req = new HttpRequest(first, url !, (options.body !== undefined ? options.body : null), {
headers,
params,
reportProgress: options.reportProgress,

View File

@ -171,7 +171,7 @@ export class HttpRequest<T> {
// the body argument is to use a known no-body method like GET.
if (mightHaveBody(this.method) || !!fourth) {
// Body is the third argument, options are the fourth.
this.body = third as T || null;
this.body = (third !== undefined) ? third as T : null;
options = fourth;
} else {
// No body required, options are the third argument. The body stays null.

View File

@ -262,7 +262,7 @@ export class HttpResponse<T> extends HttpResponseBase {
body?: T | null, headers?: HttpHeaders; status?: number; statusText?: string; url?: string;
} = {}) {
super(init);
this.body = init.body || null;
this.body = init.body !== undefined ? init.body : null;
}
readonly type: HttpEventType.Response = HttpEventType.Response;

View File

@ -180,24 +180,27 @@ export class HttpXhrBackend implements HttpBackend {
// Check whether the body needs to be parsed as JSON (in many cases the browser
// will have done that already).
if (ok && req.responseType === 'json' && typeof body === 'string') {
// Attempt the parse. If it fails, a parse error should be delivered to the user.
if (req.responseType === 'json' && typeof body === 'string') {
// Save the original body, before attempting XSSI prefix stripping.
const originalBody = body;
body = body.replace(XSSI_PREFIX, '');
try {
body = JSON.parse(body);
// Attempt the parse. If it fails, a parse error should be delivered to the user.
body = body !== '' ? JSON.parse(body) : null;
} catch (error) {
// Even though the response status was 2xx, this is still an error.
ok = false;
// The parse error contains the text of the body that failed to parse.
body = { error, text: body } as HttpJsonParseError;
}
} else if (!ok && req.responseType === 'json' && typeof body === 'string') {
try {
// Attempt to parse the body as JSON.
body = JSON.parse(body);
} catch (error) {
// Cannot be certain that the body was meant to be parsed as JSON.
// Leave the body as a string.
// Since the JSON.parse failed, it's reasonable to assume this might not have been a
// JSON response. Restore the original body (including any XSSI prefix) to deliver
// a better error response.
body = originalBody;
// If this was an error request to begin with, leave it as a string, it probably
// just isn't JSON. Otherwise, deliver the parsing error to the user.
if (ok) {
// Even though the response status was 2xx, this is still an error.
ok = false;
// The parse error contains the text of the body that failed to parse.
body = { error, text: body } as HttpJsonParseError;
}
}
}

View File

@ -115,6 +115,26 @@ export function main() {
expect(testReq.request.body).toBe(body);
testReq.flush('hello world');
});
it('with a json body of false', (done: DoneFn) => {
client.post('/test', false, {observe: 'response', responseType: 'text'}).subscribe(res => {
expect(res.ok).toBeTruthy();
expect(res.status).toBe(200);
done();
});
const testReq = backend.expectOne('/test');
expect(testReq.request.body).toBe(false);
testReq.flush('hello world');
});
it('with a json body of 0', (done: DoneFn) => {
client.post('/test', 0, {observe: 'response', responseType: 'text'}).subscribe(res => {
expect(res.ok).toBeTruthy();
expect(res.status).toBe(200);
done();
});
const testReq = backend.expectOne('/test');
expect(testReq.request.body).toBe(0);
testReq.flush('hello world');
});
it('with an arraybuffer', (done: DoneFn) => {
const body = new ArrayBuffer(4);
client.post('/test', body, {observe: 'response', responseType: 'text'}).subscribe(res => {

View File

@ -40,6 +40,10 @@ export function main() {
expect(resp.ok).toBeTruthy();
expect(resp.url).toBeNull();
});
it('accepts a falsy body', () => {
expect(new HttpResponse({body: false}).body).toEqual(false);
expect(new HttpResponse({body: 0}).body).toEqual(0);
});
});
it('.ok is determined by status', () => {
const good = new HttpResponse({status: 200});

View File

@ -25,6 +25,8 @@ const TEST_POST = new HttpRequest('POST', '/test', 'some body', {
responseType: 'text',
});
const XSSI_PREFIX = ')]}\'\n';
export function main() {
describe('XhrBackend', () => {
let factory: MockXhrFactory = null !;
@ -92,6 +94,13 @@ export function main() {
const res = events[1] as HttpResponse<{data: string}>;
expect(res.body !.data).toBe('some data');
});
it('handles a blank json response', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
factory.mock.mockFlush(200, 'OK', '');
expect(events.length).toBe(2);
const res = events[1] as HttpResponse<{data: string}>;
expect(res.body).toBeNull();
});
it('handles a json error response', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
factory.mock.mockFlush(500, 'Error', JSON.stringify({data: 'some data'}));
@ -99,6 +108,13 @@ export function main() {
const res = events[1] as any as HttpErrorResponse;
expect(res.error !.data).toBe('some data');
});
it('handles a json error response with XSSI prefix', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
factory.mock.mockFlush(500, 'Error', XSSI_PREFIX + JSON.stringify({data: 'some data'}));
expect(events.length).toBe(2);
const res = events[1] as any as HttpErrorResponse;
expect(res.error !.data).toBe('some data');
});
it('handles a json string response', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
expect(factory.mock.responseType).toEqual('text');
@ -109,7 +125,7 @@ export function main() {
});
it('handles a json response with an XSSI prefix', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
factory.mock.mockFlush(200, 'OK', ')]}\'\n' + JSON.stringify({data: 'some data'}));
factory.mock.mockFlush(200, 'OK', XSSI_PREFIX + JSON.stringify({data: 'some data'}));
expect(events.length).toBe(2);
const res = events[1] as HttpResponse<{data: string}>;
expect(res.body !.data).toBe('some data');

View File

@ -4895,6 +4895,5 @@ switch (goog.LOCALE) {
}
if (l) {
l[0] = goog.LOCALE;
registerLocaleData(l);
registerLocaleData(l, goog.LOCALE);
}

View File

@ -17,9 +17,17 @@ export const LOCALE_DATA: {[localeId: string]: any} = {};
*
* @experimental i18n support is experimental.
*/
export function registerLocaleData(data: any, extraData?: any) {
const localeId = data[LocaleDataIndex.LocaleId].toLowerCase().replace(/_/g, '-');
// The signature registerLocaleData(data: any, extraData?: any) is deprecated since v5.1
export function registerLocaleData(data: any, localeId?: string | any, extraData?: any): void {
if (typeof localeId !== 'string') {
extraData = localeId;
localeId = data[LocaleDataIndex.LocaleId];
}
localeId = localeId.toLowerCase().replace(/_/g, '-');
LOCALE_DATA[localeId] = data;
if (extraData) {
LOCALE_DATA[localeId][LocaleDataIndex.ExtraData] = extraData;
}

View File

@ -10,8 +10,8 @@ import localeCaESVALENCIA from '../../locales/ca-ES-VALENCIA';
import localeEn from '../../locales/en';
import localeFr from '../../locales/fr';
import localeFrCA from '../../locales/fr-CA';
import {findLocaleData} from '../../src/i18n/locale_data_api';
import {registerLocaleData} from '../../src/i18n/locale_data';
import {findLocaleData} from '../../src/i18n/locale_data_api';
export function main() {
describe('locale data api', () => {
@ -20,6 +20,8 @@ export function main() {
registerLocaleData(localeEn);
registerLocaleData(localeFr);
registerLocaleData(localeFrCA);
registerLocaleData(localeFr, 'fake-id');
registerLocaleData(localeFrCA, 'fake_Id2');
});
describe('findLocaleData', () => {
@ -42,6 +44,12 @@ export function main() {
expect(findLocaleData('ca-ES-VALENCIA')).toEqual(localeCaESVALENCIA);
expect(findLocaleData('CA_es_Valencia')).toEqual(localeCaESVALENCIA);
});
it(`should find the LOCALE_DATA if the locale id was registered`, () => {
expect(findLocaleData('fake-id')).toEqual(localeFr);
expect(findLocaleData('fake_iD')).toEqual(localeFr);
expect(findLocaleData('fake-id2')).toEqual(localeFrCA);
});
});
});
}

View File

@ -20,7 +20,6 @@ import {GENERATED_FILES} from './transformers/util';
import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult, filterErrorsAndWarnings} from './perform_compile';
import {performWatchCompilation, createPerformWatchHost} from './perform_watch';
import {isSyntaxError} from '@angular/compiler';
export function main(
args: string[], consoleError: (s: string) => void = console.error,

View File

@ -8,8 +8,8 @@
import * as ts from 'typescript';
import {Evaluator, errorSymbol} from './evaluator';
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, InterfaceMetadata, METADATA_VERSION, MemberMetadata, MetadataEntry, MetadataError, MetadataMap, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression, isMethodMetadata} from './schema';
import {Evaluator, errorSymbol, recordMapEntry} from './evaluator';
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, InterfaceMetadata, METADATA_VERSION, MemberMetadata, MetadataEntry, MetadataError, MetadataMap, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportDefaultReference, isMetadataImportedSymbolReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression, isMethodMetadata} from './schema';
import {Symbols} from './symbols';
const isStatic = (node: ts.Node) => ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static;
@ -76,8 +76,7 @@ export class MetadataCollector {
}
function recordEntry<T extends MetadataEntry>(entry: T, node: ts.Node): T {
nodeMap.set(entry, node);
return entry;
return recordMapEntry(entry, node, nodeMap, sourceFile);
}
function errorSym(
@ -551,6 +550,7 @@ export class MetadataCollector {
__symbolic: 'module',
version: this.options.version || METADATA_VERSION, metadata
};
if (sourceFile.moduleName) result.importAs = sourceFile.moduleName;
if (exports) result.exports = exports;
return result;
}

View File

@ -9,10 +9,11 @@
import * as ts from 'typescript';
import {CollectorOptions} from './collector';
import {MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema';
import {ClassMetadata, FunctionMetadata, InterfaceMetadata, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSourceLocationInfo, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportDefaultReference, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema';
import {Symbols} from './symbols';
// In TypeScript 2.1 the spread element kind was renamed.
const spreadElementSyntaxKind: ts.SyntaxKind =
(ts.SyntaxKind as any).SpreadElement || (ts.SyntaxKind as any).SpreadElementExpression;
@ -38,6 +39,24 @@ function isCallOf(callExpression: ts.CallExpression, ident: string): boolean {
return false;
}
/* @internal */
export function recordMapEntry<T extends MetadataEntry>(
entry: T, node: ts.Node,
nodeMap: Map<MetadataValue|ClassMetadata|InterfaceMetadata|FunctionMetadata, ts.Node>,
sourceFile?: ts.SourceFile) {
if (!nodeMap.has(entry)) {
nodeMap.set(entry, node);
if (node && (isMetadataImportedSymbolReferenceExpression(entry) ||
isMetadataImportDefaultReference(entry)) &&
entry.line == null) {
const info = sourceInfo(node, sourceFile);
if (info.line != null) entry.line = info.line;
if (info.character != null) entry.character = info.character;
}
}
return entry;
}
/**
* ts.forEachChild stops iterating children when the callback return a truthy value.
* This method inverts this to implement an `every` style iterator. It will return
@ -77,21 +96,22 @@ function getSourceFileOfNode(node: ts.Node | undefined): ts.SourceFile {
}
/* @internal */
export function errorSymbol(
message: string, node?: ts.Node, context?: {[name: string]: string},
sourceFile?: ts.SourceFile): MetadataError {
let result: MetadataError|undefined = undefined;
export function sourceInfo(
node: ts.Node | undefined, sourceFile: ts.SourceFile | undefined): MetadataSourceLocationInfo {
if (node) {
sourceFile = sourceFile || getSourceFileOfNode(node);
if (sourceFile) {
const {line, character} =
ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
result = {__symbolic: 'error', message, line, character};
return ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
}
}
if (!result) {
result = {__symbolic: 'error', message};
}
return {};
}
/* @internal */
export function errorSymbol(
message: string, node?: ts.Node, context?: {[name: string]: string},
sourceFile?: ts.SourceFile): MetadataError {
const result: MetadataError = {__symbolic: 'error', message, ...sourceInfo(node, sourceFile)};
if (context) {
result.context = context;
}
@ -242,8 +262,7 @@ export class Evaluator {
}
entry = newEntry;
}
t.nodeMap.set(entry, node);
return entry;
return recordMapEntry(entry, node, t.nodeMap);
}
function isFoldableError(value: any): value is MetadataError {
@ -256,6 +275,9 @@ export class Evaluator {
// Encode as a global reference. StaticReflector will check the reference.
return recordEntry({__symbolic: 'reference', name}, node);
}
if (reference && isMetadataSymbolicReferenceExpression(reference)) {
return recordEntry({...reference}, node);
}
return reference;
};
@ -628,7 +650,7 @@ export class Evaluator {
return recordEntry({__symbolic: 'if', condition, thenExpression, elseExpression}, node);
case ts.SyntaxKind.FunctionExpression:
case ts.SyntaxKind.ArrowFunction:
return recordEntry(errorSymbol('Function call not supported', node), node);
return recordEntry(errorSymbol('Lambda not supported', node), node);
case ts.SyntaxKind.TaggedTemplateExpression:
return recordEntry(
errorSymbol('Tagged template expressions are not supported in metadata', node), node);

View File

@ -178,7 +178,20 @@ export function isMetadataSymbolicIfExpression(value: any): value is MetadataSym
return value && value.__symbolic === 'if';
}
export interface MetadataGlobalReferenceExpression extends MetadataSymbolicExpression {
export interface MetadataSourceLocationInfo {
/**
* The line number of the error in the .ts file the metadata was created for.
*/
line?: number;
/**
* The number of utf8 code-units from the beginning of the file of the error.
*/
character?: number;
}
export interface MetadataGlobalReferenceExpression extends MetadataSymbolicExpression,
MetadataSourceLocationInfo {
__symbolic: 'reference';
name: string;
arguments?: MetadataValue[];
@ -188,7 +201,8 @@ export function isMetadataGlobalReferenceExpression(value: any):
return value && value.name && !value.module && isMetadataSymbolicReferenceExpression(value);
}
export interface MetadataModuleReferenceExpression extends MetadataSymbolicExpression {
export interface MetadataModuleReferenceExpression extends MetadataSymbolicExpression,
MetadataSourceLocationInfo {
__symbolic: 'reference';
module: string;
}
@ -198,7 +212,8 @@ export function isMetadataModuleReferenceExpression(value: any):
isMetadataSymbolicReferenceExpression(value);
}
export interface MetadataImportedSymbolReferenceExpression extends MetadataSymbolicExpression {
export interface MetadataImportedSymbolReferenceExpression extends MetadataSymbolicExpression,
MetadataSourceLocationInfo {
__symbolic: 'reference';
module: string;
name: string;
@ -209,7 +224,8 @@ export function isMetadataImportedSymbolReferenceExpression(value: any):
return value && value.module && !!value.name && isMetadataSymbolicReferenceExpression(value);
}
export interface MetadataImportedDefaultReferenceExpression extends MetadataSymbolicExpression {
export interface MetadataImportedDefaultReferenceExpression extends MetadataSymbolicExpression,
MetadataSourceLocationInfo {
__symbolic: 'reference';
module: string;
default:
@ -218,7 +234,7 @@ export interface MetadataImportedDefaultReferenceExpression extends MetadataSymb
}
export function isMetadataImportDefaultReference(value: any):
value is MetadataImportedDefaultReferenceExpression {
return value.module && value.default && isMetadataSymbolicReferenceExpression(value);
return value && value.module && value.default && isMetadataSymbolicReferenceExpression(value);
}
export type MetadataSymbolicReferenceExpression = MetadataGlobalReferenceExpression |
@ -248,7 +264,7 @@ export function isMetadataSymbolicSpreadExpression(value: any):
return value && value.__symbolic === 'spread';
}
export interface MetadataError {
export interface MetadataError extends MetadataSourceLocationInfo {
__symbolic: 'error';
/**
@ -259,16 +275,6 @@ export interface MetadataError {
*/
message: string;
/**
* The line number of the error in the .ts file the metadata was created for.
*/
line?: number;
/**
* The number of utf8 code-units from the beginning of the file of the error.
*/
character?: number;
/**
* The module of the error (only used in bundled metadata)
*/
@ -280,6 +286,7 @@ export interface MetadataError {
*/
context?: {[name: string]: string};
}
export function isMetadataError(value: any): value is MetadataError {
return value && value.__symbolic === 'error';
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {isSyntaxError, syntaxError} from '@angular/compiler';
import {Position, isSyntaxError, syntaxError} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
@ -29,30 +29,76 @@ const defaultFormatHost: ts.FormatDiagnosticsHost = {
getNewLine: () => ts.sys.newLine
};
function displayFileName(fileName: string, host: ts.FormatDiagnosticsHost): string {
return path.relative(host.getCurrentDirectory(), host.getCanonicalFileName(fileName));
}
export function formatDiagnosticPosition(
position: Position, host: ts.FormatDiagnosticsHost = defaultFormatHost): string {
return `${displayFileName(position.fileName, host)}(${position.line + 1},${position.column+1})`;
}
export function flattenDiagnosticMessageChain(
chain: api.DiagnosticMessageChain, host: ts.FormatDiagnosticsHost = defaultFormatHost): string {
let result = chain.messageText;
let indent = 1;
let current = chain.next;
const newLine = host.getNewLine();
while (current) {
result += newLine;
for (let i = 0; i < indent; i++) {
result += ' ';
}
result += current.messageText;
const position = current.position;
if (position) {
result += ` at ${formatDiagnosticPosition(position, host)}`;
}
current = current.next;
indent++;
}
return result;
}
export function formatDiagnostic(
diagnostic: api.Diagnostic, host: ts.FormatDiagnosticsHost = defaultFormatHost) {
let result = '';
const newLine = host.getNewLine();
const span = diagnostic.span;
if (span) {
result += `${formatDiagnosticPosition({
fileName: span.start.file.url,
line: span.start.line,
column: span.start.col
}, host)}: `;
} else if (diagnostic.position) {
result += `${formatDiagnosticPosition(diagnostic.position, host)}: `;
}
if (diagnostic.span && diagnostic.span.details) {
result += `: ${diagnostic.span.details}, ${diagnostic.messageText}${newLine}`;
} else if (diagnostic.chain) {
result += `${flattenDiagnosticMessageChain(diagnostic.chain, host)}.${newLine}`;
} else {
result += `: ${diagnostic.messageText}${newLine}`;
}
return result;
}
export function formatDiagnostics(
diags: Diagnostics, tsFormatHost: ts.FormatDiagnosticsHost = defaultFormatHost): string {
diags: Diagnostics, host: ts.FormatDiagnosticsHost = defaultFormatHost): string {
if (diags && diags.length) {
return diags
.map(d => {
if (api.isTsDiagnostic(d)) {
return ts.formatDiagnostics([d], tsFormatHost);
.map(diagnostic => {
if (api.isTsDiagnostic(diagnostic)) {
return ts.formatDiagnostics([diagnostic], host);
} else {
let res = ts.DiagnosticCategory[d.category];
if (d.span) {
res +=
` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`;
}
if (d.span && d.span.details) {
res += `: ${d.span.details}, ${d.messageText}\n`;
} else {
res += `: ${d.messageText}\n`;
}
return res;
return formatDiagnostic(diagnostic, host);
}
})
.join('');
} else
} else {
return '';
}
}
export interface ParsedConfiguration {

View File

@ -6,16 +6,24 @@
* found in the LICENSE file at https://angular.io/license
*/
import {GeneratedFile, ParseSourceSpan} from '@angular/compiler';
import {GeneratedFile, ParseSourceSpan, Position} from '@angular/compiler';
import * as ts from 'typescript';
export const DEFAULT_ERROR_CODE = 100;
export const UNKNOWN_ERROR_CODE = 500;
export const SOURCE = 'angular' as 'angular';
export interface DiagnosticMessageChain {
messageText: string;
position?: Position;
next?: DiagnosticMessageChain;
}
export interface Diagnostic {
messageText: string;
span?: ParseSourceSpan;
position?: Position;
chain?: DiagnosticMessageChain;
category: ts.DiagnosticCategory;
code: number;
source: 'angular';
@ -192,6 +200,13 @@ export interface CompilerHost extends ts.CompilerHost {
* cause a diagnostics diagnostic error or an exception to be thrown.
*/
readResource?(fileName: string): Promise<string>|string;
/**
* Produce an AMD module name for the source file. Used in Bazel.
*
* An AMD module can have an arbitrary name, so that it is require'd by name
* rather than by path. See http://requirejs.org/docs/whyamd.html#namedmodules
*/
amdModuleName?(sf: ts.SourceFile): string|undefined;
}
export enum EmitFlags {

View File

@ -303,6 +303,11 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter implements ts.CompilerHos
/* emitSourceMaps */ false);
const sf = ts.createSourceFile(
genFile.genFileUrl, sourceText, this.options.target || ts.ScriptTarget.Latest);
if ((this.options.module === ts.ModuleKind.AMD || this.options.module === ts.ModuleKind.UMD) &&
this.context.amdModuleName) {
const moduleName = this.context.amdModuleName(sf);
if (moduleName) sf.moduleName = moduleName;
}
this.generatedSourceFiles.set(genFile.genFileUrl, {
sourceFile: sf,
emitCtx: context, externalReferences,

View File

@ -112,6 +112,7 @@ function upgradeMetadataWithDtsData(
newMetadata.metadata[prop] = dtsMetadata.metadata[prop];
}
}
if (dtsMetadata['importAs']) newMetadata['importAs'] = dtsMetadata['importAs'];
// Only copy exports from exports from metadata prior to version 3.
// Starting with version 3 the collector began collecting exports and

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isSyntaxError} from '@angular/compiler';
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
@ -14,14 +14,13 @@ import * as ts from 'typescript';
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
import {ModuleMetadata, createBundleIndexHost} from '../metadata/index';
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused} from './util';
/**
* Maximum number of files that are emitable via calling ts.Program.emit
* passing individual targetSourceFiles.
@ -62,7 +61,7 @@ class AngularCompilerProgram implements Program {
constructor(
private rootNames: string[], private options: CompilerOptions, private host: CompilerHost,
private oldProgram?: Program) {
oldProgram?: Program) {
const [major, minor] = ts.version.split('.');
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
@ -378,10 +377,12 @@ class AngularCompilerProgram implements Program {
}
private get structuralDiagnostics(): Diagnostic[] {
if (!this._structuralDiagnostics) {
let diagnostics = this._structuralDiagnostics;
if (!diagnostics) {
this.initSync();
diagnostics = (this._structuralDiagnostics = this._structuralDiagnostics || []);
}
return this._structuralDiagnostics !;
return diagnostics;
}
private get tsProgram(): ts.Program {
@ -430,16 +431,9 @@ class AngularCompilerProgram implements Program {
this.rootNames, this.options, this.host, this.metadataCache, codegen,
this.oldProgramLibrarySummaries);
const aotOptions = getAotCompilerOptions(this.options);
this._structuralDiagnostics = [];
const errorCollector =
(this.options.collectAllErrors || this.options.fullTemplateTypeCheck) ? (err: any) => {
this._structuralDiagnostics !.push({
messageText: err.toString(),
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE
});
} : undefined;
const errorCollector = (this.options.collectAllErrors || this.options.fullTemplateTypeCheck) ?
(err: any) => this._addStructuralDiagnostics(err) :
undefined;
this._compiler = createAotCompiler(this._hostAdapter, aotOptions, errorCollector).compiler;
}
@ -522,33 +516,26 @@ class AngularCompilerProgram implements Program {
this._hostAdapter.isSourceFile = () => false;
this._tsProgram = ts.createProgram(this.rootNames, this.options, this.hostAdapter);
if (isSyntaxError(e)) {
const parserErrors = getParseErrors(e);
if (parserErrors && parserErrors.length) {
this._structuralDiagnostics = [
...(this._structuralDiagnostics || []),
...parserErrors.map<Diagnostic>(e => ({
messageText: e.contextualMessage(),
category: ts.DiagnosticCategory.Error,
span: e.span,
source: SOURCE,
code: DEFAULT_ERROR_CODE
}))
];
} else {
this._structuralDiagnostics = [
...(this._structuralDiagnostics || []), {
messageText: e.message,
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE
}
];
}
this._addStructuralDiagnostics(e);
return;
}
throw e;
}
private _addStructuralDiagnostics(error: Error) {
const diagnostics = this._structuralDiagnostics || (this._structuralDiagnostics = []);
if (isSyntaxError(error)) {
diagnostics.push(...syntaxErrorToDiagnostics(error));
} else {
diagnostics.push({
messageText: error.toString(),
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE
});
}
}
// Note: this returns a ts.Diagnostic so that we
// can return errors in a ts.EmitResult
private generateFilesForEmit(emitFlags: EmitFlags):
@ -803,9 +790,16 @@ export function i18nSerialize(
default:
serializer = new Xliff();
}
return bundle.write(
serializer, (sourcePath: string) =>
options.basePath ? path.relative(options.basePath, sourcePath) : sourcePath);
return bundle.write(serializer, getPathNormalizer(options.basePath));
}
function getPathNormalizer(basePath?: string) {
// normalize sourcepaths by removing the base path and always using "/" as a separator
return (sourcePath: string) => {
sourcePath = basePath ? path.relative(basePath, sourcePath) : sourcePath;
return sourcePath.split(path.sep).join('/');
};
}
export function i18nGetExtension(formatName: string): string {
@ -836,3 +830,56 @@ function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
}
return {diagnostics, emitSkipped, emittedFiles};
}
function diagnosticSourceOfSpan(span: ParseSourceSpan): ts.SourceFile {
// For diagnostics, TypeScript only uses the fileName and text properties.
// The redundant '()' are here is to avoid having clang-format breaking the line incorrectly.
return ({ fileName: span.start.file.url, text: span.start.file.content } as any);
}
function diagnosticSourceOfFileName(fileName: string, program: ts.Program): ts.SourceFile {
const sourceFile = program.getSourceFile(fileName);
if (sourceFile) return sourceFile;
// If we are reporting diagnostics for a source file that is not in the project then we need
// to fake a source file so the diagnostic formatting routines can emit the file name.
// The redundant '()' are here is to avoid having clang-format breaking the line incorrectly.
return ({ fileName, text: '' } as any);
}
function diagnosticChainFromFormattedDiagnosticChain(chain: FormattedMessageChain):
DiagnosticMessageChain {
return {
messageText: chain.message,
next: chain.next && diagnosticChainFromFormattedDiagnosticChain(chain.next),
position: chain.position
};
}
function syntaxErrorToDiagnostics(error: Error): Diagnostic[] {
const parserErrors = getParseErrors(error);
if (parserErrors && parserErrors.length) {
return parserErrors.map<Diagnostic>(e => ({
messageText: e.contextualMessage(),
file: diagnosticSourceOfSpan(e.span),
start: e.span.start.offset,
length: e.span.end.offset - e.span.start.offset,
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE
}));
} else {
if (isFormattedError(error)) {
return [{
messageText: error.message,
chain: error.chain && diagnosticChainFromFormattedDiagnosticChain(error.chain),
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE,
position: error.position
}];
}
}
return [];
}

View File

@ -45,6 +45,7 @@ describe('Collector', () => {
're-exports.ts',
're-exports-2.ts',
'export-as.d.ts',
'named-module.d.ts',
'static-field-reference.ts',
'static-method.ts',
'static-method-call.ts',
@ -101,6 +102,12 @@ describe('Collector', () => {
});
});
it('should preserve module names from TypeScript sources', () => {
const sourceFile = program.getSourceFile('named-module.d.ts');
const metadata = collector.getMetadata(sourceFile);
expect(metadata !['importAs']).toEqual('some-named-module');
});
it('should be able to collect a simple component\'s metadata', () => {
const sourceFile = program.getSourceFile('app/hero-detail.component.ts');
const metadata = collector.getMetadata(sourceFile);
@ -112,7 +119,13 @@ describe('Collector', () => {
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 4,
character: 7
},
arguments: [{
selector: 'my-hero-detail',
template: `
@ -132,8 +145,13 @@ describe('Collector', () => {
__symbolic: 'property',
decorators: [{
__symbolic: 'call',
expression:
{__symbolic: 'reference', module: 'angular2/core', name: 'Input'}
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Input',
line: 18,
character: 9
}
}]
}]
}
@ -153,7 +171,13 @@ describe('Collector', () => {
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 9,
character: 7
},
arguments: [{
selector: 'my-app',
template: `
@ -172,20 +196,52 @@ describe('Collector', () => {
__symbolic: 'reference',
module: './hero-detail.component',
name: 'HeroDetailComponent',
line: 22,
character: 21
},
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'}
{
__symbolic: 'reference',
module: 'angular2/common',
name: 'NgFor',
line: 22,
character: 42
}
],
providers: [{__symbolic: 'reference', module: './hero.service', default: true}],
providers: [{
__symbolic: 'reference',
module: './hero.service',
default: true,
line: 23,
character: 20
}],
pipes: [
{__symbolic: 'reference', module: 'angular2/common', name: 'LowerCasePipe'},
{__symbolic: 'reference', module: 'angular2/common', name: 'UpperCasePipe'}
{
__symbolic: 'reference',
module: 'angular2/common',
name: 'LowerCasePipe',
line: 24,
character: 16
},
{
__symbolic: 'reference',
module: 'angular2/common',
name: 'UpperCasePipe',
line: 24,
character: 38
}
]
}]
}],
members: {
__ctor__: [{
__symbolic: 'constructor',
parameters: [{__symbolic: 'reference', module: './hero.service', default: true}]
parameters: [{
__symbolic: 'reference',
module: './hero.service',
default: true,
line: 31,
character: 42
}]
}],
onSelect: [{__symbolic: 'method'}],
ngOnInit: [{__symbolic: 'method'}],
@ -236,22 +292,23 @@ describe('Collector', () => {
});
it('should record annotations on set and get declarations', () => {
const propertyData = {
const propertyData = (line: number) => ({
name: [{
__symbolic: 'property',
decorators: [{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Input'},
expression:
{__symbolic: 'reference', module: 'angular2/core', name: 'Input', line, character: 9},
arguments: ['firstName']
}]
}]
};
});
const caseGetProp = <ClassMetadata>casesMetadata.metadata['GetProp'];
expect(caseGetProp.members).toEqual(propertyData);
expect(caseGetProp.members).toEqual(propertyData(11));
const caseSetProp = <ClassMetadata>casesMetadata.metadata['SetProp'];
expect(caseSetProp.members).toEqual(propertyData);
expect(caseSetProp.members).toEqual(propertyData(19));
const caseFullProp = <ClassMetadata>casesMetadata.metadata['FullProp'];
expect(caseFullProp.members).toEqual(propertyData);
expect(caseFullProp.members).toEqual(propertyData(27));
});
it('should record references to parameterized types', () => {
@ -260,7 +317,13 @@ describe('Collector', () => {
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Injectable'}
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Injectable',
line: 40,
character: 7
}
}],
members: {
__ctor__: [{
@ -313,7 +376,7 @@ describe('Collector', () => {
const ctor = <ConstructorMetadata>someClass.members !['__ctor__'][0];
const parameters = ctor.parameters;
expect(parameters).toEqual([
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'}
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor', line: 6, character: 29}
]);
});
@ -398,7 +461,7 @@ describe('Collector', () => {
const ctor = <ConstructorMetadata>someClass.members !['__ctor__'][0];
const parameters = ctor.parameters;
expect(parameters).toEqual([
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'}
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor', line: 6, character: 29}
]);
});
@ -427,7 +490,13 @@ describe('Collector', () => {
B: 1,
C: 30,
D: 40,
E: {__symbolic: 'reference', module: './exported-consts', name: 'constValue'}
E: {
__symbolic: 'reference',
module: './exported-consts',
name: 'constValue',
line: 5,
character: 75
}
});
});
@ -457,13 +526,25 @@ describe('Collector', () => {
expect(classData).toBeDefined();
expect(classData.decorators).toEqual([{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 4,
character: 5
},
arguments: [{
providers: {
__symbolic: 'call',
expression: {
__symbolic: 'select',
expression: {__symbolic: 'reference', module: './static-method', name: 'MyModule'},
expression: {
__symbolic: 'reference',
module: './static-method',
name: 'MyModule',
line: 5,
character: 17
},
member: 'with'
},
arguments: ['a']
@ -489,13 +570,25 @@ describe('Collector', () => {
expect(classData).toBeDefined();
expect(classData.decorators).toEqual([{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 4,
character: 5
},
arguments: [{
providers: [{
provide: 'a',
useValue: {
__symbolic: 'select',
expression: {__symbolic: 'reference', module: './static-field', name: 'MyModule'},
expression: {
__symbolic: 'reference',
module: './static-field',
name: 'MyModule',
line: 5,
character: 45
},
member: 'VALUE'
}
}]
@ -578,8 +671,20 @@ describe('Collector', () => {
const metadata = collector.getMetadata(source) !;
expect(metadata.metadata).toEqual({
MyClass: Object({__symbolic: 'class'}),
OtherModule: {__symbolic: 'reference', module: './static-field-reference', name: 'Foo'},
MyOtherModule: {__symbolic: 'reference', module: './static-field', name: 'MyModule'}
OtherModule: {
__symbolic: 'reference',
module: './static-field-reference',
name: 'Foo',
line: 4,
character: 12
},
MyOtherModule: {
__symbolic: 'reference',
module: './static-field',
name: 'MyModule',
line: 4,
character: 25
}
});
});
@ -598,7 +703,13 @@ describe('Collector', () => {
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 11,
character: 5
},
arguments: [{providers: [{__symbolic: 'reference', name: 'REQUIRED_VALIDATOR'}]}]
}]
}
@ -620,7 +731,13 @@ describe('Collector', () => {
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 11,
character: 5
},
arguments: [{providers: [{__symbolic: 'reference', name: 'REQUIRED_VALIDATOR'}]}]
}]
}
@ -653,7 +770,13 @@ describe('Collector', () => {
__symbolic: 'constructor',
parameterDecorators: [[{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Inject'},
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Inject',
line: 6,
character: 19
},
arguments: ['a']
}]],
parameters: [{__symbolic: 'reference', name: 'any'}]
@ -687,13 +810,20 @@ describe('Collector', () => {
__symbolic: 'reference',
module: './external',
name: 'external',
line: 0,
character: 68,
}
});
});
it('should simplify a redundant template', () => {
e('`${external}`', 'import {external} from "./external";')
.toEqual({__symbolic: 'reference', module: './external', name: 'external'});
e('`${external}`', 'import {external} from "./external";').toEqual({
__symbolic: 'reference',
module: './external',
name: 'external',
line: 0,
character: 59
});
});
it('should be able to collect complex template with imported references', () => {
@ -710,11 +840,18 @@ describe('Collector', () => {
__symbolic: 'binop',
operator: '+',
left: 'foo:',
right: {__symbolic: 'reference', module: './external', name: 'foo'}
right: {
__symbolic: 'reference',
module: './external',
name: 'foo',
line: 0,
character: 63
}
},
right: ', bar:'
},
right: {__symbolic: 'reference', module: './external', name: 'bar'}
right:
{__symbolic: 'reference', module: './external', name: 'bar', line: 0, character: 75}
},
right: ', end'
});
@ -741,11 +878,11 @@ describe('Collector', () => {
__ctor__: [{
__symbolic: 'constructor',
parameters: [
{__symbolic: 'reference', module: './foo', name: 'Foo'},
{__symbolic: 'reference', module: './foo', name: 'Foo'},
{__symbolic: 'reference', module: './foo', name: 'Foo'},
{__symbolic: 'reference', module: './foo', name: 'Foo'},
{__symbolic: 'reference', module: './foo', name: 'Foo'}
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24}
]
}]
});
@ -825,7 +962,9 @@ describe('Collector', () => {
extends: {
__symbolic: 'reference',
module: './class-inheritance-parent',
name: 'ParentClassFromOtherFile'
name: 'ParentClassFromOtherFile',
line: 9,
character: 45,
}
});
});
@ -1384,6 +1523,10 @@ const FILES: Directory = {
declare function someFunction(): void;
export { someFunction as SomeFunction };
`,
'named-module.d.ts': `
/// <amd-module name="some-named-module" />
export type SomeType = 'a';
`,
'local-symbol-ref.ts': `
import {Component, Validators} from 'angular2/core';

View File

@ -149,12 +149,14 @@ describe('Evaluator', () => {
const newExpression = program.getSourceFile('newExpression.ts');
expect(evaluator.evaluateNode(findVarInitializer(newExpression, 'someValue'))).toEqual({
__symbolic: 'new',
expression: {__symbolic: 'reference', name: 'Value', module: './classes'},
expression:
{__symbolic: 'reference', name: 'Value', module: './classes', line: 4, character: 33},
arguments: ['name', 12]
});
expect(evaluator.evaluateNode(findVarInitializer(newExpression, 'complex'))).toEqual({
__symbolic: 'new',
expression: {__symbolic: 'reference', name: 'Value', module: './classes'},
expression:
{__symbolic: 'reference', name: 'Value', module: './classes', line: 5, character: 42},
arguments: ['name', 12]
});
});
@ -173,8 +175,7 @@ describe('Evaluator', () => {
const errors = program.getSourceFile('errors.ts');
const fDecl = findVar(errors, 'f') !;
expect(evaluator.evaluateNode(fDecl.initializer !))
.toEqual(
{__symbolic: 'error', message: 'Function call not supported', line: 1, character: 12});
.toEqual({__symbolic: 'error', message: 'Lambda not supported', line: 1, character: 12});
const eDecl = findVar(errors, 'e') !;
expect(evaluator.evaluateNode(eDecl.type !)).toEqual({
__symbolic: 'error',

View File

@ -163,7 +163,7 @@ describe('ngc transformer command-line', () => {
const exitCode = main(['-p', 'not-exist'], errorSpy);
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.calls.mostRecent().args[0]).toContain('no such file or directory');
expect(errorSpy.calls.mostRecent().args[0]).toContain('at Error (native)');
expect(errorSpy.calls.mostRecent().args[0]).toContain('at Object.fs.lstatSync');
expect(exitCode).toEqual(2);
});
@ -184,8 +184,7 @@ describe('ngc transformer command-line', () => {
const exitCode = main(['-p', basePath], errorSpy);
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.calls.mostRecent().args[0])
.toContain('Error at ' + path.join(basePath, 'mymodule.ts.MyComp.html'));
expect(errorSpy.calls.mostRecent().args[0]).toContain('mymodule.ts.MyComp.html');
expect(errorSpy.calls.mostRecent().args[0])
.toContain(`Property 'unknownProp' does not exist on type 'MyComp'`);
@ -215,8 +214,7 @@ describe('ngc transformer command-line', () => {
const exitCode = main(['-p', basePath], errorSpy);
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.calls.mostRecent().args[0])
.toContain('Error at ' + path.join(basePath, 'my.component.html(1,5):'));
expect(errorSpy.calls.mostRecent().args[0]).toContain('my.component.html(1,5):');
expect(errorSpy.calls.mostRecent().args[0])
.toContain(`Property 'unknownProp' does not exist on type 'MyComp'`);
@ -1566,4 +1564,49 @@ describe('ngc transformer command-line', () => {
expect(main(['-p', path.join(basePath, 'src/tsconfig.json')])).toBe(0);
});
});
describe('formatted messages', () => {
it('should emit a formatted error message for a structural error', () => {
write('src/tsconfig.json', `{
"extends": "../tsconfig-base.json",
"files": ["test-module.ts"]
}`);
write('src/lib/indirect2.ts', `
declare var f: any;
export const t2 = f\`<p>hello</p>\`;
`);
write('src/lib/indirect1.ts', `
import {t2} from './indirect2';
export const t1 = t2 + ' ';
`);
write('src/lib/test.component.ts', `
import {Component} from '@angular/core';
import {t1} from './indirect1';
@Component({
template: t1,
styleUrls: ['./test.component.css']
})
export class TestComponent {}
`);
write('src/test-module.ts', `
import {NgModule} from '@angular/core';
import {TestComponent} from './lib/test.component';
@NgModule({declarations: [TestComponent]})
export class TestModule {}
`);
const messages: string[] = [];
const exitCode =
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
expect(exitCode).toBe(1, 'Compile was expected to fail');
expect(messages[0])
.toEqual(`lib/test.component.ts(6,21): Error during template compile of 'TestComponent'
Tagged template expressions are not supported in metadata in 't1'
't1' references 't2' at lib/indirect1.ts(3,27)
't2' contains the error at lib/indirect2.ts(4,27).
`);
});
});
});

View File

@ -143,7 +143,7 @@ describe('perform watch', () => {
const errDiags = host.diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error);
expect(errDiags.length).toBe(1);
expect(errDiags[0].messageText).toContain('Function calls are not supported.');
expect(errDiags[0].messageText).toContain('Function expressions are not supported');
}
});
});

View File

@ -930,7 +930,7 @@ describe('ng program', () => {
const structuralErrors = program.getNgStructuralDiagnostics();
expect(structuralErrors.length).toBe(1);
expect(structuralErrors[0].messageText).toContain('Function calls are not supported.');
expect(structuralErrors[0].messageText).toContain('Function expressions are not supported');
});
it('should not throw on structural errors but collect them (loadNgStructureAsync)', (done) => {
@ -943,7 +943,7 @@ describe('ng program', () => {
program.loadNgStructureAsync().then(() => {
const structuralErrors = program.getNgStructuralDiagnostics();
expect(structuralErrors.length).toBe(1);
expect(structuralErrors[0].messageText).toContain('Function calls are not supported.');
expect(structuralErrors[0].messageText).toContain('Function expressions are not supported');
done();
});
});
@ -982,7 +982,8 @@ describe('ng program', () => {
const program = ng.createProgram({rootNames: allRootNames, options, host});
const structuralErrors = program.getNgStructuralDiagnostics();
expect(structuralErrors.length).toBe(1);
expect(structuralErrors[0].messageText).toContain('Function calls are not supported.');
expect(structuralErrors[0].messageText)
.toContain('Function expressions are not supported');
});
});
});

View File

@ -0,0 +1,60 @@
/**
* @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
*/
import {syntaxError} from '../util';
export interface Position {
fileName: string;
line: number;
column: number;
}
export interface FormattedMessageChain {
message: string;
position?: Position;
next?: FormattedMessageChain;
}
export type FormattedError = Error & {
chain: FormattedMessageChain;
position?: Position;
};
const FORMATTED_MESSAGE = 'ngFormattedMessage';
function indentStr(level: number): string {
if (level <= 0) return '';
if (level < 6) return ['', ' ', ' ', ' ', ' ', ' '][level];
const half = indentStr(Math.floor(level / 2));
return half + half + (level % 2 === 1 ? ' ' : '');
}
function formatChain(chain: FormattedMessageChain | undefined, indent: number = 0): string {
if (!chain) return '';
const position = chain.position ?
`${chain.position.fileName}(${chain.position.line+1},${chain.position.column+1})` :
'';
const prefix = position && indent === 0 ? `${position}: ` : '';
const postfix = position && indent !== 0 ? ` at ${position}` : '';
const message = `${prefix}${chain.message}${postfix}`;
return `${indentStr(indent)}${message}${(chain.next && ('\n' + formatChain(chain.next, indent + 2))) || ''}`;
}
export function formattedError(chain: FormattedMessageChain): FormattedError {
const message = formatChain(chain) + '.';
const error = syntaxError(message) as FormattedError;
(error as any)[FORMATTED_MESSAGE] = true;
error.chain = chain;
error.position = chain.position;
return error;
}
export function isFormattedError(error: Error): error is FormattedError {
return !!(error as any)[FORMATTED_MESSAGE];
}

View File

@ -13,6 +13,7 @@ import * as o from '../output/output_ast';
import {SummaryResolver} from '../summary_resolver';
import {syntaxError} from '../util';
import {FormattedMessageChain, formattedError} from './formatted_error';
import {StaticSymbol} from './static_symbol';
import {StaticSymbolResolver} from './static_symbol_resolver';
@ -98,11 +99,16 @@ export class StaticReflector implements CompileReflector {
findSymbolDeclaration(symbol: StaticSymbol): StaticSymbol {
const resolvedSymbol = this.symbolResolver.resolveSymbol(symbol);
if (resolvedSymbol && resolvedSymbol.metadata instanceof StaticSymbol) {
return this.findSymbolDeclaration(resolvedSymbol.metadata);
} else {
return symbol;
if (resolvedSymbol) {
let resolvedMetadata = resolvedSymbol.metadata;
if (resolvedMetadata && resolvedMetadata.__symbolic === 'resolved') {
resolvedMetadata = resolvedMetadata.symbol;
}
if (resolvedMetadata instanceof StaticSymbol) {
return this.findSymbolDeclaration(resolvedSymbol.metadata);
}
}
return symbol;
}
public annotations(type: StaticSymbol): any[] {
@ -130,9 +136,12 @@ export class StaticReflector implements CompileReflector {
(requiredType) => ownAnnotations.some(ann => requiredType.isTypeOf(ann)));
if (!typeHasRequiredAnnotation) {
this.reportError(
syntaxError(
`Class ${type.name} in ${type.filePath} extends from a ${CompileSummaryKind[summary.type.summaryKind!]} in another compilation unit without duplicating the decorator. ` +
`Please add a ${requiredAnnotationTypes.map((type) => type.ngMetadataName).join(' or ')} decorator to the class.`),
formatMetadataError(
metadataError(
`Class ${type.name} in ${type.filePath} extends from a ${CompileSummaryKind[summary.type.summaryKind!]} in another compilation unit without duplicating the decorator`,
/* summary */ undefined,
`Please add a ${requiredAnnotationTypes.map((type) => type.ngMetadataName).join(' or ')} decorator to the class`),
type),
type);
}
}
@ -334,14 +343,6 @@ export class StaticReflector implements CompileReflector {
return this.symbolResolver.getStaticSymbol(declarationFile, name, members);
}
private reportError(error: Error, context: StaticSymbol, path?: string) {
if (this.errorRecorder) {
this.errorRecorder(error, (context && context.filePath) || path);
} else {
throw error;
}
}
/**
* Simplify but discard any errors
*/
@ -358,6 +359,7 @@ export class StaticReflector implements CompileReflector {
const self = this;
let scope = BindingScope.empty;
const calling = new Map<StaticSymbol, boolean>();
const rootContext = context;
function simplifyInContext(
context: StaticSymbol, value: any, depth: number, references: number): any {
@ -366,17 +368,64 @@ export class StaticReflector implements CompileReflector {
return resolvedSymbol ? resolvedSymbol.metadata : null;
}
function simplifyCall(functionSymbol: StaticSymbol, targetFunction: any, args: any[]) {
function simplifyEagerly(value: any): any {
return simplifyInContext(context, value, depth, 0);
}
function simplifyLazily(value: any): any {
return simplifyInContext(context, value, depth, references + 1);
}
function simplifyNested(nestedContext: StaticSymbol, value: any): any {
if (nestedContext === context) {
// If the context hasn't changed let the exception propagate unmodified.
return simplifyInContext(nestedContext, value, depth + 1, references);
}
try {
return simplifyInContext(nestedContext, value, depth + 1, references);
} catch (e) {
if (isMetadataError(e)) {
// Propagate the message text up but add a message to the chain that explains how we got
// here.
// e.chain implies e.symbol
const summaryMsg = e.chain ? 'references \'' + e.symbol !.name + '\'' : errorSummary(e);
const summary = `'${nestedContext.name}' ${summaryMsg}`;
const chain = {message: summary, position: e.position, next: e.chain};
// TODO(chuckj): retrieve the position information indirectly from the collectors node
// map if the metadata is from a .ts file.
self.error(
{
message: e.message,
advise: e.advise,
context: e.context, chain,
symbol: nestedContext
},
context);
} else {
// It is probably an internal error.
throw e;
}
}
}
function simplifyCall(
functionSymbol: StaticSymbol, targetFunction: any, args: any[], targetExpression: any) {
if (targetFunction && targetFunction['__symbolic'] == 'function') {
if (calling.get(functionSymbol)) {
throw new Error('Recursion not supported');
self.error(
{
message: 'Recursion is not supported',
summary: `called '${functionSymbol.name}' recursively`,
value: targetFunction
},
functionSymbol);
}
try {
const value = targetFunction['value'];
if (value && (depth != 0 || value.__symbolic != 'error')) {
const parameters: string[] = targetFunction['parameters'];
const defaults: any[] = targetFunction.defaults;
args = args.map(arg => simplifyInContext(context, arg, depth + 1, references))
args = args.map(arg => simplifyNested(context, arg))
.map(arg => shouldIgnore(arg) ? undefined : arg);
if (defaults && defaults.length > args.length) {
args.push(...defaults.slice(args.length).map((value: any) => simplify(value)));
@ -390,7 +439,7 @@ export class StaticReflector implements CompileReflector {
let result: any;
try {
scope = functionScope.done();
result = simplifyInContext(functionSymbol, value, depth + 1, references);
result = simplifyNested(functionSymbol, value);
} finally {
scope = oldScope;
}
@ -407,8 +456,22 @@ export class StaticReflector implements CompileReflector {
// non-angular decorator, and we should just ignore it.
return IGNORE;
}
return simplify(
{__symbolic: 'error', message: 'Function call not supported', context: functionSymbol});
let position: Position|undefined = undefined;
if (targetExpression && targetExpression.__symbolic == 'resolved') {
const line = targetExpression.line;
const character = targetExpression.character;
const fileName = targetExpression.fileName;
if (fileName != null && line != null && character != null) {
position = {fileName, line, column: character};
}
}
self.error(
{
message: FUNCTION_CALL_NOT_SUPPORTED,
context: functionSymbol,
value: targetFunction, position
},
context);
}
function simplify(expression: any): any {
@ -422,7 +485,7 @@ export class StaticReflector implements CompileReflector {
if (item && item.__symbolic === 'spread') {
// We call with references as 0 because we require the actual value and cannot
// tolerate a reference here.
const spreadArray = simplifyInContext(context, item.expression, depth, 0);
const spreadArray = simplifyEagerly(item.expression);
if (Array.isArray(spreadArray)) {
for (const spreadItem of spreadArray) {
result.push(spreadItem);
@ -448,7 +511,7 @@ export class StaticReflector implements CompileReflector {
const staticSymbol = expression;
const declarationValue = resolveReferenceValue(staticSymbol);
if (declarationValue != null) {
return simplifyInContext(staticSymbol, declarationValue, depth + 1, references);
return simplifyNested(staticSymbol, declarationValue);
} else {
return staticSymbol;
}
@ -525,8 +588,8 @@ export class StaticReflector implements CompileReflector {
}
return null;
case 'index':
let indexTarget = simplifyInContext(context, expression['expression'], depth, 0);
let index = simplifyInContext(context, expression['index'], depth, 0);
let indexTarget = simplifyEagerly(expression['expression']);
let index = simplifyEagerly(expression['index']);
if (indexTarget && isPrimitive(index)) return indexTarget[index];
return null;
case 'select':
@ -539,26 +602,41 @@ export class StaticReflector implements CompileReflector {
self.getStaticSymbol(selectTarget.filePath, selectTarget.name, members);
const declarationValue = resolveReferenceValue(selectContext);
if (declarationValue != null) {
return simplifyInContext(
selectContext, declarationValue, depth + 1, references);
return simplifyNested(selectContext, declarationValue);
} else {
return selectContext;
}
}
if (selectTarget && isPrimitive(member))
return simplifyInContext(
selectContext, selectTarget[member], depth + 1, references);
return simplifyNested(selectContext, selectTarget[member]);
return null;
case 'reference':
// Note: This only has to deal with variable references,
// as symbol references have been converted into StaticSymbols already
// in the StaticSymbolResolver!
// Note: This only has to deal with variable references, as symbol references have
// been converted into 'resolved'
// in the StaticSymbolResolver.
const name: string = expression['name'];
const localValue = scope.resolve(name);
if (localValue != BindingScope.missing) {
return localValue;
}
break;
case 'resolved':
try {
return simplify(expression.symbol);
} catch (e) {
// If an error is reported evaluating the symbol record the position of the
// reference in the error so it can
// be reported in the error message generated from the exception.
if (isMetadataError(e) && expression.fileName != null &&
expression.line != null && expression.character != null) {
e.position = {
fileName: expression.fileName,
line: expression.line,
column: expression.character
};
}
throw e;
}
case 'class':
return context;
case 'function':
@ -580,29 +658,34 @@ export class StaticReflector implements CompileReflector {
const argExpressions: any[] = expression['arguments'] || [];
let converter = self.conversionMap.get(staticSymbol);
if (converter) {
const args =
argExpressions
.map(arg => simplifyInContext(context, arg, depth + 1, references))
.map(arg => shouldIgnore(arg) ? undefined : arg);
const args = argExpressions.map(arg => simplifyNested(context, arg))
.map(arg => shouldIgnore(arg) ? undefined : arg);
return converter(context, args);
} else {
// Determine if the function is one we can simplify.
const targetFunction = resolveReferenceValue(staticSymbol);
return simplifyCall(staticSymbol, targetFunction, argExpressions);
return simplifyCall(
staticSymbol, targetFunction, argExpressions, expression['expression']);
}
}
return IGNORE;
case 'error':
let message = produceErrorMessage(expression);
if (expression['line']) {
message =
`${message} (position ${expression['line']+1}:${expression['character']+1} in the original .ts file)`;
self.reportError(
positionalError(
message, context.filePath, expression['line'], expression['character']),
let message = expression.message;
if (expression['line'] != null) {
self.error(
{
message,
context: expression.context,
value: expression,
position: {
fileName: expression['fileName'],
line: expression['line'],
column: expression['character']
}
},
context);
} else {
self.reportError(new Error(message), context);
self.error({message, context: expression.context}, context);
}
return IGNORE;
case 'ignore':
@ -620,7 +703,7 @@ export class StaticReflector implements CompileReflector {
return simplify(value);
}
}
return simplifyInContext(context, value, depth, references + 1);
return simplifyLazily(value);
}
return simplify(value);
});
@ -628,29 +711,19 @@ export class StaticReflector implements CompileReflector {
return IGNORE;
}
try {
return simplify(value);
} catch (e) {
const members = context.members.length ? `.${context.members.join('.')}` : '';
const message =
`${e.message}, resolving symbol ${context.name}${members} in ${context.filePath}`;
if (e.fileName) {
throw positionalError(message, e.fileName, e.line, e.column);
}
throw syntaxError(message);
}
return simplify(value);
}
const recordedSimplifyInContext = (context: StaticSymbol, value: any) => {
try {
return simplifyInContext(context, value, 0, 0);
} catch (e) {
let result: any;
try {
result = simplifyInContext(context, value, 0, 0);
} catch (e) {
if (this.errorRecorder) {
this.reportError(e, context);
} else {
throw formatMetadataError(e, context);
}
};
const result = this.errorRecorder ? recordedSimplifyInContext(context, value) :
simplifyInContext(context, value, 0, 0);
}
if (shouldIgnore(result)) {
return undefined;
}
@ -662,40 +735,166 @@ export class StaticReflector implements CompileReflector {
return resolvedSymbol && resolvedSymbol.metadata ? resolvedSymbol.metadata :
{__symbolic: 'class'};
}
}
function expandedMessage(error: any): string {
switch (error.message) {
case 'Reference to non-exported class':
if (error.context && error.context.className) {
return `Reference to a non-exported class ${error.context.className}. Consider exporting the class`;
}
break;
case 'Variable not initialized':
return 'Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler';
case 'Destructuring not supported':
return 'Referencing an exported destructured variable or constant is not supported by the template compiler. Consider simplifying this to avoid destructuring';
case 'Could not resolve type':
if (error.context && error.context.typeName) {
return `Could not resolve type ${error.context.typeName}`;
}
break;
case 'Function call not supported':
let prefix =
error.context && error.context.name ? `Calling function '${error.context.name}', f` : 'F';
return prefix +
'unction calls are not supported. Consider replacing the function or lambda with a reference to an exported function';
case 'Reference to a local symbol':
if (error.context && error.context.name) {
return `Reference to a local (non-exported) symbol '${error.context.name}'. Consider exporting the symbol`;
}
break;
private reportError(error: Error, context: StaticSymbol, path?: string) {
if (this.errorRecorder) {
this.errorRecorder(
formatMetadataError(error, context), (context && context.filePath) || path);
} else {
throw error;
}
}
private error(
{message, summary, advise, position, context, value, symbol, chain}: {
message: string,
summary?: string,
advise?: string,
position?: Position,
context?: any,
value?: any,
symbol?: StaticSymbol,
chain?: MetadataMessageChain
},
reportingContext: StaticSymbol) {
this.reportError(
metadataError(message, summary, advise, position, symbol, context, chain),
reportingContext);
}
return error.message;
}
function produceErrorMessage(error: any): string {
return `Error encountered resolving symbol values statically. ${expandedMessage(error)}`;
interface Position {
fileName: string;
line: number;
column: number;
}
interface MetadataMessageChain {
message: string;
summary?: string;
position?: Position;
context?: any;
symbol?: StaticSymbol;
next?: MetadataMessageChain;
}
type MetadataError = Error & {
position?: Position;
advise?: string;
summary?: string;
context?: any;
symbol?: StaticSymbol;
chain?: MetadataMessageChain;
};
const METADATA_ERROR = 'ngMetadataError';
function metadataError(
message: string, summary?: string, advise?: string, position?: Position, symbol?: StaticSymbol,
context?: any, chain?: MetadataMessageChain): MetadataError {
const error = syntaxError(message) as MetadataError;
(error as any)[METADATA_ERROR] = true;
if (advise) error.advise = advise;
if (position) error.position = position;
if (summary) error.summary = summary;
if (context) error.context = context;
if (chain) error.chain = chain;
if (symbol) error.symbol = symbol;
return error;
}
function isMetadataError(error: Error): error is MetadataError {
return !!(error as any)[METADATA_ERROR];
}
const REFERENCE_TO_NONEXPORTED_CLASS = 'Reference to non-exported class';
const VARIABLE_NOT_INITIALIZED = 'Variable not initialized';
const DESTRUCTURE_NOT_SUPPORTED = 'Destructuring not supported';
const COULD_NOT_RESOLVE_TYPE = 'Could not resolve type';
const FUNCTION_CALL_NOT_SUPPORTED = 'Function call not supported';
const REFERENCE_TO_LOCAL_SYMBOL = 'Reference to a local symbol';
const LAMBDA_NOT_SUPPORTED = 'Lambda not supported';
function expandedMessage(message: string, context: any): string {
switch (message) {
case REFERENCE_TO_NONEXPORTED_CLASS:
if (context && context.className) {
return `References to a non-exported class are not supported in decorators but ${context.className} was referenced.`;
}
break;
case VARIABLE_NOT_INITIALIZED:
return 'Only initialized variables and constants can be referenced in decorators because the value of this variable is needed by the template compiler';
case DESTRUCTURE_NOT_SUPPORTED:
return 'Referencing an exported destructured variable or constant is not supported in decorators and this value is needed by the template compiler';
case COULD_NOT_RESOLVE_TYPE:
if (context && context.typeName) {
return `Could not resolve type ${context.typeName}`;
}
break;
case FUNCTION_CALL_NOT_SUPPORTED:
if (context && context.name) {
return `Function calls are not supported in decorators but '${context.name}' was called`;
}
return 'Function calls are not supported in decorators';
case REFERENCE_TO_LOCAL_SYMBOL:
if (context && context.name) {
return `Reference to a local (non-exported) symbols are not supported in decorators but '${context.name}' was referenced`;
}
break;
case LAMBDA_NOT_SUPPORTED:
return `Function expressions are not supported in decorators`;
}
return message;
}
function messageAdvise(message: string, context: any): string|undefined {
switch (message) {
case REFERENCE_TO_NONEXPORTED_CLASS:
if (context && context.className) {
return `Consider exporting '${context.className}'`;
}
break;
case DESTRUCTURE_NOT_SUPPORTED:
return 'Consider simplifying to avoid destructuring';
case REFERENCE_TO_LOCAL_SYMBOL:
if (context && context.name) {
return `Consider exporting '${context.name}'`;
}
break;
case LAMBDA_NOT_SUPPORTED:
return `Consider changing the function expression into an exported function`;
}
return undefined;
}
function errorSummary(error: MetadataError): string {
if (error.summary) {
return error.summary;
}
switch (error.message) {
case REFERENCE_TO_NONEXPORTED_CLASS:
if (error.context && error.context.className) {
return `references non-exported class ${error.context.className}`;
}
break;
case VARIABLE_NOT_INITIALIZED:
return 'is not initialized';
case DESTRUCTURE_NOT_SUPPORTED:
return 'is a destructured variable';
case COULD_NOT_RESOLVE_TYPE:
return 'could not be resolved';
case FUNCTION_CALL_NOT_SUPPORTED:
if (error.context && error.context.name) {
return `calls '${error.context.name}'`;
}
return `calls a function`;
case REFERENCE_TO_LOCAL_SYMBOL:
if (error.context && error.context.name) {
return `references local variable ${error.context.name}`;
}
return `references a local variable`;
}
return 'contains the error';
}
function mapStringMap(input: {[key: string]: any}, transform: (value: any, key: string) => any):
@ -751,10 +950,30 @@ class PopulatedScope extends BindingScope {
}
}
function positionalError(message: string, fileName: string, line: number, column: number): Error {
const result = syntaxError(message);
(result as any).fileName = fileName;
(result as any).line = line;
(result as any).column = column;
return result;
}
function formatMetadataMessageChain(
chain: MetadataMessageChain, advise: string | undefined): FormattedMessageChain {
const expanded = expandedMessage(chain.message, chain.context);
const nesting = chain.symbol ? ` in '${chain.symbol.name}'` : '';
const message = `${expanded}${nesting}`;
const position = chain.position;
const next: FormattedMessageChain|undefined = chain.next ?
formatMetadataMessageChain(chain.next, advise) :
advise ? {message: advise} : undefined;
return {message, position, next};
}
function formatMetadataError(e: Error, context: StaticSymbol): Error {
if (isMetadataError(e)) {
// Produce a formatted version of the and leaving enough information in the original error
// to recover the formatting information to eventually produce a diagnostic error message.
const position = e.position;
const chain: MetadataMessageChain = {
message: `Error during template compile of '${context.name}'`,
position: position,
next: {message: e.message, next: e.chain, context: e.context, symbol: e.symbol}
};
const advise = e.advise || messageAdvise(e.message, e.context);
return formattedError(formatMetadataMessageChain(chain, advise));
}
return e;
}

View File

@ -146,9 +146,9 @@ export class StaticSymbolResolver {
if (isGeneratedFile(staticSymbol.filePath)) {
return null;
}
let resolvedSymbol = this.resolveSymbol(staticSymbol);
let resolvedSymbol = unwrapResolvedMetadata(this.resolveSymbol(staticSymbol));
while (resolvedSymbol && resolvedSymbol.metadata instanceof StaticSymbol) {
resolvedSymbol = this.resolveSymbol(resolvedSymbol.metadata);
resolvedSymbol = unwrapResolvedMetadata(this.resolveSymbol(resolvedSymbol.metadata));
}
return (resolvedSymbol && resolvedSymbol.metadata && resolvedSymbol.metadata.arity) || null;
}
@ -204,7 +204,7 @@ export class StaticSymbolResolver {
if (!baseResolvedSymbol) {
return null;
}
const baseMetadata = baseResolvedSymbol.metadata;
let baseMetadata = unwrapResolvedMetadata(baseResolvedSymbol.metadata);
if (baseMetadata instanceof StaticSymbol) {
return new ResolvedStaticSymbol(
staticSymbol, this.getStaticSymbol(baseMetadata.filePath, baseMetadata.name, members));
@ -374,6 +374,19 @@ export class StaticSymbolResolver {
return new ResolvedStaticSymbol(sourceSymbol, transformedMeta);
}
let _originalFileMemo: string|undefined;
const getOriginalName: () => string = () => {
if (!_originalFileMemo) {
// Guess what hte original file name is from the reference. If it has a `.d.ts` extension
// replace it with `.ts`. If it already has `.ts` just leave it in place. If it doesn't have
// .ts or .d.ts, append `.ts'. Also, if it is in `node_modules`, trim the `node_module`
// location as it is not important to finding the file.
_originalFileMemo =
topLevelPath.replace(/((\.ts)|(\.d\.ts)|)$/, '.ts').replace(/^.*node_modules[/\\]/, '');
}
return _originalFileMemo;
};
const self = this;
class ReferenceTransformer extends ValueTransformer {
@ -397,10 +410,19 @@ export class StaticSymbolResolver {
if (!filePath) {
return {
__symbolic: 'error',
message: `Could not resolve ${module} relative to ${sourceSymbol.filePath}.`
message: `Could not resolve ${module} relative to ${sourceSymbol.filePath}.`,
line: map.line,
character: map.character,
fileName: getOriginalName()
};
}
return self.getStaticSymbol(filePath, name);
return {
__symbolic: 'resolved',
symbol: self.getStaticSymbol(filePath, name),
line: map.line,
character: map.character,
fileName: getOriginalName()
};
} else if (functionParams.indexOf(name) >= 0) {
// reference to a function parameter
return {__symbolic: 'reference', name: name};
@ -411,14 +433,17 @@ export class StaticSymbolResolver {
// ambient value
null;
}
} else if (symbolic === 'error') {
return {...map, fileName: getOriginalName()};
} else {
return super.visitStringMap(map, functionParams);
}
}
}
const transformedMeta = visitValue(metadata, new ReferenceTransformer(), []);
if (transformedMeta instanceof StaticSymbol) {
return this.createExport(sourceSymbol, transformedMeta);
let unwrappedTransformedMeta = unwrapResolvedMetadata(transformedMeta);
if (unwrappedTransformedMeta instanceof StaticSymbol) {
return this.createExport(sourceSymbol, unwrappedTransformedMeta);
}
return new ResolvedStaticSymbol(sourceSymbol, transformedMeta);
}
@ -505,3 +530,10 @@ export class StaticSymbolResolver {
export function unescapeIdentifier(identifier: string): string {
return identifier.startsWith('___') ? identifier.substr(1) : identifier;
}
export function unwrapResolvedMetadata(metadata: any): any {
if (metadata && metadata.__symbolic === 'resolved') {
return metadata.symbol;
}
return metadata;
}

View File

@ -11,7 +11,7 @@ import {Summary, SummaryResolver} from '../summary_resolver';
import {OutputContext, ValueTransformer, ValueVisitor, visitValue} from '../util';
import {StaticSymbol, StaticSymbolCache} from './static_symbol';
import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolver';
import {ResolvedStaticSymbol, StaticSymbolResolver, unwrapResolvedMetadata} from './static_symbol_resolver';
import {isLoweredSymbol, ngfactoryFilePath, summaryForJitFileName, summaryForJitName} from './util';
export function serializeSummaries(
@ -453,10 +453,10 @@ function isCall(metadata: any): boolean {
}
function isFunctionCall(metadata: any): boolean {
return isCall(metadata) && metadata.expression instanceof StaticSymbol;
return isCall(metadata) && unwrapResolvedMetadata(metadata.expression) instanceof StaticSymbol;
}
function isMethodCallOnVariable(metadata: any): boolean {
return isCall(metadata) && metadata.expression && metadata.expression.__symbolic === 'select' &&
metadata.expression.expression instanceof StaticSymbol;
unwrapResolvedMetadata(metadata.expression.expression) instanceof StaticSymbol;
}

View File

@ -35,6 +35,7 @@ export * from './aot/compiler';
export * from './aot/generated_file';
export * from './aot/compiler_options';
export * from './aot/compiler_host';
export * from './aot/formatted_error';
export * from './aot/static_reflector';
export * from './aot/static_symbol';
export * from './aot/static_symbol_resolver';

View File

@ -768,9 +768,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({providers: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Injectable in another compilation unit without duplicating the decorator. ' +
'Please add a Injectable or Pipe or Directive or Component or NgModule decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Injectable in another compilation unit without duplicating the decorator
Please add a Injectable or Pipe or Directive or Component or NgModule decorator to the class.`);
});
});
@ -792,9 +792,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({declarations: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator. ' +
'Please add a Directive or Component decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator
Please add a Directive or Component decorator to the class.`);
});
});
@ -816,9 +816,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({declarations: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator. ' +
'Please add a Directive or Component decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator
Please add a Directive or Component decorator to the class.`);
});
});
@ -840,9 +840,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({declarations: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Pipe in another compilation unit without duplicating the decorator. ' +
'Please add a Pipe decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Pipe in another compilation unit without duplicating the decorator
Please add a Pipe decorator to the class.`);
});
});
@ -864,9 +864,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a NgModule in another compilation unit without duplicating the decorator. ' +
'Please add a NgModule decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a NgModule in another compilation unit without duplicating the decorator
Please add a NgModule decorator to the class.`);
});
});
}

View File

@ -107,8 +107,11 @@ describe('StaticReflector', () => {
it('should provide context for errors reported by the collector', () => {
const SomeClass = reflector.findDeclaration('src/error-reporting', 'SomeClass');
expect(() => reflector.annotations(SomeClass))
.toThrow(new Error(
'Error encountered resolving symbol values statically. A reasonable error message (position 13:34 in the original .ts file), resolving symbol ErrorSym in /tmp/src/error-references.d.ts, resolving symbol Link2 in /tmp/src/error-references.d.ts, resolving symbol Link1 in /tmp/src/error-references.d.ts, resolving symbol SomeClass in /tmp/src/error-reporting.d.ts, resolving symbol SomeClass in /tmp/src/error-reporting.d.ts'));
.toThrow(new Error(`Error during template compile of 'SomeClass'
A reasonable error message in 'Link1'
'Link1' references 'Link2'
'Link2' references 'ErrorSym'
'ErrorSym' contains the error at /tmp/src/error-references.ts(13,34).`));
});
it('should simplify primitive into itself', () => {
@ -330,10 +333,12 @@ describe('StaticReflector', () => {
it('should error on direct recursive calls', () => {
expect(
() => simplify(
reflector.getStaticSymbol('/tmp/src/function-reference.ts', ''),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'MyComp'),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'recursion')))
.toThrow(new Error(
'Recursion not supported, resolving symbol recursive in /tmp/src/function-recursive.d.ts, resolving symbol recursion in /tmp/src/function-reference.ts, resolving symbol in /tmp/src/function-reference.ts'));
.toThrow(new Error(`Error during template compile of 'MyComp'
Recursion is not supported in 'recursion'
'recursion' references 'recursive'
'recursive' called 'recursive' recursively.`));
});
it('should throw a SyntaxError without stack trace when the required resource cannot be resolved',
@ -345,8 +350,8 @@ describe('StaticReflector', () => {
message:
'Could not resolve ./does-not-exist.component relative to /tmp/src/function-reference.ts'
})))
.toThrowError(
'Error encountered resolving symbol values statically. Could not resolve ./does-not-exist.component relative to /tmp/src/function-reference.ts, resolving symbol AppModule in /tmp/src/function-reference.ts');
.toThrowError(`Error during template compile of 'AppModule'
Could not resolve ./does-not-exist.component relative to /tmp/src/function-reference.ts.`);
});
it('should record data about the error in the exception', () => {
@ -361,7 +366,7 @@ describe('StaticReflector', () => {
simplify(
reflector.getStaticSymbol('/tmp/src/invalid-metadata.ts', ''), classData.decorators[0]);
} catch (e) {
expect(e.fileName).toBe('/tmp/src/invalid-metadata.ts');
expect(e.position).toBeDefined();
threw = true;
}
expect(threw).toBe(true);
@ -370,10 +375,13 @@ describe('StaticReflector', () => {
it('should error on indirect recursive calls', () => {
expect(
() => simplify(
reflector.getStaticSymbol('/tmp/src/function-reference.ts', ''),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'MyComp'),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'indirectRecursion')))
.toThrow(new Error(
'Recursion not supported, resolving symbol indirectRecursion2 in /tmp/src/function-recursive.d.ts, resolving symbol indirectRecursion1 in /tmp/src/function-recursive.d.ts, resolving symbol indirectRecursion in /tmp/src/function-reference.ts, resolving symbol in /tmp/src/function-reference.ts'));
.toThrow(new Error(`Error during template compile of 'MyComp'
Recursion is not supported in 'indirectRecursion'
'indirectRecursion' references 'indirectRecursion1'
'indirectRecursion1' references 'indirectRecursion2'
'indirectRecursion2' called 'indirectRecursion1' recursively.`));
});
it('should simplify a spread expression', () => {
@ -401,7 +409,8 @@ describe('StaticReflector', () => {
() => reflector.annotations(
reflector.getStaticSymbol('/tmp/src/invalid-calls.ts', 'MyComponent')))
.toThrow(new Error(
`Error encountered resolving symbol values statically. Calling function 'someFunction', function calls are not supported. Consider replacing the function or lambda with a reference to an exported function, resolving symbol MyComponent in /tmp/src/invalid-calls.ts, resolving symbol MyComponent in /tmp/src/invalid-calls.ts`));
`/tmp/src/invalid-calls.ts(8,29): Error during template compile of 'MyComponent'
Function calls are not supported in decorators but 'someFunction' was called.`));
});
it('should be able to get metadata for a class containing a static method call', () => {
@ -962,7 +971,7 @@ describe('StaticReflector', () => {
});
// Regression #18170
it('should agressively evaluate enums selects', () => {
it('should eagerly evaluate enums selects', () => {
const data = Object.create(DEFAULT_TEST_DATA);
const file = '/tmp/src/my_component.ts';
data[file] = `
@ -1078,6 +1087,228 @@ describe('StaticReflector', () => {
expect(symbolResolver.getKnownModuleName(symbol.filePath)).toBe('a');
});
});
describe('formatted error reporting', () => {
describe('function calls', () => {
const fileName = '/tmp/src/invalid/components.ts';
beforeEach(() => {
const localData = {
'/tmp/src/invalid/function-call.ts': `
import {functionToCall} from 'some-module';
export const CALL_FUNCTION = functionToCall();
`,
'/tmp/src/invalid/indirect.ts': `
import {CALL_FUNCTION} from './function-call';
export const INDIRECT_CALL_FUNCTION = CALL_FUNCTION + 1;
`,
'/tmp/src/invalid/two-levels-indirect.ts': `
import {INDIRECT_CALL_FUNCTION} from './indirect';
export const TWO_LEVELS_INDIRECT_CALL_FUNCTION = INDIRECT_CALL_FUNCTION + 1;
`,
'/tmp/src/invalid/components.ts': `
import {functionToCall} from 'some-module';
import {Component} from '@angular/core';
import {CALL_FUNCTION} from './function-call';
import {INDIRECT_CALL_FUNCTION} from './indirect';
import {TWO_LEVELS_INDIRECT_CALL_FUNCTION} from './two-levels-indirect';
@Component({
value: functionToCall()
})
export class CallImportedFunction {}
@Component({
value: CALL_FUNCTION
})
export class ReferenceCalledFunction {}
@Component({
value: INDIRECT_CALL_FUNCTION
})
export class IndirectReferenceCalledFunction {}
@Component({
value: TWO_LEVELS_INDIRECT_CALL_FUNCTION
})
export class TwoLevelsIndirectReferenceCalledFunction {}
`
};
init({...DEFAULT_TEST_DATA, ...localData});
});
it('should report a formatted error for a direct function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'CallImportedFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(9,18): Error during template compile of 'CallImportedFunction'
Function calls are not supported in decorators but 'functionToCall' was called.`);
});
it('should report a formatted error for a refernce to a function call', () => {
expect(() => {
return reflector.annotations(
reflector.getStaticSymbol(fileName, 'ReferenceCalledFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(14,18): Error during template compile of 'ReferenceCalledFunction'
Function calls are not supported in decorators but 'functionToCall' was called in 'CALL_FUNCTION'
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
it('should report a formatted error for an indirect reference to a function call', () => {
expect(() => {
return reflector.annotations(
reflector.getStaticSymbol(fileName, 'IndirectReferenceCalledFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(19,18): Error during template compile of 'IndirectReferenceCalledFunction'
Function calls are not supported in decorators but 'functionToCall' was called in 'INDIRECT_CALL_FUNCTION'
'INDIRECT_CALL_FUNCTION' references 'CALL_FUNCTION' at /tmp/src/invalid/indirect.ts(4,47)
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
it('should report a formatted error for a double-indirect refernce to a function call', () => {
expect(() => {
return reflector.annotations(
reflector.getStaticSymbol(fileName, 'TwoLevelsIndirectReferenceCalledFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(24,18): Error during template compile of 'TwoLevelsIndirectReferenceCalledFunction'
Function calls are not supported in decorators but 'functionToCall' was called in 'TWO_LEVELS_INDIRECT_CALL_FUNCTION'
'TWO_LEVELS_INDIRECT_CALL_FUNCTION' references 'INDIRECT_CALL_FUNCTION' at /tmp/src/invalid/two-levels-indirect.ts(4,58)
'INDIRECT_CALL_FUNCTION' references 'CALL_FUNCTION' at /tmp/src/invalid/indirect.ts(4,47)
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
});
describe('macro functions', () => {
const fileName = '/tmp/src/invalid/components.ts';
beforeEach(() => {
const localData = {
'/tmp/src/invalid/function-call.ts': `
import {functionToCall} from 'some-module';
export const CALL_FUNCTION = functionToCall();
`,
'/tmp/src/invalid/indirect.ts': `
import {CALL_FUNCTION} from './function-call';
export const INDIRECT_CALL_FUNCTION = CALL_FUNCTION + 1;
`,
'/tmp/src/invalid/macros.ts': `
export function someMacro(value: any) {
return [ { provide: 'key', value: value } ];
}
`,
'/tmp/src/invalid/components.ts': `
import {Component} from '@angular/core';
import {functionToCall} from 'some-module';
import {someMacro} from './macros';
import {CALL_FUNCTION} from './function-call';
import {INDIRECT_CALL_FUNCTION} from './indirect';
@Component({
template: someMacro(functionToCall())
})
export class DirectCall {}
@Component({
template: someMacro(CALL_FUNCTION)
})
export class IndirectCall {}
@Component({
template: someMacro(INDIRECT_CALL_FUNCTION)
})
export class DoubleIndirectCall {}
`
};
init({...DEFAULT_TEST_DATA, ...localData});
});
it('should report a formatted error for a direct function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'DirectCall'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(9,31): Error during template compile of 'DirectCall'
Function calls are not supported in decorators but 'functionToCall' was called.`);
});
it('should report a formatted error for a reference to a function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'IndirectCall'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(14,31): Error during template compile of 'IndirectCall'
Function calls are not supported in decorators but 'functionToCall' was called in 'CALL_FUNCTION'
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
it('should report a formatted error for an indirect refernece to a function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'DoubleIndirectCall'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(19,31): Error during template compile of 'DoubleIndirectCall'
Function calls are not supported in decorators but 'functionToCall' was called in 'INDIRECT_CALL_FUNCTION'
'INDIRECT_CALL_FUNCTION' references 'CALL_FUNCTION' at /tmp/src/invalid/indirect.ts(4,47)
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
});
describe('and give advice', () => {
// If in a reference expression, advice the user to replace with a reference.
const fileName = '/tmp/src/invalid/components.ts';
function collectError(symbol: string): string {
try {
reflector.annotations(reflector.getStaticSymbol(fileName, symbol));
} catch (e) {
return e.message;
}
fail('Expected an exception to be thrown');
return '';
}
function initWith(content: string) {
init({
...DEFAULT_TEST_DATA,
[fileName]: `import {Component} from '@angular/core';\n${content}`
});
}
it('should advise exorting a local', () => {
initWith(`const f: string; @Component({value: f}) export class MyComp {}`);
expect(collectError('MyComp')).toContain(`Consider exporting 'f'`);
});
it('should advise export a class', () => {
initWith('class Foo {} @Component({value: Foo}) export class MyComp {}');
expect(collectError('MyComp')).toContain(`Consider exporting 'Foo'`);
});
it('should advise avoiding destructuring', () => {
initWith(
'export const {foo, bar} = {foo: 1, bar: 2}; @Component({value: foo}) export class MyComp {}');
expect(collectError('MyComp')).toContain(`Consider simplifying to avoid destructuring`);
});
it('should advise converting an arrow function into an exported function', () => {
initWith('@Component({value: () => true}) export class MyComp {}');
expect(collectError('MyComp'))
.toContain(`Consider changing the function expression into an exported function`);
});
it('should advise converting a function expression into an exported function', () => {
initWith('@Component({value: function () { return true; }}) export class MyComp {}');
expect(collectError('MyComp'))
.toContain(`Consider changing the function expression into an exported function`);
});
});
});
});
const DEFAULT_TEST_DATA: {[key: string]: any} = {
@ -1467,5 +1698,5 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
export class Dep {
@Input f: Forward;
}
`
`,
};

View File

@ -234,15 +234,25 @@ describe('StaticSymbolResolver', () => {
});
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'a')).metadata)
.toEqual(symbolCache.get('/test2.ts', 'b'));
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'x')).metadata).toEqual([
symbolCache.get('/test2.ts', 'y')
]);
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'x')).metadata).toEqual([{
__symbolic: 'resolved',
symbol: symbolCache.get('/test2.ts', 'y'),
line: 3,
character: 24,
fileName: '/test.ts'
}]);
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'simpleFn')).metadata).toEqual({
__symbolic: 'function',
parameters: ['fnArg'],
value: [
symbolCache.get('/test.ts', 'a'), symbolCache.get('/test2.ts', 'y'),
Object({__symbolic: 'reference', name: 'fnArg'})
symbolCache.get('/test.ts', 'a'), {
__symbolic: 'resolved',
symbol: symbolCache.get('/test2.ts', 'y'),
line: 6,
character: 21,
fileName: '/test.ts'
},
{__symbolic: 'reference', name: 'fnArg'}
]
});
});

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