Compare commits

..

39 Commits

Author SHA1 Message Date
e6ee7c2aeb release: cut the v11.0.0-next.0 release 2020-09-02 13:06:00 -07:00
54687f7765 release: cut the v10.1.0 release 2020-09-02 13:02:58 -07:00
59c234cfb4 build: add configuration for the caretaker command (#38601)
Add configuration information for the new caretaker command

PR Close #38601
2020-09-01 13:05:32 -07:00
a6f3cd93a9 feat(dev-infra): check services/status information of the repository for caretaker (#38601)
The angular team relies on a number of services for hosting code, running CI, etc. This
tool allows for checking the operational status of all services at once as well as the current
state of the repository with respect to merge and triage ready issues and prs.

PR Close #38601
2020-09-01 13:05:30 -07:00
d9fea857db fix(forms): ensure to emit statusChanges on subsequent value update/validations (#38354)
This commit ensures that the `updateValueAndValidity` method takes the
`asyncValidator` into consideration to emit on the `statusChanges` observables.
This is necessary so that any subsequent changes are emitted properly to any
subscribers.

Closes #20424
Closes #14542

BREAKING CHANGE:

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.

PR Close #38354
2020-09-01 10:36:31 -07:00
03dbcc7a56 build: update the package.json to 11.0.0-next.0 (#38667)
Update package.json version to reflect master targetting the next major
release train.
2020-09-01 10:35:41 -07:00
c142b071eb build: upgrade cli command docs sources to 32391604b (#38652)
Updating [angular#master](https://github.com/angular/angular/tree/master) from
[cli-builds#master](https://github.com/angular/cli-builds/tree/master).

##
Relevant changes in
[commit range](ef770f1cb...32391604b):

**Modified**
- help/build.json
- help/generate.json

PR Close #38652
2020-09-01 09:03:25 -07:00
71acf9dd49 docs: Restructure table of contents to provide a more streamlined experience. (#38353)
PR Close #38353
2020-08-31 16:16:54 -07:00
f5a148b1b7 fix(compiler): incorrectly inferring namespace for HTML nodes inside SVG (#38477)
The HTML parser gets an element's namespace either from the tag name
(e.g. `<svg:rect>`) or from its parent element `<svg><rect></svg>`) which
breaks down when an element is inside of an SVG `foreignElement`,
because foreign elements allow nodes from a different namespace to be
inserted into an SVG.

These changes add another flag to the tag definitions which tells child
nodes whether to try to inherit their namespaces from their parents.
It also adds a definition for `foreignObject` with the new flag,
allowing elements placed inside it to infer their namespaces instead.

Fixes #37218.

PR Close #38477
2020-08-31 13:25:38 -07:00
4f28192d62 refactor(dev-infra): use a mixin to require a github-token for an ng-dev command (#38630)
Creates a mixin for requiring a github token to be provided to a command.  This mixin
allows for a centralized management of the requirement and handling of the github-token.

PR Close #38630
2020-08-31 12:32:27 -07:00
0fc2bef0cd docs(service-worker): add links to service worker communication guide (#36847)
PR Close #36847
2020-08-31 11:41:16 -07:00
f5d1e9a2d1 docs(service-worker): add section to explain unrecoverable state (#36847)
PR Close #36847
2020-08-31 11:41:13 -07:00
036a2faf02 feat(service-worker): add UnrecoverableStateError (#36847)
In several occasions it has been observed when the browser has evicted
eagerly cached assets from the cache and which can also not be found on the
server anymore. This can lead to broken state where only parts of the application
will load and others will fail.

This commit fixes this issue by checking for the missing asset in the cache
and on the server. If this condition is true, the broken client will be
notified about the current state through the `UnrecoverableStateError`.

Closes #36539

PR Close #36847
2020-08-31 11:41:11 -07:00
5be4edfa17 fix(service-worker): fix condition to check for a cache-busted request (#36847)
Previously, the condition to make the cache busted was executing although
the network request was successful. However, this is not valid. The cache
should only be marked as busted when the request failed. This commit fixes
the invalid condition.

PR Close #36847
2020-08-31 11:41:09 -07:00
38d6596742 test(service-worker): add helper function remove individual cache (#36847)
This commit adds a helper method to remove individual cached items.

PR Close #36847
2020-08-31 11:41:07 -07:00
0a7a5e3aff docs: Remove confusion between do/avoid templates (#38647)
PR Close #38647
2020-08-31 10:25:16 -07:00
d5fabc303d refactor(forms): remove extra space in error message (#38637)
Remove extra whitespace at package/forms/model.ts error messages

PR Close #38637
2020-08-31 09:31:55 -07:00
ebc0e46501 refactor(dev-infra): improve error message for unexpected version branches (#38622)
Currently the merge script default branch configuration throws an error
if an unexpected version branch is discovered. The error right now
assumes to much knowledge of the logic and the document outlining
the release trains conceptually.

We change it to something more easy to understand that doesn't require
full understanding of the versioning/labeling/branching document that
has been created for the Angular organization.

PR Close #38622
2020-08-31 09:29:58 -07:00
3487b549fd feat(dev-infra): write outputs of command runs to ng-dev log file (#38599)
Creates infrastructure to write outputs of command runs to ng-dev log file.
Additionally, on commands which fail with an exit code greater than 1, an
error log file is created with the posix timestamp of the commands run time
as an identifier.

PR Close #38599
2020-08-31 08:47:15 -07:00
52c7a4bfc6 docs: ng generate module command doc change (#38480)
PR Close #38480
2020-08-31 08:43:19 -07:00
827ba05914 docs: remove first person and space in CircleCI in the testing guide. (#38631)
PR Close #38631
2020-08-31 08:42:04 -07:00
b2857b4e3a docs: remove double space in start-data. (#38642)
PR Close #38642
2020-08-31 08:41:30 -07:00
5d5caf21b8 docs: fix broken markdown in start/start-data (#38644)
PR Close #38644
2020-08-31 08:40:57 -07:00
c1bc070b40 refactor(dev-infra): remove style type from commit style guide (#38639)
The `style` commit type is not part of the commit parser config,
it should be removed from the documentation.

PR Close #38639
2020-08-31 08:40:14 -07:00
930eeaf177 fix(bazel): fix integration test for bazel building (#38629)
Update the API used to request a timestamp.  The previous API we relied on for this
test application, worldclockapi.com no longer serves times and simply 403s on all
requests.  This caused our test to timeout as the HTTP request did not handle a failure
case.  By moving to a new api, the HTTP request responds as expected and timeouts
are corrected as there is not longer a pending microtask in the queue.

PR Close #38629
2020-08-28 11:16:40 -07:00
2dd29fbae7 build: update ng-dev merge config to reflect new label updates (#38620)
Update the ng-dev merge configuration to reflect the new label updates

PR Close #38620
2020-08-28 08:03:21 -07:00
9613660fee ci: update angular robot to be based on new label updates (#38620)
Update the angular robot configuration to reflect the new label updates

PR Close #38620
2020-08-28 08:03:19 -07:00
c0523fc3b4 docs(forms): exclude internal-only methods and properties from docs (#38583)
Prior to this commit, a lot of internal-only class properties and methods (such as `ngOnChanges`)
of the Forms package directives were exposed on angular.io website. These fields are not expected
to be called externally (they are used/invoked by framework only), since they are part of internal
implementations of the following interfaces:

* Angular lifecycle hook interfaces
* ControlValueAccessor interface
* Validator interface

Having these internal-only fields in docs creates unnecessary noise on directive detail pages.
This commit adds the `@nodoc` annotation to these properties and methods to keep fields in the
golden files, but hide them in docs.

PR Close #38583
2020-08-27 16:39:38 -07:00
de1cffb23b build: update the package.json to 10.2.0-next.0
When the rc was cut for 10.1.0-rc.0, the package.json for master should be updated
to to the following next version, in this case 10.2.0-next.0.
2020-08-27 15:59:52 -07:00
31f4557621 ci: update github robot to reflect new target labels (#38428)
Updates the Github robot to reflect the updated target
labels that are used as part of the canonical versioning
and labeling for the Angular organization.

PR Close #38428
2020-08-27 14:52:44 -07:00
7723bfd9ba build: use new labeling and branching in merge script (#38428)
We introduced a new shared configuration for merge script
labels that follow the proposal of:
https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU

These label semantics and the branching are set up for the Angular
framework with this commit. The goal is that labeling and merging
is consistent between all Angular projects and that clear rules
are defined for branching. This was previously not the case.

PR Close #38428
2020-08-27 14:52:42 -07:00
e8ea839df8 docs: update docs to reflect new PR targeting methods for release trains (#38401)
As part of the migration to a common strategy/method for branching and releasing across
the main angular repositories, updates need to be made to the documentation. These changes
reflect the updates made and is based on the following document which describes the
merging label expectations: https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU

PR Close #38401
2020-08-27 14:52:04 -07:00
90cec40cce build: bump to lastest sha of angular/dev-infra's lock-closed action (#38615)
Update to the latest lock-closed action to fix the link which is used.

PR Close #38615
2020-08-27 10:58:29 -07:00
4036281007 docs: Update contributor info for kyliau (#38125)
Added twitter handle, website, and a short bio.

PR Close #38125
2020-08-27 10:39:30 -07:00
164cd274a4 test(docs-infra): add commands to run the unit tests (#34537)
This commit adds the necessary custom commands to run the tests in a
node environment.

PR Close #34537
2020-08-27 09:05:59 -07:00
fedcfec346 test(docs-infra): add missing tests for obserables and promises (#34537)
This commit adds missing tests for obserables and promises which
are both stand-alone mini-apps.

PR Close #34537
2020-08-27 09:05:56 -07:00
618cb32407 docs: add Sam Vloeberghs to GDE list (#37970)
PR Close #37970
2020-08-26 12:52:56 -07:00
4aee0087ea docs: remove outdated CircleCI link from DEVELOPER.md (#38554)
CircleCI updated their UI and now the URLs are different (and cannot be
constructed based on the build number alone). This commit removes a
reference of the obsolete URL pattern to avoid confusion.

Example old URL:
https://circleci.com/gh/angular/angular/<build-number>#artifacts

Example new URL:
https://app.circleci.com/pipelines/github/angular/angular/<build-number>/workflows/<workflow-id>/jobs/<job-id>/artifacts

PR Close #38554
2020-08-26 12:52:26 -07:00
0681a20d28 fix(docs-infra): add guard to not render anchor link for private api (#38589)
This commit adds a guard that prevents rendering anchor links for
private api's.

Closes #38564

PR Close #38589
2020-08-26 12:51:04 -07:00
221 changed files with 2750 additions and 7360 deletions

View File

@ -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
View File

@ -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

View File

@ -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 browsers 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))

View File

@ -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}`);
//////////////////////////////

View File

@ -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"
}
}

View File

@ -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

View File

@ -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.
*/

View File

@ -0,0 +1,12 @@
{
"tests": [
{
"cmd": "yarn",
"args": ["tsc", "--project", "tsconfig.spec.json", "--module", "commonjs"]
},
{
"cmd": "yarn",
"args": ["jasmine", "out-tsc/**/*.spec.js"]
}
]
}

View File

@ -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();
});
});
});

View File

@ -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;
}

View File

@ -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());
});
});

View File

@ -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;
}

View File

@ -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>

View File

@ -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

View File

@ -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) { ... }

View File

@ -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.

View File

@ -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 [Schematicsfor Libraries](guide/schematics-for-libraries).

View File

@ -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'.

View File

@ -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).

View File

@ -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.

View File

@ -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}

View File

@ -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'>

View File

@ -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`.
```

View File

@ -223,6 +223,6 @@ content harmlessly. The following is the browser output
of the `evilTitle` examples.
<code-example language="bash">
"Template &lt;script&gt;alert("evil never sleeps")&lt;/script&gt; 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>

View File

@ -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

View File

@ -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` |

View File

@ -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:

View File

@ -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

View File

@ -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"]
}
}

View File

@ -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"
}
}
}

View File

@ -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."
}
]
}

View File

@ -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",

View File

@ -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 -%}

View File

@ -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: {}}]
});
}

View File

@ -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",
],
)

View 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",
],
)

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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';

View File

@ -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",

View File

@ -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) {

View File

@ -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';

View File

@ -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',

View File

@ -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);
}

View File

@ -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,
};

View 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);
}

View File

@ -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',
};

View File

@ -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;
}

View File

@ -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);

View File

@ -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',
};

View File

@ -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);
});
});
});

View File

@ -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;
}

View File

@ -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;

View File

@ -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,
};

View File

@ -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",

View File

@ -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>",

View File

@ -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",

View File

@ -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};
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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 {

View File

@ -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;

View File

@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 146989,
"main-es2015": 147573,
"polyfills-es2015": 36571
}
}

View File

@ -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",

View File

@ -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) {

View File

@ -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.

View File

@ -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);

View File

@ -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;
}

View File

@ -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);

View File

@ -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;
});
}
}

View File

@ -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();
});
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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', () => {

View File

@ -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);

View File

@ -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) :

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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.
*/

View File

@ -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')]);
});

View File

@ -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();
});
});
});
});

View File

@ -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);
}
});

View File

@ -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",

View File

@ -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

View File

@ -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(

View File

@ -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;
}
}

View File

@ -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 =

View File

@ -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

View File

@ -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",
],

View File

@ -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';

View File

@ -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`.
*/

View File

@ -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 {

View File

@ -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 {

View File

@ -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[];

View File

@ -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