Compare commits
39 Commits
10.1.2
...
11.0.0-nex
Author | SHA1 | Date | |
---|---|---|---|
e6ee7c2aeb | |||
54687f7765 | |||
59c234cfb4 | |||
a6f3cd93a9 | |||
d9fea857db | |||
03dbcc7a56 | |||
c142b071eb | |||
71acf9dd49 | |||
f5a148b1b7 | |||
4f28192d62 | |||
0fc2bef0cd | |||
f5d1e9a2d1 | |||
036a2faf02 | |||
5be4edfa17 | |||
38d6596742 | |||
0a7a5e3aff | |||
d5fabc303d | |||
ebc0e46501 | |||
3487b549fd | |||
52c7a4bfc6 | |||
827ba05914 | |||
b2857b4e3a | |||
5d5caf21b8 | |||
c1bc070b40 | |||
930eeaf177 | |||
2dd29fbae7 | |||
9613660fee | |||
c0523fc3b4 | |||
de1cffb23b | |||
31f4557621 | |||
7723bfd9ba | |||
e8ea839df8 | |||
90cec40cce | |||
4036281007 | |||
164cd274a4 | |||
fedcfec346 | |||
618cb32407 | |||
4aee0087ea | |||
0681a20d28 |
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@66462f6
|
||||
- uses: angular/dev-infra/github-actions/lock-closed@414834b2b24dd2df37c6ed00808387ee6fd91b66
|
||||
with:
|
||||
lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }}
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -40,9 +40,6 @@ yarn-error.log
|
||||
# User specific bazel settings
|
||||
.bazelrc.user
|
||||
|
||||
# User specific ng-dev settings
|
||||
.ng-dev.user*
|
||||
|
||||
.notes.md
|
||||
baseline.json
|
||||
|
||||
|
47
CHANGELOG.md
47
CHANGELOG.md
@ -1,47 +1,25 @@
|
||||
<a name="10.1.2"></a>
|
||||
## 10.1.2 (2020-09-16)
|
||||
<a name="11.0.0-next.0"></a>
|
||||
# 11.0.0-next.0 (2020-09-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** detect pipes in ICUs in template binder ([#38810](https://github.com/angular/angular/issues/38810)) ([ec2dbe7](https://github.com/angular/angular/commit/ec2dbe7)), closes [#38539](https://github.com/angular/angular/issues/38539) [#38539](https://github.com/angular/angular/issues/38539) [#38539](https://github.com/angular/angular/issues/38539)
|
||||
* **core:** clear the `RefreshTransplantedView` when detached ([#38768](https://github.com/angular/angular/issues/38768)) ([edb7f90](https://github.com/angular/angular/commit/edb7f90)), closes [#38619](https://github.com/angular/angular/issues/38619)
|
||||
* **localize:** ensure that `formatOptions` is optional ([#38787](https://github.com/angular/angular/issues/38787)) ([a47383d](https://github.com/angular/angular/commit/a47383d))
|
||||
* **router:** Ensure routes are processed in priority order and only if needed ([#38780](https://github.com/angular/angular/issues/38780)) ([9c51ba3](https://github.com/angular/angular/commit/9c51ba3)), closes [#38691](https://github.com/angular/angular/issues/38691)
|
||||
* **upgrade:** add try/catch when downgrading injectables ([#38671](https://github.com/angular/angular/issues/38671)) ([5de2ac3](https://github.com/angular/angular/commit/5de2ac3)), closes [#37579](https://github.com/angular/angular/issues/37579)
|
||||
* **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))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
### Features
|
||||
|
||||
* **compiler-cli:** only emit directive/pipe references that are used ([#38843](https://github.com/angular/angular/issues/38843)) ([5658405](https://github.com/angular/angular/commit/5658405))
|
||||
* **compiler-cli:** optimize computation of type-check scope information ([#38843](https://github.com/angular/angular/issues/38843)) ([ebede67](https://github.com/angular/angular/commit/ebede67))
|
||||
* **ngcc:** introduce cache for sharing data across entry-points ([#38840](https://github.com/angular/angular/issues/38840)) ([58411e7](https://github.com/angular/angular/commit/58411e7))
|
||||
* **ngcc:** reduce maximum worker count ([#38840](https://github.com/angular/angular/issues/38840)) ([ea36466](https://github.com/angular/angular/commit/ea36466))
|
||||
* **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
|
||||
|
||||
<a name="10.1.1"></a>
|
||||
## 10.1.1 (2020-09-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** correct confusion between field and property names ([#38685](https://github.com/angular/angular/issues/38685)) ([a1c34c6](https://github.com/angular/angular/commit/a1c34c6))
|
||||
* **compiler-cli:** compute source-mappings for localized strings ([#38747](https://github.com/angular/angular/issues/38747)) ([b4eb016](https://github.com/angular/angular/commit/b4eb016)), closes [#38588](https://github.com/angular/angular/issues/38588)
|
||||
* **compiler-cli:** ensure that a declaration is available in type-to-value conversion ([#38684](https://github.com/angular/angular/issues/38684)) ([56d5ff2](https://github.com/angular/angular/commit/56d5ff2)), closes [#38670](https://github.com/angular/angular/issues/38670)
|
||||
* **core:** reset `tView` between tests in Ivy TestBed ([#38659](https://github.com/angular/angular/issues/38659)) ([efc7606](https://github.com/angular/angular/commit/efc7606)), closes [#38600](https://github.com/angular/angular/issues/38600)
|
||||
* **localize:** do not expose NodeJS typings in $localize runtime code ([#38700](https://github.com/angular/angular/issues/38700)) ([4de8dc3](https://github.com/angular/angular/commit/4de8dc3)), closes [#38692](https://github.com/angular/angular/issues/38692)
|
||||
* **localize:** enable whitespace preservation marker in XLIFF files ([#38737](https://github.com/angular/angular/issues/38737)) ([190dca0](https://github.com/angular/angular/commit/190dca0)), closes [#38679](https://github.com/angular/angular/issues/38679)
|
||||
* **localize:** install `[@angular](https://github.com/angular)/localize` in `devDependencies` by default ([#38680](https://github.com/angular/angular/issues/38680)) ([dbab744](https://github.com/angular/angular/commit/dbab744)), closes [#38329](https://github.com/angular/angular/issues/38329)
|
||||
* **localize:** render context of translation file parse errors ([#38673](https://github.com/angular/angular/issues/38673)) ([32f33f0](https://github.com/angular/angular/commit/32f33f0)), closes [#38377](https://github.com/angular/angular/issues/38377)
|
||||
* **localize:** render location in XLIFF 2 even if there is no metadata ([#38713](https://github.com/angular/angular/issues/38713)) ([ab4f953](https://github.com/angular/angular/commit/ab4f953)), closes [#38705](https://github.com/angular/angular/issues/38705)
|
||||
* **ngcc:** use aliased exported types correctly ([#38666](https://github.com/angular/angular/issues/38666)) ([6a28675](https://github.com/angular/angular/commit/6a28675)), closes [#38238](https://github.com/angular/angular/issues/38238)
|
||||
* **router:** If users are using the Alt key when clicking the router links, prioritize browser’s default behavior ([#38375](https://github.com/angular/angular/issues/38375)) ([309709d](https://github.com/angular/angular/commit/309709d))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **core:** use `ngDevMode` to tree-shake error messages ([#38612](https://github.com/angular/angular/issues/38612)) ([b084bff](https://github.com/angular/angular/commit/b084bff))
|
||||
* **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.
|
||||
|
||||
|
||||
|
||||
@ -99,6 +77,7 @@
|
||||
* **router:** properly compare array queryParams for equality ([#37709](https://github.com/angular/angular/issues/37709)) ([#37860](https://github.com/angular/angular/issues/37860)) ([1801d0c](https://github.com/angular/angular/commit/1801d0c))
|
||||
* **router:** remove parenthesis for primary outlet segment after removing auxiliary outlet segment ([#24656](https://github.com/angular/angular/issues/24656)) ([#37163](https://github.com/angular/angular/issues/37163)) ([71f008f](https://github.com/angular/angular/commit/71f008f))
|
||||
* **router:** restore 'history.state' object for navigations coming from Angular router ([#28108](https://github.com/angular/angular/issues/28108)) ([#28176](https://github.com/angular/angular/issues/28176)) ([df76a20](https://github.com/angular/angular/commit/df76a20))
|
||||
* **router:** support lazy loading for empty path named outlets ([#38379](https://github.com/angular/angular/issues/38379)) ([7ad3264](https://github.com/angular/angular/commit/7ad3264)), closes [#12842](https://github.com/angular/angular/issues/12842)
|
||||
|
||||
### Code Refactoring
|
||||
* **router:** export DefaultRouteReuseStrategy to Router public_api ([#31575](https://github.com/angular/angular/issues/31575)) ([ca79880](https://github.com/angular/angular/commit/ca79880))
|
||||
|
@ -16,6 +16,13 @@ import {BuildNums, PrNums, SHA} from './constants';
|
||||
|
||||
const logger = new Logger('mock-external-apis');
|
||||
|
||||
const log = (...args: any[]) => {
|
||||
// Filter out non-matching URL checks
|
||||
if (!/^matching.+: false$/.test(args[0])) {
|
||||
logger.log(...args);
|
||||
}
|
||||
};
|
||||
|
||||
const AIO_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN');
|
||||
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
||||
|
||||
@ -84,8 +91,8 @@ const createArchive = (buildNum: number, prNum: number, sha: string) => {
|
||||
};
|
||||
|
||||
// Create request scopes
|
||||
const circleCiApi = nock(CIRCLE_CI_API_HOST).persist();
|
||||
const githubApi = nock(GITHUB_API_HOST).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`);
|
||||
const circleCiApi = nock(CIRCLE_CI_API_HOST).log(log).persist();
|
||||
const githubApi = nock(GITHUB_API_HOST).log(log).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`);
|
||||
|
||||
//////////////////////////////
|
||||
|
||||
|
@ -27,28 +27,28 @@
|
||||
"body-parser": "^1.19.0",
|
||||
"delete-empty": "^3.0.0",
|
||||
"express": "^4.17.1",
|
||||
"jasmine": "^3.6.1",
|
||||
"nock": "^13.0.4",
|
||||
"node-fetch": "^2.6.1",
|
||||
"jasmine": "^3.5.0",
|
||||
"nock": "^12.0.3",
|
||||
"node-fetch": "^2.6.0",
|
||||
"shelljs": "^0.8.4",
|
||||
"source-map-support": "^0.5.19",
|
||||
"tar-stream": "^2.1.3",
|
||||
"tslib": "^2.0.1"
|
||||
"tar-stream": "^2.1.2",
|
||||
"tslib": "^1.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/express": "^4.17.8",
|
||||
"@types/jasmine": "^3.5.14",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jasmine": "^3.5.10",
|
||||
"@types/nock": "^11.1.0",
|
||||
"@types/node": "^14.6.4",
|
||||
"@types/node": "^13.13.2",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/shelljs": "^0.8.8",
|
||||
"@types/supertest": "^2.0.10",
|
||||
"nodemon": "^2.0.4",
|
||||
"@types/shelljs": "^0.8.7",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"nodemon": "^2.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"supertest": "^4.0.2",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint": "^6.1.1",
|
||||
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
|
||||
"typescript": "^4.0.2"
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
}
|
||||
|
@ -214,24 +214,23 @@ describe('GithubApi', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should call \'https.request()\' with the correct options', async () => {
|
||||
it('should call \'https.request()\' with the correct options', () => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.reply(200);
|
||||
|
||||
await (api as any).request('method', '/path');
|
||||
(api as any).request('method', '/path');
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
||||
it('should add the \'Authorization\' header containing the \'githubToken\'', async () => {
|
||||
it('should add the \'Authorization\' header containing the \'githubToken\'', () => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method', undefined, {
|
||||
reqheaders: {Authorization: 'token 12345'},
|
||||
})
|
||||
.reply(200);
|
||||
|
||||
await (api as any).request('method', '/path');
|
||||
(api as any).request('method', '/path');
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
@ -245,13 +244,12 @@ describe('GithubApi', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should \'JSON.stringify\' and send the data along with the request', async () => {
|
||||
it('should \'JSON.stringify\' and send the data along with the request', () => {
|
||||
const data = {key: 'value'};
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method', JSON.stringify(data))
|
||||
.reply(200);
|
||||
|
||||
await (api as any).request('method', '/path', data);
|
||||
(api as any).request('method', '/path', data);
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,9 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"tests": [
|
||||
{
|
||||
"cmd": "yarn",
|
||||
"args": ["tsc", "--project", "tsconfig.spec.json", "--module", "commonjs"]
|
||||
},
|
||||
{
|
||||
"cmd": "yarn",
|
||||
"args": ["jasmine", "out-tsc/**/*.spec.js"]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
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,40 +1,72 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
// #docplaster
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
// #docregion observable
|
||||
export function docRegionObservable(console: Console) {
|
||||
// #docregion observable
|
||||
|
||||
// declare a publishing operation
|
||||
const observable = new Observable<number>(observer => {
|
||||
// Subscriber fn...
|
||||
});
|
||||
// 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
|
||||
});
|
||||
|
||||
// initiate execution
|
||||
observable.subscribe(() => {
|
||||
// observer handles notifications
|
||||
});
|
||||
// initiate execution
|
||||
observable.subscribe(value => {
|
||||
// observer handles notifications
|
||||
// #enddocregion observable
|
||||
// The below code is used for unit testing only
|
||||
console.log(value);
|
||||
// #docregion observable
|
||||
});
|
||||
|
||||
// #enddocregion observable
|
||||
// #enddocregion observable
|
||||
return observable;
|
||||
}
|
||||
|
||||
// #docregion unsubscribe
|
||||
export function docRegionUnsubscribe() {
|
||||
const observable = new Observable<number>(() => {
|
||||
// Subscriber fn...
|
||||
});
|
||||
// #docregion unsubscribe
|
||||
|
||||
const subscription = observable.subscribe(() => {
|
||||
// observer handles notifications
|
||||
});
|
||||
const subscription = observable.subscribe(() => {
|
||||
// observer handles notifications
|
||||
});
|
||||
|
||||
subscription.unsubscribe();
|
||||
subscription.unsubscribe();
|
||||
|
||||
// #enddocregion unsubscribe
|
||||
// #enddocregion unsubscribe
|
||||
return subscription;
|
||||
}
|
||||
|
||||
// #docregion error
|
||||
export function docRegionError() {
|
||||
const observable = new Observable<number>(() => {
|
||||
// Subscriber fn...
|
||||
});
|
||||
|
||||
observable.subscribe(() => {
|
||||
throw Error('my error');
|
||||
});
|
||||
// #docregion error
|
||||
observable.subscribe(() => {
|
||||
throw new Error('my error');
|
||||
});
|
||||
// #enddocregion error
|
||||
}
|
||||
|
||||
// #enddocregion error
|
||||
export function docRegionChain() {
|
||||
let observable = new Observable<number>(observer => {
|
||||
// Subscriber fn...
|
||||
observer.next(2);
|
||||
});
|
||||
|
||||
// #docregion chain
|
||||
observable =
|
||||
// #docregion chain
|
||||
|
||||
observable.pipe(map(v => 2 * v));
|
||||
observable.pipe(map(v => 2 * v));
|
||||
|
||||
// #enddocregion chain
|
||||
// #enddocregion chain
|
||||
return observable;
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
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,25 +1,44 @@
|
||||
// #docregion promise
|
||||
// initiate execution
|
||||
const promise = new Promise<number>((resolve, reject) => {
|
||||
// Executer fn...
|
||||
});
|
||||
// #docplaster
|
||||
|
||||
promise.then(value => {
|
||||
// handle result here
|
||||
});
|
||||
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
|
||||
|
||||
// #enddocregion promise
|
||||
return promise;
|
||||
}
|
||||
|
||||
// #docregion chain
|
||||
export function docRegionError() {
|
||||
let promise = Promise.resolve();
|
||||
promise =
|
||||
// #docregion error
|
||||
|
||||
promise.then(v => 2 * v);
|
||||
promise.then(() => {
|
||||
throw new Error('my error');
|
||||
});
|
||||
|
||||
// #enddocregion chain
|
||||
|
||||
// #docregion error
|
||||
|
||||
promise.then(() => {
|
||||
throw Error('my error');
|
||||
});
|
||||
|
||||
// #enddocregion error
|
||||
// #enddocregion error
|
||||
return promise;
|
||||
}
|
||||
|
@ -41,6 +41,7 @@
|
||||
<!-- #enddocregion translated-plural -->
|
||||
<!-- #docregion translated-select -->
|
||||
<!-- #docregion translate-select-1 -->
|
||||
</trans-unit>
|
||||
<trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" datatype="html">
|
||||
<source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
|
||||
<target>L'auteur est <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></target>
|
||||
|
@ -0,0 +1,17 @@
|
||||
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
|
@ -62,7 +62,7 @@ In the following example, the `@Component()` metadata object and the class const
|
||||
@Component({
|
||||
selector: 'app-typical',
|
||||
template: '<div>A typical component for {{data.name}}</div>'
|
||||
})
|
||||
)}
|
||||
export class TypicalComponent {
|
||||
@Input() data: TypicalData;
|
||||
constructor(private someService: SomeService) { ... }
|
||||
|
@ -125,7 +125,7 @@ Emulated is the default and most commonly used view encapsulation. For more info
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
The shadow-piercing descendant combinator is deprecated and [support is being removed from major browsers](https://www.chromestatus.com/feature/6750456638341120) and tools.
|
||||
The shadow-piercing descendant combinator is deprecated and [support is being removed from major browsers](https://www.chromestatus.com/features/6750456638341120) and tools.
|
||||
As such we plan to drop support in Angular (for all 3 of `/deep/`, `>>>` and `::ng-deep`).
|
||||
Until then `::ng-deep` should be preferred for a broader compatibility with the tools.
|
||||
|
||||
|
@ -26,7 +26,7 @@ The `ng generate` command creates the `projects/my-lib` folder in your workspace
|
||||
|
||||
</div>
|
||||
|
||||
When you generate a new library, the workspace configuration file, `angular.json`, is updated with a project of type `library`.
|
||||
When you generate a new library, the workspace configuration file, `angular.json`, is updated with a project of type 'library'.
|
||||
|
||||
<code-example format="json">
|
||||
"projects": {
|
||||
@ -109,7 +109,7 @@ If you want a dropdown that would contain different passed-in values each time,
|
||||
|
||||
Suppose you want to read a configuration file and then generate a form based on that configuration.
|
||||
If that form will need additional customization by the developer who is using your library, it might work best as a schematic.
|
||||
However, if the form will always be the same and not need much customization by developers, then you could create a dynamic component that takes the configuration and generates the form.
|
||||
However, if the forms will always be the same and not need much customization by developers, then you could create a dynamic component that takes the configuration and generates the form.
|
||||
In general, the more complex the customization, the more useful the schematic approach.
|
||||
|
||||
To learn more, see [Schematics Overview](guide/schematics) and [Schematics for Libraries](guide/schematics-for-libraries).
|
||||
|
@ -511,9 +511,9 @@ Each script tag has a `type="module"` or `nomodule` attribute. Browsers with nat
|
||||
|
||||
To include differential loading in your application builds, you must configure the Browserslist and TypeScript configuration files in your application project.
|
||||
|
||||
The following examples show a `.browserslistrc` and `tsconfig.json` file for a newly created Angular application. In this configuration, legacy browsers such as IE 9-11 are ignored, and the compilation target is ES2015.
|
||||
The following examples show a `browserlistrc` and `tsconfig.json` file for a newly created Angular application. In this configuration, legacy browsers such as IE 9-11 are ignored, and the compilation target is ES2015.
|
||||
|
||||
<code-example language="none" header=".browserslistrc">
|
||||
<code-example language="none" header="browserslistrc">
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
@ -527,7 +527,7 @@ The following examples show a `.browserslistrc` and `tsconfig.json` file for a n
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 Safari major version
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
|
@ -119,14 +119,7 @@ The recently-developed [custom elements](https://developer.mozilla.org/en-US/doc
|
||||
|
||||
In browsers that support Custom Elements natively, the specification requires developers use ES2015 classes to define Custom Elements - developers can opt-in to this by setting the `target: "es2015"` property in their project's [TypeScript configuration file](/guide/typescript-configuration). As Custom Element and ES2015 support may not be available in all browsers, developers can instead choose to use a polyfill to support older browsers and ES5 code.
|
||||
|
||||
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill:
|
||||
|
||||
<code-example language="sh">
|
||||
|
||||
ng add @angular/elements --project=*your_project_name*
|
||||
|
||||
</code-example>
|
||||
|
||||
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill: `ng add @angular/elements --project=*your_project_name*`.
|
||||
- For more information about polyfills, see [polyfill documentation](https://www.webcomponents.org/polyfills).
|
||||
|
||||
- For more information about Angular browser support, see [Browser Support](guide/browser-support).
|
||||
|
@ -76,12 +76,6 @@ All router components must be entry components. Because this would require you t
|
||||
|
||||
## The `entryComponents` array
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Since 9.0.0 with Ivy, the `entryComponents` property is no longer necessary. See [deprecations guide](guide/deprecations#entryComponents).
|
||||
|
||||
</div>
|
||||
|
||||
Though the `@NgModule` decorator has an `entryComponents` array, most of the time
|
||||
you won't have to explicitly set any entry components because Angular adds components listed in `@NgModule.bootstrap` and those in route definitions to entry components automatically. Though these two mechanisms account for most entry components, if your app happens to bootstrap or dynamically load a component by type imperatively,
|
||||
you must add it to `entryComponents` explicitly.
|
||||
|
@ -627,11 +627,6 @@ The [npm package manager](https://docs.npmjs.com/getting-started/what-is-npm) is
|
||||
|
||||
Learn more about how Angular uses [Npm Packages](guide/npm-packages).
|
||||
|
||||
{@ ngc}
|
||||
## ngc
|
||||
`ngc` is a Typescript-to-Javascript transpiler that processes Angular decorators, metadata, and templates, and emits JavaScript code.
|
||||
The most recent implementation is internally refered to as `ngtsc` because it's a minimalistic wrapper around the TypeScript compiler `tsc` that adds a transform for processing Angular code.
|
||||
|
||||
{@a O}
|
||||
|
||||
{@a observable}
|
||||
|
@ -62,8 +62,6 @@ Angular executes hook methods in the following sequence. You can use them to per
|
||||
|
||||
Called before `ngOnInit()` and whenever one or more data-bound input properties change.
|
||||
|
||||
Note that if your component has no inputs or you use it without providing any inputs, the framework will not call `ngOnChanges()`.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr style='vertical-align:top'>
|
||||
|
@ -141,7 +141,7 @@ Because the token is now an abstract class, and the injectable component impleme
|
||||
The implementation of the method (with all of its code overhead) resides in the injectable component that can be tree-shaken.
|
||||
This allows the parent to communicate with the child (if it is present) in a type-safe manner.
|
||||
|
||||
For example, the `LibCardComponent` now queries `LibHeaderToken` rather than `LibHeaderComponent`.
|
||||
For example, the `LibCardComponent` now queries`LibHeaderToken` rather than `LibHeaderComponent`.
|
||||
The following example shows how the pattern allows `LibCardComponent` to communicate with the `LibHeaderComponent` without actually referring to `LibHeaderComponent`.
|
||||
|
||||
```
|
||||
|
@ -223,6 +223,6 @@ content harmlessly. The following is the browser output
|
||||
of the `evilTitle` examples.
|
||||
|
||||
<code-example language="bash">
|
||||
"Template <script>alert("evil never sleeps")</script> Syntax" is the interpolated evil title.
|
||||
"Template Syntax" is the property bound evil title.
|
||||
"Template <script>alert("evil never sleeps")</script> Syntax" is the interpolated evil title.
|
||||
"Template alert("evil never sleeps")Syntax" is the property bound evil title.
|
||||
</code-example>
|
||||
|
@ -94,7 +94,7 @@ All of our major releases are supported for 18 months.
|
||||
|
||||
* 6 months of *active support*, during which regularly-scheduled updates and patches are released.
|
||||
|
||||
* 12 months of *long-term support (LTS)*, during which only [critical fixes and security patches](#lts-fixes) are released.
|
||||
* 12 months of *long-term support (LTS)*, during which only critical fixes and security patches are released.
|
||||
|
||||
The following table provides the status for Angular versions under support.
|
||||
|
||||
@ -102,18 +102,11 @@ The following table provides the status for Angular versions under support.
|
||||
Version | Status | Released | Active Ends | LTS Ends
|
||||
------- | ------ | ------------ | ------------ | ------------
|
||||
^10.0.0 | Active | Jun 24, 2020 | Dec 24, 2020 | Dec 24, 2021
|
||||
^9.0.0 | LTS | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
|
||||
^9.0.0 | Active | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
|
||||
^8.0.0 | LTS | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020
|
||||
|
||||
Angular versions ^4.0.0, ^5.0.0, ^6.0.0 and ^7.0.0 are no longer under support.
|
||||
|
||||
### LTS fixes
|
||||
|
||||
As a general rule, a fix is considered for an LTS version if it resolves one of:
|
||||
|
||||
* a newly identified security vulnerability,
|
||||
* a regression, since the start of LTS, caused by a 3rd party change, such as a new browser version.
|
||||
|
||||
{@a deprecation}
|
||||
## Deprecation practices
|
||||
|
||||
|
@ -53,7 +53,7 @@ RxJS provides many operators, but only a handful are used frequently. For a list
|
||||
|
||||
| Area | Operators |
|
||||
| :------------| :----------|
|
||||
| Creation | `from`, `fromEvent`, `of` |
|
||||
| Creation | `from`,`fromEvent`, `of` |
|
||||
| Combination | `combineLatest`, `concat`, `merge`, `startWith` , `withLatestFrom`, `zip` |
|
||||
| Filtering | `debounceTime`, `distinctUntilChanged`, `filter`, `take`, `takeUntil` |
|
||||
| Transformation | `bufferTime`, `concatMap`, `map`, `mergeMap`, `scan`, `switchMap` |
|
||||
|
@ -67,6 +67,33 @@ 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:
|
||||
|
@ -107,7 +107,7 @@ Notice that all of the files the browser needs to render this application are ca
|
||||
<div class="alert is-helpful">
|
||||
Pay attention to two key points:
|
||||
|
||||
1. The generated `ngsw-config.json` includes a limited list of cacheable fonts and images extensions. In some cases, you might want to modify the glob pattern to suit your needs.
|
||||
1. The generated `ngsw-config.json` includes a limited list of cacheable fonts and images extentions. In some cases, you might want to modify the glob pattern to suit your needs.
|
||||
|
||||
1. If `resourcesOutputPath` or `assets` paths are modified after the generation of configuration file, you need to change the paths manually in `ngsw-config.json`.
|
||||
</div>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 9.0 KiB |
@ -53,6 +53,9 @@
|
||||
},
|
||||
"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"
|
||||
@ -818,13 +821,5 @@
|
||||
"twitter": "samvloeberghs",
|
||||
"website": "https://samvloeberghs.be",
|
||||
"bio": "Sam is a freelance software architect and Internet entrepreneur, currently focusing on frontend technologies. Co-organiser of the Belgian Angular conference NG-BE and Angular Belgium Meetup group."
|
||||
},
|
||||
"thekiba": {
|
||||
"name": "Andrew Grekov",
|
||||
"picture": "thekiba.jpg",
|
||||
"twitter": "thekiba_io",
|
||||
"website": "https://thekiba.io",
|
||||
"bio": "Andrew is a software engineer using Angular and .NET. He spends most of his spare time staying up-to-date, helping other people, and experimenting with web tech.",
|
||||
"groups": ["GDE"]
|
||||
}
|
||||
}
|
||||
|
@ -30,10 +30,10 @@
|
||||
"url": "https://dev.to/t/angular",
|
||||
"title": "DEV Community"
|
||||
},
|
||||
"indepth-dev": {
|
||||
"desc": "Peer-reviewed Angular articles and tutorials.",
|
||||
"url": "https://indepth.dev/angular/",
|
||||
"title": "Angular inDepth"
|
||||
"angular-in-depth": {
|
||||
"desc": "The place where advanced Angular concepts are explained",
|
||||
"url": "https://blog.angularindepth.com",
|
||||
"title": "Angular In Depth"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -63,12 +63,6 @@
|
||||
"logo": "",
|
||||
"title": "NgRuAir",
|
||||
"url": "https://github.com/ngRuAir/ngruair"
|
||||
},
|
||||
"the-deep-dive": {
|
||||
"desc": "The advanced web development podcast about Angular, RxJS, TypeScript and other technologies. English, audio only.",
|
||||
"logo": "https://i.imgur.com/mmE5Feq.jpg",
|
||||
"title": "The Deep Dive",
|
||||
"url": "https://thedeepdive.simplecast.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -435,12 +429,6 @@
|
||||
"desc": "Jigsaw provides a set of web components based on Angular. It is supporting the development of all applications of Big Data Product of ZTE (https://www.zte.com.cn).",
|
||||
"title": "Awade Jigsaw (Chinese)",
|
||||
"url": "https://jigsaw-zte.gitee.io"
|
||||
},
|
||||
"material-dayjs-adapter": {
|
||||
"desc": "A DayJS implementation of @angular/material's DateAdapter that results in smaller bundle sizes than its MomentJS counterpart.",
|
||||
"rev": true,
|
||||
"title": "material-dayjs-adapter",
|
||||
"url": "https://www.npmjs.com/package/@tabuckner/material-dayjs-adapter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
@ -101,6 +101,11 @@
|
||||
"title": "Components",
|
||||
"tooltip": "Building dynamic views with data binding",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/displaying-data",
|
||||
"title": "Data binding",
|
||||
"tooltip": "Property binding helps show app data in the UI."
|
||||
},
|
||||
{
|
||||
"url": "guide/user-input",
|
||||
"title": "User Input",
|
||||
@ -537,6 +542,27 @@
|
||||
"title": "Tutorials",
|
||||
"tooltip": "End-to-end tutorials for learning Angular concepts and patterns.",
|
||||
"children": [
|
||||
{
|
||||
"title": "Routing",
|
||||
"tooltip": "End-to-end tutorials for learning about Angular's router.",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/router-tutorial",
|
||||
"title": "Using Angular Routes in a Single-page Application",
|
||||
"tooltip": "A tutorial that covers many patterns associated with Angular routing."
|
||||
},
|
||||
{
|
||||
"url": "guide/router-tutorial-toh",
|
||||
"title": "Router tutorial: tour of heroes",
|
||||
"tooltip": "Explore how to use Angular's router. Based on the Tour of Heroes example."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "guide/forms",
|
||||
"title": "Building a Template-driven Form",
|
||||
"tooltip": "Create a template-driven form using directives and Angular template syntax."
|
||||
},
|
||||
{
|
||||
"title": "Tutorial: Tour of Heroes",
|
||||
"tooltip": "The Tour of Heroes app is used as a reference point in many Angular examples.",
|
||||
@ -583,32 +609,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Routing",
|
||||
"tooltip": "End-to-end tutorials for learning about Angular's router.",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/router-tutorial",
|
||||
"title": "Using Angular Routes in a Single-page Application",
|
||||
"tooltip": "A tutorial that covers many patterns associated with Angular routing."
|
||||
},
|
||||
{
|
||||
"url": "guide/router-tutorial-toh",
|
||||
"title": "Router tutorial: tour of heroes",
|
||||
"tooltip": "Explore how to use Angular's router. Based on the Tour of Heroes example."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "guide/forms",
|
||||
"title": "Building a Template-driven Form",
|
||||
"tooltip": "Create a template-driven form using directives and Angular template syntax."
|
||||
},
|
||||
{
|
||||
"url": "guide/displaying-data",
|
||||
"title": "Data binding",
|
||||
"tooltip": "Property binding helps show app data in the UI."
|
||||
},
|
||||
{
|
||||
"url": "guide/web-worker",
|
||||
"title": "Web Workers",
|
||||
@ -954,11 +954,6 @@
|
||||
"url": "guide/styleguide",
|
||||
"title": "Coding Style Guide",
|
||||
"tooltip": "Guidelines for writing Angular code."
|
||||
},
|
||||
{
|
||||
"url": "guide/docs-style-guide",
|
||||
"title": "Documentation Style Guide",
|
||||
"tooltip": "Style guide for documentation authors."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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 ef770f1cb",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 32391604b",
|
||||
"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 -%}
|
||||
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}
|
||||
{% if clause.doc.path %}<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% else %}{$ clause.text $}{% endif %}{% 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 -%}
|
||||
|
@ -23,36 +23,27 @@ const globalOptions = {
|
||||
|
||||
const runner = createBenchpressRunner();
|
||||
|
||||
export async function runBenchmark({
|
||||
id,
|
||||
url = '',
|
||||
params = [],
|
||||
ignoreBrowserSynchronization = true,
|
||||
microMetrics,
|
||||
work,
|
||||
prepare,
|
||||
setup,
|
||||
}: {
|
||||
export async function runBenchmark(config: {
|
||||
id: string,
|
||||
url: string,
|
||||
params: {name: string, value: any}[],
|
||||
ignoreBrowserSynchronization?: boolean,
|
||||
microMetrics?: {[key: string]: string},
|
||||
work?: (() => void)|(() => Promise<unknown>),
|
||||
prepare?: (() => void)|(() => Promise<unknown>),
|
||||
setup?: (() => void)|(() => Promise<unknown>),
|
||||
work?: () => void,
|
||||
prepare?: () => void,
|
||||
setup?: () => void
|
||||
}): Promise<any> {
|
||||
openBrowser({url, params, ignoreBrowserSynchronization});
|
||||
if (setup) {
|
||||
await setup();
|
||||
openBrowser(config);
|
||||
if (config.setup) {
|
||||
await config.setup();
|
||||
}
|
||||
const description: {[key: string]: any} = {};
|
||||
params.forEach((param) => description[param.name] = param.value);
|
||||
config.params.forEach((param) => description[param.name] = param.value);
|
||||
return runner.sample({
|
||||
id,
|
||||
execute: work,
|
||||
prepare,
|
||||
microMetrics,
|
||||
id: config.id,
|
||||
execute: config.work,
|
||||
prepare: config.prepare,
|
||||
microMetrics: config.microMetrics,
|
||||
providers: [{provide: Options.SAMPLE_DESCRIPTION, useValue: {}}]
|
||||
});
|
||||
}
|
||||
|
@ -2,20 +2,25 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "caretaker",
|
||||
srcs = glob([
|
||||
"**/*.ts",
|
||||
]),
|
||||
srcs = [
|
||||
"cli.ts",
|
||||
],
|
||||
module_name = "@angular/dev-infra-private/caretaker",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/node-fetch",
|
||||
"//dev-infra/caretaker/check",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//multimatch",
|
||||
"@npm//node-fetch",
|
||||
"@npm//typed-graphqlify",
|
||||
"@npm//yaml",
|
||||
"@npm//yargs",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "config",
|
||||
srcs = [
|
||||
"config.ts",
|
||||
],
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
],
|
||||
)
|
||||
|
21
dev-infra/caretaker/check/BUILD.bazel
Normal file
21
dev-infra/caretaker/check/BUILD.bazel
Normal file
@ -0,0 +1,21 @@
|
||||
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "check",
|
||||
srcs = glob(["*.ts"]),
|
||||
module_name = "@angular/dev-infra-private/caretaker/service-statuses",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/caretaker:config",
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/fs-extra",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/node-fetch",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//multimatch",
|
||||
"@npm//node-fetch",
|
||||
"@npm//typed-graphqlify",
|
||||
"@npm//yaml",
|
||||
"@npm//yargs",
|
||||
],
|
||||
)
|
@ -9,7 +9,6 @@
|
||||
import {GitClient} from '../../utils/git';
|
||||
import {getCaretakerConfig} from '../config';
|
||||
|
||||
import {printCiStatus} from './ci';
|
||||
import {printG3Comparison} from './g3';
|
||||
import {printGithubTasks} from './github';
|
||||
import {printServiceStatuses} from './services';
|
||||
@ -22,9 +21,7 @@ export async function checkServiceStatuses(githubToken: string) {
|
||||
/** The GitClient for interacting with git and Github. */
|
||||
const git = new GitClient(githubToken, config);
|
||||
|
||||
// TODO(josephperrott): Allow these checks to be loaded in parallel.
|
||||
await printServiceStatuses();
|
||||
await printGithubTasks(git, config.caretaker);
|
||||
await printG3Comparison(git);
|
||||
await printCiStatus(git);
|
||||
}
|
||||
|
@ -1,59 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import {bold, green, info, red} from '../../utils/console';
|
||||
import {GitClient} from '../../utils/git';
|
||||
|
||||
|
||||
/** The results of checking the status of CI. */
|
||||
interface StatusCheckResult {
|
||||
status: 'success'|'failed'|'canceled'|'infrastructure_fail'|'timedout'|'failed'|'no_tests';
|
||||
timestamp: Date;
|
||||
buildUrl: string;
|
||||
}
|
||||
|
||||
/** Retrieve and log status of CI for the project. */
|
||||
export async function printCiStatus(git: GitClient) {
|
||||
info.group(bold(`CI`));
|
||||
// TODO(josephperrott): Expand list of branches checked to all active branches.
|
||||
await printStatus(git, 'master');
|
||||
info.groupEnd();
|
||||
info();
|
||||
}
|
||||
|
||||
/** Log the status of CI for a given branch to the console. */
|
||||
async function printStatus(git: GitClient, branch: string) {
|
||||
const result = await getStatusOfBranch(git, branch);
|
||||
const branchName = branch.padEnd(10);
|
||||
if (result === null) {
|
||||
info(`${branchName} was not found on CircleCI`);
|
||||
} else if (result.status === 'success') {
|
||||
info(`${branchName} ✅`);
|
||||
} else {
|
||||
info(`${branchName} ❌ (Ran at: ${result.timestamp.toLocaleString()})`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the CI status of a given branch from CircleCI. */
|
||||
async function getStatusOfBranch(git: GitClient, branch: string): Promise<StatusCheckResult|null> {
|
||||
const {owner, name} = git.remoteConfig;
|
||||
const url = `https://circleci.com/api/v1.1/project/gh/${owner}/${name}/tree/${
|
||||
branch}?limit=1&filter=completed&shallow=true`;
|
||||
const result = (await fetch(url).then(result => result.json()))?.[0];
|
||||
|
||||
if (result) {
|
||||
return {
|
||||
status: result.outcome,
|
||||
timestamp: new Date(result.stop_time),
|
||||
buildUrl: result.build_url
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {existsSync, readFileSync} from 'fs';
|
||||
import {existsSync, readFileSync} from 'fs-extra';
|
||||
import * as multimatch from 'multimatch';
|
||||
import {join} from 'path';
|
||||
import {parse as parseYaml} from 'yaml';
|
||||
|
@ -3,10 +3,18 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "commit-message",
|
||||
srcs = glob(
|
||||
["**/*.ts"],
|
||||
exclude = ["**/*.spec.ts"],
|
||||
),
|
||||
srcs = [
|
||||
"builder.ts",
|
||||
"cli.ts",
|
||||
"commit-message-draft.ts",
|
||||
"config.ts",
|
||||
"parse.ts",
|
||||
"restore-commit-message.ts",
|
||||
"validate.ts",
|
||||
"validate-file.ts",
|
||||
"validate-range.ts",
|
||||
"wizard.ts",
|
||||
],
|
||||
module_name = "@angular/dev-infra-private/commit-message",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
@ -24,7 +32,11 @@ ts_library(
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["**/*.spec.ts"]),
|
||||
srcs = [
|
||||
"builder.spec.ts",
|
||||
"parse.spec.ts",
|
||||
"validate.spec.ts",
|
||||
],
|
||||
deps = [
|
||||
":commit-message",
|
||||
"//dev-infra/utils",
|
||||
|
@ -7,19 +7,104 @@
|
||||
*/
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {RestoreCommitMessageModule} from './restore-commit-message/cli';
|
||||
import {ValidateFileModule} from './validate-file/cli';
|
||||
import {ValidateRangeModule} from './validate-range/cli';
|
||||
import {WizardModule} from './wizard/cli';
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {restoreCommitMessage} from './restore-commit-message';
|
||||
import {validateFile} from './validate-file';
|
||||
import {validateCommitRange} from './validate-range';
|
||||
import {runWizard} from './wizard';
|
||||
|
||||
/** Build the parser for the commit-message commands. */
|
||||
export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||
return localYargs.help()
|
||||
.strict()
|
||||
.command(RestoreCommitMessageModule)
|
||||
.command(WizardModule)
|
||||
.command(ValidateFileModule)
|
||||
.command(ValidateRangeModule);
|
||||
.command(
|
||||
'restore-commit-message-draft', false,
|
||||
args => {
|
||||
return args.option('file-env-variable', {
|
||||
type: 'string',
|
||||
array: true,
|
||||
conflicts: ['file'],
|
||||
required: true,
|
||||
description:
|
||||
'The key for the environment variable which holds the arguments for the\n' +
|
||||
'prepare-commit-msg hook as described here:\n' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
|
||||
coerce: arg => {
|
||||
const [file, source] = (process.env[arg] || '').split(' ');
|
||||
if (!file) {
|
||||
throw new Error(`Provided environment variable "${arg}" was not found.`);
|
||||
}
|
||||
return [file, source];
|
||||
},
|
||||
});
|
||||
},
|
||||
args => {
|
||||
restoreCommitMessage(args['file-env-variable'][0], args['file-env-variable'][1] as any);
|
||||
})
|
||||
.command(
|
||||
'wizard <filePath> [source] [commitSha]', '', ((args: any) => {
|
||||
return args
|
||||
.positional(
|
||||
'filePath',
|
||||
{description: 'The file path to write the generated commit message into'})
|
||||
.positional('source', {
|
||||
choices: ['message', 'template', 'merge', 'squash', 'commit'],
|
||||
description: 'The source of the commit message as described here: ' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
|
||||
})
|
||||
.positional(
|
||||
'commitSha', {description: 'The commit sha if source is set to `commit`'});
|
||||
}),
|
||||
async (args: any) => {
|
||||
await runWizard(args);
|
||||
})
|
||||
.command(
|
||||
'pre-commit-validate', 'Validate the most recent commit message', {
|
||||
'file': {
|
||||
type: 'string',
|
||||
conflicts: ['file-env-variable'],
|
||||
description: 'The path of the commit message file.',
|
||||
},
|
||||
'file-env-variable': {
|
||||
type: 'string',
|
||||
conflicts: ['file'],
|
||||
description:
|
||||
'The key of the environment variable for the path of the commit message file.',
|
||||
coerce: arg => {
|
||||
const file = process.env[arg];
|
||||
if (!file) {
|
||||
throw new Error(`Provided environment variable "${arg}" was not found.`);
|
||||
}
|
||||
return file;
|
||||
},
|
||||
}
|
||||
},
|
||||
args => {
|
||||
const file = args.file || args['file-env-variable'] || '.git/COMMIT_EDITMSG';
|
||||
validateFile(file);
|
||||
})
|
||||
.command(
|
||||
'validate-range', 'Validate a range of commit messages', {
|
||||
'range': {
|
||||
description: 'The range of commits to check, e.g. --range abc123..xyz456',
|
||||
demandOption: ' A range must be provided, e.g. --range abc123..xyz456',
|
||||
type: 'string',
|
||||
requiresArg: true,
|
||||
},
|
||||
},
|
||||
argv => {
|
||||
// If on CI, and not pull request number is provided, assume the branch
|
||||
// being run on is an upstream branch.
|
||||
if (process.env['CI'] && process.env['CI_PULL_REQUEST'] === 'false') {
|
||||
info(`Since valid commit messages are enforced by PR linting on CI, we do not`);
|
||||
info(`need to validate commit messages on CI runs on upstream branches.`);
|
||||
info();
|
||||
info(`Skipping check of provided commit range`);
|
||||
return;
|
||||
}
|
||||
validateCommitRange(argv.range);
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main == module) {
|
||||
|
@ -1,13 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* The source triggering the git commit message creation.
|
||||
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||
*/
|
||||
export type CommitMsgSource = 'message'|'template'|'merge'|'squash'|'commit';
|
@ -8,7 +8,6 @@
|
||||
|
||||
import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
|
||||
|
||||
/** Configuration for commit-message comands. */
|
||||
export interface CommitMessageConfig {
|
||||
maxLineLength: number;
|
||||
minBodyLength: number;
|
||||
@ -50,7 +49,7 @@ export const COMMIT_TYPES: {[key: string]: CommitType} = {
|
||||
build: {
|
||||
name: 'build',
|
||||
description: 'Changes to local repository build system and tooling',
|
||||
scope: ScopeRequirement.Optional,
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
ci: {
|
||||
name: 'ci',
|
||||
|
@ -6,12 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {info} from 'console';
|
||||
import {writeFileSync} from 'fs';
|
||||
|
||||
import {debug, log} from '../../utils/console';
|
||||
|
||||
import {loadCommitMessageDraft} from '../commit-message-draft';
|
||||
import {CommitMsgSource} from '../commit-message-source';
|
||||
import {loadCommitMessageDraft} from './commit-message-draft';
|
||||
|
||||
/**
|
||||
* Restore the commit message draft to the git to be used as the default commit message.
|
||||
@ -19,21 +17,22 @@ import {CommitMsgSource} from '../commit-message-source';
|
||||
* The source provided may be one of the sources described in
|
||||
* https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||
*/
|
||||
export function restoreCommitMessage(filePath: string, source?: CommitMsgSource) {
|
||||
export function restoreCommitMessage(
|
||||
filePath: string, source?: 'message'|'template'|'squash'|'commit') {
|
||||
if (!!source) {
|
||||
log('Skipping commit message restoration attempt');
|
||||
info('Skipping commit message restoration attempt');
|
||||
if (source === 'message') {
|
||||
debug('A commit message was already provided via the command with a -m or -F flag');
|
||||
info('A commit message was already provided via the command with a -m or -F flag');
|
||||
}
|
||||
if (source === 'template') {
|
||||
debug('A commit message was already provided via the -t flag or config.template setting');
|
||||
info('A commit message was already provided via the -t flag or config.template setting');
|
||||
}
|
||||
if (source === 'squash') {
|
||||
debug('A commit message was already provided as a merge action or via .git/MERGE_MSG');
|
||||
info('A commit message was already provided as a merge action or via .git/MERGE_MSG');
|
||||
}
|
||||
if (source === 'commit') {
|
||||
debug('A commit message was already provided through a revision specified via --fixup, -c,');
|
||||
debug('-C or --amend flag');
|
||||
info('A commit message was already provided through a revision specified via --fixup, -c,');
|
||||
info('-C or --amend flag');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {CommitMsgSource} from '../commit-message-source';
|
||||
|
||||
import {restoreCommitMessage} from './restore-commit-message';
|
||||
|
||||
export interface RestoreCommitMessageOptions {
|
||||
fileEnvVariable: string[];
|
||||
}
|
||||
|
||||
/** Builds the command. */
|
||||
function builder(yargs: Argv) {
|
||||
return yargs.option('file-env-variable' as 'fileEnvVariable', {
|
||||
type: 'string',
|
||||
array: true,
|
||||
demandOption: true,
|
||||
description: 'The key for the environment variable which holds the arguments for the\n' +
|
||||
'prepare-commit-msg hook as described here:\n' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
|
||||
coerce: arg => {
|
||||
const [file, source] = (process.env[arg] || '').split(' ');
|
||||
if (!file) {
|
||||
throw new Error(`Provided environment variable "${arg}" was not found.`);
|
||||
}
|
||||
return [file, source];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the command. */
|
||||
async function handler({fileEnvVariable}: Arguments<RestoreCommitMessageOptions>) {
|
||||
restoreCommitMessage(fileEnvVariable[0], fileEnvVariable[1] as CommitMsgSource);
|
||||
}
|
||||
|
||||
/** yargs command module describing the command. */
|
||||
export const RestoreCommitMessageModule: CommandModule<{}, RestoreCommitMessageOptions> = {
|
||||
handler,
|
||||
builder,
|
||||
command: 'restore-commit-message-draft',
|
||||
// Description: Restore a commit message draft if one has been saved from a failed commit attempt.
|
||||
// No describe is defiend to hide the command from the --help.
|
||||
describe: false,
|
||||
};
|
30
dev-infra/commit-message/validate-file.ts
Normal file
30
dev-infra/commit-message/validate-file.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {readFileSync} from 'fs';
|
||||
import {resolve} from 'path';
|
||||
|
||||
import {getRepoBaseDir} from '../utils/config';
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {deleteCommitMessageDraft, saveCommitMessageDraft} from './commit-message-draft';
|
||||
import {validateCommitMessage} from './validate';
|
||||
|
||||
/** Validate commit message at the provided file path. */
|
||||
export function validateFile(filePath: string) {
|
||||
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
|
||||
if (validateCommitMessage(commitMessage)) {
|
||||
info('√ Valid commit message');
|
||||
deleteCommitMessageDraft(filePath);
|
||||
return;
|
||||
}
|
||||
// On all invalid commit messages, the commit message should be saved as a draft to be
|
||||
// restored on the next commit attempt.
|
||||
saveCommitMessageDraft(filePath, commitMessage);
|
||||
// If the validation did not return true, exit as a failure.
|
||||
process.exit(1);
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {getUserConfig} from '../../utils/config';
|
||||
|
||||
import {validateFile} from './validate-file';
|
||||
|
||||
|
||||
export interface ValidateFileOptions {
|
||||
file?: string;
|
||||
fileEnvVariable?: string;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
/** Builds the command. */
|
||||
function builder(yargs: Argv) {
|
||||
return yargs
|
||||
.option('file', {
|
||||
type: 'string',
|
||||
conflicts: ['file-env-variable'],
|
||||
description: 'The path of the commit message file.',
|
||||
})
|
||||
.option('file-env-variable' as 'fileEnvVariable', {
|
||||
type: 'string',
|
||||
conflicts: ['file'],
|
||||
description: 'The key of the environment variable for the path of the commit message file.',
|
||||
coerce: (arg: string) => {
|
||||
const file = process.env[arg];
|
||||
if (!file) {
|
||||
throw new Error(`Provided environment variable "${arg}" was not found.`);
|
||||
}
|
||||
return file;
|
||||
},
|
||||
})
|
||||
.option('error', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether invalid commit messages should be treated as failures rather than a warning',
|
||||
default: !!getUserConfig().commitMessage?.errorOnInvalidMessage || !!process.env['CI']
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the command. */
|
||||
async function handler({error, file, fileEnvVariable}: Arguments<ValidateFileOptions>) {
|
||||
const filePath = file || fileEnvVariable || '.git/COMMIT_EDITMSG';
|
||||
validateFile(filePath, error);
|
||||
}
|
||||
|
||||
/** yargs command module describing the command. */
|
||||
export const ValidateFileModule: CommandModule<{}, ValidateFileOptions> = {
|
||||
handler,
|
||||
builder,
|
||||
command: 'pre-commit-validate',
|
||||
describe: 'Validate the most recent commit message',
|
||||
};
|
@ -1,47 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {readFileSync} from 'fs';
|
||||
import {resolve} from 'path';
|
||||
|
||||
import {getRepoBaseDir} from '../../utils/config';
|
||||
import {error, green, info, log, red, yellow} from '../../utils/console';
|
||||
|
||||
import {deleteCommitMessageDraft, saveCommitMessageDraft} from '../commit-message-draft';
|
||||
import {printValidationErrors, validateCommitMessage} from '../validate';
|
||||
|
||||
/** Validate commit message at the provided file path. */
|
||||
export function validateFile(filePath: string, isErrorMode: boolean) {
|
||||
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
|
||||
const {valid, errors} = validateCommitMessage(commitMessage);
|
||||
if (valid) {
|
||||
info(`${green('√')} Valid commit message`);
|
||||
deleteCommitMessageDraft(filePath);
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
/** Function used to print to the console log. */
|
||||
let printFn = isErrorMode ? error : log;
|
||||
|
||||
printFn(`${isErrorMode ? red('✘') : yellow('!')} Invalid commit message`);
|
||||
printValidationErrors(errors, printFn);
|
||||
if (isErrorMode) {
|
||||
printFn(red('Aborting commit attempt due to invalid commit message.'));
|
||||
printFn(
|
||||
red('Commit message aborted as failure rather than warning due to local configuration.'));
|
||||
} else {
|
||||
printFn(yellow('Before this commit can be merged into the upstream repository, it must be'));
|
||||
printFn(yellow('amended to follow commit message guidelines.'));
|
||||
}
|
||||
|
||||
// On all invalid commit messages, the commit message should be saved as a draft to be
|
||||
// restored on the next commit attempt.
|
||||
saveCommitMessageDraft(filePath, commitMessage);
|
||||
// Set the correct exit code based on if invalid commit message is an error.
|
||||
process.exitCode = isErrorMode ? 1 : 0;
|
||||
}
|
@ -5,11 +5,11 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {error, info} from '../../utils/console';
|
||||
import {exec} from '../../utils/shelljs';
|
||||
import {info} from '../utils/console';
|
||||
import {exec} from '../utils/shelljs';
|
||||
|
||||
import {parseCommitMessage} from '../parse';
|
||||
import {printValidationErrors, validateCommitMessage, ValidateCommitMessageOptions} from '../validate';
|
||||
import {parseCommitMessage} from './parse';
|
||||
import {validateCommitMessage, ValidateCommitMessageOptions} from './validate';
|
||||
|
||||
// Whether the provided commit is a fixup commit.
|
||||
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
|
||||
@ -19,20 +19,11 @@ const extractCommitHeader = (m: string) => parseCommitMessage(m).header;
|
||||
|
||||
/** Validate all commits in a provided git commit range. */
|
||||
export function validateCommitRange(range: string) {
|
||||
/**
|
||||
* A random value is used as a string to allow for a definite split point in the git log result.
|
||||
*/
|
||||
// A random value is used as a string to allow for a definite split point in the git log result.
|
||||
const randomValueSeparator = `${Math.random()}`;
|
||||
/**
|
||||
* Custom git log format that provides the commit header and body, separated as expected with the
|
||||
* custom separator as the trailing value.
|
||||
*/
|
||||
// Custom git log format that provides the commit header and body, separated as expected with
|
||||
// the custom separator as the trailing value.
|
||||
const gitLogFormat = `%s%n%n%b${randomValueSeparator}`;
|
||||
/**
|
||||
* A list of tuples containing a commit header string and the list of error messages for the
|
||||
* commit.
|
||||
*/
|
||||
const errors: [commitHeader: string, errors: string[]][] = [];
|
||||
|
||||
// Retrieve the commits in the provided range.
|
||||
const result = exec(`git log --reverse --format=${gitLogFormat} ${range}`);
|
||||
@ -54,22 +45,12 @@ export function validateCommitRange(range: string) {
|
||||
undefined :
|
||||
commits.slice(0, i).filter(isNonFixup).map(extractCommitHeader)
|
||||
};
|
||||
const {valid, errors: localErrors, commit} = validateCommitMessage(m, options);
|
||||
if (localErrors.length) {
|
||||
errors.push([commit.header, localErrors]);
|
||||
}
|
||||
return valid;
|
||||
return validateCommitMessage(m, options);
|
||||
});
|
||||
|
||||
if (allCommitsInRangeValid) {
|
||||
info('√ All commit messages in range valid.');
|
||||
} else {
|
||||
error('✘ Invalid commit message');
|
||||
errors.forEach(([header, validationErrors]) => {
|
||||
error.group(header);
|
||||
printValidationErrors(validationErrors);
|
||||
error.groupEnd();
|
||||
});
|
||||
// Exit with a non-zero exit code if invalid commit messages have
|
||||
// been discovered.
|
||||
process.exit(1);
|
@ -1,50 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {info} from '../../utils/console';
|
||||
|
||||
import {validateCommitRange} from './validate-range';
|
||||
|
||||
|
||||
export interface ValidateRangeOptions {
|
||||
range: string;
|
||||
}
|
||||
|
||||
/** Builds the command. */
|
||||
function builder(yargs: Argv) {
|
||||
return yargs.option('range', {
|
||||
description: 'The range of commits to check, e.g. --range abc123..xyz456',
|
||||
demandOption: ' A range must be provided, e.g. --range abc123..xyz456',
|
||||
type: 'string',
|
||||
requiresArg: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the command. */
|
||||
async function handler({range}: Arguments<ValidateRangeOptions>) {
|
||||
// If on CI, and no pull request number is provided, assume the branch
|
||||
// being run on is an upstream branch.
|
||||
if (process.env['CI'] && process.env['CI_PULL_REQUEST'] === 'false') {
|
||||
info(`Since valid commit messages are enforced by PR linting on CI, we do not`);
|
||||
info(`need to validate commit messages on CI runs on upstream branches.`);
|
||||
info();
|
||||
info(`Skipping check of provided commit range`);
|
||||
return;
|
||||
}
|
||||
validateCommitRange(range);
|
||||
}
|
||||
|
||||
/** yargs command module describing the command. */
|
||||
export const ValidateRangeModule: CommandModule<{}, ValidateRangeOptions> = {
|
||||
handler,
|
||||
builder,
|
||||
command: 'validate-range',
|
||||
describe: 'Validate a range of commit messages',
|
||||
};
|
@ -8,7 +8,7 @@
|
||||
|
||||
// Imports
|
||||
import * as validateConfig from './config';
|
||||
import {validateCommitMessage, ValidateCommitMessageResult} from './validate';
|
||||
import {validateCommitMessage} from './validate';
|
||||
|
||||
type CommitMessageConfig = validateConfig.CommitMessageConfig;
|
||||
|
||||
@ -31,35 +31,44 @@ const SCOPES = config.commitMessage.scopes.join(', ');
|
||||
const INVALID = false;
|
||||
const VALID = true;
|
||||
|
||||
function expectValidationResult(
|
||||
validationResult: ValidateCommitMessageResult, valid: boolean, errors: string[] = []) {
|
||||
expect(validationResult).toEqual(jasmine.objectContaining({valid, errors}));
|
||||
}
|
||||
|
||||
// TODO(josephperrott): Clean up tests to test script rather than for
|
||||
// specific commit messages we want to use.
|
||||
describe('validate-commit-message.js', () => {
|
||||
let lastError: string = '';
|
||||
|
||||
beforeEach(() => {
|
||||
lastError = '';
|
||||
|
||||
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
|
||||
spyOn(validateConfig, 'getCommitMessageConfig')
|
||||
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
|
||||
});
|
||||
|
||||
describe('validateMessage()', () => {
|
||||
it('should be valid', () => {
|
||||
expectValidationResult(validateCommitMessage('feat(packaging): something'), VALID);
|
||||
expectValidationResult(validateCommitMessage('fix(packaging): something'), VALID);
|
||||
expectValidationResult(validateCommitMessage('fixup! fix(packaging): something'), VALID);
|
||||
expectValidationResult(validateCommitMessage('squash! fix(packaging): something'), VALID);
|
||||
expectValidationResult(validateCommitMessage('Revert: "fix(packaging): something"'), VALID);
|
||||
expect(validateCommitMessage('feat(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('fixup! fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('squash! fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('Revert: "fix(packaging): something"')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
});
|
||||
|
||||
it('should validate max length', () => {
|
||||
const msg =
|
||||
'fix(compiler): something super mega extra giga tera long, maybe even longer and longer and longer and longer and longer and longer...';
|
||||
|
||||
expectValidationResult(validateCommitMessage(msg), INVALID, [
|
||||
`The commit message header is longer than ${config.commitMessage.maxLineLength} characters`
|
||||
]);
|
||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||
expect(lastError).toContain(`The commit message header is longer than ${
|
||||
config.commitMessage.maxLineLength} characters`);
|
||||
});
|
||||
|
||||
it('should skip max length limit for URLs', () => {
|
||||
@ -68,56 +77,49 @@ describe('validate-commit-message.js', () => {
|
||||
'limit. For more details see the following super long URL:\n\n' +
|
||||
'https://github.com/angular/components/commit/e2ace018ddfad10608e0e32932c43dcfef4095d7#diff-9879d6db96fd29134fc802214163b95a';
|
||||
|
||||
expectValidationResult(validateCommitMessage(msg), VALID);
|
||||
expect(validateCommitMessage(msg)).toBe(VALID);
|
||||
});
|
||||
|
||||
it('should validate "<type>(<scope>): <subject>" format', () => {
|
||||
const msg = 'not correct format';
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage(msg), INVALID,
|
||||
[`The commit message header does not match the expected format.`]);
|
||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||
expect(lastError).toContain(`The commit message header does not match the expected format.`);
|
||||
});
|
||||
|
||||
it('should fail when type is invalid', () => {
|
||||
const msg = 'weird(core): something';
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage(msg), INVALID,
|
||||
[`'weird' is not an allowed type.\n => TYPES: ${TYPES}`]);
|
||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||
expect(lastError).toContain(`'weird' is not an allowed type.\n => TYPES: ${TYPES}`);
|
||||
});
|
||||
|
||||
it('should fail when scope is invalid', () => {
|
||||
const errorMessageFor = (scope: string, header: string) =>
|
||||
`'${scope}' is not an allowed scope.\n => SCOPES: ${SCOPES}`;
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage('fix(Compiler): something'), INVALID,
|
||||
[errorMessageFor('Compiler', 'fix(Compiler): something')]);
|
||||
expect(validateCommitMessage('fix(Compiler): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('Compiler', 'fix(Compiler): something'));
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage('feat(bah): something'), INVALID,
|
||||
[errorMessageFor('bah', 'feat(bah): something')]);
|
||||
expect(validateCommitMessage('feat(bah): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('bah', 'feat(bah): something'));
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage('fix(webworker): something'), INVALID,
|
||||
[errorMessageFor('webworker', 'fix(webworker): something')]);
|
||||
expect(validateCommitMessage('fix(webworker): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('webworker', 'fix(webworker): something'));
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage('refactor(security): something'), INVALID,
|
||||
[errorMessageFor('security', 'refactor(security): something')]);
|
||||
expect(validateCommitMessage('refactor(security): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('security', 'refactor(security): something'));
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage('refactor(docs): something'), INVALID,
|
||||
[errorMessageFor('docs', 'refactor(docs): something')]);
|
||||
expect(validateCommitMessage('refactor(docs): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('docs', 'refactor(docs): something'));
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage('feat(angular): something'), INVALID,
|
||||
[errorMessageFor('angular', 'feat(angular): something')]);
|
||||
expect(validateCommitMessage('feat(angular): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('angular', 'feat(angular): something'));
|
||||
});
|
||||
|
||||
it('should allow empty scope', () => {
|
||||
expectValidationResult(validateCommitMessage('build: blablabla'), VALID);
|
||||
expect(validateCommitMessage('build: blablabla')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
});
|
||||
|
||||
// We do not want to allow WIP. It is OK to fail the PR build in this case to show that there is
|
||||
@ -125,25 +127,30 @@ describe('validate-commit-message.js', () => {
|
||||
it('should not allow "WIP: ..." syntax', () => {
|
||||
const msg = 'WIP: fix: something';
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage(msg), INVALID,
|
||||
[`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`]);
|
||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||
expect(lastError).toContain(`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`);
|
||||
});
|
||||
|
||||
describe('(revert)', () => {
|
||||
it('should allow valid "revert: ..." syntaxes', () => {
|
||||
expectValidationResult(validateCommitMessage('revert: anything'), VALID);
|
||||
expectValidationResult(validateCommitMessage('Revert: "anything"'), VALID);
|
||||
expectValidationResult(validateCommitMessage('revert anything'), VALID);
|
||||
expectValidationResult(validateCommitMessage('rEvErT anything'), VALID);
|
||||
expect(validateCommitMessage('revert: anything')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('Revert: "anything"')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('revert anything')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('rEvErT anything')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
});
|
||||
|
||||
it('should not allow "revert(scope): ..." syntax', () => {
|
||||
const msg = 'revert(compiler): reduce generated code payload size by 65%';
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage(msg), INVALID,
|
||||
[`'revert' is not an allowed type.\n => TYPES: ${TYPES}`]);
|
||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||
expect(lastError).toContain(`'revert' is not an allowed type.\n => TYPES: ${TYPES}`);
|
||||
});
|
||||
|
||||
// https://github.com/angular/angular/issues/23479
|
||||
@ -151,26 +158,28 @@ describe('validate-commit-message.js', () => {
|
||||
const msg =
|
||||
'Revert "fix(compiler): Pretty print object instead of [Object object] (#22689)" (#23442)';
|
||||
|
||||
expectValidationResult(validateCommitMessage(msg), VALID);
|
||||
expect(validateCommitMessage(msg)).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('(squash)', () => {
|
||||
describe('without `disallowSquash`', () => {
|
||||
it('should return commits as valid', () => {
|
||||
expectValidationResult(validateCommitMessage('squash! feat(core): add feature'), VALID);
|
||||
expectValidationResult(validateCommitMessage('squash! fix: a bug'), VALID);
|
||||
expectValidationResult(validateCommitMessage('squash! fix a typo'), VALID);
|
||||
expect(validateCommitMessage('squash! feat(core): add feature')).toBe(VALID);
|
||||
expect(validateCommitMessage('squash! fix: a bug')).toBe(VALID);
|
||||
expect(validateCommitMessage('squash! fix a typo')).toBe(VALID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with `disallowSquash`', () => {
|
||||
it('should fail', () => {
|
||||
expectValidationResult(
|
||||
validateCommitMessage('fix(core): something', {disallowSquash: true}), VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage('squash! fix(core): something', {disallowSquash: true}),
|
||||
INVALID, ['The commit must be manually squashed into the target commit']);
|
||||
expect(validateCommitMessage('fix(core): something', {disallowSquash: true})).toBe(VALID);
|
||||
expect(validateCommitMessage('squash! fix(core): something', {
|
||||
disallowSquash: true
|
||||
})).toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'The commit must be manually squashed into the target commit');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -178,9 +187,9 @@ describe('validate-commit-message.js', () => {
|
||||
describe('(fixup)', () => {
|
||||
describe('without `nonFixupCommitHeaders`', () => {
|
||||
it('should return commits as valid', () => {
|
||||
expectValidationResult(validateCommitMessage('fixup! feat(core): add feature'), VALID);
|
||||
expectValidationResult(validateCommitMessage('fixup! fix: a bug'), VALID);
|
||||
expectValidationResult(validateCommitMessage('fixup! fixup! fix: a bug'), VALID);
|
||||
expect(validateCommitMessage('fixup! feat(core): add feature')).toBe(VALID);
|
||||
expect(validateCommitMessage('fixup! fix: a bug')).toBe(VALID);
|
||||
expect(validateCommitMessage('fixup! fixup! fix: a bug')).toBe(VALID);
|
||||
});
|
||||
});
|
||||
|
||||
@ -188,39 +197,36 @@ describe('validate-commit-message.js', () => {
|
||||
it('should check that the fixup commit matches a non-fixup one', () => {
|
||||
const msg = 'fixup! foo';
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}),
|
||||
VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}),
|
||||
VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}),
|
||||
VALID);
|
||||
expect(validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}))
|
||||
.toBe(VALID);
|
||||
expect(validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}))
|
||||
.toBe(VALID);
|
||||
expect(validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}))
|
||||
.toBe(VALID);
|
||||
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}),
|
||||
INVALID,
|
||||
['Unable to find match for fixup commit among prior commits: \n' +
|
||||
' qux\n' +
|
||||
' quux\n' +
|
||||
' quuux']);
|
||||
expect(validateCommitMessage(
|
||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}))
|
||||
.toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'Unable to find match for fixup commit among prior commits: \n' +
|
||||
' qux\n' +
|
||||
' quux\n' +
|
||||
' quuux');
|
||||
});
|
||||
|
||||
it('should fail if `nonFixupCommitHeaders` is empty', () => {
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
'refactor(core): make reactive',
|
||||
{disallowSquash: false, nonFixupCommitHeaders: []}),
|
||||
VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}),
|
||||
INVALID, [`Unable to find match for fixup commit among prior commits: -`]);
|
||||
expect(validateCommitMessage('refactor(core): make reactive', {
|
||||
disallowSquash: false,
|
||||
nonFixupCommitHeaders: []
|
||||
})).toBe(VALID);
|
||||
expect(validateCommitMessage(
|
||||
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}))
|
||||
.toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
`Unable to find match for fixup commit among prior commits: -`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -240,27 +246,24 @@ describe('validate-commit-message.js', () => {
|
||||
});
|
||||
|
||||
it('should fail validation if the body is shorter than `minBodyLength`', () => {
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
'fix(core): something\n\n Explanation of the motivation behind this change'),
|
||||
VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage('fix(core): something\n\n too short'), INVALID,
|
||||
['The commit message body does not meet the minimum length of 30 characters']);
|
||||
expectValidationResult(validateCommitMessage('fix(core): something'), INVALID, [
|
||||
|
||||
'The commit message body does not meet the minimum length of 30 characters'
|
||||
]);
|
||||
expect(validateCommitMessage(
|
||||
'fix(core): something\n\n Explanation of the motivation behind this change'))
|
||||
.toBe(VALID);
|
||||
expect(validateCommitMessage('fix(core): something\n\n too short')).toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'The commit message body does not meet the minimum length of 30 characters');
|
||||
expect(validateCommitMessage('fix(core): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'The commit message body does not meet the minimum length of 30 characters');
|
||||
});
|
||||
|
||||
it('should pass validation if the body is shorter than `minBodyLength` but the commit type is in the `minBodyLengthTypeExclusions` list',
|
||||
() => {
|
||||
expectValidationResult(validateCommitMessage('docs: just fixing a typo'), VALID);
|
||||
expectValidationResult(validateCommitMessage('docs(core): just fixing a typo'), VALID);
|
||||
expectValidationResult(
|
||||
validateCommitMessage(
|
||||
'docs(core): just fixing a typo\n\nThis was just a silly typo.'),
|
||||
VALID);
|
||||
expect(validateCommitMessage('docs: just fixing a typo')).toBe(VALID);
|
||||
expect(validateCommitMessage('docs(core): just fixing a typo')).toBe(VALID);
|
||||
expect(validateCommitMessage(
|
||||
'docs(core): just fixing a typo\n\nThis was just a silly typo.'))
|
||||
.toBe(VALID);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -8,7 +8,7 @@
|
||||
import {error} from '../utils/console';
|
||||
|
||||
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
|
||||
import {parseCommitMessage, ParsedCommitMessage} from './parse';
|
||||
import {parseCommitMessage} from './parse';
|
||||
|
||||
/** Options for commit message validation. */
|
||||
export interface ValidateCommitMessageOptions {
|
||||
@ -16,147 +16,133 @@ export interface ValidateCommitMessageOptions {
|
||||
nonFixupCommitHeaders?: string[];
|
||||
}
|
||||
|
||||
/** The result of a commit message validation check. */
|
||||
export interface ValidateCommitMessageResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
commit: ParsedCommitMessage;
|
||||
}
|
||||
|
||||
/** Regex matching a URL for an entire commit body line. */
|
||||
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
|
||||
|
||||
/** Validate a commit message against using the local repo's config. */
|
||||
export function validateCommitMessage(
|
||||
commitMsg: string, options: ValidateCommitMessageOptions = {}): ValidateCommitMessageResult {
|
||||
commitMsg: string, options: ValidateCommitMessageOptions = {}) {
|
||||
function printError(errorMessage: string) {
|
||||
error(
|
||||
`INVALID COMMIT MSG: \n` +
|
||||
`${'─'.repeat(40)}\n` +
|
||||
`${commitMsg}\n` +
|
||||
`${'─'.repeat(40)}\n` +
|
||||
`ERROR: \n` +
|
||||
` ${errorMessage}` +
|
||||
`\n\n` +
|
||||
`The expected format for a commit is: \n` +
|
||||
`<type>(<scope>): <subject>\n\n<body>`);
|
||||
}
|
||||
|
||||
const config = getCommitMessageConfig().commitMessage;
|
||||
const commit = parseCommitMessage(commitMsg);
|
||||
const errors: string[] = [];
|
||||
|
||||
/** Perform the validation checks against the parsed commit. */
|
||||
function validateCommitAndCollectErrors() {
|
||||
// TODO(josephperrott): Remove early return calls when commit message errors are found
|
||||
////////////////////////////////////
|
||||
// Checking revert, squash, fixup //
|
||||
////////////////////////////////////
|
||||
|
||||
////////////////////////////////////
|
||||
// Checking revert, squash, fixup //
|
||||
////////////////////////////////////
|
||||
// All revert commits are considered valid.
|
||||
if (commit.isRevert) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// All revert commits are considered valid.
|
||||
if (commit.isRevert) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// All squashes are considered valid, as the commit will be squashed into another in
|
||||
// the git history anyway, unless the options provided to not allow squash commits.
|
||||
if (commit.isSquash) {
|
||||
if (options.disallowSquash) {
|
||||
errors.push('The commit must be manually squashed into the target commit');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fixups commits are considered valid, unless nonFixupCommitHeaders are provided to check
|
||||
// against. If `nonFixupCommitHeaders` is not empty, we check whether there is a corresponding
|
||||
// non-fixup commit (i.e. a commit whose header is identical to this commit's header after
|
||||
// stripping the `fixup! ` prefix), otherwise we assume this verification will happen in another
|
||||
// check.
|
||||
if (commit.isFixup) {
|
||||
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
|
||||
errors.push(
|
||||
'Unable to find match for fixup commit among prior commits: ' +
|
||||
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
// Checking commit header //
|
||||
////////////////////////////
|
||||
if (commit.header.length > config.maxLineLength) {
|
||||
errors.push(`The commit message header is longer than ${config.maxLineLength} characters`);
|
||||
// All squashes are considered valid, as the commit will be squashed into another in
|
||||
// the git history anyway, unless the options provided to not allow squash commits.
|
||||
if (commit.isSquash) {
|
||||
if (options.disallowSquash) {
|
||||
printError('The commit must be manually squashed into the target commit');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!commit.type) {
|
||||
errors.push(`The commit message header does not match the expected format.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (COMMIT_TYPES[commit.type] === undefined) {
|
||||
errors.push(`'${commit.type}' is not an allowed type.\n => TYPES: ${
|
||||
Object.keys(COMMIT_TYPES).join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The scope requirement level for the provided type of the commit message. */
|
||||
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
|
||||
errors.push(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
|
||||
commit.scope}' was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
|
||||
errors.push(
|
||||
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (commit.scope && !config.scopes.includes(commit.scope)) {
|
||||
errors.push(
|
||||
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Commits with the type of `release` do not require a commit body.
|
||||
if (commit.type === 'release') {
|
||||
return true;
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
// Checking commit body //
|
||||
//////////////////////////
|
||||
|
||||
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
|
||||
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
||||
errors.push(`The commit message body does not meet the minimum length of ${
|
||||
config.minBodyLength} characters`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bodyByLine = commit.body.split('\n');
|
||||
const lineExceedsMaxLength = bodyByLine.some(line => {
|
||||
// Check if any line exceeds the max line length limit. The limit is ignored for
|
||||
// lines that just contain an URL (as these usually cannot be wrapped or shortened).
|
||||
return line.length > config.maxLineLength && !COMMIT_BODY_URL_LINE_RE.test(line);
|
||||
});
|
||||
|
||||
if (lineExceedsMaxLength) {
|
||||
errors.push(
|
||||
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
|
||||
// Fixups commits are considered valid, unless nonFixupCommitHeaders are provided to check
|
||||
// against. If `nonFixupCommitHeaders` is not empty, we check whether there is a corresponding
|
||||
// non-fixup commit (i.e. a commit whose header is identical to this commit's header after
|
||||
// stripping the `fixup! ` prefix), otherwise we assume this verification will happen in another
|
||||
// check.
|
||||
if (commit.isFixup) {
|
||||
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
|
||||
printError(
|
||||
'Unable to find match for fixup commit among prior commits: ' +
|
||||
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return {valid: validateCommitAndCollectErrors(), errors, commit};
|
||||
}
|
||||
////////////////////////////
|
||||
// Checking commit header //
|
||||
////////////////////////////
|
||||
if (commit.header.length > config.maxLineLength) {
|
||||
printError(`The commit message header is longer than ${config.maxLineLength} characters`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!commit.type) {
|
||||
printError(`The commit message header does not match the expected format.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/** Print the error messages from the commit message validation to the console. */
|
||||
export function printValidationErrors(errors: string[], print = error) {
|
||||
print.group(`Error${errors.length === 1 ? '' : 's'}:`);
|
||||
errors.forEach(line => print(line));
|
||||
print.groupEnd();
|
||||
print();
|
||||
print('The expected format for a commit is: ');
|
||||
print('<type>(<scope>): <summary>');
|
||||
print();
|
||||
print('<body>');
|
||||
print();
|
||||
|
||||
if (COMMIT_TYPES[commit.type] === undefined) {
|
||||
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${
|
||||
Object.keys(COMMIT_TYPES).join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The scope requirement level for the provided type of the commit message. */
|
||||
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
|
||||
printError(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
|
||||
commit.scope}' was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
|
||||
printError(
|
||||
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (commit.scope && !config.scopes.includes(commit.scope)) {
|
||||
printError(
|
||||
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Commits with the type of `release` do not require a commit body.
|
||||
if (commit.type === 'release') {
|
||||
return true;
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
// Checking commit body //
|
||||
//////////////////////////
|
||||
|
||||
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
|
||||
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
||||
printError(`The commit message body does not meet the minimum length of ${
|
||||
config.minBodyLength} characters`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bodyByLine = commit.body.split('\n');
|
||||
const lineExceedsMaxLength = bodyByLine.some(line => {
|
||||
// Check if any line exceeds the max line length limit. The limit is ignored for
|
||||
// lines that just contain an URL (as these usually cannot be wrapped or shortened).
|
||||
return line.length > config.maxLineLength && !COMMIT_BODY_URL_LINE_RE.test(line);
|
||||
});
|
||||
|
||||
if (lineExceedsMaxLength) {
|
||||
printError(
|
||||
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -7,12 +7,15 @@
|
||||
*/
|
||||
import {writeFileSync} from 'fs';
|
||||
|
||||
import {getUserConfig} from '../../utils/config';
|
||||
import {debug, info} from '../../utils/console';
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {buildCommitMessage} from '../builder';
|
||||
import {CommitMsgSource} from '../commit-message-source';
|
||||
import {buildCommitMessage} from './builder';
|
||||
|
||||
/**
|
||||
* The source triggering the git commit message creation.
|
||||
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||
*/
|
||||
export type PrepareCommitMsgHookSource = 'message'|'template'|'merge'|'squash'|'commit';
|
||||
|
||||
/** The default commit message used if the wizard does not procude a commit message. */
|
||||
const defaultCommitMessage = `<type>(<scope>): <summary>
|
||||
@ -21,16 +24,11 @@ const defaultCommitMessage = `<type>(<scope>): <summary>
|
||||
# lines at 100 characters.>\n\n`;
|
||||
|
||||
export async function runWizard(
|
||||
args: {filePath: string, source?: CommitMsgSource, commitSha?: string}) {
|
||||
if (getUserConfig().commitMessage?.disableWizard) {
|
||||
debug('Skipping commit message wizard due to enabled `commitMessage.disableWizard` option in');
|
||||
debug('user config.');
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
args: {filePath: string, source?: PrepareCommitMsgHookSource, commitSha?: string}) {
|
||||
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
|
||||
|
||||
if (args.source !== undefined) {
|
||||
info(`Skipping commit message wizard because the commit was created via '${
|
||||
info(`Skipping commit message wizard due because the commit was created via '${
|
||||
args.source}' source`);
|
||||
process.exitCode = 0;
|
||||
return;
|
@ -1,54 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {CommitMsgSource} from '../commit-message-source';
|
||||
|
||||
import {runWizard} from './wizard';
|
||||
|
||||
|
||||
export interface WizardOptions {
|
||||
filePath: string;
|
||||
commitSha: string|undefined;
|
||||
source: CommitMsgSource|undefined;
|
||||
}
|
||||
|
||||
/** Builds the command. */
|
||||
function builder(yargs: Argv) {
|
||||
return yargs
|
||||
.positional('filePath', {
|
||||
description: 'The file path to write the generated commit message into',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
})
|
||||
.positional('source', {
|
||||
choices: ['message', 'template', 'merge', 'squash', 'commit'] as const,
|
||||
description: 'The source of the commit message as described here: ' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
|
||||
})
|
||||
.positional('commitSha', {
|
||||
description: 'The commit sha if source is set to `commit`',
|
||||
type: 'string',
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the command. */
|
||||
async function handler(args: Arguments<WizardOptions>) {
|
||||
await runWizard(args);
|
||||
}
|
||||
|
||||
/** yargs command module describing the command. */
|
||||
export const WizardModule: CommandModule<{}, WizardOptions> = {
|
||||
handler,
|
||||
builder,
|
||||
command: 'wizard <filePath> [source] [commitSha]',
|
||||
// Description: Run the wizard to build a base commit message before opening to complete.
|
||||
// No describe is defiend to hide the command from the --help.
|
||||
describe: false,
|
||||
};
|
@ -3,7 +3,6 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
ts_library(
|
||||
name = "common",
|
||||
srcs = glob(["*.ts"]),
|
||||
module_name = "@angular/dev-infra-private/pr/common",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
|
@ -12,13 +12,11 @@
|
||||
"@angular/benchpress": "0.2.1",
|
||||
"@octokit/graphql": "<from-root>",
|
||||
"@octokit/types": "<from-root>",
|
||||
"@octokit/rest": "<from-root>",
|
||||
"brotli": "<from-root>",
|
||||
"chalk": "<from-root>",
|
||||
"cli-progress": "<from-root>",
|
||||
"glob": "<from-root>",
|
||||
"inquirer": "<from-root>",
|
||||
"inquirer-autocomplete-prompt": "<from-root>",
|
||||
"minimatch": "<from-root>",
|
||||
"multimatch": "<from-root>",
|
||||
"node-fetch": "<from-root>",
|
||||
@ -28,7 +26,9 @@
|
||||
"tslib": "<from-root>",
|
||||
"typed-graphqlify": "<from-root>",
|
||||
"yaml": "<from-root>",
|
||||
"yargs": "<from-root>",
|
||||
"yargs": "<from-root>"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bazel/buildifier": "<from-root>",
|
||||
"clang-format": "<from-root>",
|
||||
"protractor": "<from-root>",
|
||||
|
@ -12,11 +12,13 @@ ts_library(
|
||||
"@npm//@octokit/graphql",
|
||||
"@npm//@octokit/rest",
|
||||
"@npm//@octokit/types",
|
||||
"@npm//@types/fs-extra",
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/shelljs",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//chalk",
|
||||
"@npm//fs-extra",
|
||||
"@npm//inquirer",
|
||||
"@npm//inquirer-autocomplete-prompt",
|
||||
"@npm//shelljs",
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {existsSync} from 'fs';
|
||||
import {dirname, join} from 'path';
|
||||
|
||||
import {debug, error} from './console';
|
||||
import {error} from './console';
|
||||
import {exec} from './shelljs';
|
||||
import {isTsNodeAvailable} from './ts-node';
|
||||
|
||||
@ -49,16 +49,7 @@ export type NgDevConfig<T = {}> = CommonConfig&T;
|
||||
const CONFIG_FILE_PATH = '.ng-dev/config';
|
||||
|
||||
/** The configuration for ng-dev. */
|
||||
let cachedConfig: NgDevConfig|null = null;
|
||||
|
||||
/**
|
||||
* The filename expected for local user config, without the file extension to allow a typescript,
|
||||
* javascript or json file to be used.
|
||||
*/
|
||||
const USER_CONFIG_FILE_PATH = '.ng-dev.user';
|
||||
|
||||
/** The local user configuration for ng-dev. */
|
||||
let userConfig: {[key: string]: any}|null = null;
|
||||
let CONFIG: {}|null = null;
|
||||
|
||||
/**
|
||||
* Get the configuration from the file system, returning the already loaded
|
||||
@ -66,15 +57,15 @@ let userConfig: {[key: string]: any}|null = null;
|
||||
*/
|
||||
export function getConfig(): NgDevConfig {
|
||||
// If the global config is not defined, load it from the file system.
|
||||
if (cachedConfig === null) {
|
||||
if (CONFIG === null) {
|
||||
// The full path to the configuration file.
|
||||
const configPath = join(getRepoBaseDir(), CONFIG_FILE_PATH);
|
||||
// Read the configuration and validate it before caching it for the future.
|
||||
cachedConfig = validateCommonConfig(readConfigFile(configPath));
|
||||
// Set the global config object.
|
||||
CONFIG = readConfigFile(configPath);
|
||||
}
|
||||
// Return a clone of the cached global config to ensure that a new instance of the config
|
||||
// is returned each time, preventing unexpected effects of modifications to the config object.
|
||||
return {...cachedConfig};
|
||||
// Return a clone of the global config to ensure that a new instance of the config is returned
|
||||
// each time, preventing unexpected effects of modifications to the config object.
|
||||
return validateCommonConfig({...CONFIG});
|
||||
}
|
||||
|
||||
/** Validate the common configuration has been met for the ng-dev command. */
|
||||
@ -95,11 +86,8 @@ function validateCommonConfig(config: Partial<NgDevConfig>) {
|
||||
return config as NgDevConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves and reads the specified configuration file, optionally returning an empty object if the
|
||||
* configuration file cannot be read.
|
||||
*/
|
||||
function readConfigFile(configPath: string, returnEmptyObjectOnError = false): object {
|
||||
/** Resolves and reads the specified configuration file. */
|
||||
function readConfigFile(configPath: string): object {
|
||||
// If the the `.ts` extension has not been set up already, and a TypeScript based
|
||||
// version of the given configuration seems to exist, set up `ts-node` if available.
|
||||
if (require.extensions['.ts'] === undefined && existsSync(`${configPath}.ts`) &&
|
||||
@ -115,12 +103,7 @@ function readConfigFile(configPath: string, returnEmptyObjectOnError = false): o
|
||||
try {
|
||||
return require(configPath);
|
||||
} catch (e) {
|
||||
if (returnEmptyObjectOnError) {
|
||||
debug(`Could not read configuration file at ${configPath}, returning empty object instead.`);
|
||||
debug(e);
|
||||
return {};
|
||||
}
|
||||
error(`Could not read configuration file at ${configPath}.`);
|
||||
error('Could not read configuration file.');
|
||||
error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
@ -152,23 +135,3 @@ export function getRepoBaseDir() {
|
||||
}
|
||||
return baseRepoDir.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the local user configuration from the file system, returning the already loaded copy if it is
|
||||
* defined.
|
||||
*
|
||||
* @returns The user configuration object, or an empty object if no user configuration file is
|
||||
* present. The object is an untyped object as there are no required user configurations.
|
||||
*/
|
||||
export function getUserConfig() {
|
||||
// If the global config is not defined, load it from the file system.
|
||||
if (userConfig === null) {
|
||||
// The full path to the configuration file.
|
||||
const configPath = join(getRepoBaseDir(), USER_CONFIG_FILE_PATH);
|
||||
// Set the global config object.
|
||||
userConfig = readConfigFile(configPath, true);
|
||||
}
|
||||
// Return a clone of the user config to ensure that a new instance of the config is returned
|
||||
// each time, preventing unexpected effects of modifications to the config object.
|
||||
return {...userConfig};
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {writeFileSync} from 'fs';
|
||||
import {writeFileSync} from 'fs-extra';
|
||||
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
|
||||
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
||||
import {join} from 'path';
|
||||
@ -196,9 +196,6 @@ export function captureLogOutputForCommand(argv: Arguments) {
|
||||
/** Path to the log file location. */
|
||||
const logFilePath = join(getRepoBaseDir(), '.ng-dev.log');
|
||||
|
||||
// Strip ANSI escape codes from log outputs.
|
||||
LOGGED_TEXT = LOGGED_TEXT.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]/g, '');
|
||||
|
||||
writeFileSync(logFilePath, LOGGED_TEXT);
|
||||
|
||||
// For failure codes greater than 1, the new logged lines should be written to a specific log
|
||||
|
@ -34,7 +34,7 @@ export class GitCommandError extends Error {
|
||||
/**
|
||||
* Common client for performing Git interactions.
|
||||
*
|
||||
* Takes in two optional arguments:
|
||||
* Takes in two optional arguements:
|
||||
* _githubToken: the token used for authentifation in github interactions, by default empty
|
||||
* allowing readonly actions.
|
||||
* _config: The dev-infra configuration containing GitClientConfig information, by default
|
||||
|
@ -154,9 +154,7 @@ 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.
|
||||
(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`)
|
||||
`publish_packages_as_artifacts` job and then switching to the "ARTIFACTS" tab.
|
||||
|
||||
#### Archives for each Package
|
||||
On the "Artifacts" tab, there is a list of links to compressed archives for Angular packages. The
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Triage Process and GitHub Labels for Angular
|
||||
|
||||
This document describes how the Angular team uses labels and milestones to triage issues on GitHub.
|
||||
This document describes how the Angular team uses labels and milestones to triage issues on github.
|
||||
The basic idea of the process is that caretaker only assigns a component (`comp: *`) label.
|
||||
The owner of the component is then responsible for the secondary / component-level triage.
|
||||
|
||||
@ -150,7 +150,7 @@ In addition, PRs can have the following states:
|
||||
* _**Who adds it:** Any team member._
|
||||
* _**Who removes it:** Any team member._
|
||||
|
||||
When a PR is ready for review, a review should be requested using the Reviewers interface in GitHub.
|
||||
When a PR is ready for review, a review should be requested using the Reviewers interface in Github.
|
||||
|
||||
|
||||
## PR Target
|
||||
@ -160,7 +160,7 @@ In our git workflow, we merge changes either to the `master` branch, the active
|
||||
The decision about the target must be done by the PR author and/or reviewer.
|
||||
This decision is then honored when the PR is being merged by the caretaker.
|
||||
|
||||
To communicate the target we use GitHub labels and only one target label may be applied to a PR.
|
||||
To communicate the target we use the following labels:
|
||||
|
||||
Targeting an active release train:
|
||||
|
||||
@ -176,9 +176,9 @@ Special Cases:
|
||||
|
||||
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
|
||||
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
|
||||
- `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
|
||||
|
||||
|
||||
|
@ -19,7 +19,6 @@ export declare enum ErrorCode {
|
||||
CONFIG_FLAT_MODULE_NO_INDEX = 4001,
|
||||
CONFIG_STRICT_TEMPLATES_IMPLIES_FULL_TEMPLATE_TYPECHECK = 4002,
|
||||
HOST_BINDING_PARSE_ERROR = 5001,
|
||||
TEMPLATE_PARSE_ERROR = 5002,
|
||||
NGMODULE_INVALID_DECLARATION = 6001,
|
||||
NGMODULE_INVALID_IMPORT = 6002,
|
||||
NGMODULE_INVALID_EXPORT = 6003,
|
||||
|
2
goldens/public-api/router/router.d.ts
vendored
2
goldens/public-api/router/router.d.ts
vendored
@ -432,7 +432,7 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
|
||||
constructor(router: Router, route: ActivatedRoute, locationStrategy: LocationStrategy);
|
||||
ngOnChanges(changes: SimpleChanges): any;
|
||||
ngOnDestroy(): any;
|
||||
onClick(button: number, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean): boolean;
|
||||
onClick(button: number, ctrlKey: boolean, metaKey: boolean, shiftKey: boolean): boolean;
|
||||
}
|
||||
|
||||
export declare class RouterModule {
|
||||
|
@ -29,11 +29,17 @@ 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;
|
||||
|
@ -21,7 +21,7 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 1485,
|
||||
"main-es2015": 146989,
|
||||
"main-es2015": 147573,
|
||||
"polyfills-es2015": 36571
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "10.1.2",
|
||||
"version": "11.0.0-next.0",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@ -35,8 +35,6 @@
|
||||
"tslint": "tsc -p tools/tsconfig.json && tslint -c tslint.json \"+(dev-infra|packages|modules|scripts|tools)/**/*.+(js|ts)\"",
|
||||
"public-api:check": "node goldens/public-api/manage.js test",
|
||||
"public-api:update": "node goldens/public-api/manage.js accept",
|
||||
"symbol-extractor:check": "node tools/symbol-extractor/run_all_symbols_extractor_tests.js test",
|
||||
"symbol-extractor:update": "node tools/symbol-extractor/run_all_symbols_extractor_tests.js accept",
|
||||
"ts-circular-deps": "ts-node --transpile-only -- dev-infra/ts-circular-dependencies/index.ts --config ./packages/circular-deps-test.conf.js",
|
||||
"ts-circular-deps:check": "yarn -s ts-circular-deps check",
|
||||
"ts-circular-deps:approve": "yarn -s ts-circular-deps approve",
|
||||
|
@ -14,7 +14,6 @@ import {Logger} from '../../../src/ngtsc/logging';
|
||||
import {ParsedConfiguration} from '../../../src/perform_compile';
|
||||
import {getEntryPointFormat} from '../packages/entry_point';
|
||||
import {makeEntryPointBundle} from '../packages/entry_point_bundle';
|
||||
import {createModuleResolutionCache, SharedFileCache} from '../packages/source_file_cache';
|
||||
import {PathMappings} from '../path_mappings';
|
||||
import {FileWriter} from '../writing/file_writer';
|
||||
|
||||
@ -31,8 +30,6 @@ export function getCreateCompileFn(
|
||||
return (beforeWritingFiles, onTaskCompleted) => {
|
||||
const {Transformer} = require('../packages/transformer');
|
||||
const transformer = new Transformer(fileSystem, logger, tsConfig);
|
||||
const sharedFileCache = new SharedFileCache(fileSystem);
|
||||
const moduleResolutionCache = createModuleResolutionCache(fileSystem);
|
||||
|
||||
return (task: Task) => {
|
||||
const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task;
|
||||
@ -57,8 +54,8 @@ export function getCreateCompileFn(
|
||||
logger.info(`Compiling ${entryPoint.name} : ${formatProperty} as ${format}`);
|
||||
|
||||
const bundle = makeEntryPointBundle(
|
||||
fileSystem, entryPoint, sharedFileCache, moduleResolutionCache, formatPath, isCore,
|
||||
format, processDts, pathMappings, true, enableI18nLegacyMessageIdFormat);
|
||||
fileSystem, entryPoint, formatPath, isCore, format, processDts, pathMappings, true,
|
||||
enableI18nLegacyMessageIdFormat);
|
||||
|
||||
const result = transformer.transform(bundle);
|
||||
if (result.success) {
|
||||
|
@ -7,8 +7,8 @@
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
|
||||
|
||||
import {Logger} from '../../../src/ngtsc/logging';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference, TypeValueReferenceKind, ValueUnavailableKind} from '../../../src/ngtsc/reflection';
|
||||
import {isWithinPackage} from '../analysis/util';
|
||||
@ -1593,7 +1593,35 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
constructorParamInfo[index] :
|
||||
{decorators: null, typeExpression: null};
|
||||
const nameNode = node.name;
|
||||
const typeValueReference = this.typeToValue(typeExpression);
|
||||
|
||||
let typeValueReference: TypeValueReference;
|
||||
if (typeExpression !== null) {
|
||||
// `typeExpression` is an expression in a "type" context. Resolve it to a declared value.
|
||||
// Either it's a reference to an imported type, or a type declared locally. Distinguish the
|
||||
// two cases with `getDeclarationOfExpression`.
|
||||
const decl = this.getDeclarationOfExpression(typeExpression);
|
||||
if (decl !== null && decl.node !== null && decl.viaModule !== null &&
|
||||
isNamedDeclaration(decl.node)) {
|
||||
typeValueReference = {
|
||||
kind: TypeValueReferenceKind.IMPORTED,
|
||||
valueDeclaration: decl.node,
|
||||
moduleName: decl.viaModule,
|
||||
importedName: decl.node.name.text,
|
||||
nestedPath: null,
|
||||
};
|
||||
} else {
|
||||
typeValueReference = {
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: typeExpression,
|
||||
defaultImportStatement: null,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
typeValueReference = {
|
||||
kind: TypeValueReferenceKind.UNAVAILABLE,
|
||||
reason: {kind: ValueUnavailableKind.MISSING_TYPE},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: getNameText(nameNode),
|
||||
@ -1605,29 +1633,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the `TypeValueReference` for the given `typeExpression`.
|
||||
*
|
||||
* In ngcc, all the `typeExpression` are guaranteed to be "values" because it is working in JS and
|
||||
* not TS. This means that the TS compiler is not going to remove the "type" import and so we can
|
||||
* always use a LOCAL `TypeValueReference` kind, rather than trying to force an additional import
|
||||
* for non-local expressions.
|
||||
*/
|
||||
private typeToValue(typeExpression: ts.Expression|null): TypeValueReference {
|
||||
if (typeExpression === null) {
|
||||
return {
|
||||
kind: TypeValueReferenceKind.UNAVAILABLE,
|
||||
reason: {kind: ValueUnavailableKind.MISSING_TYPE},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: typeExpression,
|
||||
defaultImportStatement: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameter type and decorators for the constructor of a class,
|
||||
* where the information is stored on a static property of the class.
|
||||
|
@ -8,6 +8,8 @@
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as os from 'os';
|
||||
|
||||
import {AbsoluteFsPath, FileSystem, resolve} from '../../src/ngtsc/file_system';
|
||||
import {Logger} from '../../src/ngtsc/logging';
|
||||
import {ParsedConfiguration} from '../../src/perform_compile';
|
||||
@ -33,7 +35,7 @@ import {composeTaskCompletedCallbacks, createLogErrorHandler, createMarkAsProces
|
||||
import {AsyncLocker} from './locking/async_locker';
|
||||
import {LockFileWithChildProcess} from './locking/lock_file_with_child_process';
|
||||
import {SyncLocker} from './locking/sync_locker';
|
||||
import {AsyncNgccOptions, getMaxNumberOfWorkers, getSharedSetup, SyncNgccOptions} from './ngcc_options';
|
||||
import {AsyncNgccOptions, getSharedSetup, SyncNgccOptions} from './ngcc_options';
|
||||
import {NgccConfiguration} from './packages/configuration';
|
||||
import {EntryPointJsonProperty, SUPPORTED_FORMAT_PROPERTIES} from './packages/entry_point';
|
||||
import {EntryPointManifest, InvalidatingEntryPointManifest} from './packages/entry_point_manifest';
|
||||
@ -90,9 +92,10 @@ export function mainNgcc(options: AsyncNgccOptions|SyncNgccOptions): void|Promis
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the number of workers to use and whether ngcc should run in parallel.
|
||||
const workerCount = async ? getMaxNumberOfWorkers() : 1;
|
||||
const inParallel = workerCount > 1;
|
||||
// Execute in parallel, if async execution is acceptable and there are more than 2 CPU cores.
|
||||
// (One CPU core is always reserved for the master process and we need at least 2 worker processes
|
||||
// in order to run tasks in parallel.)
|
||||
const inParallel = async && (os.cpus().length > 2);
|
||||
|
||||
const analyzeEntryPoints = getAnalyzeEntryPointsFn(
|
||||
logger, finder, fileSystem, supportedPropertiesToConsider, compileAllFormats,
|
||||
@ -110,7 +113,7 @@ export function mainNgcc(options: AsyncNgccOptions|SyncNgccOptions): void|Promis
|
||||
const createTaskCompletedCallback =
|
||||
getCreateTaskCompletedCallback(pkgJsonUpdater, errorOnFailedEntryPoint, logger, fileSystem);
|
||||
const executor = getExecutor(
|
||||
async, workerCount, logger, fileWriter, pkgJsonUpdater, fileSystem, config,
|
||||
async, inParallel, logger, fileWriter, pkgJsonUpdater, fileSystem, config,
|
||||
createTaskCompletedCallback);
|
||||
|
||||
return executor.execute(analyzeEntryPoints, createCompileFn);
|
||||
@ -150,7 +153,7 @@ function getCreateTaskCompletedCallback(
|
||||
}
|
||||
|
||||
function getExecutor(
|
||||
async: boolean, workerCount: number, logger: Logger, fileWriter: FileWriter,
|
||||
async: boolean, inParallel: boolean, logger: Logger, fileWriter: FileWriter,
|
||||
pkgJsonUpdater: PackageJsonUpdater, fileSystem: FileSystem, config: NgccConfiguration,
|
||||
createTaskCompletedCallback: CreateTaskCompletedCallback): Executor {
|
||||
const lockFile = new LockFileWithChildProcess(fileSystem, logger);
|
||||
@ -158,8 +161,9 @@ function getExecutor(
|
||||
// Execute asynchronously (either serially or in parallel)
|
||||
const {retryAttempts, retryDelay} = config.getLockingConfig();
|
||||
const locker = new AsyncLocker(lockFile, logger, retryDelay, retryAttempts);
|
||||
if (workerCount > 1) {
|
||||
// Execute in parallel.
|
||||
if (inParallel) {
|
||||
// Execute in parallel. Use up to 8 CPU cores for workers, always reserving one for master.
|
||||
const workerCount = Math.min(8, os.cpus().length - 1);
|
||||
return new ClusterExecutor(
|
||||
workerCount, fileSystem, logger, fileWriter, pkgJsonUpdater, locker,
|
||||
createTaskCompletedCallback);
|
||||
|
@ -5,8 +5,6 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as os from 'os';
|
||||
|
||||
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '../../src/ngtsc/file_system';
|
||||
import {ConsoleLogger, Logger, LogLevel} from '../../src/ngtsc/logging';
|
||||
import {ParsedConfiguration, readConfiguration} from '../../src/perform_compile';
|
||||
@ -256,26 +254,3 @@ function checkForSolutionStyleTsConfig(
|
||||
` ngcc ... --tsconfig "${fileSystem.relative(projectPath, tsConfig.project)}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the maximum number of workers to use for parallel execution. This can be set using the
|
||||
* NGCC_MAX_WORKERS environment variable, or is computed based on the number of available CPUs. One
|
||||
* CPU core is always reserved for the master process, so we take the number of CPUs minus one, with
|
||||
* a maximum of 4 workers. We don't scale the number of workers beyond 4 by default, as it takes
|
||||
* considerably more memory and CPU cycles while not offering a substantial improvement in time.
|
||||
*/
|
||||
export function getMaxNumberOfWorkers(): number {
|
||||
const maxWorkers = process.env.NGCC_MAX_WORKERS;
|
||||
if (maxWorkers === undefined) {
|
||||
// Use up to 4 CPU cores for workers, always reserving one for master.
|
||||
return Math.max(1, Math.min(4, os.cpus().length - 1));
|
||||
}
|
||||
|
||||
const numericMaxWorkers = +maxWorkers.trim();
|
||||
if (!Number.isInteger(numericMaxWorkers)) {
|
||||
throw new Error('NGCC_MAX_WORKERS should be an integer.');
|
||||
} else if (numericMaxWorkers < 1) {
|
||||
throw new Error('NGCC_MAX_WORKERS should be at least 1.');
|
||||
}
|
||||
return numericMaxWorkers;
|
||||
}
|
||||
|
@ -6,12 +6,11 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
|
||||
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
|
||||
import {PathMappings} from '../path_mappings';
|
||||
import {BundleProgram, makeBundleProgram} from './bundle_program';
|
||||
import {EntryPoint, EntryPointFormat} from './entry_point';
|
||||
import {NgccDtsCompilerHost, NgccSourcesCompilerHost} from './ngcc_compiler_host';
|
||||
import {EntryPointFileCache, SharedFileCache} from './source_file_cache';
|
||||
import {NgccSourcesCompilerHost} from './ngcc_compiler_host';
|
||||
|
||||
/**
|
||||
* A bundle of files and paths (and TS programs) that correspond to a particular
|
||||
@ -32,8 +31,6 @@ export interface EntryPointBundle {
|
||||
* Get an object that describes a formatted bundle for an entry-point.
|
||||
* @param fs The current file-system being used.
|
||||
* @param entryPoint The entry-point that contains the bundle.
|
||||
* @param sharedFileCache The cache to use for source files that are shared across all entry-points.
|
||||
* @param moduleResolutionCache The module resolution cache to use.
|
||||
* @param formatPath The path to the source files for this bundle.
|
||||
* @param isCore This entry point is the Angular core package.
|
||||
* @param format The underlying format of the bundle.
|
||||
@ -45,8 +42,7 @@ export interface EntryPointBundle {
|
||||
* component templates.
|
||||
*/
|
||||
export function makeEntryPointBundle(
|
||||
fs: FileSystem, entryPoint: EntryPoint, sharedFileCache: SharedFileCache,
|
||||
moduleResolutionCache: ts.ModuleResolutionCache, formatPath: string, isCore: boolean,
|
||||
fs: FileSystem, entryPoint: EntryPoint, formatPath: string, isCore: boolean,
|
||||
format: EntryPointFormat, transformDts: boolean, pathMappings?: PathMappings,
|
||||
mirrorDtsFromSrc: boolean = false,
|
||||
enableI18nLegacyMessageIdFormat: boolean = true): EntryPointBundle {
|
||||
@ -54,10 +50,8 @@ export function makeEntryPointBundle(
|
||||
const rootDir = entryPoint.packagePath;
|
||||
const options: ts
|
||||
.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings};
|
||||
const entryPointCache = new EntryPointFileCache(fs, sharedFileCache);
|
||||
const dtsHost = new NgccDtsCompilerHost(fs, options, entryPointCache, moduleResolutionCache);
|
||||
const srcHost = new NgccSourcesCompilerHost(
|
||||
fs, options, entryPointCache, moduleResolutionCache, entryPoint.packagePath);
|
||||
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.packagePath);
|
||||
const dtsHost = new NgtscCompilerHost(fs, options);
|
||||
|
||||
// Create the bundle programs, as necessary.
|
||||
const absFormatPath = fs.resolve(entryPoint.path, formatPath);
|
||||
|
@ -10,7 +10,6 @@ import * as ts from 'typescript';
|
||||
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
|
||||
import {isWithinPackage} from '../analysis/util';
|
||||
import {isRelativePath} from '../utils';
|
||||
import {EntryPointFileCache} from './source_file_cache';
|
||||
|
||||
/**
|
||||
* Represents a compiler host that resolves a module import as a JavaScript source file if
|
||||
@ -19,15 +18,11 @@ import {EntryPointFileCache} from './source_file_cache';
|
||||
* would otherwise let TypeScript prefer the .d.ts file instead of the JavaScript source file.
|
||||
*/
|
||||
export class NgccSourcesCompilerHost extends NgtscCompilerHost {
|
||||
constructor(
|
||||
fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointFileCache,
|
||||
private moduleResolutionCache: ts.ModuleResolutionCache,
|
||||
protected packagePath: AbsoluteFsPath) {
|
||||
super(fs, options);
|
||||
}
|
||||
private cache = ts.createModuleResolutionCache(
|
||||
this.getCurrentDirectory(), file => this.getCanonicalFileName(file));
|
||||
|
||||
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
|
||||
return this.cache.getCachedSourceFile(fileName, languageVersion);
|
||||
constructor(fs: FileSystem, options: ts.CompilerOptions, protected packagePath: AbsoluteFsPath) {
|
||||
super(fs, options);
|
||||
}
|
||||
|
||||
resolveModuleNames(
|
||||
@ -35,8 +30,7 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
|
||||
redirectedReference?: ts.ResolvedProjectReference): Array<ts.ResolvedModule|undefined> {
|
||||
return moduleNames.map(moduleName => {
|
||||
const {resolvedModule} = ts.resolveModuleName(
|
||||
moduleName, containingFile, this.options, this, this.moduleResolutionCache,
|
||||
redirectedReference);
|
||||
moduleName, containingFile, this.options, this, this.cache, redirectedReference);
|
||||
|
||||
// If the module request originated from a relative import in a JavaScript source file,
|
||||
// TypeScript may have resolved the module to its .d.ts declaration file if the .js source
|
||||
@ -65,31 +59,3 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A compiler host implementation that is used for the typings program. It leverages the entry-point
|
||||
* cache for source files and module resolution, as these results can be reused across the sources
|
||||
* program.
|
||||
*/
|
||||
export class NgccDtsCompilerHost extends NgtscCompilerHost {
|
||||
constructor(
|
||||
fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointFileCache,
|
||||
private moduleResolutionCache: ts.ModuleResolutionCache) {
|
||||
super(fs, options);
|
||||
}
|
||||
|
||||
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
|
||||
return this.cache.getCachedSourceFile(fileName, languageVersion);
|
||||
}
|
||||
|
||||
resolveModuleNames(
|
||||
moduleNames: string[], containingFile: string, reusedNames?: string[],
|
||||
redirectedReference?: ts.ResolvedProjectReference): Array<ts.ResolvedModule|undefined> {
|
||||
return moduleNames.map(moduleName => {
|
||||
const {resolvedModule} = ts.resolveModuleName(
|
||||
moduleName, containingFile, this.options, this, this.moduleResolutionCache,
|
||||
redirectedReference);
|
||||
return resolvedModule;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,197 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
|
||||
|
||||
/**
|
||||
* A cache that holds on to source files that can be shared for processing all entry-points in a
|
||||
* single invocation of ngcc. In particular, the following files are shared across all entry-points
|
||||
* through this cache:
|
||||
*
|
||||
* 1. Default library files such as `lib.dom.d.ts` and `lib.es5.d.ts`. These files don't change
|
||||
* and some are very large, so parsing is expensive. Therefore, the parsed `ts.SourceFile`s for
|
||||
* the default library files are cached.
|
||||
* 2. The typings of @angular scoped packages. The typing files for @angular packages are typically
|
||||
* used in the entry-points that ngcc processes, so benefit from a single source file cache.
|
||||
* Especially `@angular/core/core.d.ts` is large and expensive to parse repeatedly. In contrast
|
||||
* to default library files, we have to account for these files to be invalidated during a single
|
||||
* invocation of ngcc, as ngcc will overwrite the .d.ts files during its processing.
|
||||
*
|
||||
* The lifecycle of this cache corresponds with a single invocation of ngcc. Separate invocations,
|
||||
* e.g. the CLI's synchronous module resolution fallback will therefore all have their own cache.
|
||||
* This allows for the source file cache to be garbage collected once ngcc processing has completed.
|
||||
*/
|
||||
export class SharedFileCache {
|
||||
private sfCache = new Map<AbsoluteFsPath, ts.SourceFile>();
|
||||
|
||||
constructor(private fs: FileSystem) {}
|
||||
|
||||
/**
|
||||
* Loads a `ts.SourceFile` if the provided `fileName` is deemed appropriate to be cached. To
|
||||
* optimize for memory usage, only files that are generally used in all entry-points are cached.
|
||||
* If `fileName` is not considered to benefit from caching or the requested file does not exist,
|
||||
* then `undefined` is returned.
|
||||
*/
|
||||
getCachedSourceFile(fileName: string): ts.SourceFile|undefined {
|
||||
const absPath = this.fs.resolve(fileName);
|
||||
if (isDefaultLibrary(absPath, this.fs)) {
|
||||
return this.getStableCachedFile(absPath);
|
||||
} else if (isAngularDts(absPath, this.fs)) {
|
||||
return this.getVolatileCachedFile(absPath);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to load the source file from the cache, or parses the file into a `ts.SourceFile` if
|
||||
* it's not yet cached. This method assumes that the file will not be modified for the duration
|
||||
* that this cache is valid for. If that assumption does not hold, the `getVolatileCachedFile`
|
||||
* method is to be used instead.
|
||||
*/
|
||||
private getStableCachedFile(absPath: AbsoluteFsPath): ts.SourceFile|undefined {
|
||||
if (!this.sfCache.has(absPath)) {
|
||||
const content = readFile(absPath, this.fs);
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const sf = ts.createSourceFile(absPath, content, ts.ScriptTarget.ES2015);
|
||||
this.sfCache.set(absPath, sf);
|
||||
}
|
||||
return this.sfCache.get(absPath)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* In contrast to `getStableCachedFile`, this method always verifies that the cached source file
|
||||
* is the same as what's stored on disk. This is done for files that are expected to change during
|
||||
* ngcc's processing, such as @angular scoped packages for which the .d.ts files are overwritten
|
||||
* by ngcc. If the contents on disk have changed compared to a previously cached source file, the
|
||||
* content from disk is re-parsed and the cache entry is replaced.
|
||||
*/
|
||||
private getVolatileCachedFile(absPath: AbsoluteFsPath): ts.SourceFile|undefined {
|
||||
const content = readFile(absPath, this.fs);
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (!this.sfCache.has(absPath) || this.sfCache.get(absPath)!.text !== content) {
|
||||
const sf = ts.createSourceFile(absPath, content, ts.ScriptTarget.ES2015);
|
||||
this.sfCache.set(absPath, sf);
|
||||
}
|
||||
return this.sfCache.get(absPath)!;
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_LIB_PATTERN = ['node_modules', 'typescript', 'lib', /^lib\..+\.d\.ts$/];
|
||||
|
||||
/**
|
||||
* Determines whether the provided path corresponds with a default library file inside of the
|
||||
* typescript package.
|
||||
*
|
||||
* @param absPath The path for which to determine if it corresponds with a default library file.
|
||||
* @param fs The filesystem to use for inspecting the path.
|
||||
*/
|
||||
export function isDefaultLibrary(absPath: AbsoluteFsPath, fs: FileSystem): boolean {
|
||||
return isFile(absPath, DEFAULT_LIB_PATTERN, fs);
|
||||
}
|
||||
|
||||
const ANGULAR_DTS_PATTERN = ['node_modules', '@angular', /./, /\.d\.ts$/];
|
||||
|
||||
/**
|
||||
* Determines whether the provided path corresponds with a .d.ts file inside of an @angular
|
||||
* scoped package. This logic only accounts for the .d.ts files in the root, which is sufficient
|
||||
* to find the large, flattened entry-point files that benefit from caching.
|
||||
*
|
||||
* @param absPath The path for which to determine if it corresponds with an @angular .d.ts file.
|
||||
* @param fs The filesystem to use for inspecting the path.
|
||||
*/
|
||||
export function isAngularDts(absPath: AbsoluteFsPath, fs: FileSystem): boolean {
|
||||
return isFile(absPath, ANGULAR_DTS_PATTERN, fs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine whether a file corresponds with a given pattern of segments.
|
||||
*
|
||||
* @param path The path for which to determine if it corresponds with the provided segments.
|
||||
* @param segments Array of segments; the `path` must have ending segments that match the
|
||||
* patterns in this array.
|
||||
* @param fs The filesystem to use for inspecting the path.
|
||||
*/
|
||||
function isFile(
|
||||
path: AbsoluteFsPath, segments: ReadonlyArray<string|RegExp>, fs: FileSystem): boolean {
|
||||
for (let i = segments.length - 1; i >= 0; i--) {
|
||||
const pattern = segments[i];
|
||||
const segment = fs.basename(path);
|
||||
if (typeof pattern === 'string') {
|
||||
if (pattern !== segment) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!pattern.test(segment)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
path = fs.dirname(path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A cache for processing a single entry-point. This exists to share `ts.SourceFile`s between the
|
||||
* source and typing programs that are created for a single program.
|
||||
*/
|
||||
export class EntryPointFileCache {
|
||||
private readonly sfCache = new Map<AbsoluteFsPath, ts.SourceFile>();
|
||||
|
||||
constructor(private fs: FileSystem, private sharedFileCache: SharedFileCache) {}
|
||||
|
||||
/**
|
||||
* Returns and caches a parsed `ts.SourceFile` for the provided `fileName`. If the `fileName` is
|
||||
* cached in the shared file cache, that result is used. Otherwise, the source file is cached
|
||||
* internally. This method returns `undefined` if the requested file does not exist.
|
||||
*
|
||||
* @param fileName The path of the file to retrieve a source file for.
|
||||
* @param languageVersion The language version to use for parsing the file.
|
||||
*/
|
||||
getCachedSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
|
||||
const staticSf = this.sharedFileCache.getCachedSourceFile(fileName);
|
||||
if (staticSf !== undefined) {
|
||||
return staticSf;
|
||||
}
|
||||
|
||||
const absPath = this.fs.resolve(fileName);
|
||||
if (this.sfCache.has(absPath)) {
|
||||
return this.sfCache.get(absPath);
|
||||
}
|
||||
|
||||
const content = readFile(absPath, this.fs);
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const sf = ts.createSourceFile(fileName, content, languageVersion);
|
||||
this.sfCache.set(absPath, sf);
|
||||
return sf;
|
||||
}
|
||||
}
|
||||
|
||||
function readFile(absPath: AbsoluteFsPath, fs: FileSystem): string|undefined {
|
||||
if (!fs.exists(absPath) || !fs.stat(absPath).isFile()) {
|
||||
return undefined;
|
||||
}
|
||||
return fs.readFile(absPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `ts.ModuleResolutionCache` that uses the provided filesystem for path operations.
|
||||
*
|
||||
* @param fs The filesystem to use for path operations.
|
||||
*/
|
||||
export function createModuleResolutionCache(fs: FileSystem): ts.ModuleResolutionCache {
|
||||
return ts.createModuleResolutionCache(fs.pwd(), fileName => {
|
||||
return fs.isCaseSensitive() ? fileName : fileName.toLowerCase();
|
||||
});
|
||||
}
|
@ -14,7 +14,6 @@ import {NgccEntryPointConfig} from '../../src/packages/configuration';
|
||||
import {EntryPoint, EntryPointFormat} from '../../src/packages/entry_point';
|
||||
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
|
||||
import {NgccSourcesCompilerHost} from '../../src/packages/ngcc_compiler_host';
|
||||
import {createModuleResolutionCache, EntryPointFileCache, SharedFileCache} from '../../src/packages/source_file_cache';
|
||||
|
||||
export type TestConfig = Pick<NgccEntryPointConfig, 'generateDeepReexports'>;
|
||||
|
||||
@ -69,10 +68,7 @@ export function makeTestBundleProgram(
|
||||
const rootDir = fs.dirname(entryPointPath);
|
||||
const options: ts.CompilerOptions =
|
||||
{allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]};
|
||||
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||
const entryPointFileCache = new EntryPointFileCache(fs, new SharedFileCache(fs));
|
||||
const host =
|
||||
new NgccSourcesCompilerHost(fs, options, entryPointFileCache, moduleResolutionCache, rootDir);
|
||||
const host = new NgccSourcesCompilerHost(fs, options, rootDir);
|
||||
return makeBundleProgram(
|
||||
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
|
||||
}
|
||||
|
@ -1211,69 +1211,6 @@ exports.MissingClass2 = MissingClass2;
|
||||
});
|
||||
|
||||
describe('getConstructorParameters', () => {
|
||||
it('should always specify LOCAL type value references for decorated constructor parameter types',
|
||||
() => {
|
||||
const files = [
|
||||
{
|
||||
name: _('/node_modules/shared-lib/foo.d.ts'),
|
||||
contents: `
|
||||
declare class Foo {}
|
||||
export {Foo as Bar};
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/shared-lib/index.d.ts'),
|
||||
contents: `
|
||||
export {Bar as Baz} from './foo';
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: _('/local.js'),
|
||||
contents: `
|
||||
var Internal = (function() {
|
||||
function Internal() {
|
||||
}
|
||||
return Internal;
|
||||
}());
|
||||
exports.External = Internal;
|
||||
`
|
||||
},
|
||||
{
|
||||
name: _('/main.js'),
|
||||
contents: `
|
||||
var shared = require('shared-lib');
|
||||
var local = require('./local');
|
||||
var SameFile = (function() {
|
||||
function SameFile() {
|
||||
}
|
||||
return SameFile;
|
||||
}());
|
||||
exports.SameFile = SameFile;
|
||||
|
||||
var SomeClass = (function() {
|
||||
function SomeClass(arg1, arg2, arg3) {}
|
||||
return SomeClass;
|
||||
}());
|
||||
SomeClass.ctorParameters = function() { return [{ type: shared.Baz }, { type: local.External }, { type: SameFile }]; };
|
||||
exports.SomeClass = SomeClass;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
loadTestFiles(files);
|
||||
const bundle = makeTestBundleProgram(_('/main.js'));
|
||||
const host =
|
||||
createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode = getDeclaration(
|
||||
bundle.program, _('/main.js'), 'SomeClass', isNamedVariableDeclaration);
|
||||
|
||||
const parameters = host.getConstructorParameters(classNode)!;
|
||||
|
||||
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
|
||||
expectTypeValueReferencesForParameters(
|
||||
parameters, ['shared.Baz', 'local.External', 'SameFile']);
|
||||
});
|
||||
|
||||
it('should find the decorated constructor parameters', () => {
|
||||
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
||||
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
|
||||
|
@ -1140,57 +1140,6 @@ runInEachFileSystem(() => {
|
||||
});
|
||||
|
||||
describe('getConstructorParameters()', () => {
|
||||
it('should always specify LOCAL type value references for decorated constructor parameter types',
|
||||
() => {
|
||||
const files = [
|
||||
{
|
||||
name: _('/node_modules/shared-lib/foo.d.ts'),
|
||||
contents: `
|
||||
declare class Foo {}
|
||||
export {Foo as Bar};
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/shared-lib/index.d.ts'),
|
||||
contents: `
|
||||
export {Bar as Baz} from './foo';
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: _('/local.js'),
|
||||
contents: `
|
||||
class Internal {}
|
||||
export {Internal as External};
|
||||
`
|
||||
},
|
||||
{
|
||||
name: _('/main.js'),
|
||||
contents: `
|
||||
import {Baz} from 'shared-lib';
|
||||
import {External} from './local';
|
||||
export class SameFile {}
|
||||
|
||||
export class SomeClass {
|
||||
constructor(arg1, arg2, arg3) {}
|
||||
}
|
||||
SomeClass.ctorParameters = [{ type: Baz }, { type: External }, { type: SameFile }];
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
loadTestFiles(files);
|
||||
const bundle = makeTestBundleProgram(_('/main.js'));
|
||||
const host =
|
||||
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, _('/main.js'), 'SomeClass', isNamedClassDeclaration);
|
||||
|
||||
const parameters = host.getConstructorParameters(classNode)!;
|
||||
|
||||
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
|
||||
expectTypeValueReferencesForParameters(parameters, ['Baz', 'External', 'SameFile']);
|
||||
});
|
||||
|
||||
it('should find the decorated constructor parameters', () => {
|
||||
loadFakeCore(getFileSystem());
|
||||
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
||||
@ -1205,7 +1154,7 @@ runInEachFileSystem(() => {
|
||||
'_viewContainer', '_template', 'injected'
|
||||
]);
|
||||
expectTypeValueReferencesForParameters(
|
||||
parameters, ['ViewContainerRef', 'TemplateRef', null]);
|
||||
parameters, ['ViewContainerRef', 'TemplateRef', null], '@angular/core');
|
||||
});
|
||||
|
||||
it('should accept `ctorParameters` as an array', () => {
|
||||
|
@ -1252,67 +1252,6 @@ runInEachFileSystem(() => {
|
||||
});
|
||||
|
||||
describe('getConstructorParameters()', () => {
|
||||
it('should always specify LOCAL type value references for decorated constructor parameter types',
|
||||
() => {
|
||||
const files = [
|
||||
{
|
||||
name: _('/node_modules/shared-lib/foo.d.ts'),
|
||||
contents: `
|
||||
declare class Foo {}
|
||||
export {Foo as Bar};
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/shared-lib/index.d.ts'),
|
||||
contents: `
|
||||
export {Bar as Baz} from './foo';
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: _('/local.js'),
|
||||
contents: `
|
||||
var Internal = (function() {
|
||||
function Internal() {
|
||||
}
|
||||
return Internal;
|
||||
}());
|
||||
export {Internal as External};
|
||||
`
|
||||
},
|
||||
{
|
||||
name: _('/main.js'),
|
||||
contents: `
|
||||
import {Baz} from 'shared-lib';
|
||||
import {External} from './local';
|
||||
var SameFile = (function() {
|
||||
function SameFile() {
|
||||
}
|
||||
return SameFile;
|
||||
}());
|
||||
export SameFile;
|
||||
|
||||
var SomeClass = (function() {
|
||||
function SomeClass(arg1, arg2, arg3) {}
|
||||
return SomeClass;
|
||||
}());
|
||||
SomeClass.ctorParameters = function() { return [{ type: Baz }, { type: External }, { type: SameFile }]; };
|
||||
export SomeClass;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
loadTestFiles(files);
|
||||
const bundle = makeTestBundleProgram(_('/main.js'));
|
||||
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode = getDeclaration(
|
||||
bundle.program, _('/main.js'), 'SomeClass', isNamedVariableDeclaration);
|
||||
|
||||
const parameters = host.getConstructorParameters(classNode)!;
|
||||
|
||||
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
|
||||
expectTypeValueReferencesForParameters(parameters, ['Baz', 'External', 'SameFile']);
|
||||
});
|
||||
|
||||
it('should find the decorated constructor parameters', () => {
|
||||
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
||||
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
|
||||
|
@ -1332,78 +1332,6 @@ runInEachFileSystem(() => {
|
||||
});
|
||||
|
||||
describe('getConstructorParameters', () => {
|
||||
it('should always specify LOCAL type value references for decorated constructor parameter types',
|
||||
() => {
|
||||
const files = [
|
||||
{
|
||||
name: _('/node_modules/shared-lib/foo.d.ts'),
|
||||
contents: `
|
||||
declare class Foo {}
|
||||
export {Foo as Bar};
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/shared-lib/index.d.ts'),
|
||||
contents: `
|
||||
export {Bar as Baz} from './foo';
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: _('/local.js'),
|
||||
contents: `
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||||
typeof define === 'function' && define.amd ? define('local', ['exports'], factory) :
|
||||
(factory(global.local));
|
||||
}(this, (function (exports) { 'use strict';
|
||||
var Internal = (function() {
|
||||
function Internal() {
|
||||
}
|
||||
return Internal;
|
||||
}());
|
||||
exports.External = Internal;
|
||||
})));
|
||||
`
|
||||
},
|
||||
{
|
||||
name: _('/main.js'),
|
||||
contents: `
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('shared-lib), require('./local')) :
|
||||
typeof define === 'function' && define.amd ? define('main', ['exports', 'shared-lib', './local'], factory) :
|
||||
(factory(global.main, global.shared, global.local));
|
||||
}(this, (function (exports, shared, local) { 'use strict';
|
||||
var SameFile = (function() {
|
||||
function SameFile() {
|
||||
}
|
||||
return SameFile;
|
||||
}());
|
||||
exports.SameFile = SameFile;
|
||||
|
||||
var SomeClass = (function() {
|
||||
function SomeClass(arg1, arg2, arg3) {}
|
||||
return SomeClass;
|
||||
}());
|
||||
SomeClass.ctorParameters = function() { return [{ type: shared.Baz }, { type: local.External }, { type: SameFile }]; };
|
||||
exports.SomeClass = SomeClass;
|
||||
})));
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
loadTestFiles(files);
|
||||
const bundle = makeTestBundleProgram(_('/main.js'));
|
||||
const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode = getDeclaration(
|
||||
bundle.program, _('/main.js'), 'SomeClass', isNamedVariableDeclaration);
|
||||
|
||||
const parameters = host.getConstructorParameters(classNode)!;
|
||||
|
||||
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
|
||||
expectTypeValueReferencesForParameters(
|
||||
parameters, ['shared.Baz', 'local.External', 'SameFile']);
|
||||
});
|
||||
|
||||
it('should find the decorated constructor parameters', () => {
|
||||
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
||||
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
|
||||
@ -1663,7 +1591,7 @@ runInEachFileSystem(() => {
|
||||
`;
|
||||
break;
|
||||
case 'inlined_with_suffix':
|
||||
fileHeaderWithUmd = `
|
||||
fileHeaderWithUmd = `
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports)) :
|
||||
typeof define === 'function' && define.amd ? define('test', ['exports'], factory) :
|
||||
|
@ -13,36 +13,26 @@ import {CtorParameter, TypeValueReferenceKind} from '../../../src/ngtsc/reflecti
|
||||
* names.
|
||||
*/
|
||||
export function expectTypeValueReferencesForParameters(
|
||||
parameters: CtorParameter[], expectedParams: (string|null)[],
|
||||
fromModule: (string|null)[] = []) {
|
||||
parameters: CtorParameter[], expectedParams: (string|null)[], fromModule: string|null = null) {
|
||||
parameters!.forEach((param, idx) => {
|
||||
const expected = expectedParams[idx];
|
||||
if (expected !== null) {
|
||||
if (param.typeValueReference.kind === TypeValueReferenceKind.UNAVAILABLE) {
|
||||
fail(`Incorrect typeValueReference generated for ${param.name}, expected "${
|
||||
expected}" because "${param.typeValueReference.reason}"`);
|
||||
fail(`Incorrect typeValueReference generated, expected ${expected}`);
|
||||
} else if (
|
||||
param.typeValueReference.kind === TypeValueReferenceKind.LOCAL &&
|
||||
fromModule[idx] != null) {
|
||||
fail(`Incorrect typeValueReference generated for ${param.name}, expected non-LOCAL (from ${
|
||||
fromModule[idx]}) but was marked LOCAL`);
|
||||
param.typeValueReference.kind === TypeValueReferenceKind.LOCAL && fromModule !== null) {
|
||||
fail(`Incorrect typeValueReference generated, expected non-local`);
|
||||
} else if (
|
||||
param.typeValueReference.kind !== TypeValueReferenceKind.LOCAL &&
|
||||
fromModule[idx] == null) {
|
||||
fail(`Incorrect typeValueReference generated for ${
|
||||
param.name}, expected LOCAL but was imported from ${
|
||||
param.typeValueReference.moduleName}`);
|
||||
param.typeValueReference.kind !== TypeValueReferenceKind.LOCAL && fromModule === null) {
|
||||
fail(`Incorrect typeValueReference generated, expected local`);
|
||||
} else if (param.typeValueReference.kind === TypeValueReferenceKind.LOCAL) {
|
||||
if (!ts.isIdentifier(param.typeValueReference.expression) &&
|
||||
!ts.isPropertyAccessExpression(param.typeValueReference.expression)) {
|
||||
fail(`Incorrect typeValueReference generated for ${
|
||||
param.name}, expected an identifier but got "${
|
||||
param.typeValueReference.expression.getText()}"`);
|
||||
if (!ts.isIdentifier(param.typeValueReference.expression)) {
|
||||
fail(`Incorrect typeValueReference generated, expected identifier`);
|
||||
} else {
|
||||
expect(param.typeValueReference.expression.getText()).toEqual(expected);
|
||||
expect(param.typeValueReference.expression.text).toEqual(expected);
|
||||
}
|
||||
} else if (param.typeValueReference.kind === TypeValueReferenceKind.IMPORTED) {
|
||||
expect(param.typeValueReference.moduleName).toBe(fromModule[idx]!);
|
||||
expect(param.typeValueReference.moduleName).toBe(fromModule!);
|
||||
expect(param.typeValueReference.importedName).toBe(expected);
|
||||
}
|
||||
}
|
||||
|
@ -213,15 +213,13 @@ export function compileIntoApf(
|
||||
fs.resolve(`/node_modules/${pkgName}/package.json`), JSON.stringify(pkgJson, null, 2));
|
||||
}
|
||||
|
||||
const stdFiles = loadStandardTestFiles({fakeCore: false});
|
||||
|
||||
/**
|
||||
* Prepares a mock filesystem that contains all provided source files, which can be used to emit
|
||||
* compiled code into.
|
||||
*/
|
||||
function setupCompileFs(sources: PackageSources): {rootNames: string[], compileFs: FileSystem} {
|
||||
const compileFs = new MockFileSystemPosix(true);
|
||||
compileFs.init(stdFiles);
|
||||
compileFs.init(loadStandardTestFiles({fakeCore: false}));
|
||||
|
||||
const rootNames = Object.keys(sources);
|
||||
|
||||
|
@ -5,13 +5,12 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as os from 'os';
|
||||
|
||||
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
|
||||
import {MockLogger} from '../../src/ngtsc/logging/testing';
|
||||
|
||||
import {clearTsConfigCache, getMaxNumberOfWorkers, getSharedSetup, NgccOptions} from '../src/ngcc_options';
|
||||
import {clearTsConfigCache, getSharedSetup, NgccOptions} from '../src/ngcc_options';
|
||||
|
||||
|
||||
|
||||
@ -101,67 +100,6 @@ runInEachFileSystem(() => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMaxNumberOfWorkers', () => {
|
||||
let processEnv: NodeJS.ProcessEnv;
|
||||
let cpuSpy: jasmine.Spy;
|
||||
beforeEach(() => {
|
||||
processEnv = process.env;
|
||||
process.env = {...process.env};
|
||||
cpuSpy = spyOn(os, 'cpus');
|
||||
});
|
||||
afterEach(() => {
|
||||
process.env = processEnv;
|
||||
});
|
||||
|
||||
it('should use NGCC_MAX_WORKERS environment variable if set', () => {
|
||||
process.env.NGCC_MAX_WORKERS = '16';
|
||||
expect(getMaxNumberOfWorkers()).toBe(16);
|
||||
process.env.NGCC_MAX_WORKERS = '8';
|
||||
expect(getMaxNumberOfWorkers()).toBe(8);
|
||||
process.env.NGCC_MAX_WORKERS = ' 8 ';
|
||||
expect(getMaxNumberOfWorkers()).toBe(8);
|
||||
});
|
||||
|
||||
it('should throw an error if NGCC_MAX_WORKERS is less than 1', () => {
|
||||
process.env.NGCC_MAX_WORKERS = '0';
|
||||
expect(() => getMaxNumberOfWorkers())
|
||||
.toThrow(new Error('NGCC_MAX_WORKERS should be at least 1.'));
|
||||
process.env.NGCC_MAX_WORKERS = '-1';
|
||||
expect(() => getMaxNumberOfWorkers())
|
||||
.toThrow(new Error('NGCC_MAX_WORKERS should be at least 1.'));
|
||||
});
|
||||
|
||||
it('should throw an error if NGCC_MAX_WORKERS is not an integer', () => {
|
||||
process.env.NGCC_MAX_WORKERS = 'a';
|
||||
expect(() => getMaxNumberOfWorkers())
|
||||
.toThrow(new Error('NGCC_MAX_WORKERS should be an integer.'));
|
||||
process.env.NGCC_MAX_WORKERS = '1.5';
|
||||
expect(() => getMaxNumberOfWorkers())
|
||||
.toThrow(new Error('NGCC_MAX_WORKERS should be an integer.'));
|
||||
process.env.NGCC_MAX_WORKERS = '-';
|
||||
expect(() => getMaxNumberOfWorkers())
|
||||
.toThrow(new Error('NGCC_MAX_WORKERS should be an integer.'));
|
||||
});
|
||||
|
||||
it('should fallback to the number of cpus, minus one (for the master process), with a maximum of 4 workers',
|
||||
() => {
|
||||
simulateNumberOfCpus(1);
|
||||
expect(getMaxNumberOfWorkers()).toBe(1);
|
||||
simulateNumberOfCpus(2);
|
||||
expect(getMaxNumberOfWorkers()).toBe(1);
|
||||
simulateNumberOfCpus(4);
|
||||
expect(getMaxNumberOfWorkers()).toBe(3);
|
||||
simulateNumberOfCpus(6);
|
||||
expect(getMaxNumberOfWorkers()).toBe(4);
|
||||
simulateNumberOfCpus(8);
|
||||
expect(getMaxNumberOfWorkers()).toBe(4);
|
||||
});
|
||||
|
||||
function simulateNumberOfCpus(cpus: number): void {
|
||||
cpuSpy.and.returnValue(new Array(cpus).fill({model: 'Mock CPU'} as any));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This function creates an object that contains the minimal required properties for NgccOptions.
|
||||
*/
|
||||
|
@ -10,7 +10,6 @@ import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||
import {loadTestFiles} from '../../../test/helpers';
|
||||
import {EntryPoint} from '../../src/packages/entry_point';
|
||||
import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
|
||||
import {createModuleResolutionCache, SharedFileCache} from '../../src/packages/source_file_cache';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('entry point bundle', () => {
|
||||
@ -181,10 +180,7 @@ runInEachFileSystem(() => {
|
||||
ignoreMissingDependencies: false,
|
||||
generateDeepReexports: false,
|
||||
};
|
||||
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||
const esm5bundle = makeEntryPointBundle(
|
||||
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
|
||||
'esm5', true);
|
||||
const esm5bundle = makeEntryPointBundle(fs, entryPoint, './index.js', false, 'esm5', true);
|
||||
|
||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
||||
.toEqual(jasmine.arrayWithExactContents([
|
||||
@ -295,11 +291,8 @@ runInEachFileSystem(() => {
|
||||
ignoreMissingDependencies: false,
|
||||
generateDeepReexports: false,
|
||||
};
|
||||
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||
const esm5bundle = makeEntryPointBundle(
|
||||
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
|
||||
'esm5',
|
||||
/* transformDts */ true,
|
||||
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
|
||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
||||
|
||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => _(sf.fileName)))
|
||||
@ -335,11 +328,8 @@ runInEachFileSystem(() => {
|
||||
ignoreMissingDependencies: false,
|
||||
generateDeepReexports: false,
|
||||
};
|
||||
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||
const esm5bundle = makeEntryPointBundle(
|
||||
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
|
||||
'esm5',
|
||||
/* transformDts */ true,
|
||||
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
|
||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
||||
.toContain(absoluteFrom('/node_modules/test/internal.js'));
|
||||
@ -361,11 +351,8 @@ runInEachFileSystem(() => {
|
||||
ignoreMissingDependencies: false,
|
||||
generateDeepReexports: false,
|
||||
};
|
||||
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||
const esm5bundle = makeEntryPointBundle(
|
||||
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache,
|
||||
'./esm2015/index.js', false, 'esm2015',
|
||||
/* transformDts */ true,
|
||||
fs, entryPoint, './esm2015/index.js', false, 'esm2015', /* transformDts */ true,
|
||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
||||
.toContain(absoluteFrom('/node_modules/internal/esm2015/src/internal.js'));
|
||||
@ -387,11 +374,8 @@ runInEachFileSystem(() => {
|
||||
ignoreMissingDependencies: false,
|
||||
generateDeepReexports: false,
|
||||
};
|
||||
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||
const esm5bundle = makeEntryPointBundle(
|
||||
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
|
||||
'esm5',
|
||||
/* transformDts */ true,
|
||||
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
|
||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ false);
|
||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
||||
.toContain(absoluteFrom('/node_modules/test/internal.js'));
|
||||
@ -414,11 +398,8 @@ runInEachFileSystem(() => {
|
||||
ignoreMissingDependencies: false,
|
||||
generateDeepReexports: false,
|
||||
};
|
||||
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||
const bundle = makeEntryPointBundle(
|
||||
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
|
||||
'esm2015',
|
||||
/* transformDts */ true,
|
||||
fs, entryPoint, './index.js', false, 'esm2015', /* transformDts */ true,
|
||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
||||
expect(bundle.rootDirs).toEqual([absoluteFrom('/node_modules/primary')]);
|
||||
});
|
||||
|
@ -1,223 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFrom, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||
import {loadTestFiles} from '../../../test/helpers';
|
||||
import {EntryPointFileCache, isAngularDts, isDefaultLibrary, SharedFileCache} from '../../src/packages/source_file_cache';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('caching', () => {
|
||||
let _: typeof absoluteFrom;
|
||||
let fs: FileSystem;
|
||||
beforeEach(() => {
|
||||
_ = absoluteFrom;
|
||||
fs = getFileSystem();
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/typescript/lib/lib.es5.d.ts'),
|
||||
contents: `export declare interface Array {}`,
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/typescript/lib/lib.dom.d.ts'),
|
||||
contents: `export declare interface Window {}`,
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/@angular/core/core.d.ts'),
|
||||
contents: `export declare interface Component {}`,
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/@angular/common/common.d.ts'),
|
||||
contents: `export declare interface NgIf {}`,
|
||||
},
|
||||
{
|
||||
name: _('/index.ts'),
|
||||
contents: `export const index = true;`,
|
||||
},
|
||||
{
|
||||
name: _('/main.ts'),
|
||||
contents: `export const main = true;`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('SharedFileCache', () => {
|
||||
it('should cache a parsed source file for default libraries', () => {
|
||||
const cache = new SharedFileCache(fs);
|
||||
|
||||
const libEs5 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.es5.d.ts')!;
|
||||
expect(libEs5).not.toBeUndefined();
|
||||
expect(libEs5.text).toContain('Array');
|
||||
|
||||
const libDom = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.dom.d.ts')!;
|
||||
expect(libDom).not.toBeUndefined();
|
||||
expect(libDom.text).toContain('Window');
|
||||
|
||||
const libEs5_2 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.es5.d.ts')!;
|
||||
expect(libEs5_2).toBe(libEs5);
|
||||
|
||||
const libDom_2 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.dom.d.ts')!;
|
||||
expect(libDom_2).toBe(libDom);
|
||||
});
|
||||
|
||||
it('should cache a parsed source file for @angular scoped packages', () => {
|
||||
const cache = new SharedFileCache(fs);
|
||||
|
||||
const core = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
|
||||
expect(core).not.toBeUndefined();
|
||||
expect(core.text).toContain('Component');
|
||||
|
||||
const common = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
|
||||
expect(common).not.toBeUndefined();
|
||||
expect(common.text).toContain('NgIf');
|
||||
|
||||
const core_2 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
|
||||
expect(core_2).toBe(core);
|
||||
|
||||
const common_2 = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
|
||||
expect(common_2).toBe(common);
|
||||
});
|
||||
|
||||
it('should reparse @angular d.ts files when they change', () => {
|
||||
const cache = new SharedFileCache(fs);
|
||||
|
||||
const core = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
|
||||
expect(core).not.toBeUndefined();
|
||||
expect(core.text).toContain('Component');
|
||||
|
||||
const common = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
|
||||
expect(common).not.toBeUndefined();
|
||||
expect(common.text).toContain('NgIf');
|
||||
|
||||
fs.writeFile(
|
||||
_('/node_modules/@angular/core/core.d.ts'), `export declare interface Directive {}`);
|
||||
|
||||
const core_2 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
|
||||
expect(core_2).not.toBe(core);
|
||||
expect(core_2.text).toContain('Directive');
|
||||
|
||||
const core_3 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
|
||||
expect(core_3).toBe(core_2);
|
||||
|
||||
const common_2 = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
|
||||
expect(common_2).toBe(common);
|
||||
});
|
||||
|
||||
it('should not cache files that are not default library files inside of the typescript package',
|
||||
() => {
|
||||
const cache = new SharedFileCache(fs);
|
||||
|
||||
expect(cache.getCachedSourceFile('/node_modules/typescript/lib/typescript.d.ts'))
|
||||
.toBeUndefined();
|
||||
expect(cache.getCachedSourceFile('/typescript/lib.es5.d.ts')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDefaultLibrary()', () => {
|
||||
it('should accept lib files inside of the typescript package', () => {
|
||||
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es5.d.ts'), fs)).toBe(true);
|
||||
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.dom.d.ts'), fs)).toBe(true);
|
||||
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es2015.core.d.ts'), fs))
|
||||
.toBe(true);
|
||||
expect(isDefaultLibrary(_('/root/node_modules/typescript/lib/lib.es5.d.ts'), fs))
|
||||
.toBe(true);
|
||||
});
|
||||
it('should reject non lib files inside of the typescript package', () => {
|
||||
expect(isDefaultLibrary(_('/node_modules/typescript/lib/typescript.d.ts'), fs)).toBe(false);
|
||||
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es5.ts'), fs)).toBe(false);
|
||||
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.d.ts'), fs)).toBe(false);
|
||||
expect(isDefaultLibrary(_('/node_modules/typescript/lib.es5.d.ts'), fs)).toBe(false);
|
||||
});
|
||||
it('should reject lib files outside of the typescript package', () => {
|
||||
expect(isDefaultLibrary(_('/node_modules/ttypescript/lib/lib.es5.d.ts'), fs)).toBe(false);
|
||||
expect(isDefaultLibrary(_('/node_modules/ttypescript/lib/lib.es5.d.ts'), fs)).toBe(false);
|
||||
expect(isDefaultLibrary(_('/typescript/lib/lib.es5.d.ts'), fs)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAngularDts()', () => {
|
||||
it('should accept .d.ts files inside of the @angular scope', () => {
|
||||
expect(isAngularDts(_('/node_modules/@angular/core/core.d.ts'), fs)).toBe(true);
|
||||
expect(isAngularDts(_('/node_modules/@angular/common/common.d.ts'), fs)).toBe(true);
|
||||
});
|
||||
it('should reject non-.d.ts files inside @angular scoped packages', () => {
|
||||
expect(isAngularDts(_('/node_modules/@angular/common/src/common.ts'), fs)).toBe(false);
|
||||
});
|
||||
it('should reject .d.ts files nested deeply inside @angular scoped packages', () => {
|
||||
expect(isAngularDts(_('/node_modules/@angular/common/src/common.d.ts'), fs)).toBe(false);
|
||||
});
|
||||
it('should reject .d.ts files directly inside the @angular scope', () => {
|
||||
expect(isAngularDts(_('/node_modules/@angular/common.d.ts'), fs)).toBe(false);
|
||||
});
|
||||
it('should reject files that are not inside node_modules', () => {
|
||||
expect(isAngularDts(_('/@angular/core/core.d.ts'), fs)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EntryPointFileCache', () => {
|
||||
let sharedFileCache: SharedFileCache;
|
||||
beforeEach(() => {
|
||||
sharedFileCache = new SharedFileCache(fs);
|
||||
});
|
||||
|
||||
it('should prefer source files cached in SharedFileCache', () => {
|
||||
const cache1 = new EntryPointFileCache(fs, sharedFileCache);
|
||||
const libEs5_1 = cache1.getCachedSourceFile(
|
||||
'/node_modules/typescript/lib/lib.es5.d.ts', ts.ScriptTarget.ESNext)!;
|
||||
expect(libEs5_1).not.toBeUndefined();
|
||||
expect(libEs5_1.text).toContain('Array');
|
||||
expect(libEs5_1.languageVersion).toBe(ts.ScriptTarget.ES2015);
|
||||
|
||||
const cache2 = new EntryPointFileCache(fs, sharedFileCache);
|
||||
const libEs5_2 = cache2.getCachedSourceFile(
|
||||
'/node_modules/typescript/lib/lib.es5.d.ts', ts.ScriptTarget.ESNext)!;
|
||||
expect(libEs5_1).toBe(libEs5_2);
|
||||
});
|
||||
|
||||
it('should cache source files that are not default library files', () => {
|
||||
const cache = new EntryPointFileCache(fs, sharedFileCache);
|
||||
const index = cache.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
|
||||
expect(index).not.toBeUndefined();
|
||||
expect(index.text).toContain('index');
|
||||
expect(index.languageVersion).toBe(ts.ScriptTarget.ESNext);
|
||||
|
||||
const main = cache.getCachedSourceFile('/main.ts', ts.ScriptTarget.ESNext)!;
|
||||
expect(main).not.toBeUndefined();
|
||||
expect(main.text).toContain('main');
|
||||
expect(main.languageVersion).toBe(ts.ScriptTarget.ESNext);
|
||||
|
||||
const index_2 = cache.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
|
||||
expect(index_2).toBe(index);
|
||||
|
||||
const main_2 = cache.getCachedSourceFile('/main.ts', ts.ScriptTarget.ESNext)!;
|
||||
expect(main_2).toBe(main);
|
||||
});
|
||||
|
||||
it('should not share non-library files across multiple cache instances', () => {
|
||||
const cache1 = new EntryPointFileCache(fs, sharedFileCache);
|
||||
const cache2 = new EntryPointFileCache(fs, sharedFileCache);
|
||||
|
||||
const index1 = cache1.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
|
||||
const index2 = cache2.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
|
||||
expect(index1).not.toBe(index2);
|
||||
});
|
||||
|
||||
it('should return undefined if the file does not exist', () => {
|
||||
const cache = new EntryPointFileCache(fs, sharedFileCache);
|
||||
expect(cache.getCachedSourceFile('/nonexistent.ts', ts.ScriptTarget.ESNext))
|
||||
.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if the path is a directory', () => {
|
||||
const cache = new EntryPointFileCache(fs, sharedFileCache);
|
||||
expect(cache.getCachedSourceFile('/node_modules', ts.ScriptTarget.ESNext)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -12,7 +12,6 @@ import {loadTestFiles} from '../../../test/helpers';
|
||||
import {NgccConfiguration} from '../../src/packages/configuration';
|
||||
import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointInfo, isEntryPoint} from '../../src/packages/entry_point';
|
||||
import {EntryPointBundle, makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
|
||||
import {createModuleResolutionCache, SharedFileCache} from '../../src/packages/source_file_cache';
|
||||
import {FileWriter} from '../../src/writing/file_writer';
|
||||
import {NewEntryPointFileWriter} from '../../src/writing/new_entry_point_file_writer';
|
||||
import {DirectPackageJsonUpdater} from '../../src/writing/package_json_updater';
|
||||
@ -635,9 +634,7 @@ runInEachFileSystem(() => {
|
||||
function makeTestBundle(
|
||||
fs: FileSystem, entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty,
|
||||
format: EntryPointFormat): EntryPointBundle {
|
||||
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||
return makeEntryPointBundle(
|
||||
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache,
|
||||
entryPoint.packageJson[formatProperty]!, false, format, true);
|
||||
fs, entryPoint, entryPoint.packageJson[formatProperty]!, false, format, true);
|
||||
}
|
||||
});
|
||||
|
@ -23,7 +23,6 @@ ts_library(
|
||||
"//packages/compiler-cli/src/ngtsc/shims:api",
|
||||
"//packages/compiler-cli/src/ngtsc/transform",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"@npm//@types/node",
|
||||
"@npm//typescript",
|
||||
|
@ -10,18 +10,18 @@ import {compileComponentFromMetadata, ConstantPool, CssSelector, DEFAULT_INTERPO
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {CycleAnalyzer} from '../../cycles';
|
||||
import {ErrorCode, FatalDiagnosticError, ngErrorCode} from '../../diagnostics';
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {absoluteFrom, relative} from '../../file_system';
|
||||
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
|
||||
import {DependencyTracker} from '../../incremental/api';
|
||||
import {IndexingContext} from '../../indexer';
|
||||
import {ClassPropertyMapping, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
||||
import {DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
||||
import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance';
|
||||
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
||||
import {ComponentScopeReader, LocalModuleScopeRegistry} from '../../scope';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform';
|
||||
import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck/api';
|
||||
import {getTemplateId, makeTemplateDiagnostic} from '../../typecheck/diagnostics';
|
||||
import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
|
||||
import {SubsetOfKeys} from '../../util/src/typescript';
|
||||
|
||||
@ -30,7 +30,6 @@ import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagno
|
||||
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
|
||||
import {compileNgFactoryDefField} from './factory';
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {TypeCheckScopes} from './typecheck_scopes';
|
||||
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, makeDuplicateDeclarationError, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
|
||||
|
||||
const EMPTY_MAP = new Map<string, Expression>();
|
||||
@ -56,9 +55,6 @@ export interface ComponentAnalysisData {
|
||||
template: ParsedTemplateWithSource;
|
||||
metadataStmt: Statement|null;
|
||||
|
||||
inputs: ClassPropertyMapping;
|
||||
outputs: ClassPropertyMapping;
|
||||
|
||||
/**
|
||||
* Providers extracted from the `providers` field of the component annotation which will require
|
||||
* an Angular factory definition at runtime.
|
||||
@ -95,7 +91,6 @@ export class ComponentDecoratorHandler implements
|
||||
|
||||
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
|
||||
private elementSchemaRegistry = new DomElementSchemaRegistry();
|
||||
private typeCheckScopes = new TypeCheckScopes(this.scopeReader, this.metaReader);
|
||||
|
||||
/**
|
||||
* During the asynchronous preanalyze phase, it's necessary to parse the template to extract
|
||||
@ -195,7 +190,7 @@ export class ComponentDecoratorHandler implements
|
||||
}
|
||||
|
||||
// Next, read the `@Component`-specific fields.
|
||||
const {decorator: component, metadata, inputs, outputs} = directiveResult;
|
||||
const {decorator: component, metadata} = directiveResult;
|
||||
|
||||
// Go through the root directories for this project, and select the one with the smallest
|
||||
// relative path representation.
|
||||
@ -259,26 +254,9 @@ export class ComponentDecoratorHandler implements
|
||||
}
|
||||
}
|
||||
|
||||
let diagnostics: ts.Diagnostic[]|undefined = undefined;
|
||||
|
||||
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 => {
|
||||
const span = error.span;
|
||||
|
||||
if (span.start.offset === span.end.offset) {
|
||||
// Template errors can contain zero-length spans, if the error occurs at a single point.
|
||||
// However, TypeScript does not handle displaying a zero-length diagnostic very well, so
|
||||
// increase the ending offset by 1 for such errors, to ensure the position is shown in the
|
||||
// diagnostic.
|
||||
span.end.offset++;
|
||||
}
|
||||
|
||||
return makeTemplateDiagnostic(
|
||||
id, template.sourceMapping, span, ts.DiagnosticCategory.Error,
|
||||
ngErrorCode(ErrorCode.TEMPLATE_PARSE_ERROR), error.msg);
|
||||
});
|
||||
throw new Error(
|
||||
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
|
||||
}
|
||||
|
||||
// Figure out the set of styles. The ordering here is important: external resources (styleUrls)
|
||||
@ -332,8 +310,6 @@ export class ComponentDecoratorHandler implements
|
||||
const output: AnalysisOutput<ComponentAnalysisData> = {
|
||||
analysis: {
|
||||
baseClass: readBaseClass(node, this.reflector, this.evaluator),
|
||||
inputs,
|
||||
outputs,
|
||||
meta: {
|
||||
...metadata,
|
||||
template: {
|
||||
@ -351,7 +327,7 @@ export class ComponentDecoratorHandler implements
|
||||
i18nUseExternalIds: this.i18nUseExternalIds,
|
||||
relativeContextFilePath,
|
||||
},
|
||||
typeCheckMeta: extractDirectiveTypeCheckMeta(node, inputs, this.reflector),
|
||||
typeCheckMeta: extractDirectiveTypeCheckMeta(node, metadata.inputs, this.reflector),
|
||||
metadataStmt: generateSetClassMetadataCall(
|
||||
node, this.reflector, this.defaultImportRecorder, this.isCore,
|
||||
this.annotateForClosureCompiler),
|
||||
@ -359,7 +335,6 @@ export class ComponentDecoratorHandler implements
|
||||
providersRequiringFactory,
|
||||
viewProvidersRequiringFactory,
|
||||
},
|
||||
diagnostics,
|
||||
};
|
||||
if (changeDetection !== null) {
|
||||
output.analysis!.meta.changeDetection = changeDetection;
|
||||
@ -376,8 +351,8 @@ export class ComponentDecoratorHandler implements
|
||||
name: node.name.text,
|
||||
selector: analysis.meta.selector,
|
||||
exportAs: analysis.meta.exportAs,
|
||||
inputs: analysis.inputs,
|
||||
outputs: analysis.outputs,
|
||||
inputs: analysis.meta.inputs,
|
||||
outputs: analysis.meta.outputs,
|
||||
queries: analysis.meta.queries.map(query => query.propertyName),
|
||||
isComponent: true,
|
||||
baseClass: analysis.baseClass,
|
||||
@ -424,15 +399,36 @@ export class ComponentDecoratorHandler implements
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = this.typeCheckScopes.getTypeCheckScope(node);
|
||||
const matcher = new SelectorMatcher<DirectiveMeta>();
|
||||
const pipes = new Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>();
|
||||
let schemas: SchemaMetadata[] = [];
|
||||
|
||||
const scope = this.scopeReader.getScopeForComponent(node);
|
||||
if (scope === 'error') {
|
||||
// Don't type-check components that had errors in their scopes.
|
||||
return;
|
||||
}
|
||||
|
||||
const binder = new R3TargetBinder(scope.matcher);
|
||||
if (scope !== null) {
|
||||
for (const meta of scope.compilation.directives) {
|
||||
if (meta.selector !== null) {
|
||||
const extMeta = flattenInheritedDirectiveMetadata(this.metaReader, meta.ref);
|
||||
matcher.addSelectables(CssSelector.parse(meta.selector), extMeta);
|
||||
}
|
||||
}
|
||||
for (const {name, ref} of scope.compilation.pipes) {
|
||||
if (!ts.isClassDeclaration(ref.node)) {
|
||||
throw new Error(`Unexpected non-class declaration ${
|
||||
ts.SyntaxKind[ref.node.kind]} for pipe ${ref.debugName}`);
|
||||
}
|
||||
pipes.set(name, ref as Reference<ClassDeclaration<ts.ClassDeclaration>>);
|
||||
}
|
||||
schemas = scope.schemas;
|
||||
}
|
||||
|
||||
const binder = new R3TargetBinder(matcher);
|
||||
ctx.addTemplate(
|
||||
new Reference(node), binder, meta.template.diagNodes, scope.pipes, scope.schemas,
|
||||
new Reference(node), binder, meta.template.diagNodes, pipes, schemas,
|
||||
meta.template.sourceMapping, meta.template.file);
|
||||
}
|
||||
|
||||
@ -475,49 +471,36 @@ export class ComponentDecoratorHandler implements
|
||||
// Set up the R3TargetBinder, as well as a 'directives' array and a 'pipes' map that are later
|
||||
// fed to the TemplateDefinitionBuilder. First, a SelectorMatcher is constructed to match
|
||||
// directives that are in scope.
|
||||
type MatchedDirective = DirectiveMeta&{selector: string};
|
||||
const matcher = new SelectorMatcher<MatchedDirective>();
|
||||
const matcher = new SelectorMatcher<DirectiveMeta&{expression: Expression}>();
|
||||
const directives: {selector: string, expression: Expression}[] = [];
|
||||
|
||||
for (const dir of scope.compilation.directives) {
|
||||
if (dir.selector !== null) {
|
||||
matcher.addSelectables(CssSelector.parse(dir.selector), dir as MatchedDirective);
|
||||
const {ref, selector} = dir;
|
||||
if (selector !== null) {
|
||||
const expression = this.refEmitter.emit(ref, context);
|
||||
directives.push({selector, expression});
|
||||
matcher.addSelectables(CssSelector.parse(selector), {...dir, expression});
|
||||
}
|
||||
}
|
||||
const pipes = new Map<string, Reference<ClassDeclaration>>();
|
||||
const pipes = new Map<string, Expression>();
|
||||
for (const pipe of scope.compilation.pipes) {
|
||||
pipes.set(pipe.name, pipe.ref);
|
||||
pipes.set(pipe.name, this.refEmitter.emit(pipe.ref, context));
|
||||
}
|
||||
|
||||
// Next, the component template AST is bound using the R3TargetBinder. This produces a
|
||||
// Next, the component template AST is bound using the R3TargetBinder. This produces an
|
||||
// BoundTarget, which is similar to a ts.TypeChecker.
|
||||
const binder = new R3TargetBinder(matcher);
|
||||
const bound = binder.bind({template: metadata.template.nodes});
|
||||
|
||||
// The BoundTarget knows which directives and pipes matched the template.
|
||||
const usedDirectives = bound.getUsedDirectives().map(directive => {
|
||||
return {
|
||||
selector: directive.selector,
|
||||
expression: this.refEmitter.emit(directive.ref, context),
|
||||
};
|
||||
});
|
||||
|
||||
const usedPipes: {pipeName: string, expression: Expression}[] = [];
|
||||
for (const pipeName of bound.getUsedPipes()) {
|
||||
if (!pipes.has(pipeName)) {
|
||||
continue;
|
||||
}
|
||||
const pipe = pipes.get(pipeName)!;
|
||||
usedPipes.push({
|
||||
pipeName,
|
||||
expression: this.refEmitter.emit(pipe, context),
|
||||
});
|
||||
}
|
||||
const usedDirectives = bound.getUsedDirectives();
|
||||
const usedPipes = bound.getUsedPipes().map(name => pipes.get(name)!);
|
||||
|
||||
// Scan through the directives/pipes actually used in the template and check whether any
|
||||
// import which needs to be generated would create a cycle.
|
||||
const cycleDetected =
|
||||
usedDirectives.some(dir => this._isCyclicImport(dir.expression, context)) ||
|
||||
usedPipes.some(pipe => this._isCyclicImport(pipe.expression, context));
|
||||
usedPipes.some(pipe => this._isCyclicImport(pipe, context));
|
||||
|
||||
if (!cycleDetected) {
|
||||
// No cycle was detected. Record the imports that need to be created in the cycle detector
|
||||
@ -525,8 +508,8 @@ export class ComponentDecoratorHandler implements
|
||||
for (const {expression} of usedDirectives) {
|
||||
this._recordSyntheticImport(expression, context);
|
||||
}
|
||||
for (const {expression} of usedPipes) {
|
||||
this._recordSyntheticImport(expression, context);
|
||||
for (const pipe of usedPipes) {
|
||||
this._recordSyntheticImport(pipe, context);
|
||||
}
|
||||
|
||||
// Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures.
|
||||
@ -535,11 +518,16 @@ export class ComponentDecoratorHandler implements
|
||||
const wrapDirectivesAndPipesInClosure =
|
||||
usedDirectives.some(
|
||||
dir => isExpressionForwardReference(dir.expression, node.name, context)) ||
|
||||
usedPipes.some(
|
||||
pipe => isExpressionForwardReference(pipe.expression, node.name, context));
|
||||
usedPipes.some(pipe => isExpressionForwardReference(pipe, node.name, context));
|
||||
|
||||
data.directives = usedDirectives;
|
||||
data.pipes = new Map(usedPipes.map(pipe => [pipe.pipeName, pipe.expression]));
|
||||
// Actual compilation still uses the full scope, not the narrowed scope determined by
|
||||
// R3TargetBinder. This is a hedge against potential issues with the R3TargetBinder - right
|
||||
// now the TemplateDefinitionBuilder is the "source of truth" for which directives/pipes are
|
||||
// actually used (though the two should agree perfectly).
|
||||
//
|
||||
// TODO(alxhub): switch TemplateDefinitionBuilder over to using R3TargetBinder directly.
|
||||
data.directives = directives;
|
||||
data.pipes = pipes;
|
||||
data.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure;
|
||||
} else {
|
||||
// Declaring the directiveDefs/pipeDefs arrays directly would require imports that would
|
||||
|
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
||||
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {DefaultImportRecorder, Reference} from '../../imports';
|
||||
import {ClassPropertyMapping, DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
||||
import {DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
||||
import {extractDirectiveTypeCheckMeta} from '../../metadata/src/util';
|
||||
import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
||||
@ -39,8 +39,6 @@ export interface DirectiveHandlerData {
|
||||
meta: R3DirectiveMetadata;
|
||||
metadataStmt: Statement|null;
|
||||
providersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
|
||||
inputs: ClassPropertyMapping;
|
||||
outputs: ClassPropertyMapping;
|
||||
}
|
||||
|
||||
export class DirectiveDecoratorHandler implements
|
||||
@ -85,10 +83,11 @@ export class DirectiveDecoratorHandler implements
|
||||
const directiveResult = extractDirectiveMetadata(
|
||||
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
|
||||
flags, this.annotateForClosureCompiler);
|
||||
if (directiveResult === undefined) {
|
||||
const analysis = directiveResult && directiveResult.metadata;
|
||||
|
||||
if (analysis === undefined) {
|
||||
return {};
|
||||
}
|
||||
const analysis = directiveResult.metadata;
|
||||
|
||||
let providersRequiringFactory: Set<Reference<ClassDeclaration>>|null = null;
|
||||
if (directiveResult !== undefined && directiveResult.decorator.has('providers')) {
|
||||
@ -98,14 +97,12 @@ export class DirectiveDecoratorHandler implements
|
||||
|
||||
return {
|
||||
analysis: {
|
||||
inputs: directiveResult.inputs,
|
||||
outputs: directiveResult.outputs,
|
||||
meta: analysis,
|
||||
metadataStmt: generateSetClassMetadataCall(
|
||||
node, this.reflector, this.defaultImportRecorder, this.isCore,
|
||||
this.annotateForClosureCompiler),
|
||||
baseClass: readBaseClass(node, this.reflector, this.evaluator),
|
||||
typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector),
|
||||
typeCheckMeta: extractDirectiveTypeCheckMeta(node, analysis.inputs, this.reflector),
|
||||
providersRequiringFactory
|
||||
}
|
||||
};
|
||||
@ -120,8 +117,8 @@ export class DirectiveDecoratorHandler implements
|
||||
name: node.name.text,
|
||||
selector: analysis.meta.selector,
|
||||
exportAs: analysis.meta.exportAs,
|
||||
inputs: analysis.inputs,
|
||||
outputs: analysis.outputs,
|
||||
inputs: analysis.meta.inputs,
|
||||
outputs: analysis.meta.outputs,
|
||||
queries: analysis.meta.queries.map(query => query.propertyName),
|
||||
isComponent: false,
|
||||
baseClass: analysis.baseClass,
|
||||
@ -202,13 +199,8 @@ export class DirectiveDecoratorHandler implements
|
||||
export function extractDirectiveMetadata(
|
||||
clazz: ClassDeclaration, decorator: Readonly<Decorator|null>, reflector: ReflectionHost,
|
||||
evaluator: PartialEvaluator, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
|
||||
flags: HandlerFlags, annotateForClosureCompiler: boolean,
|
||||
defaultSelector: string|null = null): {
|
||||
decorator: Map<string, ts.Expression>,
|
||||
metadata: R3DirectiveMetadata,
|
||||
inputs: ClassPropertyMapping,
|
||||
outputs: ClassPropertyMapping,
|
||||
}|undefined {
|
||||
flags: HandlerFlags, annotateForClosureCompiler: boolean, defaultSelector: string|null = null):
|
||||
{decorator: Map<string, ts.Expression>, metadata: R3DirectiveMetadata}|undefined {
|
||||
let directive: Map<string, ts.Expression>;
|
||||
if (decorator === null || decorator.args === null || decorator.args.length === 0) {
|
||||
directive = new Map<string, ts.Expression>();
|
||||
@ -339,9 +331,6 @@ export function extractDirectiveMetadata(
|
||||
const type = wrapTypeReference(reflector, clazz);
|
||||
const internalType = new WrappedNodeExpr(reflector.getInternalNameOfClass(clazz));
|
||||
|
||||
const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields});
|
||||
const outputs = ClassPropertyMapping.fromMappedObject({...outputsFromMeta, ...outputsFromFields});
|
||||
|
||||
const metadata: R3DirectiveMetadata = {
|
||||
name: clazz.name.text,
|
||||
deps: ctorDeps,
|
||||
@ -349,8 +338,8 @@ export function extractDirectiveMetadata(
|
||||
lifecycle: {
|
||||
usesOnChanges,
|
||||
},
|
||||
inputs: inputs.toJointMappedObject(),
|
||||
outputs: outputs.toDirectMappedObject(),
|
||||
inputs: {...inputsFromMeta, ...inputsFromFields},
|
||||
outputs: {...outputsFromMeta, ...outputsFromFields},
|
||||
queries,
|
||||
viewQueries,
|
||||
selector,
|
||||
@ -363,12 +352,7 @@ export function extractDirectiveMetadata(
|
||||
exportAs,
|
||||
providers
|
||||
};
|
||||
return {
|
||||
decorator: directive,
|
||||
metadata,
|
||||
inputs,
|
||||
outputs,
|
||||
};
|
||||
return {decorator: directive, metadata};
|
||||
}
|
||||
|
||||
export function extractQueryMetadata(
|
||||
|
@ -1,105 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {CssSelector, SchemaMetadata, SelectorMatcher} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
import {DirectiveMeta, flattenInheritedDirectiveMetadata, MetadataReader} from '../../metadata';
|
||||
import {ClassDeclaration} from '../../reflection';
|
||||
import {ComponentScopeReader} from '../../scope';
|
||||
|
||||
/**
|
||||
* The scope that is used for type-check code generation of a component template.
|
||||
*/
|
||||
export interface TypeCheckScope {
|
||||
/**
|
||||
* A `SelectorMatcher` instance that contains the flattened directive metadata of all directives
|
||||
* that are in the compilation scope of the declaring NgModule.
|
||||
*/
|
||||
matcher: SelectorMatcher<DirectiveMeta>;
|
||||
|
||||
/**
|
||||
* The pipes that are available in the compilation scope.
|
||||
*/
|
||||
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>;
|
||||
|
||||
/**
|
||||
* The schemas that are used in this scope.
|
||||
*/
|
||||
schemas: SchemaMetadata[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes scope information to be used in template type checking.
|
||||
*/
|
||||
export class TypeCheckScopes {
|
||||
/**
|
||||
* Cache of flattened directive metadata. Because flattened metadata is scope-invariant it's
|
||||
* cached individually, such that all scopes refer to the same flattened metadata.
|
||||
*/
|
||||
private flattenedDirectiveMetaCache = new Map<ClassDeclaration, DirectiveMeta>();
|
||||
|
||||
/**
|
||||
* Cache of the computed type check scope per NgModule declaration.
|
||||
*/
|
||||
private scopeCache = new Map<ClassDeclaration, TypeCheckScope>();
|
||||
|
||||
constructor(private scopeReader: ComponentScopeReader, private metaReader: MetadataReader) {}
|
||||
|
||||
/**
|
||||
* Computes the type-check scope information for the component declaration. If the NgModule
|
||||
* contains an error, then 'error' is returned. If the component is not declared in any NgModule,
|
||||
* an empty type-check scope is returned.
|
||||
*/
|
||||
getTypeCheckScope(node: ClassDeclaration): TypeCheckScope|'error' {
|
||||
const matcher = new SelectorMatcher<DirectiveMeta>();
|
||||
const pipes = new Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>();
|
||||
|
||||
const scope = this.scopeReader.getScopeForComponent(node);
|
||||
if (scope === null) {
|
||||
return {matcher, pipes, schemas: []};
|
||||
} else if (scope === 'error') {
|
||||
return scope;
|
||||
}
|
||||
|
||||
if (this.scopeCache.has(scope.ngModule)) {
|
||||
return this.scopeCache.get(scope.ngModule)!;
|
||||
}
|
||||
|
||||
for (const meta of scope.compilation.directives) {
|
||||
if (meta.selector !== null) {
|
||||
const extMeta = this.getInheritedDirectiveMetadata(meta.ref);
|
||||
matcher.addSelectables(CssSelector.parse(meta.selector), extMeta);
|
||||
}
|
||||
}
|
||||
|
||||
for (const {name, ref} of scope.compilation.pipes) {
|
||||
if (!ts.isClassDeclaration(ref.node)) {
|
||||
throw new Error(`Unexpected non-class declaration ${
|
||||
ts.SyntaxKind[ref.node.kind]} for pipe ${ref.debugName}`);
|
||||
}
|
||||
pipes.set(name, ref as Reference<ClassDeclaration<ts.ClassDeclaration>>);
|
||||
}
|
||||
|
||||
const typeCheckScope: TypeCheckScope = {matcher, pipes, schemas: scope.schemas};
|
||||
this.scopeCache.set(scope.ngModule, typeCheckScope);
|
||||
return typeCheckScope;
|
||||
}
|
||||
|
||||
private getInheritedDirectiveMetadata(ref: Reference<ClassDeclaration>): DirectiveMeta {
|
||||
const clazz = ref.node;
|
||||
if (this.flattenedDirectiveMetaCache.has(clazz)) {
|
||||
return this.flattenedDirectiveMetaCache.get(clazz)!;
|
||||
}
|
||||
|
||||
const meta = flattenInheritedDirectiveMetadata(this.metaReader, ref);
|
||||
this.flattenedDirectiveMetaCache.set(clazz, meta);
|
||||
return meta;
|
||||
}
|
||||
}
|
@ -215,10 +215,8 @@ function createUnsuitableInjectionTokenError(
|
||||
makeRelatedInformation(
|
||||
reason.typeNode,
|
||||
'This type does not have a value, so it cannot be used as injection token.'),
|
||||
makeRelatedInformation(reason.decl, 'The type is declared here.'),
|
||||
];
|
||||
if (reason.decl !== null) {
|
||||
hints.push(makeRelatedInformation(reason.decl, 'The type is declared here.'));
|
||||
}
|
||||
break;
|
||||
case ValueUnavailableKind.TYPE_ONLY_IMPORT:
|
||||
chainMessage =
|
||||
|
@ -5,7 +5,6 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {CssSelector, DirectiveMeta as T2DirectiveMeta, parseTemplate, R3TargetBinder, SelectorMatcher, TmplAstElement} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFrom} from '../../file_system';
|
||||
@ -74,49 +73,6 @@ runInEachFileSystem(() => {
|
||||
expect(span.start.toString()).toContain('/entry.ts@5:22');
|
||||
expect(span.end.toString()).toContain('/entry.ts@5:29');
|
||||
});
|
||||
|
||||
it('should produce metadata compatible with template binding', () => {
|
||||
const src = `
|
||||
import {Directive, Input} from '@angular/core';
|
||||
|
||||
@Directive({selector: '[dir]'})
|
||||
export class TestDir {
|
||||
@Input('propName')
|
||||
fieldName: string;
|
||||
}
|
||||
`;
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: _('/node_modules/@angular/core/index.d.ts'),
|
||||
contents: 'export const Directive: any; export const Input: any;',
|
||||
},
|
||||
{
|
||||
name: _('/entry.ts'),
|
||||
contents: src,
|
||||
},
|
||||
]);
|
||||
|
||||
const analysis = analyzeDirective(program, 'TestDir');
|
||||
const matcher = new SelectorMatcher<T2DirectiveMeta>();
|
||||
const dirMeta: T2DirectiveMeta = {
|
||||
exportAs: null,
|
||||
inputs: analysis.inputs,
|
||||
outputs: analysis.outputs,
|
||||
isComponent: false,
|
||||
name: 'Dir',
|
||||
};
|
||||
matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta);
|
||||
|
||||
const {nodes} = parseTemplate('<div dir [propName]="expr"></div>', 'unimportant.html');
|
||||
const binder = new R3TargetBinder(matcher).bind({template: nodes});
|
||||
const propBinding = (nodes[0] as TmplAstElement).inputs[0];
|
||||
const propBindingConsumer = binder.getConsumerOfBinding(propBinding);
|
||||
|
||||
// Assert that the consumer of the binding is the directive, which means that the metadata
|
||||
// fed into the SelectorMatcher was compatible with the binder, and did not confuse property
|
||||
// and field names.
|
||||
expect(propBindingConsumer).toBe(dirMeta);
|
||||
});
|
||||
});
|
||||
|
||||
// Helpers
|
||||
|
@ -34,7 +34,6 @@ ts_library(
|
||||
"//packages/compiler-cli/src/ngtsc/transform",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"@npm//typescript",
|
||||
],
|
||||
|
@ -28,9 +28,8 @@ import {ComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeRe
|
||||
import {generatedFactoryTransform} from '../../shims';
|
||||
import {ivySwitchTransform} from '../../switch';
|
||||
import {aliasTransformFactory, declarationTransformFactory, DecoratorHandler, DtsTransformRegistry, ivyTransformFactory, TraitCompiler} from '../../transform';
|
||||
import {TemplateTypeCheckerImpl} from '../../typecheck';
|
||||
import {isTemplateDiagnostic, TemplateTypeCheckerImpl} from '../../typecheck';
|
||||
import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy} from '../../typecheck/api';
|
||||
import {isTemplateDiagnostic} from '../../typecheck/diagnostics';
|
||||
import {getSourceFileOrNull, isDtsPath, resolveModuleName} from '../../util/src/typescript';
|
||||
import {LazyRoute, NgCompilerAdapter, NgCompilerOptions} from '../api';
|
||||
|
||||
|
@ -56,11 +56,6 @@ export enum ErrorCode {
|
||||
*/
|
||||
HOST_BINDING_PARSE_ERROR = 5001,
|
||||
|
||||
/**
|
||||
* Raised when the compiler cannot parse a component's template.
|
||||
*/
|
||||
TEMPLATE_PARSE_ERROR = 5002,
|
||||
|
||||
/**
|
||||
* Raised when an NgModule contains an invalid reference in `declarations`.
|
||||
*/
|
||||
|
@ -22,10 +22,10 @@ export class InvalidFileSystem implements FileSystem {
|
||||
readFile(path: AbsoluteFsPath): string {
|
||||
throw makeError();
|
||||
}
|
||||
readFileBuffer(path: AbsoluteFsPath): Uint8Array {
|
||||
readFileBuffer(path: AbsoluteFsPath): Buffer {
|
||||
throw makeError();
|
||||
}
|
||||
writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive?: boolean): void {
|
||||
writeFile(path: AbsoluteFsPath, data: string|Buffer, exclusive?: boolean): void {
|
||||
throw makeError();
|
||||
}
|
||||
removeFile(path: AbsoluteFsPath): void {
|
||||
|
@ -23,10 +23,10 @@ export class NodeJSFileSystem implements FileSystem {
|
||||
readFile(path: AbsoluteFsPath): string {
|
||||
return fs.readFileSync(path, 'utf8');
|
||||
}
|
||||
readFileBuffer(path: AbsoluteFsPath): Uint8Array {
|
||||
readFileBuffer(path: AbsoluteFsPath): Buffer {
|
||||
return fs.readFileSync(path);
|
||||
}
|
||||
writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive: boolean = false): void {
|
||||
writeFile(path: AbsoluteFsPath, data: string|Buffer, exclusive: boolean = false): void {
|
||||
fs.writeFileSync(path, data, exclusive ? {flag: 'wx'} : undefined);
|
||||
}
|
||||
removeFile(path: AbsoluteFsPath): void {
|
||||
|
@ -37,8 +37,8 @@ export type PathSegment = BrandedPath<'PathSegment'>;
|
||||
export interface FileSystem {
|
||||
exists(path: AbsoluteFsPath): boolean;
|
||||
readFile(path: AbsoluteFsPath): string;
|
||||
readFileBuffer(path: AbsoluteFsPath): Uint8Array;
|
||||
writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive?: boolean): void;
|
||||
readFileBuffer(path: AbsoluteFsPath): Buffer;
|
||||
writeFile(path: AbsoluteFsPath, data: string|Buffer, exclusive?: boolean): void;
|
||||
removeFile(path: AbsoluteFsPath): void;
|
||||
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void;
|
||||
readdir(path: AbsoluteFsPath): PathSegment[];
|
||||
|
@ -38,16 +38,16 @@ export abstract class MockFileSystem implements FileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
readFileBuffer(path: AbsoluteFsPath): Uint8Array {
|
||||
readFileBuffer(path: AbsoluteFsPath): Buffer {
|
||||
const {entity} = this.findFromPath(path);
|
||||
if (isFile(entity)) {
|
||||
return entity instanceof Uint8Array ? entity : new Buffer(entity);
|
||||
return Buffer.isBuffer(entity) ? entity : new Buffer(entity);
|
||||
} else {
|
||||
throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`);
|
||||
}
|
||||
}
|
||||
|
||||
writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive: boolean = false): void {
|
||||
writeFile(path: AbsoluteFsPath, data: string|Buffer, exclusive: boolean = false): void {
|
||||
const [folderPath, basename] = this.splitIntoFolderAndFile(path);
|
||||
const {entity} = this.findFromPath(folderPath);
|
||||
if (entity === null || !isFolder(entity)) {
|
||||
@ -295,7 +295,7 @@ export type Entity = Folder|File|SymLink;
|
||||
export interface Folder {
|
||||
[pathSegments: string]: Entity;
|
||||
}
|
||||
export type File = string|Uint8Array;
|
||||
export type File = string|Buffer;
|
||||
export class SymLink {
|
||||
constructor(public path: AbsoluteFsPath) {}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user