Compare commits
69 Commits
11.0.0-nex
...
10.1.1
Author | SHA1 | Date | |
---|---|---|---|
1c156eb304 | |||
396548442e | |||
c54161098d | |||
f1b355b54f | |||
e40ffb95c8 | |||
20564f997f | |||
b1398d1771 | |||
7e9134aae8 | |||
0a55058440 | |||
bb1122d087 | |||
2ea49c7add | |||
83d69978fd | |||
62de2131e1 | |||
e156e29edd | |||
775c305771 | |||
190dca0fdc | |||
309709d4b2 | |||
028ef30b34 | |||
56d5ff2a89 | |||
b4eb016e56 | |||
6b0dba48b1 | |||
cfd4c0b4dc | |||
38762020d3 | |||
a1c34c6f0a | |||
b084bffb64 | |||
6a28675a5e | |||
4de8dc3554 | |||
ab4f953c78 | |||
ee432aaab8 | |||
5863537575 | |||
fcd2eb2ffb | |||
251a28cb15 | |||
54bb1c3d6a | |||
6c6dd5f38c | |||
9794f20674 | |||
027b041cfd | |||
4886cf5965 | |||
f21d50d2e6 | |||
0ef985368e | |||
0a277c6c40 | |||
9bf32c4dcb | |||
1c2ccfed4d | |||
25afbcc459 | |||
29c89c9297 | |||
efc76064d9 | |||
dbab74429f | |||
6aac499ee7 | |||
32f33f095f | |||
b0bd777ba9 | |||
c01bd0fe8e | |||
5588324802 | |||
437ecc8090 | |||
0dda97ea66 | |||
5e4aeaa348 | |||
cbbf8b542f | |||
91dfb18840 | |||
e44ddf5baa | |||
6b1a505566 | |||
659705ad78 | |||
8864b0ed69 | |||
4e596b672f | |||
83866827c3 | |||
7006cac50a | |||
dd82f2fefd | |||
bf003340ab | |||
5e35edd724 | |||
c132dcd0ae | |||
bbe331569b | |||
21e9a0032c |
@ -653,10 +653,8 @@ jobs:
|
||||
name: Starting Saucelabs tunnel service
|
||||
command: ./tools/saucelabs/sauce-service.sh run
|
||||
background: true
|
||||
# add module umd tsc compile option so the test can work
|
||||
# properly in the legacy browsers
|
||||
- run: yarn tsc -p packages --module UMD
|
||||
- run: yarn tsc -p modules --module UMD
|
||||
- run: yarn tsc -p packages
|
||||
- run: yarn tsc -p modules
|
||||
- run: yarn bazel build //packages/zone.js:npm_package
|
||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
||||
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
|
||||
|
2
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
2
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
@ -1,5 +1,5 @@
|
||||
---
|
||||
name: "\U0001F41E Bug report"
|
||||
name: "\U0001F41EBug report"
|
||||
about: Report a bug in the Angular Framework
|
||||
---
|
||||
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅
|
||||
|
2
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
2
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
@ -1,5 +1,5 @@
|
||||
---
|
||||
name: "\U0001F680 Feature request"
|
||||
name: "\U0001F680Feature request"
|
||||
about: Suggest a feature for Angular Framework
|
||||
|
||||
---
|
||||
|
2
.github/ISSUE_TEMPLATE/5-support-request.md
vendored
2
.github/ISSUE_TEMPLATE/5-support-request.md
vendored
@ -1,5 +1,5 @@
|
||||
---
|
||||
name: "❓ Support request"
|
||||
name: "❓Support request"
|
||||
about: Questions and requests for support
|
||||
|
||||
---
|
||||
|
2
.github/ISSUE_TEMPLATE/6-angular-cli.md
vendored
2
.github/ISSUE_TEMPLATE/6-angular-cli.md
vendored
@ -1,5 +1,5 @@
|
||||
---
|
||||
name: "\U0001F6E0️ Angular CLI"
|
||||
name: "\U0001F6E0️Angular CLI"
|
||||
about: Issues and feature requests for Angular CLI
|
||||
|
||||
---
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
name: "\U0001F48E Angular Components"
|
||||
name: "\U0001F48EAngular Components"
|
||||
about: Issues and feature requests for Angular Components
|
||||
|
||||
---
|
||||
|
2
.github/workflows/lock-closed.yml
vendored
2
.github/workflows/lock-closed.yml
vendored
@ -10,6 +10,6 @@ jobs:
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/lock-closed@414834b2b24dd2df37c6ed00808387ee6fd91b66
|
||||
- uses: angular/dev-infra/github-actions/lock-closed@66462f6
|
||||
with:
|
||||
lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }}
|
||||
|
43
CHANGELOG.md
43
CHANGELOG.md
@ -1,21 +1,3 @@
|
||||
<a name="11.0.0-next.1"></a>
|
||||
# 11.0.0-next.1 (2020-09-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** compute source-mappings for localized strings ([#38645](https://github.com/angular/angular/issues/38645)) ([7e0b3fd](https://github.com/angular/angular/commit/7e0b3fd)), closes [#38588](https://github.com/angular/angular/issues/38588)
|
||||
* **core:** remove CollectionChangeRecord symbol ([#38668](https://github.com/angular/angular/issues/38668)) ([fdea180](https://github.com/angular/angular/commit/fdea180))
|
||||
* **router:** support lazy loading for empty path named outlets ([#38379](https://github.com/angular/angular/issues/38379)) ([926ffcd](https://github.com/angular/angular/commit/926ffcd)), closes [#12842](https://github.com/angular/angular/issues/12842)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **core:** CollectionChangeRecord has been removed, use IterableChangeRecord
|
||||
instead
|
||||
|
||||
|
||||
|
||||
<a name="10.1.1"></a>
|
||||
## 10.1.1 (2020-09-09)
|
||||
|
||||
@ -41,31 +23,6 @@ instead
|
||||
|
||||
|
||||
|
||||
<a name="11.0.0-next.0"></a>
|
||||
# 11.0.0-next.0 (2020-09-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **forms:** ensure to emit `statusChanges` on subsequent value update/validations ([#38354](https://github.com/angular/angular/issues/38354)) ([d9fea85](https://github.com/angular/angular/commit/d9fea85)), closes [#20424](https://github.com/angular/angular/issues/20424) [#14542](https://github.com/angular/angular/issues/14542)
|
||||
* **service-worker:** fix condition to check for a cache-busted request ([#36847](https://github.com/angular/angular/issues/36847)) ([5be4edf](https://github.com/angular/angular/commit/5be4edf))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **service-worker:** add `UnrecoverableStateError` ([#36847](https://github.com/angular/angular/issues/36847)) ([036a2fa](https://github.com/angular/angular/commit/036a2fa)), closes [#36539](https://github.com/angular/angular/issues/36539)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **forms:** Previously if FormControl, FormGroup and FormArray class instances had async validators
|
||||
defined at initialization time, the status change event was not emitted once async validator
|
||||
completed. After this change the status event is emitted into the `statusChanges` observable.
|
||||
If your code relies on the old behavior, you can filter/ignore this additional status change
|
||||
event.
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0"></a>
|
||||
# 10.1.0 (2020-09-02)
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
/*
|
||||
* This example project is special in that it is not a cli app. To run tests appropriate for this
|
||||
* project, the test command is overwritten in `aio/content/examples/observables/example-config.json`.
|
||||
*
|
||||
* This is an empty placeholder file to ensure that `aio/tools/examples/run-example-e2e.js` runs
|
||||
* tests for this project.
|
||||
*
|
||||
* TODO: Fix our infrastructure/tooling, so that this hack is not necessary.
|
||||
*/
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"tests": [
|
||||
{
|
||||
"cmd": "yarn",
|
||||
"args": ["tsc", "--project", "tsconfig.spec.json", "--module", "commonjs"]
|
||||
},
|
||||
{
|
||||
"cmd": "yarn",
|
||||
"args": ["jasmine", "out-tsc/**/*.spec.js"]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { docRegionChain, docRegionObservable, docRegionUnsubscribe } from './observables';
|
||||
|
||||
describe('observables', () => {
|
||||
it('should print 2', (doneFn: DoneFn) => {
|
||||
const consoleLogSpy = spyOn(console, 'log');
|
||||
const observable = docRegionObservable(console);
|
||||
observable.subscribe(() => {
|
||||
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(2);
|
||||
doneFn();
|
||||
});
|
||||
});
|
||||
|
||||
it('should close the subscription', () => {
|
||||
const subscription = docRegionUnsubscribe();
|
||||
expect(subscription.closed).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should chain an observable', (doneFn: DoneFn) => {
|
||||
const observable = docRegionChain();
|
||||
observable.subscribe(value => {
|
||||
expect(value).toBe(4);
|
||||
doneFn();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,72 +1,40 @@
|
||||
// #docplaster
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export function docRegionObservable(console: Console) {
|
||||
// #docregion observable
|
||||
// #docregion observable
|
||||
|
||||
// declare a publishing operation
|
||||
const observable = new Observable<number>(observer => {
|
||||
// Subscriber fn...
|
||||
// #enddocregion observable
|
||||
// The below code is used for unit testing only
|
||||
observer.next(2);
|
||||
// #docregion observable
|
||||
});
|
||||
// declare a publishing operation
|
||||
const observable = new Observable<number>(observer => {
|
||||
// Subscriber fn...
|
||||
});
|
||||
|
||||
// initiate execution
|
||||
observable.subscribe(value => {
|
||||
// observer handles notifications
|
||||
// #enddocregion observable
|
||||
// The below code is used for unit testing only
|
||||
console.log(value);
|
||||
// #docregion observable
|
||||
});
|
||||
// initiate execution
|
||||
observable.subscribe(() => {
|
||||
// observer handles notifications
|
||||
});
|
||||
|
||||
// #enddocregion observable
|
||||
return observable;
|
||||
}
|
||||
// #enddocregion observable
|
||||
|
||||
export function docRegionUnsubscribe() {
|
||||
const observable = new Observable<number>(() => {
|
||||
// Subscriber fn...
|
||||
});
|
||||
// #docregion unsubscribe
|
||||
// #docregion unsubscribe
|
||||
|
||||
const subscription = observable.subscribe(() => {
|
||||
// observer handles notifications
|
||||
});
|
||||
const subscription = observable.subscribe(() => {
|
||||
// observer handles notifications
|
||||
});
|
||||
|
||||
subscription.unsubscribe();
|
||||
subscription.unsubscribe();
|
||||
|
||||
// #enddocregion unsubscribe
|
||||
return subscription;
|
||||
}
|
||||
// #enddocregion unsubscribe
|
||||
|
||||
export function docRegionError() {
|
||||
const observable = new Observable<number>(() => {
|
||||
// Subscriber fn...
|
||||
});
|
||||
// #docregion error
|
||||
|
||||
// #docregion error
|
||||
observable.subscribe(() => {
|
||||
throw new Error('my error');
|
||||
});
|
||||
// #enddocregion error
|
||||
}
|
||||
observable.subscribe(() => {
|
||||
throw Error('my error');
|
||||
});
|
||||
|
||||
export function docRegionChain() {
|
||||
let observable = new Observable<number>(observer => {
|
||||
// Subscriber fn...
|
||||
observer.next(2);
|
||||
});
|
||||
// #enddocregion error
|
||||
|
||||
observable =
|
||||
// #docregion chain
|
||||
// #docregion chain
|
||||
|
||||
observable.pipe(map(v => 2 * v));
|
||||
observable.pipe(map(v => 2 * v));
|
||||
|
||||
// #enddocregion chain
|
||||
return observable;
|
||||
}
|
||||
// #enddocregion chain
|
||||
|
@ -1,23 +0,0 @@
|
||||
import { docRegionError, docRegionPromise } from './promises';
|
||||
|
||||
describe('promises', () => {
|
||||
it('should print 2', (doneFn: DoneFn) => {
|
||||
const consoleLogSpy = spyOn(console, 'log');
|
||||
const pr = docRegionPromise(console, 2);
|
||||
pr.then((value) => {
|
||||
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(2);
|
||||
expect(value).toBe(4);
|
||||
doneFn();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error', (doneFn: DoneFn) => {
|
||||
const promise = docRegionError();
|
||||
promise
|
||||
.then(() => {
|
||||
throw new Error('Promise should be rejected.');
|
||||
},
|
||||
() => doneFn());
|
||||
});
|
||||
});
|
@ -1,44 +1,25 @@
|
||||
// #docplaster
|
||||
// #docregion promise
|
||||
// initiate execution
|
||||
const promise = new Promise<number>((resolve, reject) => {
|
||||
// Executer fn...
|
||||
});
|
||||
|
||||
export function docRegionPromise(console: Console, inputValue: number) {
|
||||
// #docregion promise
|
||||
// initiate execution
|
||||
let promise = new Promise<number>((resolve, reject) => {
|
||||
// Executer fn...
|
||||
// #enddocregion promise
|
||||
// The below is used in the unit tests.
|
||||
resolve(inputValue);
|
||||
// #docregion promise
|
||||
});
|
||||
// #enddocregion promise
|
||||
promise =
|
||||
// #docregion promise
|
||||
promise.then(value => {
|
||||
// handle result here
|
||||
// #enddocregion promise
|
||||
// The below is used in the unit tests.
|
||||
console.log(value);
|
||||
return value;
|
||||
// #docregion promise
|
||||
});
|
||||
// #enddocregion promise
|
||||
promise =
|
||||
// #docregion chain
|
||||
promise.then(v => 2 * v);
|
||||
// #enddocregion chain
|
||||
promise.then(value => {
|
||||
// handle result here
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
// #enddocregion promise
|
||||
|
||||
export function docRegionError() {
|
||||
let promise = Promise.resolve();
|
||||
promise =
|
||||
// #docregion error
|
||||
// #docregion chain
|
||||
|
||||
promise.then(() => {
|
||||
throw new Error('my error');
|
||||
});
|
||||
promise.then(v => 2 * v);
|
||||
|
||||
// #enddocregion error
|
||||
return promise;
|
||||
}
|
||||
// #enddocregion chain
|
||||
|
||||
// #docregion error
|
||||
|
||||
promise.then(() => {
|
||||
throw Error('my error');
|
||||
});
|
||||
|
||||
// #enddocregion error
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
|
||||
function notifyUser(message: string): void { }
|
||||
|
||||
// #docregion sw-unrecoverable-state
|
||||
@Injectable()
|
||||
export class HandleUnrecoverableStateService {
|
||||
constructor(updates: SwUpdate) {
|
||||
updates.unrecoverable.subscribe(event => {
|
||||
notifyUser(
|
||||
`An error occurred that we cannot recover from:\n${event.reason}\n\n` +
|
||||
'Please reload the page.');
|
||||
});
|
||||
}
|
||||
}
|
||||
// #enddocregion sw-unrecoverable-state
|
@ -38,6 +38,7 @@ v9 - v12
|
||||
| `@angular/bazel` | [`Bazel builder and schematics`](#bazelbuilder) | v10 |
|
||||
| `@angular/common` | [`ReflectiveInjector`](#reflectiveinjector) | <!--v8--> v11 |
|
||||
| `@angular/common` | [`CurrencyPipe` - `DEFAULT_CURRENCY_CODE`](api/common/CurrencyPipe#currency-code-deprecation) | <!--v9--> v11 |
|
||||
| `@angular/core` | [`CollectionChangeRecord`](#core) | <!--v7--> v11 |
|
||||
| `@angular/core` | [`DefaultIterableDiffer`](#core) | <!--v7--> v11 |
|
||||
| `@angular/core` | [`ReflectiveKey`](#core) | <!--v8--> v11 |
|
||||
| `@angular/core` | [`RenderComponentType`](#core) | <!--v7--> v11 |
|
||||
@ -88,6 +89,7 @@ Tip: In the [API reference section](api) of this doc site, deprecated APIs are i
|
||||
|
||||
| API | Replacement | Deprecation announced | Notes |
|
||||
| --- | ----------- | --------------------- | ----- |
|
||||
| [`CollectionChangeRecord`](api/core/CollectionChangeRecord) | [`IterableChangeRecord`](api/core/IterableChangeRecord) | v4 | none |
|
||||
| [`DefaultIterableDiffer`](api/core/DefaultIterableDiffer) | n/a | v4 | Not part of public API. |
|
||||
| [`ReflectiveInjector`](api/core/ReflectiveInjector) | [`Injector.create`](api/core/Injector#create) | v5 | See [`ReflectiveInjector`](#reflectiveinjector) |
|
||||
| [`ReflectiveKey`](api/core/ReflectiveKey) | none | v5 | none |
|
||||
|
@ -67,33 +67,6 @@ Therefore, it is recommended to reload the page once the promise returned by `ac
|
||||
|
||||
</div>
|
||||
|
||||
### Handling an unrecoverable state
|
||||
|
||||
In some cases, the version of the app used by the service worker to serve a client might be in a broken state that cannot be recovered from without a full page reload.
|
||||
|
||||
For example, imagine the following scenario:
|
||||
- A user opens the app for the first time and the service worker caches the latest version of the app.
|
||||
Let's assume the app's cached assets include `index.html`, `main.<main-hash-1>.js` and `lazy-chunk.<lazy-hash-1>.js`.
|
||||
- The user closes the app and does not open it for a while.
|
||||
- After some time, a new version of the app is deployed to the server.
|
||||
This newer version includes the files `index.html`, `main.<main-hash-2>.js` and `lazy-chunk.<lazy-hash-2>.js` (note that the hashes are different now, because the content of the files has changed).
|
||||
The old version is no longer available on the server.
|
||||
- In the meantime, the user's browser decides to evict `lazy-chunk.<lazy-hash-1>.js` from its cache.
|
||||
Browsers may decide to evict specific (or all) resources from a cache in order to reclaim disk space.
|
||||
- The user opens the app again.
|
||||
The service worker serves the latest version known to it at this point, namely the old version (`index.html` and `main.<main-hash-1>.js`).
|
||||
- At some later point, the app requests the lazy bundle, `lazy-chunk.<lazy-hash-1>.js`.
|
||||
- The service worker is unable to find the asset in the cache (remember that the browser evicted it).
|
||||
Nor is it able to retrieve it from the server (since the server now only has `lazy-chunk.<lazy-hash-2>.js` from the newer version).
|
||||
|
||||
In the above scenario, the service worker is not able to serve an asset that would normally be cached.
|
||||
That particular app version is broken and there is no way to fix the state of the client without reloading the page.
|
||||
In such cases, the service worker notifies the client by sending an `UnrecoverableStateEvent` event.
|
||||
You can subscribe to `SwUpdate#unrecoverable` to be notified and handle these errors.
|
||||
|
||||
<code-example path="service-worker-getting-started/src/app/handle-unrecoverable-state.service.ts" header="handle-unrecoverable-state.service.ts" region="sw-unrecoverable-state"></code-example>
|
||||
|
||||
|
||||
## More on Angular service workers
|
||||
|
||||
You may also be interested in the following:
|
||||
|
@ -53,9 +53,6 @@
|
||||
},
|
||||
"kyliau": {
|
||||
"name": "Keen Yee Liau",
|
||||
"twitter": "liauky",
|
||||
"website": "https://github.com/kyliau",
|
||||
"bio": "Keen works on language service and CLI. He also maintains Karma and Protractor.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "igorminar",
|
||||
"picture": "kyliau.jpg"
|
||||
|
@ -56,35 +56,35 @@
|
||||
"tooltip": "Set up your environment and learn basic concepts",
|
||||
"children": [
|
||||
{
|
||||
"title": "Try it",
|
||||
"tooltip": "Examine and work with a ready-made sample app, with no setup.",
|
||||
"children": [
|
||||
{
|
||||
"url": "start",
|
||||
"title": "A Sample App",
|
||||
"tooltip": "Take a look at Angular's component model, template syntax, and component communication."
|
||||
},
|
||||
{
|
||||
"url": "start/start-routing",
|
||||
"title": "In-app Navigation",
|
||||
"tooltip": "Navigate among different page views using the browser's URL."
|
||||
},
|
||||
{
|
||||
"url": "start/start-data",
|
||||
"title": "Manage Data",
|
||||
"tooltip": "Use services and access external data via HTTP."
|
||||
},
|
||||
{
|
||||
"url": "start/start-forms",
|
||||
"title": "Forms for User Input",
|
||||
"tooltip": "Learn about fetching and managing data from users with forms."
|
||||
},
|
||||
{
|
||||
"url": "start/start-deployment",
|
||||
"title": "Deployment",
|
||||
"tooltip": "Move to local development, or deploy your application to Firebase or your own server."
|
||||
}
|
||||
]
|
||||
"title": "Try it",
|
||||
"tooltip": "Examine and work with a ready-made sample app, with no setup.",
|
||||
"children": [
|
||||
{
|
||||
"url": "start",
|
||||
"title": "A Sample App",
|
||||
"tooltip": "Take a look at Angular's component model, template syntax, and component communication."
|
||||
},
|
||||
{
|
||||
"url": "start/start-routing",
|
||||
"title": "In-app Navigation",
|
||||
"tooltip": "Navigate among different page views using the browser's URL."
|
||||
},
|
||||
{
|
||||
"url": "start/start-data",
|
||||
"title": "Manage Data",
|
||||
"tooltip": "Use services and access external data via HTTP."
|
||||
},
|
||||
{
|
||||
"url": "start/start-forms",
|
||||
"title": "Forms for User Input",
|
||||
"tooltip": "Learn about fetching and managing data from users with forms."
|
||||
},
|
||||
{
|
||||
"url": "start/start-deployment",
|
||||
"title": "Deployment",
|
||||
"tooltip": "Move to local development, or deploy your application to Firebase or your own server."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "guide/setup-local",
|
||||
|
@ -23,7 +23,7 @@
|
||||
"build-local-with-viewengine": "yarn ~~build",
|
||||
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
|
||||
"build-local-with-viewengine-ci": "yarn ~~build --progress=false",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 32391604b",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js ef770f1cb",
|
||||
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
|
||||
"test": "yarn check-env && ng test",
|
||||
"pree2e": "yarn check-env && yarn update-webdriver",
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
{%- macro renderHeritage(exportDoc) -%}
|
||||
{%- if exportDoc.extendsClauses.length %} extends {% for clause in exportDoc.extendsClauses -%}
|
||||
{% if clause.doc.path %}<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% else %}{$ clause.text $}{% endif %}{% if not loop.last %}, {% endif -%}
|
||||
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}
|
||||
{% endfor %}{% endif %}
|
||||
{%- if exportDoc.implementsClauses.length %} implements {% for clause in exportDoc.implementsClauses -%}
|
||||
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}
|
||||
|
@ -4,23 +4,71 @@ Caretaker is responsible for merging PRs into the individual branches and intern
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Draining the queue of PRs ready to be merged. (PRs with [`action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22action%3A+merge%22) label)
|
||||
- Draining the queue of PRs ready to be merged. (PRs with [`PR action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22PR+action%3A+merge%22) label)
|
||||
- Assigning [new issues](https://github.com/angular/angular/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) to individual component authors.
|
||||
|
||||
## Merging the PR
|
||||
|
||||
A PR needs to have `action: merge` and `target: *` labels to be considered
|
||||
ready to merge. Merging is performed by running `ng-dev pr merge` with a PR number to merge.
|
||||
|
||||
The tooling automatically verifies the given PR is ready for merge. If the PR passes the tests, the
|
||||
tool will automatically merge it based on the applied target label.
|
||||
A PR needs to have `PR action: merge` and `PR target: *` labels to be considered
|
||||
ready to merge. Merging is performed by running `merge-pr` with a PR number to merge.
|
||||
|
||||
To merge a PR run:
|
||||
|
||||
```
|
||||
$ yarn ng-dev pr merge <pr number>
|
||||
$ ./scripts/github/merge-pr 1234
|
||||
```
|
||||
|
||||
The `merge-pr` script will:
|
||||
- Ensure that all appropriate labels are on the PR.
|
||||
- Fetches the latest PR code from the `angular/angular` repo.
|
||||
- It will `cherry-pick` all of the SHAs from the PR into the current corresponding branches `master` and or `?.?.x` (patch).
|
||||
- It will rewrite commit history by automatically adding `Close #1234` and `(#1234)` into the commit message.
|
||||
|
||||
NOTE: The `merge-pr` will land the PR on `master` and or `?.?.x` (patch) as described by `PR target: *` label.
|
||||
|
||||
### Recovering from failed `merge-pr` due to conflicts
|
||||
|
||||
The `ng-dev pr merge` tool will automatically restore to the previous git state when a merge fails.
|
||||
When running `merge-pr` the script will output the commands which it is about to run.
|
||||
|
||||
```
|
||||
$ ./scripts/github/merge-pr 1234
|
||||
======================
|
||||
GitHub Merge PR Steps
|
||||
======================
|
||||
git cherry-pick angular/pr/1234~1..angular/pr/1234
|
||||
git filter-branch -f --msg-filter "/home/misko/angular/scripts/github/utils/github.closes 1234" HEAD~1..HEAD
|
||||
```
|
||||
|
||||
If the `cherry-pick` command fails than resolve conflicts and use `git cherry-pick --continue` once ready. After the `cherry-pick` is done cut&paste and run the `filter-branch` command to properly rewrite the messages
|
||||
|
||||
## Cherry-picking PRs into patch branch
|
||||
|
||||
In addition to merging PRs into the master branch, many PRs need to be also merged into a patch branch.
|
||||
Follow these steps to get patch branch up to date.
|
||||
|
||||
1. Check out the most recent patch branch: `git checkout 4.3.x`
|
||||
2. Get a list of PRs merged into master: `git log master --oneline -n10`
|
||||
3. For each PR number in the commit message run: `./scripts/github/merge-pr 1234`
|
||||
- The PR will only merge if the `PR target:` matches the branch.
|
||||
|
||||
Once all of the PRs are in patch branch, push the all branches and tags to github using `push-upstream` script.
|
||||
|
||||
|
||||
## Pushing merged PRs into github
|
||||
|
||||
Use `push-upstream` script to push all of the branch and tags to github.
|
||||
|
||||
```
|
||||
$ ./scripts/github/push-upstream
|
||||
git push git@github.com:angular/angular.git master:master 4.3.x:4.3.x
|
||||
Counting objects: 25, done.
|
||||
Delta compression using up to 6 threads.
|
||||
Compressing objects: 100% (17/17), done.
|
||||
Writing objects: 100% (25/25), 2.22 KiB | 284.00 KiB/s, done.
|
||||
Total 25 (delta 22), reused 8 (delta 7)
|
||||
remote: Resolving deltas: 100% (22/22), completed with 18 local objects.
|
||||
To github.com:angular/angular.git
|
||||
079d884b6..d1c4a94bb master -> master
|
||||
git push --tags -f git@github.com:angular/angular.git patch_sync:patch_sync
|
||||
Everything up-to-date
|
||||
```
|
||||
|
@ -12,7 +12,7 @@ Change approvals in our monorepo are managed via [PullApprove](https://docs.pull
|
||||
# Merging
|
||||
|
||||
Once a change has all of the required approvals, either the last approver or the PR author (if PR author has the project collaborator status)
|
||||
should mark the PR with the `action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
|
||||
should mark the PR with the `PR action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
|
||||
This signals to the caretaker that the PR should be merged. See [merge instructions](CARETAKER.md).
|
||||
|
||||
# Who is the Caretaker?
|
||||
|
@ -154,7 +154,9 @@ available as a long-term distribution mechanism, but they are guaranteed to be a
|
||||
time of the build.
|
||||
|
||||
You can access the artifacts for a specific CI run by going to the workflow page, clicking on the
|
||||
`publish_packages_as_artifacts` job and then switching to the "ARTIFACTS" tab.
|
||||
`publish_packages_as_artifacts` job and then switching to the "Artifacts" tab.
|
||||
(If you happen to know the build number of the job, the URL will be something like:
|
||||
`https://circleci.com/gh/angular/angular/<build-number>#artifacts`)
|
||||
|
||||
#### Archives for each Package
|
||||
On the "Artifacts" tab, there is a list of links to compressed archives for Angular packages. The
|
||||
|
@ -125,28 +125,28 @@ Triaging PRs is the same as triaging issues, except that the labels `frequency:
|
||||
|
||||
PRs also have additional label categories that should be used to signal their state.
|
||||
|
||||
Every triaged PR must have a `action: *` label assigned to it:
|
||||
Every triaged PR must have a `PR action` label assigned to it:
|
||||
|
||||
* `action: discuss`: Discussion is needed, to be led by the author.
|
||||
* `PR action: discuss`: Discussion is needed, to be led by the author.
|
||||
* _**Who adds it:** Typically the PR author._
|
||||
* _**Who removes it:** Whoever added it._
|
||||
* `action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
|
||||
* `PR action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
|
||||
* _**Who adds it:** Any team member. The caretaker can use it to differentiate PRs pending review from merge-ready PRs._
|
||||
* _**Who removes it:** Whoever added it or the reviewer adding the last missing review._
|
||||
* `action: cleanup`: More work is needed from the author.
|
||||
* `PR action: cleanup`: More work is needed from the author.
|
||||
* _**Who adds it:** The reviewer requesting changes to the PR._
|
||||
* _**Who removes it:** Either the author (after implementing the requested changes) or the reviewer (after confirming the requested changes have been implemented)._
|
||||
* `action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
|
||||
* `PR action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
|
||||
* _**Who adds it:** Typically the PR author._
|
||||
* _**Who removes it:** Whoever added it._
|
||||
|
||||
|
||||
In addition, PRs can have the following states:
|
||||
|
||||
* `state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
|
||||
* `PR state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
|
||||
* _**Who adds it:** The PR author._
|
||||
* _**Who removes it:** Whoever added it._
|
||||
* `state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
|
||||
* `PR state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
|
||||
* _**Who adds it:** Any team member._
|
||||
* _**Who removes it:** Any team member._
|
||||
|
||||
@ -162,27 +162,13 @@ This decision is then honored when the PR is being merged by the caretaker.
|
||||
|
||||
To communicate the target we use the following labels:
|
||||
|
||||
Targeting an active release train:
|
||||
* `PR target: master & patch`: the PR should me merged into the master branch and cherry-picked into the most recent patch branch. All PRs with fixes, docs and refactorings should use this target.
|
||||
* `PR target: master-only`: the PR should be merged only into the `master` branch. All PRs with new features, API changes or high-risk changes should use this target.
|
||||
* `PR target: patch-only`: the PR should be merged only into the most recent patch branch (e.g. 5.0.x). This target is useful if a `master & patch` PR can't be cleanly cherry-picked into the stable branch and a new PR is needed.
|
||||
* `PR target: LTS-only`: the PR should be merged only into the active LTS branch(es). Only security and critical fixes are allowed in these branches. Always send a new PR targeting just the LTS branch and request review approval from @IgorMinar.
|
||||
* `PR target: TBD`: the target is yet to be determined.
|
||||
|
||||
* `target: major`: Any breaking change
|
||||
* `target: minor`: Any new feature
|
||||
* `target: patch`: Bug fixes, refactorings, documentation changes, etc. that pose no or very low risk of adversely
|
||||
affecting existing applications.
|
||||
|
||||
Special Cases:
|
||||
* `target: rc`: A critical fix for an active release-train while it is in a feature freeze or RC phase
|
||||
* `target: lts`: A criticial fix for a specific release-train that is still within the long term support phase
|
||||
|
||||
|
||||
Notes:
|
||||
- To land a change only in a patch/RC branch, without landing it in any other active release-train branch (such
|
||||
as `master`), the patch/RC branch can be targeted in the Github UI with the appropriate
|
||||
`target: patch`/`target: rc` label.
|
||||
- `target: lts` PRs must target the specific LTS branch they would need to merge into in the Github UI, in
|
||||
cases which a change is desired in multiple LTS branches, individual PRs for each LTS branch must be created
|
||||
|
||||
|
||||
If a PR is missing the `target:*` label, it will be marked as pending by the angular robot status checks.
|
||||
If a PR is missing the `PR target: *` label, or if the label is set to "TBD" when the PR is sent to the caretaker, the caretaker should reject the PR and request the appropriate target label to be applied before the PR is merged.
|
||||
|
||||
|
||||
## PR Approvals
|
||||
@ -196,7 +182,7 @@ In any case, the reviewer should actually look through the code and provide feed
|
||||
|
||||
Note that approved state does not mean a PR is ready to be merged.
|
||||
For example, a reviewer might approve the PR but request a minor tweak that doesn't need further review, e.g., a rebase or small uncontroversial change.
|
||||
Only the `action: merge` label means that the PR is ready for merging.
|
||||
Only the `PR action: merge` label means that the PR is ready for merging.
|
||||
|
||||
|
||||
## Special Labels
|
||||
@ -215,7 +201,7 @@ Only issues with `cla:yes` should be merged into master.
|
||||
|
||||
Applying this label to a PR makes the angular.io preview available regardless of the author. [More info](../aio/aio-builds-setup/docs/overview--security-model.md)
|
||||
|
||||
### `action: merge-assistance`
|
||||
### `PR action: merge-assistance`
|
||||
* _**Who adds it:** Any team member._
|
||||
* _**Who removes it:** Any team member._
|
||||
|
||||
@ -225,7 +211,7 @@ The comment should be formatted like this: `merge-assistance: <explain what kind
|
||||
|
||||
For example, the PR owner might not be a Googler and needs help to run g3sync; or one of the checks is failing due to external causes and the PR should still be merged.
|
||||
|
||||
### `action: rerun CI at HEAD`
|
||||
### `PR action: rerun CI at HEAD`
|
||||
* _**Who adds it:** Any team member._
|
||||
* _**Who removes it:** The Angular Bot, once it triggers the CI rerun._
|
||||
|
||||
|
4
goldens/public-api/core/core.d.ts
vendored
4
goldens/public-api/core/core.d.ts
vendored
@ -85,6 +85,10 @@ export declare interface ClassSansProvider {
|
||||
useClass: Type<any>;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export declare interface CollectionChangeRecord<V> extends IterableChangeRecord<V> {
|
||||
}
|
||||
|
||||
export declare class Compiler {
|
||||
compileModuleAndAllComponentsAsync: <T>(moduleType: Type<T>) => Promise<ModuleWithComponentFactories<T>>;
|
||||
compileModuleAndAllComponentsSync: <T>(moduleType: Type<T>) => ModuleWithComponentFactories<T>;
|
||||
|
@ -29,17 +29,11 @@ export declare class SwUpdate {
|
||||
readonly activated: Observable<UpdateActivatedEvent>;
|
||||
readonly available: Observable<UpdateAvailableEvent>;
|
||||
get isEnabled(): boolean;
|
||||
readonly unrecoverable: Observable<UnrecoverableStateEvent>;
|
||||
constructor(sw: ɵangular_packages_service_worker_service_worker_a);
|
||||
activateUpdate(): Promise<void>;
|
||||
checkForUpdate(): Promise<void>;
|
||||
}
|
||||
|
||||
export declare interface UnrecoverableStateEvent {
|
||||
reason: string;
|
||||
type: 'UNRECOVERABLE_STATE';
|
||||
}
|
||||
|
||||
export declare interface UpdateActivatedEvent {
|
||||
current: {
|
||||
hash: string;
|
||||
|
@ -39,7 +39,7 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2289,
|
||||
"main-es2015": 245303,
|
||||
"main-es2015": 245351,
|
||||
"polyfills-es2015": 36938,
|
||||
"5-es2015": 751
|
||||
}
|
||||
@ -49,7 +49,7 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2289,
|
||||
"main-es2015": 221887,
|
||||
"main-es2015": 221939,
|
||||
"polyfills-es2015": 36723,
|
||||
"5-es2015": 781
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "11.0.0-next.1",
|
||||
"version": "10.1.1",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {compileComponentFromMetadata, ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, makeBindingParser, ParsedTemplate, ParseSourceFile, parseTemplate, R3ComponentMetadata, R3FactoryTarget, R3TargetBinder, SchemaMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr} from '@angular/compiler';
|
||||
import {compileComponentFromMetadata, ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, makeBindingParser, ParseError, ParseSourceFile, parseTemplate, ParseTemplateOptions, R3ComponentMetadata, R3FactoryTarget, R3TargetBinder, SchemaMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {CycleAnalyzer} from '../../cycles';
|
||||
@ -31,7 +31,7 @@ import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagno
|
||||
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
|
||||
import {compileNgFactoryDefField} from './factory';
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
|
||||
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, makeDuplicateDeclarationError, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
|
||||
|
||||
const EMPTY_MAP = new Map<string, Expression>();
|
||||
const EMPTY_ARRAY: any[] = [];
|
||||
@ -260,7 +260,7 @@ export class ComponentDecoratorHandler implements
|
||||
|
||||
let diagnostics: ts.Diagnostic[]|undefined = undefined;
|
||||
|
||||
if (template.errors !== null) {
|
||||
if (template.errors !== undefined) {
|
||||
// If there are any template parsing errors, convert them to `ts.Diagnostic`s for display.
|
||||
const id = getTemplateId(node);
|
||||
diagnostics = template.errors.map(error => {
|
||||
@ -336,11 +336,11 @@ export class ComponentDecoratorHandler implements
|
||||
meta: {
|
||||
...metadata,
|
||||
template: {
|
||||
nodes: template.nodes,
|
||||
nodes: template.emitNodes,
|
||||
ngContentSelectors: template.ngContentSelectors,
|
||||
},
|
||||
encapsulation,
|
||||
interpolation: template.interpolationConfig ?? DEFAULT_INTERPOLATION_CONFIG,
|
||||
interpolation: template.interpolation,
|
||||
styles: styles || [],
|
||||
|
||||
// These will be replaced during the compilation step, after all `NgModule`s have been
|
||||
@ -772,7 +772,7 @@ export class ComponentDecoratorHandler implements
|
||||
|
||||
private _parseTemplate(
|
||||
component: Map<string, ts.Expression>, templateStr: string, templateUrl: string,
|
||||
templateRange: LexerRange|undefined, escapedString: boolean): ParsedComponentTemplate {
|
||||
templateRange: LexerRange|undefined, escapedString: boolean): ParsedTemplate {
|
||||
let preserveWhitespaces: boolean = this.defaultPreserveWhitespaces;
|
||||
if (component.has('preserveWhitespaces')) {
|
||||
const expr = component.get('preserveWhitespaces')!;
|
||||
@ -783,7 +783,7 @@ export class ComponentDecoratorHandler implements
|
||||
preserveWhitespaces = value;
|
||||
}
|
||||
|
||||
let interpolationConfig = DEFAULT_INTERPOLATION_CONFIG;
|
||||
let interpolation: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG;
|
||||
if (component.has('interpolation')) {
|
||||
const expr = component.get('interpolation')!;
|
||||
const value = this.evaluator.evaluate(expr);
|
||||
@ -792,20 +792,18 @@ export class ComponentDecoratorHandler implements
|
||||
throw createValueHasWrongTypeError(
|
||||
expr, value, 'interpolation must be an array with 2 elements of string type');
|
||||
}
|
||||
interpolationConfig = InterpolationConfig.fromArray(value as [string, string]);
|
||||
interpolation = InterpolationConfig.fromArray(value as [string, string]);
|
||||
}
|
||||
|
||||
// We always normalize line endings if the template has been escaped (i.e. is inline).
|
||||
const i18nNormalizeLineEndingsInICUs = escapedString || this.i18nNormalizeLineEndingsInICUs;
|
||||
|
||||
const parsedTemplate = parseTemplate(templateStr, templateUrl, {
|
||||
preserveWhitespaces,
|
||||
interpolationConfig,
|
||||
range: templateRange,
|
||||
escapedString,
|
||||
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
|
||||
i18nNormalizeLineEndingsInICUs,
|
||||
});
|
||||
const {errors, nodes: emitNodes, styleUrls, styles, ngContentSelectors} =
|
||||
parseTemplate(templateStr, templateUrl, {
|
||||
preserveWhitespaces,
|
||||
interpolationConfig: interpolation,
|
||||
range: templateRange,
|
||||
escapedString,
|
||||
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
|
||||
i18nNormalizeLineEndingsInICUs: this.i18nNormalizeLineEndingsInICUs,
|
||||
});
|
||||
|
||||
// Unfortunately, the primary parse of the template above may not contain accurate source map
|
||||
// information. If used directly, it would result in incorrect code locations in template
|
||||
@ -822,17 +820,22 @@ export class ComponentDecoratorHandler implements
|
||||
|
||||
const {nodes: diagNodes} = parseTemplate(templateStr, templateUrl, {
|
||||
preserveWhitespaces: true,
|
||||
interpolationConfig,
|
||||
interpolationConfig: interpolation,
|
||||
range: templateRange,
|
||||
escapedString,
|
||||
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
|
||||
i18nNormalizeLineEndingsInICUs,
|
||||
i18nNormalizeLineEndingsInICUs: this.i18nNormalizeLineEndingsInICUs,
|
||||
leadingTriviaChars: [],
|
||||
});
|
||||
|
||||
return {
|
||||
...parsedTemplate,
|
||||
interpolation,
|
||||
emitNodes,
|
||||
diagNodes,
|
||||
styleUrls,
|
||||
styles,
|
||||
ngContentSelectors,
|
||||
errors,
|
||||
template: templateStr,
|
||||
templateUrl,
|
||||
isInline: component.has('template'),
|
||||
@ -899,7 +902,12 @@ function sourceMapUrl(resourceUrl: string): string {
|
||||
* This contains the actual parsed template as well as any metadata collected during its parsing,
|
||||
* some of which might be useful for re-parsing the template with different options.
|
||||
*/
|
||||
export interface ParsedComponentTemplate extends ParsedTemplate {
|
||||
export interface ParsedTemplate {
|
||||
/**
|
||||
* The `InterpolationConfig` specified by the user.
|
||||
*/
|
||||
interpolation: InterpolationConfig;
|
||||
|
||||
/**
|
||||
* A full path to the file which contains the template.
|
||||
*
|
||||
@ -909,10 +917,22 @@ export interface ParsedComponentTemplate extends ParsedTemplate {
|
||||
templateUrl: string;
|
||||
|
||||
/**
|
||||
* True if the original template was stored inline;
|
||||
* False if the template was in an external file.
|
||||
* The string contents of the template.
|
||||
*
|
||||
* This is the "logical" template string, after expansion of any escaped characters (for inline
|
||||
* templates). This may differ from the actual template bytes as they appear in the .ts file.
|
||||
*/
|
||||
isInline: boolean;
|
||||
template: string;
|
||||
|
||||
/**
|
||||
* Any errors from parsing the template the first time.
|
||||
*/
|
||||
errors?: ParseError[]|undefined;
|
||||
|
||||
/**
|
||||
* The template AST, parsed according to the user's specifications.
|
||||
*/
|
||||
emitNodes: TmplAstNode[];
|
||||
|
||||
/**
|
||||
* The template AST, parsed in a manner which preserves source map information for diagnostics.
|
||||
@ -921,12 +941,36 @@ export interface ParsedComponentTemplate extends ParsedTemplate {
|
||||
*/
|
||||
diagNodes: TmplAstNode[];
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Any styleUrls extracted from the metadata.
|
||||
*/
|
||||
styleUrls: string[];
|
||||
|
||||
/**
|
||||
* Any inline styles extracted from the metadata.
|
||||
*/
|
||||
styles: string[];
|
||||
|
||||
/**
|
||||
* Any ng-content selectors extracted from the template.
|
||||
*/
|
||||
ngContentSelectors: string[];
|
||||
|
||||
/**
|
||||
* Whether the template was inline.
|
||||
*/
|
||||
isInline: boolean;
|
||||
|
||||
/**
|
||||
* The `ParseSourceFile` for the template.
|
||||
*/
|
||||
file: ParseSourceFile;
|
||||
}
|
||||
|
||||
export interface ParsedTemplateWithSource extends ParsedComponentTemplate {
|
||||
export interface ParsedTemplateWithSource extends ParsedTemplate {
|
||||
sourceMapping: TemplateSourceMapping;
|
||||
}
|
||||
|
@ -246,7 +246,7 @@ class TemplateVisitor extends TmplAstRecursiveVisitor {
|
||||
name = node.name;
|
||||
kind = IdentifierKind.Element;
|
||||
}
|
||||
const sourceSpan = node.startSourceSpan;
|
||||
const {sourceSpan} = node;
|
||||
// An element's or template's source span can be of the form `<element>`, `<element />`, or
|
||||
// `<element></element>`. Only the selector is interesting to the indexer, so the source is
|
||||
// searched for the first occurrence of the element (selector) name.
|
||||
|
@ -79,7 +79,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||
leadingTriviaChars: [],
|
||||
});
|
||||
|
||||
if (errors !== null) {
|
||||
if (errors !== undefined) {
|
||||
return {nodes, errors};
|
||||
}
|
||||
|
||||
|
@ -94,7 +94,7 @@ export class RegistryDomSchemaChecker implements DomSchemaChecker {
|
||||
}
|
||||
|
||||
const diag = makeTemplateDiagnostic(
|
||||
id, mapping, element.startSourceSpan, ts.DiagnosticCategory.Error,
|
||||
id, mapping, element.sourceSpan, ts.DiagnosticCategory.Error,
|
||||
ngErrorCode(ErrorCode.SCHEMA_INVALID_ELEMENT), errorMsg);
|
||||
this._diagnostics.push(diag);
|
||||
}
|
||||
|
@ -354,7 +354,7 @@ export function setup(targets: TypeCheckingTarget[], overrides: {
|
||||
const templateUrl = `${className}.html`;
|
||||
const templateFile = new ParseSourceFile(template, templateUrl);
|
||||
const {nodes, errors} = parseTemplate(template, templateUrl);
|
||||
if (errors !== null) {
|
||||
if (errors !== undefined) {
|
||||
throw new Error('Template parse errors: \n' + errors.join('\n'));
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,7 @@ const EXPECTED_XMB = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<msg id="5811701742971715242" desc="with ICU and other things"><source>src/icu.html:4,6</source>
|
||||
foo <ph name="ICU"><ex>{ count, plural, =1 {...} other {...}}</ex>{ count, plural, =1 {...} other {...}}</ph>
|
||||
</msg>
|
||||
<msg id="7254052530614200029" desc="with placeholders"><source>src/placeholders.html:1,3</source>Name: <ph name="START_BOLD_TEXT"><ex><b></ex><b></ph><ph name="NAME"><ex>{{
|
||||
<msg id="7254052530614200029" desc="with placeholders"><source>src/placeholders.html:1</source>Name: <ph name="START_BOLD_TEXT"><ex><b></ex><b></ph><ph name="NAME"><ex>{{
|
||||
name // i18n(ph="name")
|
||||
}}</ex>{{
|
||||
name // i18n(ph="name")
|
||||
@ -182,7 +182,7 @@ const EXPECTED_XLIFF2 = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<unit id="7254052530614200029">
|
||||
<notes>
|
||||
<note category="description">with placeholders</note>
|
||||
<note category="location">src/placeholders.html:1,3</note>
|
||||
<note category="location">src/placeholders.html:1</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>Name: <pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt" dispStart="<b>" dispEnd="</b>"><ph id="1" equiv="NAME" disp="{{
|
||||
|
@ -342,7 +342,7 @@ runInEachFileSystem((os) => {
|
||||
expect(mappings).toContain(
|
||||
{source: '<h3>', generated: 'i0.ɵɵelementStart(0, "h3")', sourceUrl: '../test.ts'});
|
||||
expect(mappings).toContain({
|
||||
source: '<ng-content select="title"></ng-content>',
|
||||
source: '<ng-content select="title">',
|
||||
generated: 'i0.ɵɵprojection(1)',
|
||||
sourceUrl: '../test.ts'
|
||||
});
|
||||
@ -351,7 +351,7 @@ runInEachFileSystem((os) => {
|
||||
expect(mappings).toContain(
|
||||
{source: '<div>', generated: 'i0.ɵɵelementStart(2, "div")', sourceUrl: '../test.ts'});
|
||||
expect(mappings).toContain({
|
||||
source: '<ng-content></ng-content>',
|
||||
source: '<ng-content>',
|
||||
generated: 'i0.ɵɵprojection(3, 1)',
|
||||
sourceUrl: '../test.ts'
|
||||
});
|
||||
|
@ -99,7 +99,7 @@ export {Identifiers as R3Identifiers} from './render3/r3_identifiers';
|
||||
export {R3DependencyMetadata, R3ResolvedDependencyType, compileFactoryFunction, R3FactoryMetadata, R3FactoryTarget} from './render3/r3_factory';
|
||||
export {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata} from './render3/r3_module_compiler';
|
||||
export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler';
|
||||
export {makeBindingParser, ParsedTemplate, parseTemplate, ParseTemplateOptions} from './render3/view/template';
|
||||
export {makeBindingParser, parseTemplate, ParseTemplateOptions} from './render3/view/template';
|
||||
export {R3Reference} from './render3/util';
|
||||
export {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, ParsedHostBindings, verifyHostBindings} from './render3/view/compiler';
|
||||
export {publishFacade} from './jit_compiler_facade';
|
||||
|
@ -124,7 +124,7 @@ class _Visitor implements html.Visitor {
|
||||
this._translations = translations;
|
||||
|
||||
// Construct a single fake root element
|
||||
const wrapper = new html.Element('wrapper', [], nodes, undefined!, undefined!, undefined);
|
||||
const wrapper = new html.Element('wrapper', [], nodes, undefined!, undefined, undefined);
|
||||
|
||||
const translatedNode = wrapper.visit(this, null);
|
||||
|
||||
@ -492,7 +492,7 @@ class _Visitor implements html.Visitor {
|
||||
}
|
||||
|
||||
private _reportError(node: html.Node, msg: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, msg));
|
||||
this._errors.push(new I18nError(node.sourceSpan!, msg));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ class _I18nVisitor implements html.Visitor {
|
||||
const isVoid: boolean = getHtmlTagDefinition(el.name).isVoid;
|
||||
const startPhName =
|
||||
context.placeholderRegistry.getStartTagPlaceholderName(el.name, attrs, isVoid);
|
||||
context.placeholderToContent[startPhName] = el.startSourceSpan.toString();
|
||||
context.placeholderToContent[startPhName] = el.sourceSpan!.toString();
|
||||
|
||||
let closePhName = '';
|
||||
|
||||
@ -104,7 +104,7 @@ class _I18nVisitor implements html.Visitor {
|
||||
}
|
||||
|
||||
visitText(text: html.Text, context: I18nMessageVisitorContext): i18n.Node {
|
||||
const node = this._visitTextWithInterpolation(text.value, text.sourceSpan, context);
|
||||
const node = this._visitTextWithInterpolation(text.value, text.sourceSpan!, context);
|
||||
return context.visitNodeFn(text, node);
|
||||
}
|
||||
|
||||
|
@ -231,9 +231,9 @@ class XliffParser implements ml.Visitor {
|
||||
break;
|
||||
|
||||
case _TARGET_TAG:
|
||||
const innerTextStart = element.startSourceSpan.end.offset;
|
||||
const innerTextStart = element.startSourceSpan!.end.offset;
|
||||
const innerTextEnd = element.endSourceSpan!.start.offset;
|
||||
const content = element.startSourceSpan.start.file.content;
|
||||
const content = element.startSourceSpan!.start.file.content;
|
||||
const innerText = content.slice(innerTextStart, innerTextEnd);
|
||||
this._unitMlString = innerText;
|
||||
break;
|
||||
@ -264,7 +264,7 @@ class XliffParser implements ml.Visitor {
|
||||
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {}
|
||||
|
||||
private _addError(node: ml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
this._errors.push(new I18nError(node.sourceSpan!, message));
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,14 +288,14 @@ class XmlToI18n implements ml.Visitor {
|
||||
}
|
||||
|
||||
visitText(text: ml.Text, context: any) {
|
||||
return new i18n.Text(text.value, text.sourceSpan);
|
||||
return new i18n.Text(text.value, text.sourceSpan!);
|
||||
}
|
||||
|
||||
visitElement(el: ml.Element, context: any): i18n.Placeholder|ml.Node[]|null {
|
||||
if (el.name === _PLACEHOLDER_TAG) {
|
||||
const nameAttr = el.attrs.find((attr) => attr.name === 'id');
|
||||
if (nameAttr) {
|
||||
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan);
|
||||
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan!);
|
||||
}
|
||||
|
||||
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "id" attribute`);
|
||||
@ -332,7 +332,7 @@ class XmlToI18n implements ml.Visitor {
|
||||
visitAttribute(attribute: ml.Attribute, context: any) {}
|
||||
|
||||
private _addError(node: ml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
this._errors.push(new I18nError(node.sourceSpan!, message));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,9 +246,9 @@ class Xliff2Parser implements ml.Visitor {
|
||||
break;
|
||||
|
||||
case _TARGET_TAG:
|
||||
const innerTextStart = element.startSourceSpan.end.offset;
|
||||
const innerTextStart = element.startSourceSpan!.end.offset;
|
||||
const innerTextEnd = element.endSourceSpan!.start.offset;
|
||||
const content = element.startSourceSpan.start.file.content;
|
||||
const content = element.startSourceSpan!.start.file.content;
|
||||
const innerText = content.slice(innerTextStart, innerTextEnd);
|
||||
this._unitMlString = innerText;
|
||||
break;
|
||||
|
@ -130,9 +130,9 @@ class XtbParser implements ml.Visitor {
|
||||
if (this._msgIdToHtml.hasOwnProperty(id)) {
|
||||
this._addError(element, `Duplicated translations for msg ${id}`);
|
||||
} else {
|
||||
const innerTextStart = element.startSourceSpan.end.offset;
|
||||
const innerTextStart = element.startSourceSpan!.end.offset;
|
||||
const innerTextEnd = element.endSourceSpan!.start.offset;
|
||||
const content = element.startSourceSpan.start.file.content;
|
||||
const content = element.startSourceSpan!.start.file.content;
|
||||
const innerText = content.slice(innerTextStart!, innerTextEnd!);
|
||||
this._msgIdToHtml[id] = innerText;
|
||||
}
|
||||
@ -155,7 +155,7 @@ class XtbParser implements ml.Visitor {
|
||||
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {}
|
||||
|
||||
private _addError(node: ml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
this._errors.push(new I18nError(node.sourceSpan!, message));
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +179,7 @@ class XmlToI18n implements ml.Visitor {
|
||||
}
|
||||
|
||||
visitText(text: ml.Text, context: any) {
|
||||
return new i18n.Text(text.value, text.sourceSpan);
|
||||
return new i18n.Text(text.value, text.sourceSpan!);
|
||||
}
|
||||
|
||||
visitExpansion(icu: ml.Expansion, context: any) {
|
||||
@ -203,7 +203,7 @@ class XmlToI18n implements ml.Visitor {
|
||||
if (el.name === _PLACEHOLDER_TAG) {
|
||||
const nameAttr = el.attrs.find((attr) => attr.name === 'name');
|
||||
if (nameAttr) {
|
||||
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan);
|
||||
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan!);
|
||||
}
|
||||
|
||||
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
|
||||
@ -218,6 +218,6 @@ class XmlToI18n implements ml.Visitor {
|
||||
visitAttribute(attribute: ml.Attribute, context: any) {}
|
||||
|
||||
private _addError(node: ml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
this._errors.push(new I18nError(node.sourceSpan!, message));
|
||||
}
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ export class CompilerFacadeImpl implements CompilerFacade {
|
||||
const template = parseTemplate(
|
||||
facade.template, sourceMapUrl,
|
||||
{preserveWhitespaces: facade.preserveWhitespaces, interpolationConfig});
|
||||
if (template.errors !== null) {
|
||||
if (template.errors !== undefined) {
|
||||
const errors = template.errors.map(err => err.toString()).join(', ');
|
||||
throw new Error(`Errors during JIT compilation of template for ${facade.name}: ${errors}`);
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export class Attribute extends NodeWithI18n {
|
||||
export class Element extends NodeWithI18n {
|
||||
constructor(
|
||||
public name: string, public attrs: Attribute[], public children: Node[],
|
||||
sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
|
||||
sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null = null,
|
||||
public endSourceSpan: ParseSourceSpan|null = null, i18n?: I18nMeta) {
|
||||
super(sourceSpan, i18n);
|
||||
}
|
||||
|
@ -642,11 +642,13 @@ class _Tokenizer {
|
||||
this._beginToken(TokenType.RAW_TEXT);
|
||||
const condition = this._readUntil(chars.$COMMA);
|
||||
const normalizedCondition = this._processCarriageReturns(condition);
|
||||
if (this._i18nNormalizeLineEndingsInICUs) {
|
||||
// We explicitly want to normalize line endings for this text.
|
||||
if (this._escapedString || this._i18nNormalizeLineEndingsInICUs) {
|
||||
// Either the template is inline or,
|
||||
// we explicitly want to normalize line endings for this text.
|
||||
this._endToken([normalizedCondition]);
|
||||
} else {
|
||||
// We are not normalizing line endings.
|
||||
// The expression is in an external template and, for backward compatibility,
|
||||
// we are not normalizing line endings.
|
||||
const conditionToken = this._endToken([condition]);
|
||||
if (normalizedCondition !== condition) {
|
||||
this.nonNormalizedIcuExpressions.push(conditionToken);
|
||||
|
@ -258,9 +258,7 @@ class _TreeBuilder {
|
||||
}
|
||||
const end = this._peek.sourceSpan.start;
|
||||
const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
|
||||
// Create a separate `startSpan` because `span` will be modified when there is an `end` span.
|
||||
const startSpan = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
|
||||
const el = new html.Element(fullName, attrs, [], span, startSpan, undefined);
|
||||
const el = new html.Element(fullName, attrs, [], span, span, undefined);
|
||||
this._pushElement(el);
|
||||
if (selfClosing) {
|
||||
// Elements that are self-closed have their `endSourceSpan` set to the full span, as the
|
||||
@ -303,7 +301,6 @@ class _TreeBuilder {
|
||||
// removed from the element stack at this point are closed implicitly, so they won't get
|
||||
// an end source span (as there is no explicit closing element).
|
||||
el.endSourceSpan = endSourceSpan;
|
||||
el.sourceSpan.end = endSourceSpan.end || el.sourceSpan.end;
|
||||
|
||||
this._elementStack.splice(stackIndex, this._elementStack.length - stackIndex);
|
||||
return true;
|
||||
|
@ -79,8 +79,13 @@ export class Element implements Node {
|
||||
constructor(
|
||||
public name: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
|
||||
public outputs: BoundEvent[], public children: Node[], public references: Reference[],
|
||||
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
|
||||
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nMeta) {}
|
||||
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null,
|
||||
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nMeta) {
|
||||
// If the element is empty then the source span should include any closing tag
|
||||
if (children.length === 0 && startSourceSpan && endSourceSpan) {
|
||||
this.sourceSpan = new ParseSourceSpan(sourceSpan.start, endSourceSpan.end);
|
||||
}
|
||||
}
|
||||
visit<Result>(visitor: Visitor<Result>): Result {
|
||||
return visitor.visitElement(this);
|
||||
}
|
||||
@ -91,7 +96,7 @@ export class Template implements Node {
|
||||
public tagName: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
|
||||
public outputs: BoundEvent[], public templateAttrs: (BoundAttribute|TextAttribute)[],
|
||||
public children: Node[], public references: Reference[], public variables: Variable[],
|
||||
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
|
||||
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null,
|
||||
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nMeta) {}
|
||||
visit<Result>(visitor: Visitor<Result>): Result {
|
||||
return visitor.visitTemplate(this);
|
||||
|
@ -544,7 +544,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
|
||||
private addNamespaceInstruction(nsInstruction: o.ExternalReference, element: t.Element) {
|
||||
this._namespace = nsInstruction;
|
||||
this.creationInstruction(element.startSourceSpan, nsInstruction);
|
||||
this.creationInstruction(element.sourceSpan, nsInstruction);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -671,16 +671,15 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
trimTrailingNulls(parameters));
|
||||
} else {
|
||||
this.creationInstruction(
|
||||
element.startSourceSpan, isNgContainer ? R3.elementContainerStart : R3.elementStart,
|
||||
element.sourceSpan, isNgContainer ? R3.elementContainerStart : R3.elementStart,
|
||||
trimTrailingNulls(parameters));
|
||||
|
||||
if (isNonBindableMode) {
|
||||
this.creationInstruction(element.startSourceSpan, R3.disableBindings);
|
||||
this.creationInstruction(element.sourceSpan, R3.disableBindings);
|
||||
}
|
||||
|
||||
if (i18nAttrs.length > 0) {
|
||||
this.i18nAttributesInstruction(
|
||||
elementIndex, i18nAttrs, element.startSourceSpan ?? element.sourceSpan);
|
||||
this.i18nAttributesInstruction(elementIndex, i18nAttrs, element.sourceSpan);
|
||||
}
|
||||
|
||||
// Generate Listeners (outputs)
|
||||
@ -696,7 +695,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// Note: it's important to keep i18n/i18nStart instructions after i18nAttributes and
|
||||
// listeners, to make sure i18nAttributes instruction targets current element at runtime.
|
||||
if (isI18nRootElement) {
|
||||
this.i18nStart(element.startSourceSpan, element.i18n!, createSelfClosingI18nInstruction);
|
||||
this.i18nStart(element.sourceSpan, element.i18n!, createSelfClosingI18nInstruction);
|
||||
}
|
||||
}
|
||||
|
||||
@ -828,7 +827,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
|
||||
if (!createSelfClosingInstruction) {
|
||||
// Finish element construction mode.
|
||||
const span = element.endSourceSpan ?? element.sourceSpan;
|
||||
const span = element.endSourceSpan || element.sourceSpan;
|
||||
if (isI18nRootElement) {
|
||||
this.i18nEnd(span, createSelfClosingI18nInstruction);
|
||||
}
|
||||
@ -920,8 +919,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// elements, in case of inline templates, corresponding instructions will be generated in the
|
||||
// nested template function.
|
||||
if (i18nAttrs.length > 0) {
|
||||
this.i18nAttributesInstruction(
|
||||
templateIndex, i18nAttrs, template.startSourceSpan ?? template.sourceSpan);
|
||||
this.i18nAttributesInstruction(templateIndex, i18nAttrs, template.sourceSpan);
|
||||
}
|
||||
|
||||
// Add the input bindings
|
||||
@ -2025,7 +2023,13 @@ export interface ParseTemplateOptions {
|
||||
* @param options options to modify how the template is parsed
|
||||
*/
|
||||
export function parseTemplate(
|
||||
template: string, templateUrl: string, options: ParseTemplateOptions = {}): ParsedTemplate {
|
||||
template: string, templateUrl: string, options: ParseTemplateOptions = {}): {
|
||||
errors?: ParseError[],
|
||||
nodes: t.Node[],
|
||||
styleUrls: string[],
|
||||
styles: string[],
|
||||
ngContentSelectors: string[]
|
||||
} {
|
||||
const {interpolationConfig, preserveWhitespaces, enableI18nLegacyMessageIdFormat} = options;
|
||||
const bindingParser = makeBindingParser(interpolationConfig);
|
||||
const htmlParser = new HtmlParser();
|
||||
@ -2035,9 +2039,6 @@ export function parseTemplate(
|
||||
|
||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
||||
return {
|
||||
interpolationConfig,
|
||||
preserveWhitespaces,
|
||||
template,
|
||||
errors: parseResult.errors,
|
||||
nodes: [],
|
||||
styleUrls: [],
|
||||
@ -2073,28 +2074,10 @@ export function parseTemplate(
|
||||
const {nodes, errors, styleUrls, styles, ngContentSelectors} =
|
||||
htmlAstToRender3Ast(rootNodes, bindingParser);
|
||||
if (errors && errors.length > 0) {
|
||||
return {
|
||||
interpolationConfig,
|
||||
preserveWhitespaces,
|
||||
template,
|
||||
errors,
|
||||
nodes: [],
|
||||
styleUrls: [],
|
||||
styles: [],
|
||||
ngContentSelectors: []
|
||||
};
|
||||
return {errors, nodes: [], styleUrls: [], styles: [], ngContentSelectors: []};
|
||||
}
|
||||
|
||||
return {
|
||||
interpolationConfig,
|
||||
preserveWhitespaces,
|
||||
errors: null,
|
||||
template,
|
||||
nodes,
|
||||
styleUrls,
|
||||
styles,
|
||||
ngContentSelectors
|
||||
};
|
||||
return {nodes, styleUrls, styles, ngContentSelectors};
|
||||
}
|
||||
|
||||
const elementRegistry = new DomElementSchemaRegistry();
|
||||
@ -2211,54 +2194,3 @@ function createClosureModeGuard(): o.BinaryOperatorExpr {
|
||||
.notIdentical(o.literal('undefined', o.STRING_TYPE))
|
||||
.and(o.variable(NG_I18N_CLOSURE_MODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the template which was extracted during parsing.
|
||||
*
|
||||
* This contains the actual parsed template as well as any metadata collected during its parsing,
|
||||
* some of which might be useful for re-parsing the template with different options.
|
||||
*/
|
||||
export interface ParsedTemplate {
|
||||
/**
|
||||
* Include whitespace nodes in the parsed output.
|
||||
*/
|
||||
preserveWhitespaces?: boolean;
|
||||
|
||||
/**
|
||||
* How to parse interpolation markers.
|
||||
*/
|
||||
interpolationConfig?: InterpolationConfig;
|
||||
|
||||
/**
|
||||
* The string contents of the template.
|
||||
*
|
||||
* This is the "logical" template string, after expansion of any escaped characters (for inline
|
||||
* templates). This may differ from the actual template bytes as they appear in the .ts file.
|
||||
*/
|
||||
template: string;
|
||||
|
||||
/**
|
||||
* Any errors from parsing the template the first time.
|
||||
*/
|
||||
errors: ParseError[]|null;
|
||||
|
||||
/**
|
||||
* The template AST, parsed from the template.
|
||||
*/
|
||||
nodes: t.Node[];
|
||||
|
||||
/**
|
||||
* Any styleUrls extracted from the metadata.
|
||||
*/
|
||||
styleUrls: string[];
|
||||
|
||||
/**
|
||||
* Any inline styles extracted from the metadata.
|
||||
*/
|
||||
styles: string[];
|
||||
|
||||
/**
|
||||
* Any ng-content selectors extracted from the template.
|
||||
*/
|
||||
ngContentSelectors: string[];
|
||||
}
|
||||
|
@ -244,9 +244,9 @@ class TemplateParseVisitor implements html.Visitor {
|
||||
visitText(text: html.Text, parent: ElementContext): any {
|
||||
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR())!;
|
||||
const valueNoNgsp = replaceNgsp(text.value);
|
||||
const expr = this._bindingParser.parseInterpolation(valueNoNgsp, text.sourceSpan);
|
||||
return expr ? new t.BoundTextAst(expr, ngContentIndex, text.sourceSpan) :
|
||||
new t.TextAst(valueNoNgsp, ngContentIndex, text.sourceSpan);
|
||||
const expr = this._bindingParser.parseInterpolation(valueNoNgsp, text.sourceSpan!);
|
||||
return expr ? new t.BoundTextAst(expr, ngContentIndex, text.sourceSpan!) :
|
||||
new t.TextAst(valueNoNgsp, ngContentIndex, text.sourceSpan!);
|
||||
}
|
||||
|
||||
visitAttribute(attribute: html.Attribute, context: any): any {
|
||||
@ -335,14 +335,14 @@ class TemplateParseVisitor implements html.Visitor {
|
||||
const boundDirectivePropNames = new Set<string>();
|
||||
const directiveAsts = this._createDirectiveAsts(
|
||||
isTemplateElement, element.name, directiveMetas, elementOrDirectiveProps,
|
||||
elementOrDirectiveRefs, element.sourceSpan, references, boundDirectivePropNames);
|
||||
elementOrDirectiveRefs, element.sourceSpan!, references, boundDirectivePropNames);
|
||||
const elementProps: t.BoundElementPropertyAst[] = this._createElementPropertyAsts(
|
||||
element.name, elementOrDirectiveProps, boundDirectivePropNames);
|
||||
const isViewRoot = parent.isTemplateElement || hasInlineTemplates;
|
||||
|
||||
const providerContext = new ProviderElementContext(
|
||||
this.providerViewContext, parent.providerContext!, isViewRoot, directiveAsts, attrs,
|
||||
references, isTemplateElement, queryStartIndex, element.sourceSpan);
|
||||
references, isTemplateElement, queryStartIndex, element.sourceSpan!);
|
||||
|
||||
const children: t.TemplateAst[] = html.visitAll(
|
||||
preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this, element.children,
|
||||
@ -360,26 +360,26 @@ class TemplateParseVisitor implements html.Visitor {
|
||||
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
|
||||
// `<ng-content>` element
|
||||
if (element.children && !element.children.every(_isEmptyTextNode)) {
|
||||
this._reportError(`<ng-content> element cannot have content.`, element.sourceSpan);
|
||||
this._reportError(`<ng-content> element cannot have content.`, element.sourceSpan!);
|
||||
}
|
||||
|
||||
parsedElement = new t.NgContentAst(
|
||||
this.ngContentCount++, hasInlineTemplates ? null! : ngContentIndex, element.sourceSpan);
|
||||
this.ngContentCount++, hasInlineTemplates ? null! : ngContentIndex, element.sourceSpan!);
|
||||
} else if (isTemplateElement) {
|
||||
// `<ng-template>` element
|
||||
this._assertAllEventsPublishedByDirectives(directiveAsts, events);
|
||||
this._assertNoComponentsNorElementBindingsOnTemplate(
|
||||
directiveAsts, elementProps, element.sourceSpan);
|
||||
directiveAsts, elementProps, element.sourceSpan!);
|
||||
|
||||
parsedElement = new t.EmbeddedTemplateAst(
|
||||
attrs, events, references, elementVars, providerContext.transformedDirectiveAsts,
|
||||
providerContext.transformProviders, providerContext.transformedHasViewContainer,
|
||||
providerContext.queryMatches, children, hasInlineTemplates ? null! : ngContentIndex,
|
||||
element.sourceSpan);
|
||||
element.sourceSpan!);
|
||||
} else {
|
||||
// element other than `<ng-content>` and `<ng-template>`
|
||||
this._assertElementExists(matchElement, element);
|
||||
this._assertOnlyOneComponent(directiveAsts, element.sourceSpan);
|
||||
this._assertOnlyOneComponent(directiveAsts, element.sourceSpan!);
|
||||
|
||||
const ngContentIndex =
|
||||
hasInlineTemplates ? null : parent.findNgContentIndex(projectionSelector);
|
||||
@ -397,22 +397,22 @@ class TemplateParseVisitor implements html.Visitor {
|
||||
const {directives} = this._parseDirectives(this.selectorMatcher, templateSelector);
|
||||
const templateBoundDirectivePropNames = new Set<string>();
|
||||
const templateDirectiveAsts = this._createDirectiveAsts(
|
||||
true, elName, directives, templateElementOrDirectiveProps, [], element.sourceSpan, [],
|
||||
true, elName, directives, templateElementOrDirectiveProps, [], element.sourceSpan!, [],
|
||||
templateBoundDirectivePropNames);
|
||||
const templateElementProps: t.BoundElementPropertyAst[] = this._createElementPropertyAsts(
|
||||
elName, templateElementOrDirectiveProps, templateBoundDirectivePropNames);
|
||||
this._assertNoComponentsNorElementBindingsOnTemplate(
|
||||
templateDirectiveAsts, templateElementProps, element.sourceSpan);
|
||||
templateDirectiveAsts, templateElementProps, element.sourceSpan!);
|
||||
const templateProviderContext = new ProviderElementContext(
|
||||
this.providerViewContext, parent.providerContext!, parent.isTemplateElement,
|
||||
templateDirectiveAsts, [], [], true, templateQueryStartIndex, element.sourceSpan);
|
||||
templateDirectiveAsts, [], [], true, templateQueryStartIndex, element.sourceSpan!);
|
||||
templateProviderContext.afterElement();
|
||||
|
||||
parsedElement = new t.EmbeddedTemplateAst(
|
||||
[], [], [], templateElementVars, templateProviderContext.transformedDirectiveAsts,
|
||||
templateProviderContext.transformProviders,
|
||||
templateProviderContext.transformedHasViewContainer, templateProviderContext.queryMatches,
|
||||
[parsedElement], ngContentIndex, element.sourceSpan);
|
||||
[parsedElement], ngContentIndex, element.sourceSpan!);
|
||||
}
|
||||
|
||||
return parsedElement;
|
||||
@ -707,7 +707,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||
errorMsg +=
|
||||
`2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`;
|
||||
}
|
||||
this._reportError(errorMsg, element.sourceSpan);
|
||||
this._reportError(errorMsg, element.sourceSpan!);
|
||||
}
|
||||
}
|
||||
|
||||
@ -815,7 +815,7 @@ class NonBindableVisitor implements html.Visitor {
|
||||
|
||||
visitText(text: html.Text, parent: ElementContext): t.TextAst {
|
||||
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR())!;
|
||||
return new t.TextAst(text.value, ngContentIndex, text.sourceSpan);
|
||||
return new t.TextAst(text.value, ngContentIndex, text.sourceSpan!);
|
||||
}
|
||||
|
||||
visitExpansion(expansion: html.Expansion, context: any): any {
|
||||
|
@ -322,28 +322,19 @@ import {serializeNodes as serializeHtmlNodes} from '../ml_parser/util/util';
|
||||
describe('elements', () => {
|
||||
it('should report nested translatable elements', () => {
|
||||
expect(extractErrors(`<p i18n><b i18n></b></p>`)).toEqual([
|
||||
[
|
||||
'Could not mark an element as translatable inside a translatable section',
|
||||
'<b i18n></b>'
|
||||
],
|
||||
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report translatable elements in implicit elements', () => {
|
||||
expect(extractErrors(`<p><b i18n></b></p>`, ['p'])).toEqual([
|
||||
[
|
||||
'Could not mark an element as translatable inside a translatable section',
|
||||
'<b i18n></b>'
|
||||
],
|
||||
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report translatable elements in translatable blocks', () => {
|
||||
expect(extractErrors(`<!-- i18n --><b i18n></b><!-- /i18n -->`)).toEqual([
|
||||
[
|
||||
'Could not mark an element as translatable inside a translatable section',
|
||||
'<b i18n></b>'
|
||||
],
|
||||
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -379,7 +370,7 @@ import {serializeNodes as serializeHtmlNodes} from '../ml_parser/util/util';
|
||||
it('should report when start and end of a block are not at the same level', () => {
|
||||
expect(extractErrors(`<!-- i18n --><p><!-- /i18n --></p>`)).toEqual([
|
||||
['I18N blocks should not cross element boundaries', '<!--'],
|
||||
['Unclosed block', '<p><!-- /i18n --></p>'],
|
||||
['Unclosed block', '<p>'],
|
||||
]);
|
||||
|
||||
expect(extractErrors(`<p><!-- i18n --></p><!-- /i18n -->`)).toEqual([
|
||||
|
@ -42,7 +42,7 @@ class _Humanizer implements html.Visitor {
|
||||
visitElement(element: html.Element, context: any): any {
|
||||
const res = this._appendContext(element, [html.Element, element.name, this.elDepth++]);
|
||||
if (this.includeSourceSpan) {
|
||||
res.push(element.startSourceSpan.toString() ?? null);
|
||||
res.push(element.startSourceSpan?.toString() ?? null);
|
||||
res.push(element.endSourceSpan?.toString() ?? null);
|
||||
}
|
||||
this.result.push(res);
|
||||
@ -82,7 +82,7 @@ class _Humanizer implements html.Visitor {
|
||||
|
||||
private _appendContext(ast: html.Node, input: any[]): any[] {
|
||||
if (!this.includeSourceSpan) return input;
|
||||
input.push(ast.sourceSpan.toString());
|
||||
input.push(ast.sourceSpan!.toString());
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
@ -332,75 +332,40 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
|
||||
]);
|
||||
});
|
||||
|
||||
it('should normalize line-endings in expansion forms in inline templates if `i18nNormalizeLineEndingsInICUs` is true',
|
||||
() => {
|
||||
const parsed = parser.parse(
|
||||
`<div>\r\n` +
|
||||
` {\r\n` +
|
||||
` messages.length,\r\n` +
|
||||
` plural,\r\n` +
|
||||
` =0 {You have \r\nno\r\n messages}\r\n` +
|
||||
` =1 {One {{message}}}}\r\n` +
|
||||
`</div>`,
|
||||
'TestComp', {
|
||||
tokenizeExpansionForms: true,
|
||||
escapedString: true,
|
||||
i18nNormalizeLineEndingsInICUs: true,
|
||||
});
|
||||
it('should normalize line-endings in expansion forms in inline templates', () => {
|
||||
const parsed = parser.parse(
|
||||
`<div>\r\n` +
|
||||
` {\r\n` +
|
||||
` messages.length,\r\n` +
|
||||
` plural,\r\n` +
|
||||
` =0 {You have \r\nno\r\n messages}\r\n` +
|
||||
` =1 {One {{message}}}}\r\n` +
|
||||
`</div>`,
|
||||
'TestComp', {
|
||||
tokenizeExpansionForms: true,
|
||||
escapedString: true,
|
||||
});
|
||||
|
||||
expect(humanizeDom(parsed)).toEqual([
|
||||
[html.Element, 'div', 0],
|
||||
[html.Text, '\n ', 1],
|
||||
[html.Expansion, '\n messages.length', 'plural', 1],
|
||||
[html.ExpansionCase, '=0', 2],
|
||||
[html.ExpansionCase, '=1', 2],
|
||||
[html.Text, '\n', 1],
|
||||
]);
|
||||
const cases = (<any>parsed.rootNodes[0]).children[1].cases;
|
||||
expect(humanizeDom(parsed)).toEqual([
|
||||
[html.Element, 'div', 0],
|
||||
[html.Text, '\n ', 1],
|
||||
[html.Expansion, '\n messages.length', 'plural', 1],
|
||||
[html.ExpansionCase, '=0', 2],
|
||||
[html.ExpansionCase, '=1', 2],
|
||||
[html.Text, '\n', 1],
|
||||
]);
|
||||
const cases = (<any>parsed.rootNodes[0]).children[1].cases;
|
||||
|
||||
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
|
||||
[html.Text, 'You have \nno\n messages', 0],
|
||||
]);
|
||||
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
|
||||
[html.Text, 'You have \nno\n messages', 0],
|
||||
]);
|
||||
|
||||
expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([
|
||||
[html.Text, 'One {{message}}', 0]
|
||||
]);
|
||||
expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([
|
||||
[html.Text, 'One {{message}}', 0]
|
||||
]);
|
||||
|
||||
expect(parsed.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not normalize line-endings in ICU expressions in external templates when `i18nNormalizeLineEndingsInICUs` is not set',
|
||||
() => {
|
||||
const parsed = parser.parse(
|
||||
`<div>\r\n` +
|
||||
` {\r\n` +
|
||||
` messages.length,\r\n` +
|
||||
` plural,\r\n` +
|
||||
` =0 {You have \r\nno\r\n messages}\r\n` +
|
||||
` =1 {One {{message}}}}\r\n` +
|
||||
`</div>`,
|
||||
'TestComp', {tokenizeExpansionForms: true, escapedString: true});
|
||||
|
||||
expect(humanizeDom(parsed)).toEqual([
|
||||
[html.Element, 'div', 0],
|
||||
[html.Text, '\n ', 1],
|
||||
[html.Expansion, '\r\n messages.length', 'plural', 1],
|
||||
[html.ExpansionCase, '=0', 2],
|
||||
[html.ExpansionCase, '=1', 2],
|
||||
[html.Text, '\n', 1],
|
||||
]);
|
||||
const cases = (<any>parsed.rootNodes[0]).children[1].cases;
|
||||
|
||||
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
|
||||
[html.Text, 'You have \nno\n messages', 0],
|
||||
]);
|
||||
|
||||
expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([
|
||||
[html.Text, 'One {{message}}', 0]
|
||||
]);
|
||||
|
||||
expect(parsed.errors).toEqual([]);
|
||||
});
|
||||
expect(parsed.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should normalize line-endings in expansion forms in external templates if `i18nNormalizeLineEndingsInICUs` is true',
|
||||
() => {
|
||||
@ -503,67 +468,33 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
|
||||
]);
|
||||
});
|
||||
|
||||
it('should normalize line endings in nested expansion forms for inline templates, when `i18nNormalizeLineEndingsInICUs` is true',
|
||||
() => {
|
||||
const parsed = parser.parse(
|
||||
`{\r\n` +
|
||||
` messages.length, plural,\r\n` +
|
||||
` =0 { zero \r\n` +
|
||||
` {\r\n` +
|
||||
` p.gender, select,\r\n` +
|
||||
` male {m}\r\n` +
|
||||
` }\r\n` +
|
||||
` }\r\n` +
|
||||
`}`,
|
||||
'TestComp', {
|
||||
tokenizeExpansionForms: true,
|
||||
escapedString: true,
|
||||
i18nNormalizeLineEndingsInICUs: true
|
||||
});
|
||||
expect(humanizeDom(parsed)).toEqual([
|
||||
[html.Expansion, '\n messages.length', 'plural', 0],
|
||||
[html.ExpansionCase, '=0', 1],
|
||||
]);
|
||||
it('should normalize line endings in nested expansion forms for inline templates', () => {
|
||||
const parsed = parser.parse(
|
||||
`{\r\n` +
|
||||
` messages.length, plural,\r\n` +
|
||||
` =0 { zero \r\n` +
|
||||
` {\r\n` +
|
||||
` p.gender, select,\r\n` +
|
||||
` male {m}\r\n` +
|
||||
` }\r\n` +
|
||||
` }\r\n` +
|
||||
`}`,
|
||||
'TestComp', {tokenizeExpansionForms: true, escapedString: true});
|
||||
expect(humanizeDom(parsed)).toEqual([
|
||||
[html.Expansion, '\n messages.length', 'plural', 0],
|
||||
[html.ExpansionCase, '=0', 1],
|
||||
]);
|
||||
|
||||
const expansion = parsed.rootNodes[0] as html.Expansion;
|
||||
expect(humanizeDom(new ParseTreeResult(expansion.cases[0].expression, []))).toEqual([
|
||||
[html.Text, 'zero \n ', 0],
|
||||
[html.Expansion, '\n p.gender', 'select', 0],
|
||||
[html.ExpansionCase, 'male', 1],
|
||||
[html.Text, '\n ', 0],
|
||||
]);
|
||||
const expansion = parsed.rootNodes[0] as html.Expansion;
|
||||
expect(humanizeDom(new ParseTreeResult(expansion.cases[0].expression, []))).toEqual([
|
||||
[html.Text, 'zero \n ', 0],
|
||||
[html.Expansion, '\n p.gender', 'select', 0],
|
||||
[html.ExpansionCase, 'male', 1],
|
||||
[html.Text, '\n ', 0],
|
||||
]);
|
||||
|
||||
expect(parsed.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not normalize line endings in nested expansion forms for inline templates, when `i18nNormalizeLineEndingsInICUs` is not defined',
|
||||
() => {
|
||||
const parsed = parser.parse(
|
||||
`{\r\n` +
|
||||
` messages.length, plural,\r\n` +
|
||||
` =0 { zero \r\n` +
|
||||
` {\r\n` +
|
||||
` p.gender, select,\r\n` +
|
||||
` male {m}\r\n` +
|
||||
` }\r\n` +
|
||||
` }\r\n` +
|
||||
`}`,
|
||||
'TestComp', {tokenizeExpansionForms: true, escapedString: true});
|
||||
expect(humanizeDom(parsed)).toEqual([
|
||||
[html.Expansion, '\r\n messages.length', 'plural', 0],
|
||||
[html.ExpansionCase, '=0', 1],
|
||||
]);
|
||||
|
||||
const expansion = parsed.rootNodes[0] as html.Expansion;
|
||||
expect(humanizeDom(new ParseTreeResult(expansion.cases[0].expression, []))).toEqual([
|
||||
[html.Text, 'zero \n ', 0],
|
||||
[html.Expansion, '\r\n p.gender', 'select', 0],
|
||||
[html.ExpansionCase, 'male', 1],
|
||||
[html.Text, '\n ', 0],
|
||||
]);
|
||||
|
||||
expect(parsed.errors).toEqual([]);
|
||||
});
|
||||
expect(parsed.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not normalize line endings in nested expansion forms for external templates, when `i18nNormalizeLineEndingsInICUs` is not set',
|
||||
() => {
|
||||
@ -653,8 +584,7 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
|
||||
'<div [prop]="v1" (e)="do()" attr="v2" noValue>\na\n</div>', 'TestComp')))
|
||||
.toEqual([
|
||||
[
|
||||
html.Element, 'div', 0,
|
||||
'<div [prop]="v1" (e)="do()" attr="v2" noValue>\na\n</div>',
|
||||
html.Element, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>',
|
||||
'<div [prop]="v1" (e)="do()" attr="v2" noValue>', '</div>'
|
||||
],
|
||||
[html.Attribute, '[prop]', 'v1', '[prop]="v1"'],
|
||||
@ -668,8 +598,8 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
|
||||
it('should set the start and end source spans', () => {
|
||||
const node = <html.Element>parser.parse('<div>a</div>', 'TestComp').rootNodes[0];
|
||||
|
||||
expect(node.startSourceSpan.start.offset).toEqual(0);
|
||||
expect(node.startSourceSpan.end.offset).toEqual(5);
|
||||
expect(node.startSourceSpan!.start.offset).toEqual(0);
|
||||
expect(node.startSourceSpan!.end.offset).toEqual(5);
|
||||
|
||||
expect(node.endSourceSpan!.start.offset).toEqual(6);
|
||||
expect(node.endSourceSpan!.end.offset).toEqual(12);
|
||||
@ -677,14 +607,14 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
|
||||
|
||||
it('should not set the end source span for void elements', () => {
|
||||
expect(humanizeDomSourceSpans(parser.parse('<div><br></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0, '<div><br></div>', '<div>', '</div>'],
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Element, 'br', 1, '<br>', '<br>', null],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not set the end source span for multiple void elements', () => {
|
||||
expect(humanizeDomSourceSpans(parser.parse('<div><br><hr></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0, '<div><br><hr></div>', '<div>', '</div>'],
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Element, 'br', 1, '<br>', '<br>', null],
|
||||
[html.Element, 'hr', 1, '<hr>', '<hr>', null],
|
||||
]);
|
||||
@ -704,19 +634,19 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
|
||||
|
||||
it('should set the end source span for self-closing elements', () => {
|
||||
expect(humanizeDomSourceSpans(parser.parse('<div><br/></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0, '<div><br/></div>', '<div>', '</div>'],
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Element, 'br', 1, '<br/>', '<br/>', '<br/>'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not set the end source span for elements that are implicitly closed', () => {
|
||||
expect(humanizeDomSourceSpans(parser.parse('<div><p></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0, '<div><p></div>', '<div>', '</div>'],
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Element, 'p', 1, '<p>', '<p>', null],
|
||||
]);
|
||||
expect(humanizeDomSourceSpans(parser.parse('<div><li>A<li>B</div>', 'TestComp')))
|
||||
.toEqual([
|
||||
[html.Element, 'div', 0, '<div><li>A<li>B</div>', '<div>', '</div>'],
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Element, 'li', 1, '<li>', '<li>', null],
|
||||
[html.Text, 'A', 2, 'A'],
|
||||
[html.Element, 'li', 1, '<li>', '<li>', null],
|
||||
@ -729,7 +659,7 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
|
||||
'<div>{count, plural, =0 {msg}}</div>', 'TestComp',
|
||||
{tokenizeExpansionForms: true})))
|
||||
.toEqual([
|
||||
[html.Element, 'div', 0, '<div>{count, plural, =0 {msg}}</div>', '<div>', '</div>'],
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Expansion, 'count', 'plural', 1, '{count, plural, =0 {msg}}'],
|
||||
[html.ExpansionCase, '=0', 2, '=0 {msg}'],
|
||||
]);
|
||||
|
@ -56,10 +56,10 @@ import {humanizeNodes} from './ast_spec_utils';
|
||||
const nodes = expand(`{messages.length, plural,=0 {<b>bold</b>}}`).nodes;
|
||||
|
||||
const container: html.Element = <html.Element>nodes[0];
|
||||
expect(container.sourceSpan.start.col).toEqual(0);
|
||||
expect(container.sourceSpan.end.col).toEqual(42);
|
||||
expect(container.startSourceSpan.start.col).toEqual(0);
|
||||
expect(container.startSourceSpan.end.col).toEqual(42);
|
||||
expect(container.sourceSpan!.start.col).toEqual(0);
|
||||
expect(container.sourceSpan!.end.col).toEqual(42);
|
||||
expect(container.startSourceSpan!.start.col).toEqual(0);
|
||||
expect(container.startSourceSpan!.end.col).toEqual(42);
|
||||
expect(container.endSourceSpan!.start.col).toEqual(0);
|
||||
expect(container.endSourceSpan!.end.col).toEqual(42);
|
||||
|
||||
@ -68,15 +68,15 @@ import {humanizeNodes} from './ast_spec_utils';
|
||||
expect(switchExp.sourceSpan.end.col).toEqual(16);
|
||||
|
||||
const template: html.Element = <html.Element>container.children[0];
|
||||
expect(template.sourceSpan.start.col).toEqual(25);
|
||||
expect(template.sourceSpan.end.col).toEqual(41);
|
||||
expect(template.sourceSpan!.start.col).toEqual(25);
|
||||
expect(template.sourceSpan!.end.col).toEqual(41);
|
||||
|
||||
const switchCheck = template.attrs[0];
|
||||
expect(switchCheck.sourceSpan.start.col).toEqual(25);
|
||||
expect(switchCheck.sourceSpan.end.col).toEqual(28);
|
||||
|
||||
const b: html.Element = <html.Element>template.children[0];
|
||||
expect(b.sourceSpan.start.col).toEqual(29);
|
||||
expect(b.sourceSpan!.start.col).toEqual(29);
|
||||
expect(b.endSourceSpan!.end.col).toEqual(40);
|
||||
});
|
||||
|
||||
|
@ -885,114 +885,75 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u
|
||||
|
||||
describe('[line ending normalization', () => {
|
||||
describe('{escapedString: true}', () => {
|
||||
it('should normalize line-endings in expansion forms if `i18nNormalizeLineEndingsInICUs` is true',
|
||||
() => {
|
||||
const result = tokenizeWithoutErrors(
|
||||
`{\r\n` +
|
||||
` messages.length,\r\n` +
|
||||
` plural,\r\n` +
|
||||
` =0 {You have \r\nno\r\n messages}\r\n` +
|
||||
` =1 {One {{message}}}}\r\n`,
|
||||
{
|
||||
tokenizeExpansionForms: true,
|
||||
escapedString: true,
|
||||
i18nNormalizeLineEndingsInICUs: true
|
||||
});
|
||||
it('should normalize line-endings in expansion forms', () => {
|
||||
const result = tokenizeWithoutErrors(
|
||||
`{\r\n` +
|
||||
` messages.length,\r\n` +
|
||||
` plural,\r\n` +
|
||||
` =0 {You have \r\nno\r\n messages}\r\n` +
|
||||
` =1 {One {{message}}}}\r\n`,
|
||||
{
|
||||
tokenizeExpansionForms: true,
|
||||
escapedString: true,
|
||||
});
|
||||
|
||||
expect(humanizeParts(result.tokens)).toEqual([
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, '\n messages.length'],
|
||||
[lex.TokenType.RAW_TEXT, 'plural'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=0'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'You have \nno\n messages'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=1'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'One {{message}}'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
[lex.TokenType.TEXT, '\n'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
expect(humanizeParts(result.tokens)).toEqual([
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, '\n messages.length'],
|
||||
[lex.TokenType.RAW_TEXT, 'plural'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=0'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'You have \nno\n messages'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=1'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'One {{message}}'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
[lex.TokenType.TEXT, '\n'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
|
||||
expect(result.nonNormalizedIcuExpressions).toEqual([]);
|
||||
});
|
||||
expect(result.nonNormalizedIcuExpressions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not normalize line-endings in ICU expressions when `i18nNormalizeLineEndingsInICUs` is not defined',
|
||||
() => {
|
||||
const result = tokenizeWithoutErrors(
|
||||
`{\r\n` +
|
||||
` messages.length,\r\n` +
|
||||
` plural,\r\n` +
|
||||
` =0 {You have \r\nno\r\n messages}\r\n` +
|
||||
` =1 {One {{message}}}}\r\n`,
|
||||
{tokenizeExpansionForms: true, escapedString: true});
|
||||
it('should normalize line endings in nested expansion forms for inline templates', () => {
|
||||
const result = tokenizeWithoutErrors(
|
||||
`{\r\n` +
|
||||
` messages.length, plural,\r\n` +
|
||||
` =0 { zero \r\n` +
|
||||
` {\r\n` +
|
||||
` p.gender, select,\r\n` +
|
||||
` male {m}\r\n` +
|
||||
` }\r\n` +
|
||||
` }\r\n` +
|
||||
`}`,
|
||||
{tokenizeExpansionForms: true, escapedString: true});
|
||||
expect(humanizeParts(result.tokens)).toEqual([
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, '\n messages.length'],
|
||||
[lex.TokenType.RAW_TEXT, 'plural'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=0'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'zero \n '],
|
||||
|
||||
expect(humanizeParts(result.tokens)).toEqual([
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, '\r\n messages.length'],
|
||||
[lex.TokenType.RAW_TEXT, 'plural'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=0'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'You have \nno\n messages'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=1'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'One {{message}}'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
[lex.TokenType.TEXT, '\n'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, '\n p.gender'],
|
||||
[lex.TokenType.RAW_TEXT, 'select'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, 'male'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'm'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
|
||||
expect(result.nonNormalizedIcuExpressions!.length).toBe(1);
|
||||
expect(result.nonNormalizedIcuExpressions![0].sourceSpan.toString())
|
||||
.toEqual('\r\n messages.length');
|
||||
});
|
||||
[lex.TokenType.TEXT, '\n '],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
|
||||
it('should not normalize line endings in nested expansion forms when `i18nNormalizeLineEndingsInICUs` is not defined',
|
||||
() => {
|
||||
const result = tokenizeWithoutErrors(
|
||||
`{\r\n` +
|
||||
` messages.length, plural,\r\n` +
|
||||
` =0 { zero \r\n` +
|
||||
` {\r\n` +
|
||||
` p.gender, select,\r\n` +
|
||||
` male {m}\r\n` +
|
||||
` }\r\n` +
|
||||
` }\r\n` +
|
||||
`}`,
|
||||
{tokenizeExpansionForms: true, escapedString: true});
|
||||
expect(humanizeParts(result.tokens)).toEqual([
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, '\r\n messages.length'],
|
||||
[lex.TokenType.RAW_TEXT, 'plural'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=0'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'zero \n '],
|
||||
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, '\r\n p.gender'],
|
||||
[lex.TokenType.RAW_TEXT, 'select'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, 'male'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'm'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
|
||||
[lex.TokenType.TEXT, '\n '],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
|
||||
expect(result.nonNormalizedIcuExpressions!.length).toBe(2);
|
||||
expect(result.nonNormalizedIcuExpressions![0].sourceSpan.toString())
|
||||
.toEqual('\r\n messages.length');
|
||||
expect(result.nonNormalizedIcuExpressions![1].sourceSpan.toString())
|
||||
.toEqual('\r\n p.gender');
|
||||
});
|
||||
expect(result.nonNormalizedIcuExpressions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('{escapedString: false}', () => {
|
||||
|
@ -173,7 +173,7 @@ describe('R3 AST source spans', () => {
|
||||
describe('templates', () => {
|
||||
it('is correct for * directives', () => {
|
||||
expectFromHtml('<div *ngIf></div>').toEqual([
|
||||
['Template', '0:17', '0:11', '11:17'],
|
||||
['Template', '0:11', '0:11', '11:17'],
|
||||
['TextAttribute', '5:10', '<empty>'],
|
||||
['Element', '0:17', '0:11', '11:17'],
|
||||
]);
|
||||
@ -181,48 +181,48 @@ describe('R3 AST source spans', () => {
|
||||
|
||||
it('is correct for <ng-template>', () => {
|
||||
expectFromHtml('<ng-template></ng-template>').toEqual([
|
||||
['Template', '0:27', '0:13', '13:27'],
|
||||
['Template', '0:13', '0:13', '13:27'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('is correct for reference via #...', () => {
|
||||
expectFromHtml('<ng-template #a></ng-template>').toEqual([
|
||||
['Template', '0:30', '0:16', '16:30'],
|
||||
['Template', '0:16', '0:16', '16:30'],
|
||||
['Reference', '13:15', '<empty>'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('is correct for reference with name', () => {
|
||||
expectFromHtml('<ng-template #a="b"></ng-template>').toEqual([
|
||||
['Template', '0:34', '0:20', '20:34'],
|
||||
['Template', '0:20', '0:20', '20:34'],
|
||||
['Reference', '13:19', '17:18'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('is correct for reference via ref-...', () => {
|
||||
expectFromHtml('<ng-template ref-a></ng-template>').toEqual([
|
||||
['Template', '0:33', '0:19', '19:33'],
|
||||
['Template', '0:19', '0:19', '19:33'],
|
||||
['Reference', '13:18', '<empty>'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('is correct for variables via let-...', () => {
|
||||
expectFromHtml('<ng-template let-a="b"></ng-template>').toEqual([
|
||||
['Template', '0:37', '0:23', '23:37'],
|
||||
['Template', '0:23', '0:23', '23:37'],
|
||||
['Variable', '13:22', '20:21'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('is correct for attributes', () => {
|
||||
expectFromHtml('<ng-template k1="v1"></ng-template>').toEqual([
|
||||
['Template', '0:35', '0:21', '21:35'],
|
||||
['Template', '0:21', '0:21', '21:35'],
|
||||
['TextAttribute', '13:20', '17:19'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('is correct for bound attributes', () => {
|
||||
expectFromHtml('<ng-template [k1]="v1"></ng-template>').toEqual([
|
||||
['Template', '0:37', '0:23', '23:37'],
|
||||
['Template', '0:23', '0:23', '23:37'],
|
||||
['BoundAttribute', '13:22', '19:21'],
|
||||
]);
|
||||
});
|
||||
@ -236,7 +236,7 @@ describe('R3 AST source spans', () => {
|
||||
// <div></div>
|
||||
// </ng-template>
|
||||
expectFromHtml('<div *ngFor="let item of items"></div>').toEqual([
|
||||
['Template', '0:38', '0:32', '32:38'],
|
||||
['Template', '0:32', '0:32', '32:38'],
|
||||
['TextAttribute', '5:31', '<empty>'],
|
||||
['BoundAttribute', '5:31', '25:30'], // *ngFor="let item of items" -> items
|
||||
['Variable', '13:22', '<empty>'], // let item
|
||||
@ -250,7 +250,7 @@ describe('R3 AST source spans', () => {
|
||||
// <div></div>
|
||||
// </ng-template>
|
||||
expectFromHtml('<div *ngFor="item of items"></div>').toEqual([
|
||||
['Template', '0:34', '0:28', '28:34'],
|
||||
['Template', '0:28', '0:28', '28:34'],
|
||||
['BoundAttribute', '5:27', '13:17'], // ngFor="item of items" -> item
|
||||
['BoundAttribute', '5:27', '21:26'], // ngFor="item of items" -> items
|
||||
['Element', '0:34', '0:28', '28:34'],
|
||||
@ -259,7 +259,7 @@ describe('R3 AST source spans', () => {
|
||||
|
||||
it('is correct for variables via let ...', () => {
|
||||
expectFromHtml('<div *ngIf="let a=b"></div>').toEqual([
|
||||
['Template', '0:27', '0:21', '21:27'],
|
||||
['Template', '0:21', '0:21', '21:27'],
|
||||
['TextAttribute', '5:20', '<empty>'],
|
||||
['Variable', '12:19', '18:19'], // let a=b -> b
|
||||
['Element', '0:27', '0:21', '21:27'],
|
||||
@ -268,7 +268,7 @@ describe('R3 AST source spans', () => {
|
||||
|
||||
it('is correct for variables via as ...', () => {
|
||||
expectFromHtml('<div *ngIf="expr as local"></div>').toEqual([
|
||||
['Template', '0:33', '0:27', '27:33'],
|
||||
['Template', '0:27', '0:27', '27:33'],
|
||||
['BoundAttribute', '5:26', '12:16'], // ngIf="expr as local" -> expr
|
||||
['Variable', '6:25', '6:10'], // ngIf="expr as local -> ngIf
|
||||
['Element', '0:33', '0:27', '27:33'],
|
||||
|
@ -140,7 +140,7 @@ class TemplateHumanizer implements TemplateAstVisitor {
|
||||
|
||||
private _appendSourceSpan(ast: TemplateAst, input: any[]): any[] {
|
||||
if (!this.includeSourceSpan) return input;
|
||||
input.push(ast.sourceSpan.toString());
|
||||
input.push(ast.sourceSpan!.toString());
|
||||
return input;
|
||||
}
|
||||
}
|
||||
@ -2046,7 +2046,7 @@ Property binding a not used by any directive on an embedded template. Make sure
|
||||
|
||||
it('should support embedded template', () => {
|
||||
expect(humanizeTplAstSourceSpans(parse('<ng-template></ng-template>', []))).toEqual([
|
||||
[EmbeddedTemplateAst, '<ng-template></ng-template>']
|
||||
[EmbeddedTemplateAst, '<ng-template>']
|
||||
]);
|
||||
});
|
||||
|
||||
@ -2058,14 +2058,14 @@ Property binding a not used by any directive on an embedded template. Make sure
|
||||
|
||||
it('should support references', () => {
|
||||
expect(humanizeTplAstSourceSpans(parse('<div #a></div>', []))).toEqual([
|
||||
[ElementAst, 'div', '<div #a></div>'], [ReferenceAst, 'a', null, '#a']
|
||||
[ElementAst, 'div', '<div #a>'], [ReferenceAst, 'a', null, '#a']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support variables', () => {
|
||||
expect(humanizeTplAstSourceSpans(parse('<ng-template let-a="b"></ng-template>', [])))
|
||||
.toEqual([
|
||||
[EmbeddedTemplateAst, '<ng-template let-a="b"></ng-template>'],
|
||||
[EmbeddedTemplateAst, '<ng-template let-a="b">'],
|
||||
[VariableAst, 'a', 'b', 'let-a="b"'],
|
||||
]);
|
||||
});
|
||||
@ -2128,7 +2128,7 @@ Property binding a not used by any directive on an embedded template. Make sure
|
||||
expect(humanizeTplAstSourceSpans(
|
||||
parse('<svg><circle /><use xlink:href="Port" /></svg>', [tagSel, attrSel])))
|
||||
.toEqual([
|
||||
[ElementAst, ':svg:svg', '<svg><circle /><use xlink:href="Port" /></svg>'],
|
||||
[ElementAst, ':svg:svg', '<svg>'],
|
||||
[ElementAst, ':svg:circle', '<circle />'],
|
||||
[DirectiveAst, tagSel, '<circle />'],
|
||||
[ElementAst, ':svg:use', '<use xlink:href="Port" />'],
|
||||
@ -2144,8 +2144,7 @@ Property binding a not used by any directive on an embedded template. Make sure
|
||||
inputs: ['aProp']
|
||||
}).toSummary();
|
||||
expect(humanizeTplAstSourceSpans(parse('<div [aProp]="foo"></div>', [dirA]))).toEqual([
|
||||
[ElementAst, 'div', '<div [aProp]="foo"></div>'],
|
||||
[DirectiveAst, dirA, '<div [aProp]="foo"></div>'],
|
||||
[ElementAst, 'div', '<div [aProp]="foo">'], [DirectiveAst, dirA, '<div [aProp]="foo">'],
|
||||
[BoundDirectivePropertyAst, 'aProp', 'foo', '[aProp]="foo"']
|
||||
]);
|
||||
});
|
||||
|
@ -12,4 +12,4 @@
|
||||
* Change detection enables data binding in Angular.
|
||||
*/
|
||||
|
||||
export {ChangeDetectionStrategy, ChangeDetectorRef, DefaultIterableDiffer, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers, NgIterable, PipeTransform, SimpleChange, SimpleChanges, TrackByFunction, WrappedValue} from './change_detection/change_detection';
|
||||
export {ChangeDetectionStrategy, ChangeDetectorRef, CollectionChangeRecord, DefaultIterableDiffer, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers, NgIterable, PipeTransform, SimpleChange, SimpleChanges, TrackByFunction, WrappedValue} from './change_detection/change_detection';
|
||||
|
@ -18,7 +18,7 @@ export {ChangeDetectionStrategy, ChangeDetectorStatus, isDefaultChangeDetectionS
|
||||
export {DefaultIterableDifferFactory} from './differs/default_iterable_differ';
|
||||
export {DefaultIterableDiffer} from './differs/default_iterable_differ';
|
||||
export {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
|
||||
export {IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFunction} from './differs/iterable_differs';
|
||||
export {CollectionChangeRecord, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFunction} from './differs/iterable_differs';
|
||||
export {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
|
||||
export {PipeTransform} from './pipe_transform';
|
||||
|
||||
|
@ -593,7 +593,7 @@ export class IterableChangeRecord_<V> implements IterableChangeRecord<V> {
|
||||
constructor(public item: V, public trackById: any) {}
|
||||
}
|
||||
|
||||
// A linked list of IterableChangeRecords with the same IterableChangeRecord_.item
|
||||
// A linked list of CollectionChangeRecords with the same IterableChangeRecord_.item
|
||||
class _DuplicateItemRecordList<V> {
|
||||
/** @internal */
|
||||
_head: IterableChangeRecord_<V>|null = null;
|
||||
|
@ -112,6 +112,12 @@ export interface IterableChangeRecord<V> {
|
||||
readonly trackById: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated v4.0.0 - Use IterableChangeRecord instead.
|
||||
* @publicApi
|
||||
*/
|
||||
export interface CollectionChangeRecord<V> extends IterableChangeRecord<V> {}
|
||||
|
||||
/**
|
||||
* An optional function passed into the `NgForOf` directive that defines how to track
|
||||
* changes for items in an iterable.
|
||||
|
@ -1149,15 +1149,8 @@ export class FormControl extends AbstractControl {
|
||||
super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts));
|
||||
this._applyFormState(formState);
|
||||
this._setUpdateStrategy(validatorOrOpts);
|
||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
this._initObservables();
|
||||
this.updateValueAndValidity({
|
||||
onlySelf: true,
|
||||
// If `asyncValidator` is present, it will trigger control status change from `PENDING` to
|
||||
// `VALID` or `INVALID`.
|
||||
// The status should be broadcasted via the `statusChanges` observable, so we set `emitEvent`
|
||||
// to `true` to allow that during the control creation process.
|
||||
emitEvent: !!asyncValidator
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1410,13 +1403,7 @@ export class FormGroup extends AbstractControl {
|
||||
this._initObservables();
|
||||
this._setUpdateStrategy(validatorOrOpts);
|
||||
this._setUpControls();
|
||||
this.updateValueAndValidity({
|
||||
onlySelf: true,
|
||||
// If `asyncValidator` is present, it will trigger control status change from `PENDING` to
|
||||
// `VALID` or `INVALID`. The status should be broadcasted via the `statusChanges` observable,
|
||||
// so we set `emitEvent` to `true` to allow that during the control creation process.
|
||||
emitEvent: !!asyncValidator
|
||||
});
|
||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1836,14 +1823,7 @@ export class FormArray extends AbstractControl {
|
||||
this._initObservables();
|
||||
this._setUpdateStrategy(validatorOrOpts);
|
||||
this._setUpControls();
|
||||
this.updateValueAndValidity({
|
||||
onlySelf: true,
|
||||
// If `asyncValidator` is present, it will trigger control status change from `PENDING` to
|
||||
// `VALID` or `INVALID`.
|
||||
// The status should be broadcasted via the `statusChanges` observable, so we set `emitEvent`
|
||||
// to `true` to allow that during the control creation process.
|
||||
emitEvent: !!asyncValidator
|
||||
});
|
||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1825,391 +1825,6 @@ describe('FormGroup', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('emit `statusChanges` and `valueChanges` with/without async/sync validators', () => {
|
||||
const attachEventsLogger = (control: AbstractControl, log: string[], controlName?: string) => {
|
||||
const name = controlName ? ` (${controlName})` : '';
|
||||
control.statusChanges.subscribe(status => log.push(`status${name}: ${status}`));
|
||||
control.valueChanges.subscribe(value => log.push(`value${name}: ${JSON.stringify(value)}`));
|
||||
};
|
||||
|
||||
describe('stand alone controls', () => {
|
||||
it('should run the async validator on stand alone controls and set status to `INVALID`',
|
||||
fakeAsync(() => {
|
||||
const logs: string[] = [];
|
||||
const c =
|
||||
new FormControl('', null, simpleAsyncValidator({timeout: 0, shouldFail: true}));
|
||||
|
||||
attachEventsLogger(c, logs);
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
|
||||
tick(1);
|
||||
|
||||
c.setValue('new!', {emitEvent: true});
|
||||
|
||||
tick(1);
|
||||
|
||||
// Note that above `simpleAsyncValidator` is called with `timeout:0`. When the timeout
|
||||
// is set to `0`, the function returns `of(error)`, and the function behaves in a
|
||||
// synchronous manner. Because of this there is no `PENDING` state as seen in the
|
||||
// `logs`.
|
||||
expect(logs).toEqual([
|
||||
'status: INVALID', // status change emitted as a result of initial async validator run
|
||||
'value: "new!"', // value change emitted by `setValue`
|
||||
'status: INVALID' // async validator run after `setValue` call
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should run the async validator on stand alone controls and set status to `VALID`',
|
||||
fakeAsync(() => {
|
||||
const logs: string[] = [];
|
||||
const c = new FormControl('', null, asyncValidator('new!'));
|
||||
|
||||
attachEventsLogger(c, logs);
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
|
||||
tick(1);
|
||||
|
||||
c.setValue('new!', {emitEvent: true});
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(logs).toEqual([
|
||||
'status: INVALID', // status change emitted as a result of initial async validator run
|
||||
'value: "new!"', // value change emitted by `setValue`
|
||||
'status: PENDING', // status change emitted by `setValue`
|
||||
'status: VALID' // async validator run after `setValue` call
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should run the async validator on stand alone controls, include `PENDING` and set status to `INVALID`',
|
||||
fakeAsync(() => {
|
||||
const logs: string[] = [];
|
||||
const c =
|
||||
new FormControl('', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
|
||||
|
||||
attachEventsLogger(c, logs);
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
|
||||
tick(1);
|
||||
|
||||
c.setValue('new!', {emitEvent: true});
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(logs).toEqual([
|
||||
'status: INVALID', // status change emitted as a result of initial async validator run
|
||||
'value: "new!"', // value change emitted by `setValue`
|
||||
'status: PENDING', // status change emitted by `setValue`
|
||||
'status: INVALID' // async validator run after `setValue` call
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should run setValue before the initial async validator and set status to `VALID`',
|
||||
fakeAsync(() => {
|
||||
const logs: string[] = [];
|
||||
const c = new FormControl('', null, asyncValidator('new!'));
|
||||
|
||||
attachEventsLogger(c, logs);
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
|
||||
c.setValue('new!', {emitEvent: true});
|
||||
|
||||
tick(1);
|
||||
|
||||
// The `setValue` call invoked synchronously cancels the initial run of the
|
||||
// `asyncValidator` (which would cause the control status to be changed to `INVALID`), so
|
||||
// the log contains only events after calling `setValue`.
|
||||
expect(logs).toEqual([
|
||||
'value: "new!"', // value change emitted by `setValue`
|
||||
'status: PENDING', // status change emitted by `setValue`
|
||||
'status: VALID' // async validator run after `setValue` call
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should run setValue before the initial async validator and set status to `INVALID`',
|
||||
fakeAsync(() => {
|
||||
const logs: string[] = [];
|
||||
const c =
|
||||
new FormControl('', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
|
||||
|
||||
attachEventsLogger(c, logs);
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
|
||||
c.setValue('new!', {emitEvent: true});
|
||||
|
||||
tick(1);
|
||||
|
||||
// The `setValue` call invoked synchronously cancels the initial run of the
|
||||
// `asyncValidator` (which would cause the control status to be changed to `INVALID`), so
|
||||
// the log contains only events after calling `setValue`.
|
||||
expect(logs).toEqual([
|
||||
'value: "new!"', // value change emitted by `setValue`
|
||||
'status: PENDING', // status change emitted by `setValue`
|
||||
'status: INVALID' // async validator run after `setValue` call
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should cancel initial run of the async validator and not emit anything', fakeAsync(() => {
|
||||
const logger: string[] = [];
|
||||
const c =
|
||||
new FormControl('', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
|
||||
|
||||
attachEventsLogger(c, logger);
|
||||
|
||||
expect(logger.length).toBe(0);
|
||||
|
||||
c.setValue('new!', {emitEvent: false});
|
||||
|
||||
tick(1);
|
||||
|
||||
// Because we are calling `setValue` with `emitEvent: false`, nothing is emitted
|
||||
// and our logger remains empty
|
||||
expect(logger).toEqual([]);
|
||||
}));
|
||||
|
||||
it('should run the sync validator on stand alone controls and set status to `INVALID`',
|
||||
fakeAsync(() => {
|
||||
const logs: string[] = [];
|
||||
const c = new FormControl('new!', Validators.required);
|
||||
|
||||
attachEventsLogger(c, logs);
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
|
||||
tick(1);
|
||||
|
||||
c.setValue('', {emitEvent: true});
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(logs).toEqual([
|
||||
'value: ""', // value change emitted by `setValue`
|
||||
'status: INVALID' // status change emitted by `setValue`
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should run the sync validator on stand alone controls and set status to `VALID`',
|
||||
fakeAsync(() => {
|
||||
const logs: string[] = [];
|
||||
const c = new FormControl('', Validators.required);
|
||||
|
||||
attachEventsLogger(c, logs);
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
|
||||
tick(1);
|
||||
|
||||
c.setValue('new!', {emitEvent: true});
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(logs).toEqual([
|
||||
'value: "new!"', // value change emitted by `setValue`
|
||||
'status: VALID' // status change emitted by `setValue`
|
||||
]);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('combination of multiple form controls', () => {
|
||||
it('should run the async validator on the FormControl added to the FormGroup and set status to `VALID`',
|
||||
fakeAsync(() => {
|
||||
const logs: string[] = [];
|
||||
const c1 = new FormControl('one');
|
||||
const g1 = new FormGroup({'one': c1});
|
||||
|
||||
// Initial state of the controls
|
||||
expect(currentStateOf([c1, g1])).toEqual([
|
||||
{errors: null, pending: false, status: 'VALID'}, // Control 1
|
||||
{errors: null, pending: false, status: 'VALID'}, // Group
|
||||
]);
|
||||
|
||||
attachEventsLogger(g1, logs, 'g1');
|
||||
|
||||
const c2 = new FormControl('new!', null, asyncValidator('new!'));
|
||||
|
||||
attachEventsLogger(c2, logs, 'c2');
|
||||
|
||||
// Initial state of the new control
|
||||
expect(currentStateOf([c2])).toEqual([
|
||||
{errors: null, pending: true, status: 'PENDING'}, // Control 2
|
||||
]);
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
|
||||
g1.setControl('one', c2);
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(logs).toEqual([
|
||||
'value (g1): {"one":"new!"}', // value change emitted by `setControl`
|
||||
'status (g1): PENDING', // value change emitted by `setControl`
|
||||
'status (c2): VALID', // async validator run after `setControl` call
|
||||
'status (g1): VALID' // status changed from the `setControl` call
|
||||
]);
|
||||
|
||||
// Final state of all controls
|
||||
expect(currentStateOf([g1, c2])).toEqual([
|
||||
{errors: null, pending: false, status: 'VALID'}, // Group
|
||||
{errors: null, pending: false, status: 'VALID'}, // Control 2
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should run the async validator on the FormControl added to the FormGroup and set status to `INVALID`',
|
||||
fakeAsync(() => {
|
||||
const logs: string[] = [];
|
||||
const c1 = new FormControl('one');
|
||||
const g1 = new FormGroup({'one': c1});
|
||||
|
||||
// Initial state of the controls
|
||||
expect(currentStateOf([c1, g1])).toEqual([
|
||||
{errors: null, pending: false, status: 'VALID'}, // Control 1
|
||||
{errors: null, pending: false, status: 'VALID'}, // Group
|
||||
]);
|
||||
|
||||
attachEventsLogger(g1, logs, 'g1');
|
||||
|
||||
const c2 =
|
||||
new FormControl('new!', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
|
||||
|
||||
attachEventsLogger(c2, logs, 'c2');
|
||||
|
||||
// Initial state of the new control
|
||||
expect(currentStateOf([c2])).toEqual([
|
||||
{errors: null, pending: true, status: 'PENDING'}, // Control 2
|
||||
]);
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
|
||||
g1.setControl('one', c2);
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(logs).toEqual([
|
||||
'value (g1): {"one":"new!"}',
|
||||
'status (g1): PENDING', // g1 async validator is invoked after `g1.setControl` call
|
||||
'status (c2): INVALID', // c2 async validator trigger at c2 init, completed with the
|
||||
// `INVALID` status
|
||||
'status (g1): INVALID' // g1 validator completed with the `INVALID` status
|
||||
]);
|
||||
|
||||
// Final state of all controls
|
||||
expect(currentStateOf([g1, c2])).toEqual([
|
||||
{errors: null, pending: false, status: 'INVALID'}, // Group
|
||||
{errors: {async: true}, pending: false, status: 'INVALID'}, // Control 2
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should run the async validator at `FormControl` and `FormGroup` level and set status to `INVALID`',
|
||||
fakeAsync(() => {
|
||||
const logs: string[] = [];
|
||||
const c1 = new FormControl('one');
|
||||
const g1 = new FormGroup(
|
||||
{'one': c1}, null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
|
||||
|
||||
// Initial state of the controls
|
||||
expect(currentStateOf([c1, g1])).toEqual([
|
||||
{errors: null, pending: false, status: 'VALID'}, // Control 1
|
||||
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||
]);
|
||||
|
||||
attachEventsLogger(g1, logs, 'g1');
|
||||
|
||||
const c2 =
|
||||
new FormControl('new!', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
|
||||
|
||||
attachEventsLogger(c2, logs, 'c2');
|
||||
|
||||
// Initial state of the new control
|
||||
expect(currentStateOf([c2])).toEqual([
|
||||
{errors: null, pending: true, status: 'PENDING'}, // Control 2
|
||||
]);
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
|
||||
g1.setControl('one', c2);
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(logs).toEqual([
|
||||
'value (g1): {"one":"new!"}',
|
||||
'status (g1): PENDING', // g1 async validator is invoked after `g1.setControl` call
|
||||
'status (c2): INVALID', // c2 async validator trigger at c2 init, completed with the
|
||||
// `INVALID` status
|
||||
'status (g1): PENDING', // c2 update triggered g1 to re-run validation
|
||||
'status (g1): INVALID' // g1 validator completed with the `INVALID` status
|
||||
]);
|
||||
|
||||
// Final state of all controls
|
||||
expect(currentStateOf([g1, c2])).toEqual([
|
||||
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group
|
||||
{errors: {async: true}, pending: false, status: 'INVALID'}, // Control 2
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should run the async validator on a `FormArray` and a `FormControl` and status to `INVALID`',
|
||||
fakeAsync(() => {
|
||||
const logs: string[] = [];
|
||||
const c1 = new FormControl('one');
|
||||
const g1 = new FormGroup(
|
||||
{'one': c1}, null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
|
||||
const fa =
|
||||
new FormArray([g1], null!, simpleAsyncValidator({timeout: 1, shouldFail: true}));
|
||||
|
||||
attachEventsLogger(g1, logs, 'g1');
|
||||
|
||||
// Initial state of the controls
|
||||
expect(currentStateOf([c1, g1, fa])).toEqual([
|
||||
{errors: null, pending: false, status: 'VALID'}, // Control 1
|
||||
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||
{errors: null, pending: true, status: 'PENDING'}, // FormArray
|
||||
]);
|
||||
|
||||
attachEventsLogger(fa, logs, 'fa');
|
||||
|
||||
const c2 =
|
||||
new FormControl('new!', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
|
||||
|
||||
attachEventsLogger(c2, logs, 'c2');
|
||||
|
||||
// Initial state of the new control
|
||||
expect(currentStateOf([c2])).toEqual([
|
||||
{errors: null, pending: true, status: 'PENDING'}, // Control 2
|
||||
]);
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
|
||||
g1.setControl('one', c2);
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(logs).toEqual([
|
||||
'value (g1): {"one":"new!"}', // g1's call to `setControl` triggered value update
|
||||
'status (g1): PENDING', // g1's call to `setControl` triggered status update
|
||||
'value (fa): [{"one":"new!"}]', // g1 update triggers the `FormArray` value update
|
||||
'status (fa): PENDING', // g1 update triggers the `FormArray` status update
|
||||
'status (c2): INVALID', // async validator run after `setControl` call
|
||||
'status (g1): PENDING', // async validator run after `setControl` call
|
||||
'status (fa): PENDING', // async validator run after `setControl` call
|
||||
'status (g1): INVALID', // g1 validator completed with the `INVALID` status
|
||||
'status (fa): PENDING', // fa validator still running
|
||||
'status (fa): INVALID' // fa validator completed with the `INVALID` status
|
||||
]);
|
||||
|
||||
// Final state of all controls
|
||||
expect(currentStateOf([g1, fa, c2])).toEqual([
|
||||
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group
|
||||
{errors: {async: true}, pending: false, status: 'INVALID'}, // FormArray
|
||||
{errors: {async: true}, pending: false, status: 'INVALID'}, // Control 2
|
||||
]);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('pending', () => {
|
||||
let c: FormControl;
|
||||
let g: FormGroup;
|
||||
|
@ -14,7 +14,7 @@ import {findNodeAtPosition, isExpressionNode, isTemplateNode} from '../hybrid_vi
|
||||
|
||||
interface ParseResult {
|
||||
nodes: t.Node[];
|
||||
errors: ParseError[]|null;
|
||||
errors?: ParseError[];
|
||||
position: number;
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ function parse(template: string): ParseResult {
|
||||
describe('findNodeAtPosition for template AST', () => {
|
||||
it('should locate element in opening tag', () => {
|
||||
const {errors, nodes, position} = parse(`<di¦v></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
@ -42,7 +42,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate element in closing tag', () => {
|
||||
const {errors, nodes, position} = parse(`<div></di¦v>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
@ -50,7 +50,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate element when cursor is at the beginning', () => {
|
||||
const {errors, nodes, position} = parse(`<¦div></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
@ -58,7 +58,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate element when cursor is at the end', () => {
|
||||
const {errors, nodes, position} = parse(`<div¦></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
@ -66,7 +66,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate attribute key', () => {
|
||||
const {errors, nodes, position} = parse(`<div cla¦ss="foo"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.TextAttribute);
|
||||
@ -74,7 +74,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate attribute value', () => {
|
||||
const {errors, nodes, position} = parse(`<div class="fo¦o"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
// TODO: Note that we do not have the ability to detect the RHS (yet)
|
||||
@ -83,7 +83,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate bound attribute key', () => {
|
||||
const {errors, nodes, position} = parse(`<test-cmp [fo¦o]="bar"></test-cmp>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
@ -91,7 +91,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate bound attribute value', () => {
|
||||
const {errors, nodes, position} = parse(`<test-cmp [foo]="b¦ar"></test-cmp>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
@ -99,7 +99,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate bound event key', () => {
|
||||
const {errors, nodes, position} = parse(`<test-cmp (fo¦o)="bar()"></test-cmp>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundEvent);
|
||||
@ -107,7 +107,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate bound event value', () => {
|
||||
const {errors, nodes, position} = parse(`<test-cmp (foo)="b¦ar()"></test-cmp>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.MethodCall);
|
||||
@ -115,7 +115,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate element children', () => {
|
||||
const {errors, nodes, position} = parse(`<div><sp¦an></span></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
@ -124,7 +124,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate element reference', () => {
|
||||
const {errors, nodes, position} = parse(`<div #my¦div></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Reference);
|
||||
@ -132,7 +132,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template text attribute', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template ng¦If></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.TextAttribute);
|
||||
@ -140,7 +140,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template bound attribute key', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template [ng¦If]="foo"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
@ -148,7 +148,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template bound attribute value', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template [ngIf]="f¦oo"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
@ -156,7 +156,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template bound attribute key in two-way binding', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template [(f¦oo)]="bar"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
@ -165,7 +165,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template bound attribute value in two-way binding', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template [(foo)]="b¦ar"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
@ -174,7 +174,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template bound event key', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template (cl¦ick)="foo()"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundEvent);
|
||||
@ -182,14 +182,14 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template bound event value', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template (click)="f¦oo()"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(node).toBeInstanceOf(e.MethodCall);
|
||||
});
|
||||
|
||||
it('should locate template attribute key', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template i¦d="foo"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.TextAttribute);
|
||||
@ -197,7 +197,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template attribute value', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template id="f¦oo"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
// TODO: Note that we do not have the ability to detect the RHS (yet)
|
||||
@ -206,7 +206,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template reference key via the # notation', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template #f¦oo></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Reference);
|
||||
@ -215,7 +215,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template reference key via the ref- notation', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template ref-fo¦o></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Reference);
|
||||
@ -224,7 +224,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template reference value via the # notation', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template #foo="export¦As"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Reference);
|
||||
@ -234,7 +234,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template reference value via the ref- notation', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template ref-foo="export¦As"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Reference);
|
||||
@ -244,7 +244,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template variable key', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template let-f¦oo="bar"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Variable);
|
||||
@ -252,7 +252,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template variable value', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template let-foo="b¦ar"></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Variable);
|
||||
@ -260,7 +260,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate template children', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-template><d¦iv></div></ng-template>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
@ -268,7 +268,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate ng-content', () => {
|
||||
const {errors, nodes, position} = parse(`<ng-co¦ntent></ng-content>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Content);
|
||||
@ -276,7 +276,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate ng-content attribute key', () => {
|
||||
const {errors, nodes, position} = parse('<ng-content cla¦ss="red"></ng-content>');
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.TextAttribute);
|
||||
@ -284,7 +284,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate ng-content attribute value', () => {
|
||||
const {errors, nodes, position} = parse('<ng-content class="r¦ed"></ng-content>');
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
// TODO: Note that we do not have the ability to detect the RHS (yet)
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
@ -293,7 +293,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should not locate implicit receiver', () => {
|
||||
const {errors, nodes, position} = parse(`<div [foo]="¦bar"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
@ -301,7 +301,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate bound attribute key in two-way binding', () => {
|
||||
const {errors, nodes, position} = parse(`<cmp [(f¦oo)]="bar"></cmp>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
@ -310,7 +310,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
|
||||
it('should locate bound attribute value in two-way binding', () => {
|
||||
const {errors, nodes, position} = parse(`<cmp [(foo)]="b¦ar"></cmp>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
@ -321,7 +321,7 @@ describe('findNodeAtPosition for template AST', () => {
|
||||
describe('findNodeAtPosition for expression AST', () => {
|
||||
it('should not locate implicit receiver', () => {
|
||||
const {errors, nodes, position} = parse(`{{ ¦title }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
@ -330,7 +330,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate property read', () => {
|
||||
const {errors, nodes, position} = parse(`{{ ti¦tle }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
@ -339,7 +339,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate safe property read', () => {
|
||||
const {errors, nodes, position} = parse(`{{ foo?¦.bar }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.SafePropertyRead);
|
||||
@ -348,7 +348,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate keyed read', () => {
|
||||
const {errors, nodes, position} = parse(`{{ foo['bar']¦ }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.KeyedRead);
|
||||
@ -356,7 +356,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate property write', () => {
|
||||
const {errors, nodes, position} = parse(`<div (foo)="b¦ar=$event"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyWrite);
|
||||
@ -364,7 +364,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate keyed write', () => {
|
||||
const {errors, nodes, position} = parse(`<div (foo)="bar['baz']¦=$event"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.KeyedWrite);
|
||||
@ -372,7 +372,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate binary', () => {
|
||||
const {errors, nodes, position} = parse(`{{ 1 +¦ 2 }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.Binary);
|
||||
@ -380,7 +380,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate binding pipe with an identifier', () => {
|
||||
const {errors, nodes, position} = parse(`{{ title | p¦ }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.BindingPipe);
|
||||
@ -391,7 +391,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
// TODO: We are not able to locate pipe if identifier is missing because the
|
||||
// parser throws an error. This case is important for autocomplete.
|
||||
// const {errors, nodes, position} = parse(`{{ title | ¦ }}`);
|
||||
// expect(errors).toBe(null);
|
||||
// expect(errors).toBeUndefined();
|
||||
// const node = findNodeAtPosition(nodes, position);
|
||||
// expect(isExpressionNode(node!)).toBe(true);
|
||||
// expect(node).toBeInstanceOf(e.BindingPipe);
|
||||
@ -399,7 +399,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate method call', () => {
|
||||
const {errors, nodes, position} = parse(`{{ title.toString(¦) }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.MethodCall);
|
||||
@ -407,7 +407,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate safe method call', () => {
|
||||
const {errors, nodes, position} = parse(`{{ title?.toString(¦) }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.SafeMethodCall);
|
||||
@ -415,7 +415,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate literal primitive in interpolation', () => {
|
||||
const {errors, nodes, position} = parse(`{{ title.indexOf('t¦') }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.LiteralPrimitive);
|
||||
@ -424,7 +424,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate literal primitive in binding', () => {
|
||||
const {errors, nodes, position} = parse(`<div [id]="'t¦'"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.LiteralPrimitive);
|
||||
@ -433,7 +433,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate empty expression', () => {
|
||||
const {errors, nodes, position} = parse(`<div [id]="¦"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.EmptyExpr);
|
||||
@ -441,7 +441,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate literal array', () => {
|
||||
const {errors, nodes, position} = parse(`{{ [1, 2,¦ 3] }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.LiteralArray);
|
||||
@ -449,7 +449,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate literal map', () => {
|
||||
const {errors, nodes, position} = parse(`{{ { hello:¦ "world" } }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.LiteralMap);
|
||||
@ -457,7 +457,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
|
||||
it('should locate conditional', () => {
|
||||
const {errors, nodes, position} = parse(`{{ cond ?¦ true : false }}`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.Conditional);
|
||||
@ -467,7 +467,7 @@ describe('findNodeAtPosition for expression AST', () => {
|
||||
describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
it('should locate template key', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ng¦If="foo"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
@ -475,7 +475,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
|
||||
it('should locate template value', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngIf="f¦oo"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
@ -485,7 +485,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ng¦For="let item of items"></div>`);
|
||||
// ngFor is a text attribute because the desugared form is
|
||||
// <ng-template ngFor let-item [ngForOf]="items">
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
// TODO: this is currently wrong because it should point to ngFor text
|
||||
// attribute instead of ngForOf bound attribute
|
||||
@ -493,7 +493,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
|
||||
it('should locate not let keyword', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="l¦et item of items"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
// TODO: this is currently wrong because node is currently pointing to
|
||||
// "item". In this case, it should return undefined.
|
||||
@ -501,7 +501,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
|
||||
it('should locate let variable', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="let i¦tem of items"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Variable);
|
||||
@ -510,7 +510,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
|
||||
it('should locate bound attribute key', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="let item o¦f items"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.BoundAttribute);
|
||||
@ -519,7 +519,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
|
||||
it('should locate bound attribute value', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="let item of it¦ems"></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
@ -528,7 +528,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
|
||||
it('should locate template children', () => {
|
||||
const {errors, nodes, position} = parse(`<di¦v *ngIf></div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Element);
|
||||
@ -540,7 +540,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
<div *ngFor="let item of items; let i=index">
|
||||
{{ i¦ }}
|
||||
</div>`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isExpressionNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||
@ -548,7 +548,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
|
||||
it('should locate LHS of variable declaration', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="let item of items; let i¦=index">`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Variable);
|
||||
@ -558,7 +558,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
|
||||
|
||||
it('should locate RHS of variable declaration', () => {
|
||||
const {errors, nodes, position} = parse(`<div *ngFor="let item of items; let i=in¦dex">`);
|
||||
expect(errors).toBe(null);
|
||||
expect(errors).toBeUndefined();
|
||||
const node = findNodeAtPosition(nodes, position);
|
||||
expect(isTemplateNode(node!)).toBe(true);
|
||||
expect(node).toBeInstanceOf(t.Variable);
|
||||
|
@ -65,7 +65,7 @@ function getBoundedWordSpan(
|
||||
// The HTML tag may include `-` (e.g. `app-root`),
|
||||
// so use the HtmlAst to get the span before ayazhafiz refactor the code.
|
||||
return {
|
||||
start: templateInfo.template.span.start + ast.startSourceSpan.start.offset + 1,
|
||||
start: templateInfo.template.span.start + ast.startSourceSpan!.start.offset + 1,
|
||||
length: ast.name.length
|
||||
};
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export function parseInnerRange(element: Element): ParseTreeResult {
|
||||
* @param element The element whose inner range we want to compute.
|
||||
*/
|
||||
function getInnerRange(element: Element): LexerRange {
|
||||
const start = element.startSourceSpan.end;
|
||||
const start = element.startSourceSpan!.end;
|
||||
const end = element.endSourceSpan!.start;
|
||||
return {
|
||||
startPos: start.offset,
|
||||
|
@ -7,8 +7,8 @@
|
||||
*/
|
||||
|
||||
import {Injector, NgModuleRef} from '@angular/core';
|
||||
import {defer, EmptyError, from, Observable, Observer, of} from 'rxjs';
|
||||
import {catchError, combineAll, concatMap, first, map, mergeMap, tap} from 'rxjs/operators';
|
||||
import {defer, EmptyError, Observable, Observer, of} from 'rxjs';
|
||||
import {catchError, concatAll, first, map, mergeMap, tap} from 'rxjs/operators';
|
||||
|
||||
import {LoadedRouterConfig, Route, Routes} from './config';
|
||||
import {CanLoadFn} from './interfaces';
|
||||
@ -17,7 +17,6 @@ import {RouterConfigLoader} from './router_config_loader';
|
||||
import {defaultUrlMatcher, navigationCancelingError, Params, PRIMARY_OUTLET} from './shared';
|
||||
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
|
||||
import {forEach, waitForMap, wrapIntoObservable} from './utils/collection';
|
||||
import {getOutlet, groupRoutesByOutlet} from './utils/config';
|
||||
import {isCanLoad, isFunction, isUrlTree} from './utils/type_guards';
|
||||
|
||||
class NoMatch {
|
||||
@ -149,52 +148,28 @@ class ApplyRedirects {
|
||||
ngModule: NgModuleRef<any>, segmentGroup: UrlSegmentGroup, routes: Route[],
|
||||
segments: UrlSegment[], outlet: string,
|
||||
allowRedirects: boolean): Observable<UrlSegmentGroup> {
|
||||
// We need to expand each outlet group independently to ensure that we not only load modules
|
||||
// for routes matching the given `outlet`, but also those which will be activated because
|
||||
// their path is empty string. This can result in multiple outlets being activated at once.
|
||||
const routesByOutlet: Map<string, Route[]> = groupRoutesByOutlet(routes);
|
||||
if (!routesByOutlet.has(outlet)) {
|
||||
routesByOutlet.set(outlet, []);
|
||||
}
|
||||
|
||||
const expandRoutes = (routes: Route[]) => {
|
||||
return from(routes).pipe(
|
||||
concatMap((r: Route) => {
|
||||
const expanded$ = this.expandSegmentAgainstRoute(
|
||||
ngModule, segmentGroup, routes, r, segments, outlet, allowRedirects);
|
||||
return expanded$.pipe(catchError(e => {
|
||||
if (e instanceof NoMatch) {
|
||||
return of(null);
|
||||
}
|
||||
throw e;
|
||||
}));
|
||||
}),
|
||||
first((s: UrlSegmentGroup|null): s is UrlSegmentGroup => s !== null),
|
||||
catchError(e => {
|
||||
if (e instanceof EmptyError || e.name === 'EmptyError') {
|
||||
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
|
||||
return of(new UrlSegmentGroup([], {}));
|
||||
}
|
||||
throw new NoMatch(segmentGroup);
|
||||
return of(...routes).pipe(
|
||||
map((r: any) => {
|
||||
const expanded$ = this.expandSegmentAgainstRoute(
|
||||
ngModule, segmentGroup, routes, r, segments, outlet, allowRedirects);
|
||||
return expanded$.pipe(catchError((e: any) => {
|
||||
if (e instanceof NoMatch) {
|
||||
// TODO(i): this return type doesn't match the declared Observable<UrlSegmentGroup> -
|
||||
// talk to Jason
|
||||
return of(null) as any;
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const expansions = Array.from(routesByOutlet.entries()).map(([routeOutlet, routes]) => {
|
||||
const expanded = expandRoutes(routes);
|
||||
// Map all results from outlets we aren't activating to `null` so they can be ignored later
|
||||
return routeOutlet === outlet ? expanded :
|
||||
expanded.pipe(map(() => null), catchError(() => of(null)));
|
||||
});
|
||||
return from(expansions)
|
||||
.pipe(
|
||||
combineAll(),
|
||||
first(),
|
||||
// Return only the expansion for the route outlet we are trying to activate.
|
||||
map(results => results.find(result => result !== null)!),
|
||||
);
|
||||
}));
|
||||
}),
|
||||
concatAll(), first((s: any) => !!s), catchError((e: any, _: any) => {
|
||||
if (e instanceof EmptyError || e.name === 'EmptyError') {
|
||||
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
|
||||
return of(new UrlSegmentGroup([], {}));
|
||||
}
|
||||
throw new NoMatch(segmentGroup);
|
||||
}
|
||||
throw e;
|
||||
}));
|
||||
}
|
||||
|
||||
private noLeftoversInUrl(segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string):
|
||||
@ -205,9 +180,7 @@ class ApplyRedirects {
|
||||
private expandSegmentAgainstRoute(
|
||||
ngModule: NgModuleRef<any>, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
|
||||
paths: UrlSegment[], outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
|
||||
// Empty string segments are special because multiple outlets can match a single path, i.e.
|
||||
// `[{path: '', component: B}, {path: '', loadChildren: () => {}, outlet: "about"}]`
|
||||
if (getOutlet(route) !== outlet && route.path !== '') {
|
||||
if (getOutlet(route) !== outlet) {
|
||||
return noMatch(segmentGroup);
|
||||
}
|
||||
|
||||
@ -578,3 +551,7 @@ function isEmptyPathRedirect(
|
||||
|
||||
return r.path === '' && r.redirectTo !== undefined;
|
||||
}
|
||||
|
||||
function getOutlet(route: Route): string {
|
||||
return route.outlet || PRIMARY_OUTLET;
|
||||
}
|
||||
|
@ -113,21 +113,3 @@ export function standardizeConfig(r: Route): Route {
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/** Returns of `Map` of outlet names to the `Route`s for that outlet. */
|
||||
export function groupRoutesByOutlet(routes: Route[]): Map<string, Route[]> {
|
||||
return routes.reduce((map, route) => {
|
||||
const routeOutlet = getOutlet(route);
|
||||
if (map.has(routeOutlet)) {
|
||||
map.get(routeOutlet)!.push(route);
|
||||
} else {
|
||||
map.set(routeOutlet, [route]);
|
||||
}
|
||||
return map;
|
||||
}, new Map<string, Route[]>());
|
||||
}
|
||||
|
||||
/** Returns the `route.outlet` or PRIMARY_OUTLET if none exists. */
|
||||
export function getOutlet(route: Route): string {
|
||||
return route.outlet || PRIMARY_OUTLET;
|
||||
}
|
||||
|
@ -7,9 +7,9 @@
|
||||
*/
|
||||
|
||||
import {NgModuleRef} from '@angular/core';
|
||||
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {delay, tap} from 'rxjs/operators';
|
||||
import {delay} from 'rxjs/operators';
|
||||
|
||||
import {applyRedirects} from '../src/apply_redirects';
|
||||
import {LoadedRouterConfig, Route, Routes} from '../src/config';
|
||||
@ -482,88 +482,6 @@ describe('applyRedirects', () => {
|
||||
expect((config[0] as any)._loadedConfig).toBe(loadedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
it('should load all matching configurations of empty path, including an auxiliary outlets',
|
||||
fakeAsync(() => {
|
||||
const loadedConfig =
|
||||
new LoadedRouterConfig([{path: '', component: ComponentA}], testModule);
|
||||
let loadCalls = 0;
|
||||
let loaded: string[] = [];
|
||||
const loader = {
|
||||
load: (injector: any, p: Route) => {
|
||||
loadCalls++;
|
||||
return of(loadedConfig)
|
||||
.pipe(
|
||||
delay(100 * loadCalls),
|
||||
tap(() => loaded.push(p.loadChildren! as string)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const config: Routes =
|
||||
[{path: '', loadChildren: 'root'}, {path: '', loadChildren: 'aux', outlet: 'popup'}];
|
||||
|
||||
applyRedirects(testModule.injector, <any>loader, serializer, tree(''), config).subscribe();
|
||||
expect(loadCalls).toBe(2);
|
||||
tick(100);
|
||||
expect(loaded).toEqual(['root']);
|
||||
tick(100);
|
||||
expect(loaded).toEqual(['root', 'aux']);
|
||||
}));
|
||||
|
||||
it('loads only the first match when two Routes with the same outlet have the same path', () => {
|
||||
const loadedConfig = new LoadedRouterConfig([{path: '', component: ComponentA}], testModule);
|
||||
let loadCalls = 0;
|
||||
let loaded: string[] = [];
|
||||
const loader = {
|
||||
load: (injector: any, p: Route) => {
|
||||
loadCalls++;
|
||||
return of(loadedConfig)
|
||||
.pipe(
|
||||
tap(() => loaded.push(p.loadChildren! as string)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const config: Routes =
|
||||
[{path: 'a', loadChildren: 'first'}, {path: 'a', loadChildren: 'second'}];
|
||||
|
||||
applyRedirects(testModule.injector, <any>loader, serializer, tree('a'), config).subscribe();
|
||||
expect(loadCalls).toBe(1);
|
||||
expect(loaded).toEqual(['first']);
|
||||
});
|
||||
|
||||
it('should load the configuration of empty root path if the entry is an aux outlet',
|
||||
fakeAsync(() => {
|
||||
const loadedConfig =
|
||||
new LoadedRouterConfig([{path: '', component: ComponentA}], testModule);
|
||||
let loaded: string[] = [];
|
||||
const rootDelay = 100;
|
||||
const auxDelay = 1;
|
||||
const loader = {
|
||||
load: (injector: any, p: Route) => {
|
||||
const delayMs = p.loadChildren! as string === 'aux' ? auxDelay : rootDelay;
|
||||
return of(loadedConfig)
|
||||
.pipe(
|
||||
delay(delayMs),
|
||||
tap(() => loaded.push(p.loadChildren! as string)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const config: Routes = [
|
||||
// Define aux route first so it matches before the primary outlet
|
||||
{path: 'modal', loadChildren: 'aux', outlet: 'popup'},
|
||||
{path: '', loadChildren: 'root'},
|
||||
];
|
||||
|
||||
applyRedirects(testModule.injector, <any>loader, serializer, tree('(popup:modal)'), config)
|
||||
.subscribe();
|
||||
tick(auxDelay);
|
||||
expect(loaded).toEqual(['aux']);
|
||||
tick(rootDelay);
|
||||
expect(loaded).toEqual(['aux', 'root']);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('empty paths', () => {
|
||||
@ -836,46 +754,6 @@ describe('applyRedirects', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple matches with empty path named outlets', () => {
|
||||
it('should work with redirects when other outlet comes before the one being activated', () => {
|
||||
applyRedirects(
|
||||
testModule.injector, null!, serializer, tree(''),
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
children: [
|
||||
{path: '', component: ComponentA, outlet: 'aux'},
|
||||
{path: '', redirectTo: 'b', pathMatch: 'full'},
|
||||
{path: 'b', component: ComponentB},
|
||||
],
|
||||
},
|
||||
])
|
||||
.subscribe(
|
||||
(tree: UrlTree) => {
|
||||
expect(tree.toString()).toEqual('/b');
|
||||
},
|
||||
() => {
|
||||
fail('should not be reached');
|
||||
});
|
||||
});
|
||||
|
||||
it('should work when entry point is named outlet', () => {
|
||||
applyRedirects(
|
||||
testModule.injector, null!, serializer, tree('(popup:modal)'),
|
||||
[
|
||||
{path: '', component: ComponentA},
|
||||
{path: 'modal', component: ComponentB, outlet: 'popup'},
|
||||
])
|
||||
.subscribe(
|
||||
(tree: UrlTree) => {
|
||||
expect(tree.toString()).toEqual('/(popup:modal)');
|
||||
},
|
||||
(e) => {
|
||||
fail('should not be reached' + e.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirecting to named outlets', () => {
|
||||
it('should work when using absolute redirects', () => {
|
||||
checkRedirect(
|
||||
@ -916,18 +794,6 @@ describe('applyRedirects', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// internal failure b/165719418
|
||||
it('does not fail with large configs', () => {
|
||||
const config: Routes = [];
|
||||
for (let i = 0; i < 400; i++) {
|
||||
config.push({path: 'no_match', component: ComponentB});
|
||||
}
|
||||
config.push({path: 'match', component: ComponentA});
|
||||
applyRedirects(testModule.injector, null!, serializer, tree('match'), config).forEach(r => {
|
||||
expectTreeToBe(r, 'match');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function checkRedirect(config: Routes, url: string, callback: any): void {
|
||||
|
@ -14,7 +14,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export {UnrecoverableStateEvent, UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
|
||||
export {UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
|
||||
export {ServiceWorkerModule, SwRegistrationOptions} from './module';
|
||||
export {SwPush} from './push';
|
||||
export {SwUpdate} from './update';
|
||||
|
@ -14,8 +14,6 @@ export const ERR_SW_NOT_SUPPORTED = 'Service workers are disabled or not support
|
||||
/**
|
||||
* An event emitted when a new version of the app is available.
|
||||
*
|
||||
* @see {@link guide/service-worker-communications Service worker communication guide}
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface UpdateAvailableEvent {
|
||||
@ -27,8 +25,6 @@ export interface UpdateAvailableEvent {
|
||||
/**
|
||||
* An event emitted when a new version of the app has been downloaded and activated.
|
||||
*
|
||||
* @see {@link guide/service-worker-communications Service worker communication guide}
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface UpdateActivatedEvent {
|
||||
@ -37,24 +33,6 @@ export interface UpdateActivatedEvent {
|
||||
current: {hash: string, appData?: Object};
|
||||
}
|
||||
|
||||
/**
|
||||
* An event emitted when the version of the app used by the service worker to serve this client is
|
||||
* in a broken state that cannot be recovered from and a full page reload is required.
|
||||
*
|
||||
* For example, the service worker may not be able to retrieve a required resource, neither from the
|
||||
* cache nor from the server. This could happen if a new version is deployed to the server and the
|
||||
* service worker cache has been partially cleaned by the browser, removing some files of a previous
|
||||
* app version but not all.
|
||||
*
|
||||
* @see {@link guide/service-worker-communications Service worker communication guide}
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface UnrecoverableStateEvent {
|
||||
type: 'UNRECOVERABLE_STATE';
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event emitted when a `PushEvent` is received by the service worker.
|
||||
*/
|
||||
@ -63,7 +41,7 @@ export interface PushEvent {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export type IncomingEvent = UpdateAvailableEvent|UpdateActivatedEvent|UnrecoverableStateEvent;
|
||||
export type IncomingEvent = UpdateAvailableEvent|UpdateActivatedEvent;
|
||||
|
||||
export interface TypedEvent {
|
||||
type: string;
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {NEVER, Observable} from 'rxjs';
|
||||
|
||||
import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, UnrecoverableStateEvent, UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
|
||||
import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
|
||||
|
||||
|
||||
|
||||
@ -17,8 +17,6 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, UnrecoverableStateEvent, UpdateAc
|
||||
* Subscribe to update notifications from the Service Worker, trigger update
|
||||
* checks, and forcibly activate updates.
|
||||
*
|
||||
* @see {@link guide/service-worker-communications Service worker communication guide}
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
@Injectable()
|
||||
@ -33,13 +31,6 @@ export class SwUpdate {
|
||||
*/
|
||||
readonly activated: Observable<UpdateActivatedEvent>;
|
||||
|
||||
/**
|
||||
* Emits an `UnrecoverableStateEvent` event whenever the version of the app used by the service
|
||||
* worker to serve this client is in a broken state that cannot be recovered from without a full
|
||||
* page reload.
|
||||
*/
|
||||
readonly unrecoverable: Observable<UnrecoverableStateEvent>;
|
||||
|
||||
/**
|
||||
* True if the Service Worker is enabled (supported by the browser and enabled via
|
||||
* `ServiceWorkerModule`).
|
||||
@ -52,12 +43,10 @@ export class SwUpdate {
|
||||
if (!sw.isEnabled) {
|
||||
this.available = NEVER;
|
||||
this.activated = NEVER;
|
||||
this.unrecoverable = NEVER;
|
||||
return;
|
||||
}
|
||||
this.available = this.sw.eventsOfType<UpdateAvailableEvent>('UPDATE_AVAILABLE');
|
||||
this.activated = this.sw.eventsOfType<UpdateActivatedEvent>('UPDATE_ACTIVATED');
|
||||
this.unrecoverable = this.sw.eventsOfType<UnrecoverableStateEvent>('UNRECOVERABLE_STATE');
|
||||
}
|
||||
|
||||
checkForUpdate(): Promise<void> {
|
||||
|
@ -435,14 +435,6 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
|
||||
},
|
||||
});
|
||||
});
|
||||
it('processes unrecoverable notifications when sent', done => {
|
||||
update.unrecoverable.subscribe(event => {
|
||||
expect(event.reason).toEqual('Invalid Resource');
|
||||
expect(event.type).toEqual('UNRECOVERABLE_STATE');
|
||||
done();
|
||||
});
|
||||
mock.sendMessage({type: 'UNRECOVERABLE_STATE', reason: 'Invalid Resource'});
|
||||
});
|
||||
it('processes update activation notifications when sent', done => {
|
||||
update.activated.subscribe(event => {
|
||||
expect(event.previous).toEqual({hash: 'A'});
|
||||
@ -508,7 +500,6 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
|
||||
update = new SwUpdate(comm);
|
||||
update.available.toPromise().catch(err => fail(err));
|
||||
update.activated.toPromise().catch(err => fail(err));
|
||||
update.unrecoverable.toPromise().catch(err => fail(err));
|
||||
});
|
||||
it('gives an error when checking for updates', done => {
|
||||
update = new SwUpdate(comm);
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {Adapter, Context} from './adapter';
|
||||
import {CacheState, NormalizedUrl, UpdateCacheStatus, UpdateSource, UrlMetadata} from './api';
|
||||
import {Database, Table} from './database';
|
||||
import {errorToString, SwCriticalError, SwUnrecoverableStateError} from './error';
|
||||
import {errorToString, SwCriticalError} from './error';
|
||||
import {IdleScheduler} from './idle';
|
||||
import {AssetGroupConfig} from './manifest';
|
||||
import {sha1Binary} from './sha1';
|
||||
@ -145,7 +145,6 @@ export abstract class AssetGroup {
|
||||
return cachedResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// No already-cached response exists, so attempt a fetch/cache operation. The original request
|
||||
// may specify things like credential inclusion, but for assets these are not honored in order
|
||||
// to avoid issues with opaque responses. The SW requests the data itself.
|
||||
@ -394,8 +393,8 @@ export abstract class AssetGroup {
|
||||
// reasons: either the non-cache-busted request failed (hopefully transiently) or if the
|
||||
// hash of the content retrieved does not match the canonical hash from the manifest. It's
|
||||
// only valid to access the content of the first response if the request was successful.
|
||||
let makeCacheBustedRequest: boolean = !networkResult.ok;
|
||||
if (networkResult.ok) {
|
||||
let makeCacheBustedRequest: boolean = networkResult.ok;
|
||||
if (makeCacheBustedRequest) {
|
||||
// The request was successful. A cache-busted request is only necessary if the hashes
|
||||
// don't match. Compare them, making sure to clone the response so it can be used later
|
||||
// if it proves to be valid.
|
||||
@ -415,16 +414,10 @@ export abstract class AssetGroup {
|
||||
|
||||
// If the response was unsuccessful, there's nothing more that can be done.
|
||||
if (!cacheBustedResult.ok) {
|
||||
if (cacheBustedResult.status === 404) {
|
||||
throw new SwUnrecoverableStateError(
|
||||
`Failed to retrieve hashed resource from the server. (AssetGroup: ${
|
||||
this.config.name} | URL: ${url})`);
|
||||
} else {
|
||||
throw new SwCriticalError(
|
||||
`Response not Ok (cacheBustedFetchFromNetwork): cache busted request for ${
|
||||
req.url} returned response ${cacheBustedResult.status} ${
|
||||
cacheBustedResult.statusText}`);
|
||||
}
|
||||
throw new SwCriticalError(
|
||||
`Response not Ok (cacheBustedFetchFromNetwork): cache busted request for ${
|
||||
req.url} returned response ${cacheBustedResult.status} ${
|
||||
cacheBustedResult.statusText}`);
|
||||
}
|
||||
|
||||
// Hash the contents.
|
||||
|
@ -436,9 +436,6 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
// network.
|
||||
res = await appVersion.handleFetch(event.request, event);
|
||||
} catch (err) {
|
||||
if (err.isUnrecoverableState) {
|
||||
await this.notifyClientsAboutUnrecoverableState(appVersion, err.message);
|
||||
}
|
||||
if (err.isCritical) {
|
||||
// Something went wrong with the activation of this version.
|
||||
await this.versionFailed(appVersion, err);
|
||||
@ -1012,26 +1009,6 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
};
|
||||
}
|
||||
|
||||
async notifyClientsAboutUnrecoverableState(appVersion: AppVersion, reason: string):
|
||||
Promise<void> {
|
||||
const broken =
|
||||
Array.from(this.versions.entries()).find(([hash, version]) => version === appVersion);
|
||||
if (broken === undefined) {
|
||||
// This version is no longer in use anyway, so nobody cares.
|
||||
return;
|
||||
}
|
||||
|
||||
const brokenHash = broken[0];
|
||||
const affectedClients = Array.from(this.clientVersionMap.entries())
|
||||
.filter(([clientId, hash]) => hash === brokenHash)
|
||||
.map(([clientId]) => clientId);
|
||||
|
||||
affectedClients.forEach(async clientId => {
|
||||
const client = await this.scope.clients.get(clientId);
|
||||
client.postMessage({type: 'UNRECOVERABLE_STATE', reason});
|
||||
});
|
||||
}
|
||||
|
||||
async notifyClientsAboutUpdate(next: AppVersion): Promise<void> {
|
||||
await this.initialized;
|
||||
|
||||
|
@ -17,7 +17,3 @@ export function errorToString(error: any): string {
|
||||
return `${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class SwUnrecoverableStateError extends SwCriticalError {
|
||||
readonly isUnrecoverableState: boolean = true;
|
||||
}
|
||||
|
@ -1738,169 +1738,6 @@ describe('Driver', () => {
|
||||
expect(requestUrls2).toContain(httpsRequestUrl);
|
||||
});
|
||||
|
||||
describe('unrecoverable state', () => {
|
||||
const generateMockServerState = (fileSystem: MockFileSystem) => {
|
||||
const manifest: Manifest = {
|
||||
configVersion: 1,
|
||||
timestamp: 1234567890123,
|
||||
index: '/index.html',
|
||||
assetGroups: [{
|
||||
name: 'assets',
|
||||
installMode: 'prefetch',
|
||||
updateMode: 'prefetch',
|
||||
urls: fileSystem.list(),
|
||||
patterns: [],
|
||||
cacheQueryOptions: {ignoreVary: true},
|
||||
}],
|
||||
dataGroups: [],
|
||||
navigationUrls: processNavigationUrls(''),
|
||||
hashTable: tmpHashTableForFs(fileSystem),
|
||||
};
|
||||
|
||||
return {
|
||||
serverState: new MockServerStateBuilder()
|
||||
.withManifest(manifest)
|
||||
.withStaticFiles(fileSystem)
|
||||
.build(),
|
||||
manifest,
|
||||
};
|
||||
};
|
||||
|
||||
it('notifies affected clients', async () => {
|
||||
const {serverState: serverState1} = generateMockServerState(
|
||||
new MockFileSystemBuilder()
|
||||
.addFile('/index.html', '<script src="foo.hash.js"></script>')
|
||||
.addFile('/foo.hash.js', 'console.log("FOO");')
|
||||
.build());
|
||||
|
||||
const {serverState: serverState2, manifest: manifest2} = generateMockServerState(
|
||||
new MockFileSystemBuilder()
|
||||
.addFile('/index.html', '<script src="bar.hash.js"></script>')
|
||||
.addFile('/bar.hash.js', 'console.log("BAR");')
|
||||
.build());
|
||||
|
||||
const {serverState: serverState3} = generateMockServerState(
|
||||
new MockFileSystemBuilder()
|
||||
.addFile('/index.html', '<script src="baz.hash.js"></script>')
|
||||
.addFile('/baz.hash.js', 'console.log("BAZ");')
|
||||
.build());
|
||||
|
||||
// Create initial server state and initialize the SW.
|
||||
scope = new SwTestHarnessBuilder().withServerState(serverState1).build();
|
||||
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
|
||||
|
||||
// Verify that all three clients are able to make the request.
|
||||
expect(await makeRequest(scope, '/foo.hash.js', 'client1')).toBe('console.log("FOO");');
|
||||
expect(await makeRequest(scope, '/foo.hash.js', 'client2')).toBe('console.log("FOO");');
|
||||
expect(await makeRequest(scope, '/foo.hash.js', 'client3')).toBe('console.log("FOO");');
|
||||
|
||||
await driver.initialized;
|
||||
serverState1.clearRequests();
|
||||
|
||||
// Verify that the `foo.hash.js` file is cached.
|
||||
expect(await makeRequest(scope, '/foo.hash.js')).toBe('console.log("FOO");');
|
||||
serverState1.assertNoRequestFor('/foo.hash.js');
|
||||
|
||||
// Update the ServiceWorker to the second version.
|
||||
scope.updateServerState(serverState2);
|
||||
expect(await driver.checkForUpdate()).toEqual(true);
|
||||
|
||||
// Update the first two clients to the latest version, keep `client3` as is.
|
||||
const [client1, client2] =
|
||||
await Promise.all([scope.clients.get('client1'), scope.clients.get('client2')]);
|
||||
|
||||
await Promise.all([driver.updateClient(client1), driver.updateClient(client2)]);
|
||||
|
||||
// Update the ServiceWorker to the latest version
|
||||
scope.updateServerState(serverState3);
|
||||
expect(await driver.checkForUpdate()).toEqual(true);
|
||||
|
||||
// Remove `bar.hash.js` from the cache to emulate the browser evicting files from the cache.
|
||||
await removeAssetFromCache(scope, manifest2, '/bar.hash.js');
|
||||
|
||||
// Get all clients and verify their messages
|
||||
const mockClient1 = scope.clients.getMock('client1')!;
|
||||
const mockClient2 = scope.clients.getMock('client2')!;
|
||||
const mockClient3 = scope.clients.getMock('client3')!;
|
||||
|
||||
// Try to retrieve `bar.hash.js`, which is neither in the cache nor on the server.
|
||||
// This should put the SW in an unrecoverable state and notify clients.
|
||||
expect(await makeRequest(scope, '/bar.hash.js', 'client1')).toBeNull();
|
||||
serverState2.assertSawRequestFor('/bar.hash.js');
|
||||
const unrecoverableMessage = {
|
||||
type: 'UNRECOVERABLE_STATE',
|
||||
reason:
|
||||
'Failed to retrieve hashed resource from the server. (AssetGroup: assets | URL: /bar.hash.js)'
|
||||
};
|
||||
|
||||
expect(mockClient1.messages).toContain(unrecoverableMessage);
|
||||
expect(mockClient2.messages).toContain(unrecoverableMessage);
|
||||
expect(mockClient3.messages).not.toContain(unrecoverableMessage);
|
||||
|
||||
// Because `client1` failed, `client1` and `client2` have been moved to the latest version.
|
||||
// Verify that by retrieving `baz.hash.js`.
|
||||
expect(await makeRequest(scope, '/baz.hash.js', 'client1')).toBe('console.log("BAZ");');
|
||||
serverState2.assertNoRequestFor('/baz.hash.js');
|
||||
expect(await makeRequest(scope, '/baz.hash.js', 'client2')).toBe('console.log("BAZ");');
|
||||
serverState2.assertNoRequestFor('/baz.hash.js');
|
||||
|
||||
// Ensure that `client3` remains on the first version and can request `foo.hash.js`.
|
||||
expect(await makeRequest(scope, '/foo.hash.js', 'client3')).toBe('console.log("FOO");');
|
||||
serverState2.assertNoRequestFor('/foo.hash.js');
|
||||
});
|
||||
|
||||
it('enters degraded mode', async () => {
|
||||
const originalFiles = new MockFileSystemBuilder()
|
||||
.addFile('/index.html', '<script src="foo.hash.js"></script>')
|
||||
.addFile('/foo.hash.js', 'console.log("FOO");')
|
||||
.build();
|
||||
|
||||
const updatedFiles = new MockFileSystemBuilder()
|
||||
.addFile('/index.html', '<script src="bar.hash.js"></script>')
|
||||
.addFile('/bar.hash.js', 'console.log("BAR");')
|
||||
.build();
|
||||
|
||||
const {serverState: originalServer, manifest} = generateMockServerState(originalFiles);
|
||||
const {serverState: updatedServer} = generateMockServerState(updatedFiles);
|
||||
|
||||
// Create initial server state and initialize the SW.
|
||||
scope = new SwTestHarnessBuilder().withServerState(originalServer).build();
|
||||
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
|
||||
|
||||
expect(await makeRequest(scope, '/foo.hash.js')).toBe('console.log("FOO");');
|
||||
await driver.initialized;
|
||||
originalServer.clearRequests();
|
||||
|
||||
// Verify that the `foo.hash.js` file is cached.
|
||||
expect(await makeRequest(scope, '/foo.hash.js')).toBe('console.log("FOO");');
|
||||
originalServer.assertNoRequestFor('/foo.hash.js');
|
||||
|
||||
// Update the server state to emulate deploying a new version (where `foo.hash.js` does not
|
||||
// exist any more). Keep the cache though.
|
||||
scope = new SwTestHarnessBuilder()
|
||||
.withCacheState(scope.caches.dehydrate())
|
||||
.withServerState(updatedServer)
|
||||
.build();
|
||||
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
|
||||
|
||||
// The SW is still able to serve `foo.hash.js` from the cache.
|
||||
expect(await makeRequest(scope, '/foo.hash.js')).toBe('console.log("FOO");');
|
||||
updatedServer.assertNoRequestFor('/foo.hash.js');
|
||||
|
||||
// Remove `foo.hash.js` from the cache to emulate the browser evicting files from the cache.
|
||||
await removeAssetFromCache(scope, manifest, '/foo.hash.js');
|
||||
|
||||
// Try to retrieve `foo.hash.js`, which is neither in the cache nor on the server.
|
||||
// This should put the SW in an unrecoverable state and notify clients.
|
||||
expect(await makeRequest(scope, '/foo.hash.js')).toBeNull();
|
||||
updatedServer.assertSawRequestFor('/foo.hash.js');
|
||||
|
||||
// This should also enter the `SW` into degraded mode, because the broken version was the
|
||||
// latest one.
|
||||
expect(driver.state).toEqual(DriverReadyState.EXISTING_CLIENTS_ONLY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backwards compatibility with v5', () => {
|
||||
beforeEach(() => {
|
||||
const serverV5 = new MockServerStateBuilder()
|
||||
@ -1925,16 +1762,6 @@ describe('Driver', () => {
|
||||
});
|
||||
})();
|
||||
|
||||
async function removeAssetFromCache(
|
||||
scope: SwTestHarness, appVersionManifest: Manifest, assetPath: string) {
|
||||
const assetGroupName =
|
||||
appVersionManifest.assetGroups?.find(group => group.urls.includes(assetPath))?.name;
|
||||
const cacheName = `${scope.cacheNamePrefix}:${sha1(JSON.stringify(appVersionManifest))}:assets:${
|
||||
assetGroupName}:cache`;
|
||||
const cache = await scope.caches.open(cacheName);
|
||||
return cache.delete(assetPath);
|
||||
}
|
||||
|
||||
async function makeRequest(
|
||||
scope: SwTestHarness, url: string, clientId: string|null = 'default',
|
||||
init?: Object): Promise<string|null> {
|
||||
|
@ -102,20 +102,6 @@ function _tryDefineProperty(obj: any, prop: string, desc: any, originalConfigura
|
||||
try {
|
||||
return _defineProperty(obj, prop, desc);
|
||||
} catch (error) {
|
||||
let swallowError = false;
|
||||
if (prop === 'createdCallback' || prop === 'attachedCallback' ||
|
||||
prop === 'detachedCallback' || prop === 'attributeChangedCallback') {
|
||||
// We only swallow the error in registerElement patch
|
||||
// this is the work around since some applications
|
||||
// fail if we throw the error
|
||||
swallowError = true;
|
||||
}
|
||||
if (!swallowError) {
|
||||
throw error;
|
||||
}
|
||||
// TODO: @JiaLiPassion, Some application such as `registerElement` patch
|
||||
// still need to swallow the error, in the future after these applications
|
||||
// are updated, the following logic can be removed.
|
||||
let descJson: string|null = null;
|
||||
try {
|
||||
descJson = JSON.stringify(desc);
|
||||
|
Reference in New Issue
Block a user