Compare commits
83 Commits
Author | SHA1 | Date | |
---|---|---|---|
fbcb66e70c | |||
3d94919800 | |||
af3b401e15 | |||
e4c12c8f9e | |||
ea36466060 | |||
58411e7ad9 | |||
84bd1a233d | |||
ef13d8f33a | |||
dc4f85888e | |||
2bdfb14be0 | |||
26deef2d3e | |||
f52a494248 | |||
ebede67433 | |||
565840515c | |||
c62d1cb80a | |||
aa43cbf8c5 | |||
b05d79d14a | |||
04c2bb9580 | |||
ec2dbe7fb4 | |||
8096c63c66 | |||
6ff9e6e2bd | |||
31d0ee4cbf | |||
a47383d1e8 | |||
9078187378 | |||
dcb473db34 | |||
edb7f90363 | |||
9c51ba321e | |||
d8714d045d | |||
5de2ac3e1b | |||
7669bd856f | |||
18d911d807 | |||
38ff66dc32 | |||
5672aba2f9 | |||
5567bdc48e | |||
1c156eb304 | |||
396548442e | |||
c54161098d | |||
f1b355b54f | |||
e40ffb95c8 | |||
20564f997f | |||
b1398d1771 | |||
7e9134aae8 | |||
0a55058440 | |||
bb1122d087 | |||
2ea49c7add | |||
83d69978fd | |||
62de2131e1 | |||
e156e29edd | |||
775c305771 | |||
190dca0fdc | |||
309709d4b2 | |||
028ef30b34 | |||
56d5ff2a89 | |||
b4eb016e56 | |||
6b0dba48b1 | |||
cfd4c0b4dc | |||
38762020d3 | |||
a1c34c6f0a | |||
b084bffb64 | |||
6a28675a5e | |||
4de8dc3554 | |||
ab4f953c78 | |||
ee432aaab8 | |||
5863537575 | |||
fcd2eb2ffb | |||
251a28cb15 | |||
54bb1c3d6a | |||
6c6dd5f38c | |||
9794f20674 | |||
027b041cfd | |||
4886cf5965 | |||
f21d50d2e6 | |||
0ef985368e | |||
0a277c6c40 | |||
9bf32c4dcb | |||
1c2ccfed4d | |||
25afbcc459 | |||
29c89c9297 | |||
efc76064d9 | |||
dbab74429f | |||
6aac499ee7 | |||
32f33f095f | |||
b0bd777ba9 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -40,6 +40,9 @@ yarn-error.log
|
|||||||
# User specific bazel settings
|
# User specific bazel settings
|
||||||
.bazelrc.user
|
.bazelrc.user
|
||||||
|
|
||||||
|
# User specific ng-dev settings
|
||||||
|
.ng-dev.user*
|
||||||
|
|
||||||
.notes.md
|
.notes.md
|
||||||
baseline.json
|
baseline.json
|
||||||
|
|
||||||
|
48
CHANGELOG.md
48
CHANGELOG.md
@ -1,3 +1,50 @@
|
|||||||
|
<a name="10.1.2"></a>
|
||||||
|
## 10.1.2 (2020-09-16)
|
||||||
|
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* **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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="10.1.1"></a>
|
||||||
|
## 10.1.1 (2020-09-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **compiler:** correct confusion between field and property names ([#38685](https://github.com/angular/angular/issues/38685)) ([a1c34c6](https://github.com/angular/angular/commit/a1c34c6))
|
||||||
|
* **compiler-cli:** compute source-mappings for localized strings ([#38747](https://github.com/angular/angular/issues/38747)) ([b4eb016](https://github.com/angular/angular/commit/b4eb016)), closes [#38588](https://github.com/angular/angular/issues/38588)
|
||||||
|
* **compiler-cli:** ensure that a declaration is available in type-to-value conversion ([#38684](https://github.com/angular/angular/issues/38684)) ([56d5ff2](https://github.com/angular/angular/commit/56d5ff2)), closes [#38670](https://github.com/angular/angular/issues/38670)
|
||||||
|
* **core:** reset `tView` between tests in Ivy TestBed ([#38659](https://github.com/angular/angular/issues/38659)) ([efc7606](https://github.com/angular/angular/commit/efc7606)), closes [#38600](https://github.com/angular/angular/issues/38600)
|
||||||
|
* **localize:** do not expose NodeJS typings in $localize runtime code ([#38700](https://github.com/angular/angular/issues/38700)) ([4de8dc3](https://github.com/angular/angular/commit/4de8dc3)), closes [#38692](https://github.com/angular/angular/issues/38692)
|
||||||
|
* **localize:** enable whitespace preservation marker in XLIFF files ([#38737](https://github.com/angular/angular/issues/38737)) ([190dca0](https://github.com/angular/angular/commit/190dca0)), closes [#38679](https://github.com/angular/angular/issues/38679)
|
||||||
|
* **localize:** install `[@angular](https://github.com/angular)/localize` in `devDependencies` by default ([#38680](https://github.com/angular/angular/issues/38680)) ([dbab744](https://github.com/angular/angular/commit/dbab744)), closes [#38329](https://github.com/angular/angular/issues/38329)
|
||||||
|
* **localize:** render context of translation file parse errors ([#38673](https://github.com/angular/angular/issues/38673)) ([32f33f0](https://github.com/angular/angular/commit/32f33f0)), closes [#38377](https://github.com/angular/angular/issues/38377)
|
||||||
|
* **localize:** render location in XLIFF 2 even if there is no metadata ([#38713](https://github.com/angular/angular/issues/38713)) ([ab4f953](https://github.com/angular/angular/commit/ab4f953)), closes [#38705](https://github.com/angular/angular/issues/38705)
|
||||||
|
* **ngcc:** use aliased exported types correctly ([#38666](https://github.com/angular/angular/issues/38666)) ([6a28675](https://github.com/angular/angular/commit/6a28675)), closes [#38238](https://github.com/angular/angular/issues/38238)
|
||||||
|
* **router:** If users are using the Alt key when clicking the router links, prioritize browser’s default behavior ([#38375](https://github.com/angular/angular/issues/38375)) ([309709d](https://github.com/angular/angular/commit/309709d))
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* **core:** use `ngDevMode` to tree-shake error messages ([#38612](https://github.com/angular/angular/issues/38612)) ([b084bff](https://github.com/angular/angular/commit/b084bff))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="10.1.0"></a>
|
<a name="10.1.0"></a>
|
||||||
# 10.1.0 (2020-09-02)
|
# 10.1.0 (2020-09-02)
|
||||||
|
|
||||||
@ -52,7 +99,6 @@
|
|||||||
* **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:** 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:** 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:** 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
|
### 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))
|
* **router:** export DefaultRouteReuseStrategy to Router public_api ([#31575](https://github.com/angular/angular/issues/31575)) ([ca79880](https://github.com/angular/angular/commit/ca79880))
|
||||||
|
@ -16,13 +16,6 @@ import {BuildNums, PrNums, SHA} from './constants';
|
|||||||
|
|
||||||
const logger = new Logger('mock-external-apis');
|
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_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN');
|
||||||
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
||||||
|
|
||||||
@ -91,8 +84,8 @@ const createArchive = (buildNum: number, prNum: number, sha: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create request scopes
|
// Create request scopes
|
||||||
const circleCiApi = nock(CIRCLE_CI_API_HOST).log(log).persist();
|
const circleCiApi = nock(CIRCLE_CI_API_HOST).persist();
|
||||||
const githubApi = nock(GITHUB_API_HOST).log(log).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`);
|
const githubApi = nock(GITHUB_API_HOST).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`);
|
||||||
|
|
||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
|
|
||||||
|
@ -27,28 +27,28 @@
|
|||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"delete-empty": "^3.0.0",
|
"delete-empty": "^3.0.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"jasmine": "^3.5.0",
|
"jasmine": "^3.6.1",
|
||||||
"nock": "^12.0.3",
|
"nock": "^13.0.4",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.1",
|
||||||
"shelljs": "^0.8.4",
|
"shelljs": "^0.8.4",
|
||||||
"source-map-support": "^0.5.19",
|
"source-map-support": "^0.5.19",
|
||||||
"tar-stream": "^2.1.2",
|
"tar-stream": "^2.1.3",
|
||||||
"tslib": "^1.11.1"
|
"tslib": "^2.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/body-parser": "^1.19.0",
|
"@types/body-parser": "^1.19.0",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.8",
|
||||||
"@types/jasmine": "^3.5.10",
|
"@types/jasmine": "^3.5.14",
|
||||||
"@types/nock": "^11.1.0",
|
"@types/nock": "^11.1.0",
|
||||||
"@types/node": "^13.13.2",
|
"@types/node": "^14.6.4",
|
||||||
"@types/node-fetch": "^2.5.7",
|
"@types/node-fetch": "^2.5.7",
|
||||||
"@types/shelljs": "^0.8.7",
|
"@types/shelljs": "^0.8.8",
|
||||||
"@types/supertest": "^2.0.8",
|
"@types/supertest": "^2.0.10",
|
||||||
"nodemon": "^2.0.3",
|
"nodemon": "^2.0.4",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"supertest": "^4.0.2",
|
"supertest": "^4.0.2",
|
||||||
"tslint": "^6.1.1",
|
"tslint": "^6.1.3",
|
||||||
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
|
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^4.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,23 +214,24 @@ describe('GithubApi', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should call \'https.request()\' with the correct options', () => {
|
it('should call \'https.request()\' with the correct options', async () => {
|
||||||
const requestHandler = nock('https://api.github.com')
|
const requestHandler = nock('https://api.github.com')
|
||||||
.intercept('/path', 'method')
|
.intercept('/path', 'method')
|
||||||
.reply(200);
|
.reply(200);
|
||||||
|
|
||||||
(api as any).request('method', '/path');
|
await (api as any).request('method', '/path');
|
||||||
requestHandler.done();
|
requestHandler.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should add the \'Authorization\' header containing the \'githubToken\'', () => {
|
it('should add the \'Authorization\' header containing the \'githubToken\'', async () => {
|
||||||
const requestHandler = nock('https://api.github.com')
|
const requestHandler = nock('https://api.github.com')
|
||||||
.intercept('/path', 'method', undefined, {
|
.intercept('/path', 'method', undefined, {
|
||||||
reqheaders: {Authorization: 'token 12345'},
|
reqheaders: {Authorization: 'token 12345'},
|
||||||
})
|
})
|
||||||
.reply(200);
|
.reply(200);
|
||||||
(api as any).request('method', '/path');
|
|
||||||
|
await (api as any).request('method', '/path');
|
||||||
requestHandler.done();
|
requestHandler.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -244,12 +245,13 @@ describe('GithubApi', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should \'JSON.stringify\' and send the data along with the request', () => {
|
it('should \'JSON.stringify\' and send the data along with the request', async () => {
|
||||||
const data = {key: 'value'};
|
const data = {key: 'value'};
|
||||||
const requestHandler = nock('https://api.github.com')
|
const requestHandler = nock('https://api.github.com')
|
||||||
.intercept('/path', 'method', JSON.stringify(data))
|
.intercept('/path', 'method', JSON.stringify(data))
|
||||||
.reply(200);
|
.reply(200);
|
||||||
(api as any).request('method', '/path', data);
|
|
||||||
|
await (api as any).request('method', '/path', data);
|
||||||
requestHandler.done();
|
requestHandler.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -41,7 +41,6 @@
|
|||||||
<!-- #enddocregion translated-plural -->
|
<!-- #enddocregion translated-plural -->
|
||||||
<!-- #docregion translated-select -->
|
<!-- #docregion translated-select -->
|
||||||
<!-- #docregion translate-select-1 -->
|
<!-- #docregion translate-select-1 -->
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" datatype="html">
|
<trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" datatype="html">
|
||||||
<source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
|
<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>
|
<target>L'auteur est <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></target>
|
||||||
|
@ -62,7 +62,7 @@ In the following example, the `@Component()` metadata object and the class const
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-typical',
|
selector: 'app-typical',
|
||||||
template: '<div>A typical component for {{data.name}}</div>'
|
template: '<div>A typical component for {{data.name}}</div>'
|
||||||
)}
|
})
|
||||||
export class TypicalComponent {
|
export class TypicalComponent {
|
||||||
@Input() data: TypicalData;
|
@Input() data: TypicalData;
|
||||||
constructor(private someService: SomeService) { ... }
|
constructor(private someService: SomeService) { ... }
|
||||||
|
@ -125,7 +125,7 @@ Emulated is the default and most commonly used view encapsulation. For more info
|
|||||||
|
|
||||||
<div class="alert is-important">
|
<div class="alert is-important">
|
||||||
|
|
||||||
The shadow-piercing descendant combinator is deprecated and [support is being removed from major browsers](https://www.chromestatus.com/features/6750456638341120) and tools.
|
The shadow-piercing descendant combinator is deprecated and [support is being removed from major browsers](https://www.chromestatus.com/feature/6750456638341120) and tools.
|
||||||
As such we plan to drop support in Angular (for all 3 of `/deep/`, `>>>` and `::ng-deep`).
|
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.
|
Until then `::ng-deep` should be preferred for a broader compatibility with the tools.
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ The `ng generate` command creates the `projects/my-lib` folder in your workspace
|
|||||||
|
|
||||||
</div>
|
</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">
|
<code-example format="json">
|
||||||
"projects": {
|
"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.
|
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.
|
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 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.
|
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.
|
||||||
In general, the more complex the customization, the more useful the schematic approach.
|
In general, the more complex the customization, the more useful the schematic approach.
|
||||||
|
|
||||||
To learn more, see [Schematics Overview](guide/schematics) and [Schematics for Libraries](guide/schematics-for-libraries).
|
To learn more, see [Schematics Overview](guide/schematics) and [Schematics for Libraries](guide/schematics-for-libraries).
|
||||||
|
@ -511,9 +511,9 @@ Each script tag has a `type="module"` or `nomodule` attribute. Browsers with nat
|
|||||||
|
|
||||||
To include differential loading in your application builds, you must configure the Browserslist and TypeScript configuration files in your application project.
|
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 `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.
|
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.
|
||||||
|
|
||||||
<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.
|
# 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:
|
# For additional information regarding the format and rule options, please see:
|
||||||
# https://github.com/browserslist/browserslist#queries
|
# https://github.com/browserslist/browserslist#queries
|
||||||
@ -527,7 +527,7 @@ The following examples show a `browserlistrc` and `tsconfig.json` file for a new
|
|||||||
last 1 Chrome version
|
last 1 Chrome version
|
||||||
last 1 Firefox version
|
last 1 Firefox version
|
||||||
last 2 Edge major versions
|
last 2 Edge major versions
|
||||||
last 2 Safari major version
|
last 2 Safari major versions
|
||||||
last 2 iOS major versions
|
last 2 iOS major versions
|
||||||
Firefox ESR
|
Firefox ESR
|
||||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||||
|
@ -119,7 +119,14 @@ 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.
|
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: `ng add @angular/elements --project=*your_project_name*`.
|
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>
|
||||||
|
|
||||||
- For more information about polyfills, see [polyfill documentation](https://www.webcomponents.org/polyfills).
|
- 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).
|
- For more information about Angular browser support, see [Browser Support](guide/browser-support).
|
||||||
|
@ -76,6 +76,12 @@ All router components must be entry components. Because this would require you t
|
|||||||
|
|
||||||
## The `entryComponents` array
|
## 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
|
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 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.
|
you must add it to `entryComponents` explicitly.
|
||||||
|
@ -627,6 +627,11 @@ 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).
|
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 O}
|
||||||
|
|
||||||
{@a observable}
|
{@a observable}
|
||||||
|
@ -62,6 +62,8 @@ 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.
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style='vertical-align:top'>
|
<tr style='vertical-align:top'>
|
||||||
|
@ -223,6 +223,6 @@ content harmlessly. The following is the browser output
|
|||||||
of the `evilTitle` examples.
|
of the `evilTitle` examples.
|
||||||
|
|
||||||
<code-example language="bash">
|
<code-example language="bash">
|
||||||
"Template <script>alert("evil never sleeps")</script> Syntax" is the interpolated 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.
|
"Template Syntax" is the property bound evil title.
|
||||||
</code-example>
|
</code-example>
|
||||||
|
@ -94,7 +94,7 @@ All of our major releases are supported for 18 months.
|
|||||||
|
|
||||||
* 6 months of *active support*, during which regularly-scheduled updates and patches are released.
|
* 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 are released.
|
* 12 months of *long-term support (LTS)*, during which only [critical fixes and security patches](#lts-fixes) are released.
|
||||||
|
|
||||||
The following table provides the status for Angular versions under support.
|
The following table provides the status for Angular versions under support.
|
||||||
|
|
||||||
@ -102,11 +102,18 @@ The following table provides the status for Angular versions under support.
|
|||||||
Version | Status | Released | Active Ends | LTS Ends
|
Version | Status | Released | Active Ends | LTS Ends
|
||||||
------- | ------ | ------------ | ------------ | ------------
|
------- | ------ | ------------ | ------------ | ------------
|
||||||
^10.0.0 | Active | Jun 24, 2020 | Dec 24, 2020 | Dec 24, 2021
|
^10.0.0 | Active | Jun 24, 2020 | Dec 24, 2020 | Dec 24, 2021
|
||||||
^9.0.0 | Active | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
|
^9.0.0 | LTS | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
|
||||||
^8.0.0 | LTS | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020
|
^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.
|
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}
|
{@a deprecation}
|
||||||
## Deprecation practices
|
## Deprecation practices
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ Notice that all of the files the browser needs to render this application are ca
|
|||||||
<div class="alert is-helpful">
|
<div class="alert is-helpful">
|
||||||
Pay attention to two key points:
|
Pay attention to two key points:
|
||||||
|
|
||||||
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. 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. If `resourcesOutputPath` or `assets` paths are modified after the generation of configuration file, you need to change the paths manually in `ngsw-config.json`.
|
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>
|
</div>
|
||||||
|
BIN
aio/content/images/bios/samvloeberghs.jpg
Normal file
BIN
aio/content/images/bios/samvloeberghs.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
aio/content/images/bios/thekiba.jpg
Normal file
BIN
aio/content/images/bios/thekiba.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
@ -810,5 +810,21 @@
|
|||||||
"website": "kreuzercode.com",
|
"website": "kreuzercode.com",
|
||||||
"bio": "Kevin is a passionate freelance front-end engineer and Google Developer Expert based in Switzerland. He is a JavaScript enthusiast and fascinated by Angular. Kevin always tries to learn new things, expand his knowledge, and share it with others in the form of blog posts, workshops, podcasts, or presentations. He is a writer for various publications and the most active writer on Angular in-depth in 2019. Contributing to multiple projects and maintaining 7 npm packages, Kevin is also a big believer in open source. Furthermore, Kevin is a big football fan. Since his childhood, he has supported Real Madrid, which you might notice in a lot of his blog posts and tutorials.",
|
"bio": "Kevin is a passionate freelance front-end engineer and Google Developer Expert based in Switzerland. He is a JavaScript enthusiast and fascinated by Angular. Kevin always tries to learn new things, expand his knowledge, and share it with others in the form of blog posts, workshops, podcasts, or presentations. He is a writer for various publications and the most active writer on Angular in-depth in 2019. Contributing to multiple projects and maintaining 7 npm packages, Kevin is also a big believer in open source. Furthermore, Kevin is a big football fan. Since his childhood, he has supported Real Madrid, which you might notice in a lot of his blog posts and tutorials.",
|
||||||
"groups": ["GDE"]
|
"groups": ["GDE"]
|
||||||
|
},
|
||||||
|
"samvloeberghs": {
|
||||||
|
"name": "Sam Vloeberghs",
|
||||||
|
"picture": "samvloeberghs.jpg",
|
||||||
|
"groups": ["GDE"],
|
||||||
|
"twitter": "samvloeberghs",
|
||||||
|
"website": "https://samvloeberghs.be",
|
||||||
|
"bio": "Sam is a freelance software architect and Internet entrepreneur, currently focusing on frontend technologies. Co-organiser of the Belgian Angular conference NG-BE and Angular Belgium Meetup group."
|
||||||
|
},
|
||||||
|
"thekiba": {
|
||||||
|
"name": "Andrew Grekov",
|
||||||
|
"picture": "thekiba.jpg",
|
||||||
|
"twitter": "thekiba_io",
|
||||||
|
"website": "https://thekiba.io",
|
||||||
|
"bio": "Andrew is a software engineer using Angular and .NET. He spends most of his spare time staying up-to-date, helping other people, and experimenting with web tech.",
|
||||||
|
"groups": ["GDE"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,10 @@
|
|||||||
"url": "https://dev.to/t/angular",
|
"url": "https://dev.to/t/angular",
|
||||||
"title": "DEV Community"
|
"title": "DEV Community"
|
||||||
},
|
},
|
||||||
"angular-in-depth": {
|
"indepth-dev": {
|
||||||
"desc": "The place where advanced Angular concepts are explained",
|
"desc": "Peer-reviewed Angular articles and tutorials.",
|
||||||
"url": "https://blog.angularindepth.com",
|
"url": "https://indepth.dev/angular/",
|
||||||
"title": "Angular In Depth"
|
"title": "Angular inDepth"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -63,6 +63,12 @@
|
|||||||
"logo": "",
|
"logo": "",
|
||||||
"title": "NgRuAir",
|
"title": "NgRuAir",
|
||||||
"url": "https://github.com/ngRuAir/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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -429,6 +435,12 @@
|
|||||||
"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).",
|
"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)",
|
"title": "Awade Jigsaw (Chinese)",
|
||||||
"url": "https://jigsaw-zte.gitee.io"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,54 +48,13 @@
|
|||||||
{
|
{
|
||||||
"url": "docs",
|
"url": "docs",
|
||||||
"title": "Introduction",
|
"title": "Introduction",
|
||||||
"tooltip": "Introduction to the Angular documentation",
|
"tooltip": "Welcome to the Angular documentation set.",
|
||||||
"hidden": false
|
"hidden": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Getting Started",
|
"title": "Getting Started",
|
||||||
"tooltip": "Set up your environment and learn basic concepts",
|
"tooltip": "Set up your environment and learn basic concepts",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"url": "guide/setup-local",
|
|
||||||
"title": "Setup",
|
|
||||||
"tooltip": "Setting up for local development with the Angular CLI."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Angular Concepts",
|
|
||||||
"tooltip": "Introduction to basic concepts for Angular applications.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"url": "guide/architecture",
|
|
||||||
"title": "Intro to Basic Concepts",
|
|
||||||
"tooltip": "Basic building blocks of Angular applications."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/architecture-modules",
|
|
||||||
"title": "Intro to Modules",
|
|
||||||
"tooltip": "About NgModules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/architecture-components",
|
|
||||||
"title": "Intro to Components",
|
|
||||||
"tooltip": "About Components, Templates, and Views."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/architecture-services",
|
|
||||||
"title": "Intro to Services and DI",
|
|
||||||
"tooltip": "About services and dependency injection."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/architecture-next-steps",
|
|
||||||
"title": "Next Steps",
|
|
||||||
"tooltip": "Beyond the basics."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/glossary",
|
|
||||||
"title": "Angular Glossary",
|
|
||||||
"tooltip": "Brief definitions of the most important words in the Angular vocabulary."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"title": "Try it",
|
"title": "Try it",
|
||||||
"tooltip": "Examine and work with a ready-made sample app, with no setup.",
|
"tooltip": "Examine and work with a ready-made sample app, with no setup.",
|
||||||
@ -128,68 +87,59 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Tutorial: Tour of Heroes",
|
"url": "guide/setup-local",
|
||||||
"tooltip": "The Tour of Heroes app is used as a reference point in many Angular examples.",
|
"title": "Setup",
|
||||||
"children": [
|
"tooltip": "Setting up for local development with the Angular CLI."
|
||||||
{
|
|
||||||
"url": "tutorial",
|
|
||||||
"title": "Introduction",
|
|
||||||
"tooltip": "Introduction to the Tour of Heroes app and tutorial"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt0",
|
|
||||||
"title": "Create a Project",
|
|
||||||
"tooltip": "Creating the application shell"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt1",
|
|
||||||
"title": "1. The Hero Editor",
|
|
||||||
"tooltip": "Part 1: Build a simple editor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt2",
|
|
||||||
"title": "2. Display a List",
|
|
||||||
"tooltip": "Part 2: Build a master/detail page with a list of heroes."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt3",
|
|
||||||
"title": "3. Create a Feature Component",
|
|
||||||
"tooltip": "Part 3: Refactor the master/detail views into separate components."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt4",
|
|
||||||
"title": "4. Add Services",
|
|
||||||
"tooltip": "Part 4: Create a reusable service to manage hero data."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt5",
|
|
||||||
"title": "5. Add In-app Navigation",
|
|
||||||
"tooltip": "Part 5: Add the Angular router and navigate among the views."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt6",
|
|
||||||
"title": "6. Get Data from a Server",
|
|
||||||
"tooltip": "Part 6: Use HTTP to retrieve and save hero data."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Fundamentals",
|
"title": "Main Concepts",
|
||||||
"tooltip": "The fundamentals of Angular",
|
"tooltip": "Learn the concepts essential to becoming a proficient Angular developer.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"title": "Components & Templates",
|
"title": "Components",
|
||||||
"tooltip": "Building dynamic views with data binding",
|
"tooltip": "Building dynamic views with data binding",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"url": "guide/displaying-data",
|
"url": "guide/user-input",
|
||||||
"title": "Displaying Data",
|
"title": "User Input",
|
||||||
"tooltip": "Property binding helps show app data in the UI."
|
"tooltip": "User input triggers DOM events. Angular listens to those events with event bindings that funnel updated values back into your app's components and models."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Template Syntax",
|
"url": "guide/pipes",
|
||||||
|
"title": "Pipes",
|
||||||
|
"tooltip": "Pipes transform displayed values within a template."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/lifecycle-hooks",
|
||||||
|
"title": "Component Lifecycle",
|
||||||
|
"tooltip": "Angular calls lifecycle hook methods on directives and components as it creates, changes, and destroys them."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/component-interaction",
|
||||||
|
"title": "Component Interaction",
|
||||||
|
"tooltip": "Share information between different directives and components."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/component-styles",
|
||||||
|
"title": "Component Styles",
|
||||||
|
"tooltip": "Add CSS styles that are specific to a component."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/dynamic-component-loader",
|
||||||
|
"title": "Dynamic Components",
|
||||||
|
"tooltip": "Load components dynamically."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/elements",
|
||||||
|
"title": "Angular Elements",
|
||||||
|
"tooltip": "Convert components to Custom Elements."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Templates",
|
||||||
"tooltip": "Syntax to use in templates for binding, expressions, and directives.",
|
"tooltip": "Syntax to use in templates for binding, expressions, and directives.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
@ -232,11 +182,6 @@
|
|||||||
"title": "Two-way binding",
|
"title": "Two-way binding",
|
||||||
"tooltip": "Introductory guide to sharing data between a class and a template."
|
"tooltip": "Introductory guide to sharing data between a class and a template."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"url": "guide/built-in-directives",
|
|
||||||
"title": "Built-in directives",
|
|
||||||
"tooltip": "Introductory guide to some of the most popular built-in directives."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "guide/template-reference-variables",
|
"url": "guide/template-reference-variables",
|
||||||
"title": "Template reference variables",
|
"title": "Template reference variables",
|
||||||
@ -260,9 +205,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/user-input",
|
"title": "Directives",
|
||||||
"title": "User Input",
|
"tooltip": "Control the behavior of elements and the layout of your pages with directives.",
|
||||||
"tooltip": "User input triggers DOM events. Angular listens to those events with event bindings that funnel updated values back into your app's components and models."
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/built-in-directives",
|
||||||
|
"title": "Built-in directives",
|
||||||
|
"tooltip": "Introductory guide to some of the most popular built-in directives."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/attribute-directives",
|
"url": "guide/attribute-directives",
|
||||||
@ -273,164 +222,6 @@
|
|||||||
"url": "guide/structural-directives",
|
"url": "guide/structural-directives",
|
||||||
"title": "Structural Directives",
|
"title": "Structural Directives",
|
||||||
"tooltip": "Structural directives manipulate the layout of the page."
|
"tooltip": "Structural directives manipulate the layout of the page."
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/pipes",
|
|
||||||
"title": "Pipes",
|
|
||||||
"tooltip": "Pipes transform displayed values within a template."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/lifecycle-hooks",
|
|
||||||
"title": "Hook into the Component Lifecycle",
|
|
||||||
"tooltip": "Angular calls lifecycle hook methods on directives and components as it creates, changes, and destroys them."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/component-interaction",
|
|
||||||
"title": "Component Interaction",
|
|
||||||
"tooltip": "Share information between different directives and components."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/component-styles",
|
|
||||||
"title": "Component Styles",
|
|
||||||
"tooltip": "Add CSS styles that are specific to a component."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/dynamic-component-loader",
|
|
||||||
"title": "Dynamic Components",
|
|
||||||
"tooltip": "Load components dynamically."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/elements",
|
|
||||||
"title": "Angular Elements",
|
|
||||||
"tooltip": "Convert components to Custom Elements."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Forms for User Input",
|
|
||||||
"tooltip": "Forms creates a cohesive, effective, and compelling data entry experience.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"url": "guide/forms-overview",
|
|
||||||
"title": "Introduction",
|
|
||||||
"tooltip": "An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/reactive-forms",
|
|
||||||
"title": "Reactive Forms",
|
|
||||||
"tooltip": "Create a reactive form using FormBuilder, groups, and arrays."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/form-validation",
|
|
||||||
"title": "Validate form input",
|
|
||||||
"tooltip": "Validate user's form entries."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/dynamic-form",
|
|
||||||
"title": "Building Dynamic Forms",
|
|
||||||
"tooltip": "Create dynamic form templates using FormGroup."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Observables & RxJS",
|
|
||||||
"tooltip": "Using observables for message passing in Angular.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"url": "guide/observables",
|
|
||||||
"title": "Observables Overview",
|
|
||||||
"tooltip": "Using observables to pass values synchronously or asynchronously."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/rx-library",
|
|
||||||
"title": "The RxJS Library",
|
|
||||||
"tooltip": "A library for reactive programming using observables to compose asynchronous or callback-based code."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/observables-in-angular",
|
|
||||||
"title": "Observables in Angular",
|
|
||||||
"tooltip": "How Angular subsystems use and expect observables."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/practical-observable-usage",
|
|
||||||
"title": "Practical Usage",
|
|
||||||
"tooltip": "Domains in which observables are particularly useful."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/comparing-observables",
|
|
||||||
"title": "Compare to Other Techniques",
|
|
||||||
"tooltip": "How observables compare to promises and other message passing techniques."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "NgModules",
|
|
||||||
"tooltip": "NgModules.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"url": "guide/ngmodules",
|
|
||||||
"title": "NgModules Introduction",
|
|
||||||
"tooltip": "Use NgModules to make your apps efficient."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/ngmodule-vs-jsmodule",
|
|
||||||
"title": "JS Modules vs NgModules",
|
|
||||||
"tooltip": "Differentiate between JavaScript modules and NgModules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/bootstrapping",
|
|
||||||
"title": "Launching Apps with a Root Module",
|
|
||||||
"tooltip": "Tell Angular how to construct and bootstrap the app in the root \"AppModule\"."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/frequent-ngmodules",
|
|
||||||
"title": "Frequently Used NgModules",
|
|
||||||
"tooltip": "Introduction to the most frequently used NgModules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/module-types",
|
|
||||||
"title": "Types of Feature Modules",
|
|
||||||
"tooltip": "Description of the different types of feature modules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/entry-components",
|
|
||||||
"title": "Entry Components",
|
|
||||||
"tooltip": "All about entry components in Angular."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/feature-modules",
|
|
||||||
"title": "Feature Modules",
|
|
||||||
"tooltip": "Create feature modules to organize your code."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/providers",
|
|
||||||
"title": "Providing Dependencies",
|
|
||||||
"tooltip": "Providing dependencies to NgModules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/singleton-services",
|
|
||||||
"title": "Singleton Services",
|
|
||||||
"tooltip": "Creating singleton services."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/lazy-loading-ngmodules",
|
|
||||||
"title": "Lazy Loading Feature Modules",
|
|
||||||
"tooltip": "Lazy load modules to speed up your apps."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/sharing-ngmodules",
|
|
||||||
"title": "Sharing NgModules",
|
|
||||||
"tooltip": "Share NgModules to streamline your apps."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/ngmodule-api",
|
|
||||||
"title": "NgModule API",
|
|
||||||
"tooltip": "Understand the details of NgModules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/ngmodule-faq",
|
|
||||||
"title": "NgModule FAQs",
|
|
||||||
"tooltip": "Answers to frequently asked questions about NgModules."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -464,28 +255,54 @@
|
|||||||
"tooltip": "Use the injection tree to find parent components."
|
"tooltip": "Use the injection tree to find parent components."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/http",
|
"title": "Built-in Features",
|
||||||
"title": "Access Servers over HTTP",
|
"tooltip": "Learn how to add Angular's built-in features to add functionality to your applications.",
|
||||||
"tooltip": "Use HTTP to talk to a remote server."
|
"children": [
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "guide/router",
|
"url": "guide/router",
|
||||||
"title": "Routing & Navigation",
|
"title": "Routing & Navigation",
|
||||||
"tooltip": "Build in-app navigation among views using the Angular Router."
|
"tooltip": "Build in-app navigation among views using the Angular Router."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/security",
|
"title": "Forms",
|
||||||
"title": "Security",
|
"tooltip": "Forms creates a cohesive, effective, and compelling data entry experience.",
|
||||||
"tooltip": "Developing for content security in Angular applications."
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/forms-overview",
|
||||||
|
"title": "Introduction",
|
||||||
|
"tooltip": "An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/reactive-forms",
|
||||||
|
"title": "Reactive Forms",
|
||||||
|
"tooltip": "Create a reactive form using FormBuilder, groups, and arrays."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/form-validation",
|
||||||
|
"title": "Validate form input",
|
||||||
|
"tooltip": "Validate user's form entries."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/dynamic-form",
|
||||||
|
"title": "Building Dynamic Forms",
|
||||||
|
"tooltip": "Create dynamic form templates using FormGroup."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Techniques",
|
"url": "guide/http",
|
||||||
"tooltip": "Techniques for putting Angular to work in your environment",
|
"title": "HTTP Client",
|
||||||
"children": [
|
"tooltip": "Use HTTP to talk to a remote server."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/i18n",
|
||||||
|
"title": "Internationalization (i18n)",
|
||||||
|
"tooltip": "Translate the app's template text into multiple languages."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Animations",
|
"title": "Animations",
|
||||||
"tooltip": "Enhance the user experience with animation.",
|
"tooltip": "Enhance the user experience with animation.",
|
||||||
@ -518,14 +335,25 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/i18n",
|
"title": "Schematics",
|
||||||
"title": "Internationalization (i18n)",
|
"tooltip": "Understanding schematics.",
|
||||||
"tooltip": "Translate the app's template text into multiple languages."
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/schematics",
|
||||||
|
"title": "Schematics Overview",
|
||||||
|
"tooltip": "Extending CLI generation capabilities."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/accessibility",
|
"url": "guide/schematics-authoring",
|
||||||
"title": "Accessibility",
|
"title": "Authoring Schematics",
|
||||||
"tooltip": "Design apps to be accessible to all users."
|
"tooltip": "Understand the structure of a schematic."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/schematics-for-libraries",
|
||||||
|
"title": "Schematics for Libraries",
|
||||||
|
"tooltip": "Use schematics to integrate your library with the Angular CLI."
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Service Workers & PWA",
|
"title": "Service Workers & PWA",
|
||||||
@ -562,53 +390,27 @@
|
|||||||
"tooltip": "Configuring service worker caching behavior."
|
"tooltip": "Configuring service worker caching behavior."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/web-worker",
|
|
||||||
"title": "Web Workers",
|
|
||||||
"tooltip": "Using web workers for background processing."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/universal",
|
|
||||||
"title": "Server-side Rendering",
|
|
||||||
"tooltip": "Render HTML server-side with Angular Universal."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Dev Workflow",
|
"title": "Best Practices",
|
||||||
"tooltip": "Build, testing, and deployment information.",
|
"tooltip": "Learn how to build robust, scalable applications.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"title": "AOT Compiler",
|
"url": "guide/security",
|
||||||
"tooltip": "Understanding ahead-of-time compilation.",
|
"title": "Security",
|
||||||
"children": [
|
"tooltip": "Developing for content security in Angular applications."
|
||||||
{
|
|
||||||
"url": "guide/aot-compiler",
|
|
||||||
"title": "Ahead-of-Time Compilation",
|
|
||||||
"tooltip": "Learn why and how to use the Ahead-of-Time (AOT) compiler."
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/angular-compiler-options",
|
"url": "guide/accessibility",
|
||||||
"title": "Angular Compiler Options",
|
"title": "Accessibility",
|
||||||
"tooltip": "Configuring AOT compilation."
|
"tooltip": "Design apps to be accessible to all users."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/aot-metadata-errors",
|
"url": "guide/updating",
|
||||||
"title": "AOT Metadata Errors",
|
"title": "Keeping Up-to-Date",
|
||||||
"tooltip": "Troubleshooting AOT compilation."
|
"tooltip": "Information about updating Angular applications and libraries to the latest version."
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/template-typecheck",
|
|
||||||
"title": "Template Type-checking",
|
|
||||||
"tooltip": "Template type-checking in Angular."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/build",
|
|
||||||
"title": "Building & Serving",
|
|
||||||
"tooltip": "Building and serving Angular apps."
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Testing",
|
"title": "Testing",
|
||||||
@ -661,89 +463,48 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Dev Workflow",
|
||||||
|
"tooltip": "Build, and deployment information.",
|
||||||
|
"children": [
|
||||||
{
|
{
|
||||||
"url": "guide/deployment",
|
"url": "guide/deployment",
|
||||||
"title": "Deployment",
|
"title": "Deploying applications",
|
||||||
"tooltip": "Learn how to deploy your Angular app."
|
"tooltip": "Learn how to deploy your Angular app."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Dev Tool Integration",
|
"title": "AOT Compiler",
|
||||||
"tooltip": "Integrate with your development environment and tools.",
|
"tooltip": "Understanding ahead-of-time compilation.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"url": "guide/language-service",
|
"url": "guide/aot-compiler",
|
||||||
"title": "Language Service",
|
"title": "Ahead-of-Time Compilation",
|
||||||
"tooltip": "Use Angular Language Service to speed up dev time."
|
"tooltip": "Learn why and how to use the Ahead-of-Time (AOT) compiler."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/visual-studio-2015",
|
"url": "guide/angular-compiler-options",
|
||||||
"title": "Visual Studio 2015",
|
"title": "Angular Compiler Options",
|
||||||
"tooltip": "Using Angular with Visual Studio 2015.",
|
"tooltip": "Configuring AOT compilation."
|
||||||
"hidden": true
|
},
|
||||||
}
|
{
|
||||||
]
|
"url": "guide/aot-metadata-errors",
|
||||||
|
"title": "AOT Metadata Errors",
|
||||||
|
"tooltip": "Troubleshooting AOT compilation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/template-typecheck",
|
||||||
|
"title": "Template Type-checking",
|
||||||
|
"tooltip": "Template type-checking in Angular."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Configuration",
|
"url": "guide/build",
|
||||||
"tooltip": "Workspace and project file structure and configuration.",
|
"title": "Building & Serving",
|
||||||
"children": [
|
"tooltip": "Building and serving Angular apps."
|
||||||
{
|
|
||||||
"url": "guide/file-structure",
|
|
||||||
"title": "Project File Structure",
|
|
||||||
"tooltip": "How your Angular workspace looks on your filesystem."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/workspace-config",
|
|
||||||
"title": "Workspace Configuration",
|
|
||||||
"tooltip": "The \"angular.json\" file contains workspace and project configuration defaults for Angular CLI commands."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/npm-packages",
|
|
||||||
"title": "npm Dependencies",
|
|
||||||
"tooltip": "Description of npm packages required at development time and at runtime."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/typescript-configuration",
|
|
||||||
"title": "TypeScript Configuration",
|
|
||||||
"tooltip": "TypeScript configuration for Angular developers."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/browser-support",
|
|
||||||
"title": "Browser Support",
|
|
||||||
"tooltip": "Browser support and polyfills guide."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/strict-mode",
|
|
||||||
"title": "Strict mode",
|
|
||||||
"tooltip": "Reference documentation for Angular's strict mode."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "Extending Angular",
|
|
||||||
"tooltip": "Working with libraries and extending the CLI.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"title": "Angular Libraries",
|
|
||||||
"tooltip": "Extending Angular with shared libraries.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"url": "guide/libraries",
|
|
||||||
"title": "Libraries Overview",
|
|
||||||
"tooltip": "Understand how and when to use or create libraries."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/using-libraries",
|
|
||||||
"title": "Using Published Libraries",
|
|
||||||
"tooltip": "Integrate published libraries into an app."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/creating-libraries",
|
|
||||||
"title": "Creating Libraries",
|
|
||||||
"tooltip": "Extend Angular by creating, publishing, and using your own libraries."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "guide/lightweight-injection-tokens",
|
"url": "guide/lightweight-injection-tokens",
|
||||||
"title": "Lightweight Injection Tokens for Libraries",
|
"title": "Lightweight Injection Tokens for Libraries",
|
||||||
@ -752,30 +513,23 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Schematics",
|
"title": "Angular Tools",
|
||||||
"tooltip": "Understanding schematics.",
|
"tooltip": "Tools to help you build your Angular applications.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"url": "guide/schematics",
|
|
||||||
"title": "Schematics Overview",
|
|
||||||
"tooltip": "Extending CLI generation capabilities."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/schematics-authoring",
|
|
||||||
"title": "Authoring Schematics",
|
|
||||||
"tooltip": "Understand the structure of a schematic."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/schematics-for-libraries",
|
|
||||||
"title": "Schematics for Libraries",
|
|
||||||
"tooltip": "Use schematics to integrate your library with the Angular CLI."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "guide/cli-builder",
|
"url": "guide/cli-builder",
|
||||||
"title": "CLI Builders",
|
"title": "CLI Builders",
|
||||||
"tooltip": "Using builders to customize Angular CLI."
|
"tooltip": "Using builders to customize Angular CLI."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/universal",
|
||||||
|
"title": "Server-side Rendering",
|
||||||
|
"tooltip": "Render HTML server-side with Angular Universal."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/language-service",
|
||||||
|
"title": "Language Service",
|
||||||
|
"tooltip": "Use Angular Language Service to speed up dev time."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -783,6 +537,52 @@
|
|||||||
"title": "Tutorials",
|
"title": "Tutorials",
|
||||||
"tooltip": "End-to-end tutorials for learning Angular concepts and patterns.",
|
"tooltip": "End-to-end tutorials for learning Angular concepts and patterns.",
|
||||||
"children": [
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Tutorial: Tour of Heroes",
|
||||||
|
"tooltip": "The Tour of Heroes app is used as a reference point in many Angular examples.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "tutorial",
|
||||||
|
"title": "Introduction",
|
||||||
|
"tooltip": "Introduction to the Tour of Heroes app and tutorial"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt0",
|
||||||
|
"title": "Create a Project",
|
||||||
|
"tooltip": "Creating the application shell"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt1",
|
||||||
|
"title": "1. The Hero Editor",
|
||||||
|
"tooltip": "Part 1: Build a simple editor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt2",
|
||||||
|
"title": "2. Display a List",
|
||||||
|
"tooltip": "Part 2: Build a master/detail page with a list of heroes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt3",
|
||||||
|
"title": "3. Create a Feature Component",
|
||||||
|
"tooltip": "Part 3: Refactor the master/detail views into separate components."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt4",
|
||||||
|
"title": "4. Add Services",
|
||||||
|
"tooltip": "Part 4: Create a reusable service to manage hero data."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt5",
|
||||||
|
"title": "5. Add In-app Navigation",
|
||||||
|
"tooltip": "Part 5: Add the Angular router and navigate among the views."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt6",
|
||||||
|
"title": "6. Get Data from a Server",
|
||||||
|
"tooltip": "Part 6: Use HTTP to retrieve and save hero data."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Routing",
|
"title": "Routing",
|
||||||
"tooltip": "End-to-end tutorials for learning about Angular's router.",
|
"tooltip": "End-to-end tutorials for learning about Angular's router.",
|
||||||
@ -803,6 +603,37 @@
|
|||||||
"url": "guide/forms",
|
"url": "guide/forms",
|
||||||
"title": "Building a Template-driven Form",
|
"title": "Building a Template-driven Form",
|
||||||
"tooltip": "Create a template-driven form using directives and Angular template syntax."
|
"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",
|
||||||
|
"tooltip": "Using web workers for background processing."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Angular Libraries",
|
||||||
|
"tooltip": "Extending Angular with shared libraries.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/libraries",
|
||||||
|
"title": "Libraries Overview",
|
||||||
|
"tooltip": "Understand how and when to use or create libraries."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/using-libraries",
|
||||||
|
"title": "Using Published Libraries",
|
||||||
|
"tooltip": "Integrate published libraries into an app."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/creating-libraries",
|
||||||
|
"title": "Creating Libraries",
|
||||||
|
"tooltip": "Extend Angular by creating, publishing, and using your own libraries."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -810,11 +641,6 @@
|
|||||||
"title": "Release Information",
|
"title": "Release Information",
|
||||||
"tooltip": "Angular release practices, updating, and upgrading.",
|
"tooltip": "Angular release practices, updating, and upgrading.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"url": "guide/updating",
|
|
||||||
"title": "Keeping Up-to-Date",
|
|
||||||
"tooltip": "Information about updating Angular applications and libraries to the latest version."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "guide/releases",
|
"url": "guide/releases",
|
||||||
"title": "Release Practices",
|
"title": "Release Practices",
|
||||||
@ -911,23 +737,181 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Angular Style and Usage",
|
"title": "Reference",
|
||||||
"tooltip": "Summaries of Angular syntax, coding, and doc styles.",
|
"tooltip": "Reference guides for Angular features and tools.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"url": "guide/cheatsheet",
|
"title": "Conceptual Reference",
|
||||||
"title": "Quick Reference",
|
"tooltip": "Reference documentation that explains how Angular features work.",
|
||||||
"tooltip": "A quick guide to common Angular coding techniques."
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Angular Concepts",
|
||||||
|
"tooltip": "Introduction to basic concepts for Angular applications.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/architecture",
|
||||||
|
"title": "Intro to Basic Concepts",
|
||||||
|
"tooltip": "Basic building blocks of Angular applications."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/styleguide",
|
"url": "guide/architecture-modules",
|
||||||
"title": "Coding Style Guide",
|
"title": "Intro to Modules",
|
||||||
"tooltip": "Guidelines for writing Angular code."
|
"tooltip": "About NgModules."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/docs-style-guide",
|
"url": "guide/architecture-components",
|
||||||
"title": "Documentation Style Guide",
|
"title": "Intro to Components",
|
||||||
"tooltip": "Style guide for documentation authors."
|
"tooltip": "About Components, Templates, and Views."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/architecture-services",
|
||||||
|
"title": "Intro to Services and DI",
|
||||||
|
"tooltip": "About services and dependency injection."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/architecture-next-steps",
|
||||||
|
"title": "Next Steps",
|
||||||
|
"tooltip": "Beyond the basics."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Workspace and project structure",
|
||||||
|
"tooltip": "Workspace and project file structure and configuration.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/file-structure",
|
||||||
|
"title": "Project File Structure",
|
||||||
|
"tooltip": "How your Angular workspace looks on your filesystem."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/workspace-config",
|
||||||
|
"title": "Workspace Configuration",
|
||||||
|
"tooltip": "The \"angular.json\" file contains workspace and project configuration defaults for Angular CLI commands."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/npm-packages",
|
||||||
|
"title": "npm Dependencies",
|
||||||
|
"tooltip": "Description of npm packages required at development time and at runtime."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/typescript-configuration",
|
||||||
|
"title": "TypeScript Configuration",
|
||||||
|
"tooltip": "TypeScript configuration for Angular developers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/browser-support",
|
||||||
|
"title": "Browser Support",
|
||||||
|
"tooltip": "Browser support and polyfills guide."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/strict-mode",
|
||||||
|
"title": "Strict mode",
|
||||||
|
"tooltip": "Reference documentation for Angular's strict mode."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "NgModules",
|
||||||
|
"tooltip": "NgModules.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/ngmodules",
|
||||||
|
"title": "NgModules Introduction",
|
||||||
|
"tooltip": "Use NgModules to make your apps efficient."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/ngmodule-vs-jsmodule",
|
||||||
|
"title": "JS Modules vs NgModules",
|
||||||
|
"tooltip": "Differentiate between JavaScript modules and NgModules."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/bootstrapping",
|
||||||
|
"title": "Launching Apps with a Root Module",
|
||||||
|
"tooltip": "Tell Angular how to construct and bootstrap the app in the root \"AppModule\"."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/frequent-ngmodules",
|
||||||
|
"title": "Frequently Used NgModules",
|
||||||
|
"tooltip": "Introduction to the most frequently used NgModules."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/module-types",
|
||||||
|
"title": "Types of Feature Modules",
|
||||||
|
"tooltip": "Description of the different types of feature modules."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/entry-components",
|
||||||
|
"title": "Entry Components",
|
||||||
|
"tooltip": "All about entry components in Angular."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/feature-modules",
|
||||||
|
"title": "Feature Modules",
|
||||||
|
"tooltip": "Create feature modules to organize your code."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/providers",
|
||||||
|
"title": "Providing Dependencies",
|
||||||
|
"tooltip": "Providing dependencies to NgModules."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/singleton-services",
|
||||||
|
"title": "Singleton Services",
|
||||||
|
"tooltip": "Creating singleton services."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/lazy-loading-ngmodules",
|
||||||
|
"title": "Lazy Loading Feature Modules",
|
||||||
|
"tooltip": "Lazy load modules to speed up your apps."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/sharing-ngmodules",
|
||||||
|
"title": "Sharing NgModules",
|
||||||
|
"tooltip": "Share NgModules to streamline your apps."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/ngmodule-api",
|
||||||
|
"title": "NgModule API",
|
||||||
|
"tooltip": "Understand the details of NgModules."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/ngmodule-faq",
|
||||||
|
"title": "NgModule FAQs",
|
||||||
|
"tooltip": "Answers to frequently asked questions about NgModules."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Observables & RxJS",
|
||||||
|
"tooltip": "Using observables for message passing in Angular.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/observables",
|
||||||
|
"title": "Observables Overview",
|
||||||
|
"tooltip": "Using observables to pass values synchronously or asynchronously."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/rx-library",
|
||||||
|
"title": "The RxJS Library",
|
||||||
|
"tooltip": "A library for reactive programming using observables to compose asynchronous or callback-based code."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/observables-in-angular",
|
||||||
|
"title": "Observables in Angular",
|
||||||
|
"tooltip": "How Angular subsystems use and expect observables."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/practical-observable-usage",
|
||||||
|
"title": "Practical Usage",
|
||||||
|
"tooltip": "Domains in which observables are particularly useful."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/comparing-observables",
|
||||||
|
"title": "Compare to Other Techniques",
|
||||||
|
"tooltip": "How observables compare to promises and other message passing techniques."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -951,6 +935,34 @@
|
|||||||
"title": "API Reference",
|
"title": "API Reference",
|
||||||
"tooltip": "Details of the Angular packages, classes, interfaces, and other types.",
|
"tooltip": "Details of the Angular packages, classes, interfaces, and other types.",
|
||||||
"url": "api"
|
"url": "api"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/glossary",
|
||||||
|
"title": "Angular Glossary",
|
||||||
|
"tooltip": "Brief definitions of the most important words in the Angular vocabulary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Angular Style and Usage",
|
||||||
|
"tooltip": "Summaries of Angular syntax, coding, and doc styles.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/cheatsheet",
|
||||||
|
"title": "Quick Reference",
|
||||||
|
"tooltip": "A quick guide to common Angular coding techniques."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Footer": [
|
"Footer": [
|
||||||
|
@ -99,10 +99,11 @@ describe('site App', function() {
|
|||||||
describe('scrolling to the top', () => {
|
describe('scrolling to the top', () => {
|
||||||
it('should scroll to the top when navigating to another page', () => {
|
it('should scroll to the top when navigating to another page', () => {
|
||||||
page.navigateTo('guide/security');
|
page.navigateTo('guide/security');
|
||||||
|
|
||||||
page.scrollTo('bottom');
|
page.scrollTo('bottom');
|
||||||
expect(page.getScrollTop()).toBeGreaterThan(0);
|
expect(page.getScrollTop()).toBeGreaterThan(0);
|
||||||
|
// Navigate to Reference section, then check
|
||||||
|
// Find the navigation item that has the text "api"
|
||||||
|
page.click(page.getNavItem(/reference/i));
|
||||||
page.click(page.getNavItem(/api/i));
|
page.click(page.getNavItem(/api/i));
|
||||||
expect(page.locationPath()).toBe('/api');
|
expect(page.locationPath()).toBe('/api');
|
||||||
expect(page.getScrollTop()).toBe(0);
|
expect(page.getScrollTop()).toBe(0);
|
||||||
|
@ -23,27 +23,36 @@ const globalOptions = {
|
|||||||
|
|
||||||
const runner = createBenchpressRunner();
|
const runner = createBenchpressRunner();
|
||||||
|
|
||||||
export async function runBenchmark(config: {
|
export async function runBenchmark({
|
||||||
|
id,
|
||||||
|
url = '',
|
||||||
|
params = [],
|
||||||
|
ignoreBrowserSynchronization = true,
|
||||||
|
microMetrics,
|
||||||
|
work,
|
||||||
|
prepare,
|
||||||
|
setup,
|
||||||
|
}: {
|
||||||
id: string,
|
id: string,
|
||||||
url: string,
|
url: string,
|
||||||
params: {name: string, value: any}[],
|
params: {name: string, value: any}[],
|
||||||
ignoreBrowserSynchronization?: boolean,
|
ignoreBrowserSynchronization?: boolean,
|
||||||
microMetrics?: {[key: string]: string},
|
microMetrics?: {[key: string]: string},
|
||||||
work?: () => void,
|
work?: (() => void)|(() => Promise<unknown>),
|
||||||
prepare?: () => void,
|
prepare?: (() => void)|(() => Promise<unknown>),
|
||||||
setup?: () => void
|
setup?: (() => void)|(() => Promise<unknown>),
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
openBrowser(config);
|
openBrowser({url, params, ignoreBrowserSynchronization});
|
||||||
if (config.setup) {
|
if (setup) {
|
||||||
await config.setup();
|
await setup();
|
||||||
}
|
}
|
||||||
const description: {[key: string]: any} = {};
|
const description: {[key: string]: any} = {};
|
||||||
config.params.forEach((param) => description[param.name] = param.value);
|
params.forEach((param) => description[param.name] = param.value);
|
||||||
return runner.sample({
|
return runner.sample({
|
||||||
id: config.id,
|
id,
|
||||||
execute: config.work,
|
execute: work,
|
||||||
prepare: config.prepare,
|
prepare,
|
||||||
microMetrics: config.microMetrics,
|
microMetrics,
|
||||||
providers: [{provide: Options.SAMPLE_DESCRIPTION, useValue: {}}]
|
providers: [{provide: Options.SAMPLE_DESCRIPTION, useValue: {}}]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,25 +2,20 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
|||||||
|
|
||||||
ts_library(
|
ts_library(
|
||||||
name = "caretaker",
|
name = "caretaker",
|
||||||
srcs = [
|
srcs = glob([
|
||||||
"cli.ts",
|
"**/*.ts",
|
||||||
],
|
]),
|
||||||
module_name = "@angular/dev-infra-private/caretaker",
|
module_name = "@angular/dev-infra-private/caretaker",
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
"//dev-infra/caretaker/check",
|
"//dev-infra/utils",
|
||||||
|
"@npm//@types/node",
|
||||||
|
"@npm//@types/node-fetch",
|
||||||
"@npm//@types/yargs",
|
"@npm//@types/yargs",
|
||||||
|
"@npm//multimatch",
|
||||||
|
"@npm//node-fetch",
|
||||||
|
"@npm//typed-graphqlify",
|
||||||
|
"@npm//yaml",
|
||||||
"@npm//yargs",
|
"@npm//yargs",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
ts_library(
|
|
||||||
name = "config",
|
|
||||||
srcs = [
|
|
||||||
"config.ts",
|
|
||||||
],
|
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
|
||||||
deps = [
|
|
||||||
"//dev-infra/utils",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
|
||||||
|
|
||||||
ts_library(
|
|
||||||
name = "check",
|
|
||||||
srcs = glob(["*.ts"]),
|
|
||||||
module_name = "@angular/dev-infra-private/caretaker/service-statuses",
|
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
|
||||||
deps = [
|
|
||||||
"//dev-infra/caretaker:config",
|
|
||||||
"//dev-infra/utils",
|
|
||||||
"@npm//@types/fs-extra",
|
|
||||||
"@npm//@types/node",
|
|
||||||
"@npm//@types/node-fetch",
|
|
||||||
"@npm//@types/yargs",
|
|
||||||
"@npm//multimatch",
|
|
||||||
"@npm//node-fetch",
|
|
||||||
"@npm//typed-graphqlify",
|
|
||||||
"@npm//yaml",
|
|
||||||
"@npm//yargs",
|
|
||||||
],
|
|
||||||
)
|
|
@ -9,6 +9,7 @@
|
|||||||
import {GitClient} from '../../utils/git';
|
import {GitClient} from '../../utils/git';
|
||||||
import {getCaretakerConfig} from '../config';
|
import {getCaretakerConfig} from '../config';
|
||||||
|
|
||||||
|
import {printCiStatus} from './ci';
|
||||||
import {printG3Comparison} from './g3';
|
import {printG3Comparison} from './g3';
|
||||||
import {printGithubTasks} from './github';
|
import {printGithubTasks} from './github';
|
||||||
import {printServiceStatuses} from './services';
|
import {printServiceStatuses} from './services';
|
||||||
@ -21,7 +22,9 @@ export async function checkServiceStatuses(githubToken: string) {
|
|||||||
/** The GitClient for interacting with git and Github. */
|
/** The GitClient for interacting with git and Github. */
|
||||||
const git = new GitClient(githubToken, config);
|
const git = new GitClient(githubToken, config);
|
||||||
|
|
||||||
|
// TODO(josephperrott): Allow these checks to be loaded in parallel.
|
||||||
await printServiceStatuses();
|
await printServiceStatuses();
|
||||||
await printGithubTasks(git, config.caretaker);
|
await printGithubTasks(git, config.caretaker);
|
||||||
await printG3Comparison(git);
|
await printG3Comparison(git);
|
||||||
|
await printCiStatus(git);
|
||||||
}
|
}
|
||||||
|
59
dev-infra/caretaker/check/ci.ts
Normal file
59
dev-infra/caretaker/check/ci.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
import {bold, green, info, red} from '../../utils/console';
|
||||||
|
import {GitClient} from '../../utils/git';
|
||||||
|
|
||||||
|
|
||||||
|
/** The results of checking the status of CI. */
|
||||||
|
interface StatusCheckResult {
|
||||||
|
status: 'success'|'failed'|'canceled'|'infrastructure_fail'|'timedout'|'failed'|'no_tests';
|
||||||
|
timestamp: Date;
|
||||||
|
buildUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieve and log status of CI for the project. */
|
||||||
|
export async function printCiStatus(git: GitClient) {
|
||||||
|
info.group(bold(`CI`));
|
||||||
|
// TODO(josephperrott): Expand list of branches checked to all active branches.
|
||||||
|
await printStatus(git, 'master');
|
||||||
|
info.groupEnd();
|
||||||
|
info();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Log the status of CI for a given branch to the console. */
|
||||||
|
async function printStatus(git: GitClient, branch: string) {
|
||||||
|
const result = await getStatusOfBranch(git, branch);
|
||||||
|
const branchName = branch.padEnd(10);
|
||||||
|
if (result === null) {
|
||||||
|
info(`${branchName} was not found on CircleCI`);
|
||||||
|
} else if (result.status === 'success') {
|
||||||
|
info(`${branchName} ✅`);
|
||||||
|
} else {
|
||||||
|
info(`${branchName} ❌ (Ran at: ${result.timestamp.toLocaleString()})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the CI status of a given branch from CircleCI. */
|
||||||
|
async function getStatusOfBranch(git: GitClient, branch: string): Promise<StatusCheckResult|null> {
|
||||||
|
const {owner, name} = git.remoteConfig;
|
||||||
|
const url = `https://circleci.com/api/v1.1/project/gh/${owner}/${name}/tree/${
|
||||||
|
branch}?limit=1&filter=completed&shallow=true`;
|
||||||
|
const result = (await fetch(url).then(result => result.json()))?.[0];
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
status: result.outcome,
|
||||||
|
timestamp: new Date(result.stop_time),
|
||||||
|
buildUrl: result.build_url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {existsSync, readFileSync} from 'fs-extra';
|
import {existsSync, readFileSync} from 'fs';
|
||||||
import * as multimatch from 'multimatch';
|
import * as multimatch from 'multimatch';
|
||||||
import {join} from 'path';
|
import {join} from 'path';
|
||||||
import {parse as parseYaml} from 'yaml';
|
import {parse as parseYaml} from 'yaml';
|
||||||
|
@ -3,18 +3,10 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
|||||||
|
|
||||||
ts_library(
|
ts_library(
|
||||||
name = "commit-message",
|
name = "commit-message",
|
||||||
srcs = [
|
srcs = glob(
|
||||||
"builder.ts",
|
["**/*.ts"],
|
||||||
"cli.ts",
|
exclude = ["**/*.spec.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",
|
module_name = "@angular/dev-infra-private/commit-message",
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
@ -32,11 +24,7 @@ ts_library(
|
|||||||
ts_library(
|
ts_library(
|
||||||
name = "test_lib",
|
name = "test_lib",
|
||||||
testonly = True,
|
testonly = True,
|
||||||
srcs = [
|
srcs = glob(["**/*.spec.ts"]),
|
||||||
"builder.spec.ts",
|
|
||||||
"parse.spec.ts",
|
|
||||||
"validate.spec.ts",
|
|
||||||
],
|
|
||||||
deps = [
|
deps = [
|
||||||
":commit-message",
|
":commit-message",
|
||||||
"//dev-infra/utils",
|
"//dev-infra/utils",
|
||||||
|
@ -7,104 +7,19 @@
|
|||||||
*/
|
*/
|
||||||
import * as yargs from 'yargs';
|
import * as yargs from 'yargs';
|
||||||
|
|
||||||
import {info} from '../utils/console';
|
import {RestoreCommitMessageModule} from './restore-commit-message/cli';
|
||||||
|
import {ValidateFileModule} from './validate-file/cli';
|
||||||
import {restoreCommitMessage} from './restore-commit-message';
|
import {ValidateRangeModule} from './validate-range/cli';
|
||||||
import {validateFile} from './validate-file';
|
import {WizardModule} from './wizard/cli';
|
||||||
import {validateCommitRange} from './validate-range';
|
|
||||||
import {runWizard} from './wizard';
|
|
||||||
|
|
||||||
/** Build the parser for the commit-message commands. */
|
/** Build the parser for the commit-message commands. */
|
||||||
export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||||
return localYargs.help()
|
return localYargs.help()
|
||||||
.strict()
|
.strict()
|
||||||
.command(
|
.command(RestoreCommitMessageModule)
|
||||||
'restore-commit-message-draft', false,
|
.command(WizardModule)
|
||||||
args => {
|
.command(ValidateFileModule)
|
||||||
return args.option('file-env-variable', {
|
.command(ValidateRangeModule);
|
||||||
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) {
|
if (require.main == module) {
|
||||||
|
13
dev-infra/commit-message/commit-message-source.ts
Normal file
13
dev-infra/commit-message/commit-message-source.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The source triggering the git commit message creation.
|
||||||
|
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||||
|
*/
|
||||||
|
export type CommitMsgSource = 'message'|'template'|'merge'|'squash'|'commit';
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
|
import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
|
||||||
|
|
||||||
|
/** Configuration for commit-message comands. */
|
||||||
export interface CommitMessageConfig {
|
export interface CommitMessageConfig {
|
||||||
maxLineLength: number;
|
maxLineLength: number;
|
||||||
minBodyLength: number;
|
minBodyLength: number;
|
||||||
@ -49,7 +50,7 @@ export const COMMIT_TYPES: {[key: string]: CommitType} = {
|
|||||||
build: {
|
build: {
|
||||||
name: 'build',
|
name: 'build',
|
||||||
description: 'Changes to local repository build system and tooling',
|
description: 'Changes to local repository build system and tooling',
|
||||||
scope: ScopeRequirement.Forbidden,
|
scope: ScopeRequirement.Optional,
|
||||||
},
|
},
|
||||||
ci: {
|
ci: {
|
||||||
name: 'ci',
|
name: 'ci',
|
||||||
|
51
dev-infra/commit-message/restore-commit-message/cli.ts
Normal file
51
dev-infra/commit-message/restore-commit-message/cli.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* @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,
|
||||||
|
};
|
@ -6,10 +6,12 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {info} from 'console';
|
|
||||||
import {writeFileSync} from 'fs';
|
import {writeFileSync} from 'fs';
|
||||||
|
|
||||||
import {loadCommitMessageDraft} from './commit-message-draft';
|
import {debug, log} from '../../utils/console';
|
||||||
|
|
||||||
|
import {loadCommitMessageDraft} from '../commit-message-draft';
|
||||||
|
import {CommitMsgSource} from '../commit-message-source';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore the commit message draft to the git to be used as the default commit message.
|
* Restore the commit message draft to the git to be used as the default commit message.
|
||||||
@ -17,22 +19,21 @@ import {loadCommitMessageDraft} from './commit-message-draft';
|
|||||||
* The source provided may be one of the sources described in
|
* The source provided may be one of the sources described in
|
||||||
* https://git-scm.com/docs/githooks#_prepare_commit_msg
|
* https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||||
*/
|
*/
|
||||||
export function restoreCommitMessage(
|
export function restoreCommitMessage(filePath: string, source?: CommitMsgSource) {
|
||||||
filePath: string, source?: 'message'|'template'|'squash'|'commit') {
|
|
||||||
if (!!source) {
|
if (!!source) {
|
||||||
info('Skipping commit message restoration attempt');
|
log('Skipping commit message restoration attempt');
|
||||||
if (source === 'message') {
|
if (source === 'message') {
|
||||||
info('A commit message was already provided via the command with a -m or -F flag');
|
debug('A commit message was already provided via the command with a -m or -F flag');
|
||||||
}
|
}
|
||||||
if (source === 'template') {
|
if (source === 'template') {
|
||||||
info('A commit message was already provided via the -t flag or config.template setting');
|
debug('A commit message was already provided via the -t flag or config.template setting');
|
||||||
}
|
}
|
||||||
if (source === 'squash') {
|
if (source === 'squash') {
|
||||||
info('A commit message was already provided as a merge action or via .git/MERGE_MSG');
|
debug('A commit message was already provided as a merge action or via .git/MERGE_MSG');
|
||||||
}
|
}
|
||||||
if (source === 'commit') {
|
if (source === 'commit') {
|
||||||
info('A commit message was already provided through a revision specified via --fixup, -c,');
|
debug('A commit message was already provided through a revision specified via --fixup, -c,');
|
||||||
info('-C or --amend flag');
|
debug('-C or --amend flag');
|
||||||
}
|
}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
@ -1,30 +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 {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);
|
|
||||||
}
|
|
62
dev-infra/commit-message/validate-file/cli.ts
Normal file
62
dev-infra/commit-message/validate-file/cli.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* @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',
|
||||||
|
};
|
47
dev-infra/commit-message/validate-file/validate-file.ts
Normal file
47
dev-infra/commit-message/validate-file/validate-file.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
50
dev-infra/commit-message/validate-range/cli.ts
Normal file
50
dev-infra/commit-message/validate-range/cli.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @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',
|
||||||
|
};
|
@ -5,11 +5,11 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {info} from '../utils/console';
|
import {error, info} from '../../utils/console';
|
||||||
import {exec} from '../utils/shelljs';
|
import {exec} from '../../utils/shelljs';
|
||||||
|
|
||||||
import {parseCommitMessage} from './parse';
|
import {parseCommitMessage} from '../parse';
|
||||||
import {validateCommitMessage, ValidateCommitMessageOptions} from './validate';
|
import {printValidationErrors, validateCommitMessage, ValidateCommitMessageOptions} from '../validate';
|
||||||
|
|
||||||
// Whether the provided commit is a fixup commit.
|
// Whether the provided commit is a fixup commit.
|
||||||
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
|
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
|
||||||
@ -19,11 +19,20 @@ const extractCommitHeader = (m: string) => parseCommitMessage(m).header;
|
|||||||
|
|
||||||
/** Validate all commits in a provided git commit range. */
|
/** Validate all commits in a provided git commit range. */
|
||||||
export function validateCommitRange(range: string) {
|
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()}`;
|
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}`;
|
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.
|
// Retrieve the commits in the provided range.
|
||||||
const result = exec(`git log --reverse --format=${gitLogFormat} ${range}`);
|
const result = exec(`git log --reverse --format=${gitLogFormat} ${range}`);
|
||||||
@ -45,12 +54,22 @@ export function validateCommitRange(range: string) {
|
|||||||
undefined :
|
undefined :
|
||||||
commits.slice(0, i).filter(isNonFixup).map(extractCommitHeader)
|
commits.slice(0, i).filter(isNonFixup).map(extractCommitHeader)
|
||||||
};
|
};
|
||||||
return validateCommitMessage(m, options);
|
const {valid, errors: localErrors, commit} = validateCommitMessage(m, options);
|
||||||
|
if (localErrors.length) {
|
||||||
|
errors.push([commit.header, localErrors]);
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (allCommitsInRangeValid) {
|
if (allCommitsInRangeValid) {
|
||||||
info('√ All commit messages in range valid.');
|
info('√ All commit messages in range valid.');
|
||||||
} else {
|
} 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
|
// Exit with a non-zero exit code if invalid commit messages have
|
||||||
// been discovered.
|
// been discovered.
|
||||||
process.exit(1);
|
process.exit(1);
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
// Imports
|
// Imports
|
||||||
import * as validateConfig from './config';
|
import * as validateConfig from './config';
|
||||||
import {validateCommitMessage} from './validate';
|
import {validateCommitMessage, ValidateCommitMessageResult} from './validate';
|
||||||
|
|
||||||
type CommitMessageConfig = validateConfig.CommitMessageConfig;
|
type CommitMessageConfig = validateConfig.CommitMessageConfig;
|
||||||
|
|
||||||
@ -31,44 +31,35 @@ const SCOPES = config.commitMessage.scopes.join(', ');
|
|||||||
const INVALID = false;
|
const INVALID = false;
|
||||||
const VALID = true;
|
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
|
// TODO(josephperrott): Clean up tests to test script rather than for
|
||||||
// specific commit messages we want to use.
|
// specific commit messages we want to use.
|
||||||
describe('validate-commit-message.js', () => {
|
describe('validate-commit-message.js', () => {
|
||||||
let lastError: string = '';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
lastError = '';
|
|
||||||
|
|
||||||
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
|
|
||||||
spyOn(validateConfig, 'getCommitMessageConfig')
|
spyOn(validateConfig, 'getCommitMessageConfig')
|
||||||
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
|
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validateMessage()', () => {
|
describe('validateMessage()', () => {
|
||||||
it('should be valid', () => {
|
it('should be valid', () => {
|
||||||
expect(validateCommitMessage('feat(packaging): something')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('feat(packaging): something'), VALID);
|
||||||
expect(lastError).toBe('');
|
expectValidationResult(validateCommitMessage('fix(packaging): something'), VALID);
|
||||||
|
expectValidationResult(validateCommitMessage('fixup! fix(packaging): something'), VALID);
|
||||||
expect(validateCommitMessage('fix(packaging): something')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('squash! fix(packaging): something'), VALID);
|
||||||
expect(lastError).toBe('');
|
expectValidationResult(validateCommitMessage('Revert: "fix(packaging): something"'), VALID);
|
||||||
|
|
||||||
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', () => {
|
it('should validate max length', () => {
|
||||||
const msg =
|
const msg =
|
||||||
'fix(compiler): something super mega extra giga tera long, maybe even longer and longer and longer and longer and longer and longer...';
|
'fix(compiler): something super mega extra giga tera long, maybe even longer and longer and longer and longer and longer and longer...';
|
||||||
|
|
||||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
expectValidationResult(validateCommitMessage(msg), INVALID, [
|
||||||
expect(lastError).toContain(`The commit message header is longer than ${
|
`The commit message header is longer than ${config.commitMessage.maxLineLength} characters`
|
||||||
config.commitMessage.maxLineLength} characters`);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip max length limit for URLs', () => {
|
it('should skip max length limit for URLs', () => {
|
||||||
@ -77,49 +68,56 @@ describe('validate-commit-message.js', () => {
|
|||||||
'limit. For more details see the following super long URL:\n\n' +
|
'limit. For more details see the following super long URL:\n\n' +
|
||||||
'https://github.com/angular/components/commit/e2ace018ddfad10608e0e32932c43dcfef4095d7#diff-9879d6db96fd29134fc802214163b95a';
|
'https://github.com/angular/components/commit/e2ace018ddfad10608e0e32932c43dcfef4095d7#diff-9879d6db96fd29134fc802214163b95a';
|
||||||
|
|
||||||
expect(validateCommitMessage(msg)).toBe(VALID);
|
expectValidationResult(validateCommitMessage(msg), VALID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate "<type>(<scope>): <subject>" format', () => {
|
it('should validate "<type>(<scope>): <subject>" format', () => {
|
||||||
const msg = 'not correct format';
|
const msg = 'not correct format';
|
||||||
|
|
||||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
expectValidationResult(
|
||||||
expect(lastError).toContain(`The commit message header does not match the expected format.`);
|
validateCommitMessage(msg), INVALID,
|
||||||
|
[`The commit message header does not match the expected format.`]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail when type is invalid', () => {
|
it('should fail when type is invalid', () => {
|
||||||
const msg = 'weird(core): something';
|
const msg = 'weird(core): something';
|
||||||
|
|
||||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
expectValidationResult(
|
||||||
expect(lastError).toContain(`'weird' is not an allowed type.\n => TYPES: ${TYPES}`);
|
validateCommitMessage(msg), INVALID,
|
||||||
|
[`'weird' is not an allowed type.\n => TYPES: ${TYPES}`]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail when scope is invalid', () => {
|
it('should fail when scope is invalid', () => {
|
||||||
const errorMessageFor = (scope: string, header: string) =>
|
const errorMessageFor = (scope: string, header: string) =>
|
||||||
`'${scope}' is not an allowed scope.\n => SCOPES: ${SCOPES}`;
|
`'${scope}' is not an allowed scope.\n => SCOPES: ${SCOPES}`;
|
||||||
|
|
||||||
expect(validateCommitMessage('fix(Compiler): something')).toBe(INVALID);
|
expectValidationResult(
|
||||||
expect(lastError).toContain(errorMessageFor('Compiler', 'fix(Compiler): something'));
|
validateCommitMessage('fix(Compiler): something'), INVALID,
|
||||||
|
[errorMessageFor('Compiler', 'fix(Compiler): something')]);
|
||||||
|
|
||||||
expect(validateCommitMessage('feat(bah): something')).toBe(INVALID);
|
expectValidationResult(
|
||||||
expect(lastError).toContain(errorMessageFor('bah', 'feat(bah): something'));
|
validateCommitMessage('feat(bah): something'), INVALID,
|
||||||
|
[errorMessageFor('bah', 'feat(bah): something')]);
|
||||||
|
|
||||||
expect(validateCommitMessage('fix(webworker): something')).toBe(INVALID);
|
expectValidationResult(
|
||||||
expect(lastError).toContain(errorMessageFor('webworker', 'fix(webworker): something'));
|
validateCommitMessage('fix(webworker): something'), INVALID,
|
||||||
|
[errorMessageFor('webworker', 'fix(webworker): something')]);
|
||||||
|
|
||||||
expect(validateCommitMessage('refactor(security): something')).toBe(INVALID);
|
expectValidationResult(
|
||||||
expect(lastError).toContain(errorMessageFor('security', 'refactor(security): something'));
|
validateCommitMessage('refactor(security): something'), INVALID,
|
||||||
|
[errorMessageFor('security', 'refactor(security): something')]);
|
||||||
|
|
||||||
expect(validateCommitMessage('refactor(docs): something')).toBe(INVALID);
|
expectValidationResult(
|
||||||
expect(lastError).toContain(errorMessageFor('docs', 'refactor(docs): something'));
|
validateCommitMessage('refactor(docs): something'), INVALID,
|
||||||
|
[errorMessageFor('docs', 'refactor(docs): something')]);
|
||||||
|
|
||||||
expect(validateCommitMessage('feat(angular): something')).toBe(INVALID);
|
expectValidationResult(
|
||||||
expect(lastError).toContain(errorMessageFor('angular', 'feat(angular): something'));
|
validateCommitMessage('feat(angular): something'), INVALID,
|
||||||
|
[errorMessageFor('angular', 'feat(angular): something')]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow empty scope', () => {
|
it('should allow empty scope', () => {
|
||||||
expect(validateCommitMessage('build: blablabla')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('build: blablabla'), 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
|
// We do not want to allow WIP. It is OK to fail the PR build in this case to show that there is
|
||||||
@ -127,30 +125,25 @@ describe('validate-commit-message.js', () => {
|
|||||||
it('should not allow "WIP: ..." syntax', () => {
|
it('should not allow "WIP: ..." syntax', () => {
|
||||||
const msg = 'WIP: fix: something';
|
const msg = 'WIP: fix: something';
|
||||||
|
|
||||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
expectValidationResult(
|
||||||
expect(lastError).toContain(`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`);
|
validateCommitMessage(msg), INVALID,
|
||||||
|
[`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('(revert)', () => {
|
describe('(revert)', () => {
|
||||||
it('should allow valid "revert: ..." syntaxes', () => {
|
it('should allow valid "revert: ..." syntaxes', () => {
|
||||||
expect(validateCommitMessage('revert: anything')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('revert: anything'), VALID);
|
||||||
expect(lastError).toBe('');
|
expectValidationResult(validateCommitMessage('Revert: "anything"'), VALID);
|
||||||
|
expectValidationResult(validateCommitMessage('revert anything'), VALID);
|
||||||
expect(validateCommitMessage('Revert: "anything"')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('rEvErT anything'), 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', () => {
|
it('should not allow "revert(scope): ..." syntax', () => {
|
||||||
const msg = 'revert(compiler): reduce generated code payload size by 65%';
|
const msg = 'revert(compiler): reduce generated code payload size by 65%';
|
||||||
|
|
||||||
expect(validateCommitMessage(msg)).toBe(INVALID);
|
expectValidationResult(
|
||||||
expect(lastError).toContain(`'revert' is not an allowed type.\n => TYPES: ${TYPES}`);
|
validateCommitMessage(msg), INVALID,
|
||||||
|
[`'revert' is not an allowed type.\n => TYPES: ${TYPES}`]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// https://github.com/angular/angular/issues/23479
|
// https://github.com/angular/angular/issues/23479
|
||||||
@ -158,28 +151,26 @@ describe('validate-commit-message.js', () => {
|
|||||||
const msg =
|
const msg =
|
||||||
'Revert "fix(compiler): Pretty print object instead of [Object object] (#22689)" (#23442)';
|
'Revert "fix(compiler): Pretty print object instead of [Object object] (#22689)" (#23442)';
|
||||||
|
|
||||||
expect(validateCommitMessage(msg)).toBe(VALID);
|
expectValidationResult(validateCommitMessage(msg), VALID);
|
||||||
expect(lastError).toBe('');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('(squash)', () => {
|
describe('(squash)', () => {
|
||||||
describe('without `disallowSquash`', () => {
|
describe('without `disallowSquash`', () => {
|
||||||
it('should return commits as valid', () => {
|
it('should return commits as valid', () => {
|
||||||
expect(validateCommitMessage('squash! feat(core): add feature')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('squash! feat(core): add feature'), VALID);
|
||||||
expect(validateCommitMessage('squash! fix: a bug')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('squash! fix: a bug'), VALID);
|
||||||
expect(validateCommitMessage('squash! fix a typo')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('squash! fix a typo'), VALID);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with `disallowSquash`', () => {
|
describe('with `disallowSquash`', () => {
|
||||||
it('should fail', () => {
|
it('should fail', () => {
|
||||||
expect(validateCommitMessage('fix(core): something', {disallowSquash: true})).toBe(VALID);
|
expectValidationResult(
|
||||||
expect(validateCommitMessage('squash! fix(core): something', {
|
validateCommitMessage('fix(core): something', {disallowSquash: true}), VALID);
|
||||||
disallowSquash: true
|
expectValidationResult(
|
||||||
})).toBe(INVALID);
|
validateCommitMessage('squash! fix(core): something', {disallowSquash: true}),
|
||||||
expect(lastError).toContain(
|
INVALID, ['The commit must be manually squashed into the target commit']);
|
||||||
'The commit must be manually squashed into the target commit');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -187,9 +178,9 @@ describe('validate-commit-message.js', () => {
|
|||||||
describe('(fixup)', () => {
|
describe('(fixup)', () => {
|
||||||
describe('without `nonFixupCommitHeaders`', () => {
|
describe('without `nonFixupCommitHeaders`', () => {
|
||||||
it('should return commits as valid', () => {
|
it('should return commits as valid', () => {
|
||||||
expect(validateCommitMessage('fixup! feat(core): add feature')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('fixup! feat(core): add feature'), VALID);
|
||||||
expect(validateCommitMessage('fixup! fix: a bug')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('fixup! fix: a bug'), VALID);
|
||||||
expect(validateCommitMessage('fixup! fixup! fix: a bug')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('fixup! fixup! fix: a bug'), VALID);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -197,36 +188,39 @@ describe('validate-commit-message.js', () => {
|
|||||||
it('should check that the fixup commit matches a non-fixup one', () => {
|
it('should check that the fixup commit matches a non-fixup one', () => {
|
||||||
const msg = 'fixup! foo';
|
const msg = 'fixup! foo';
|
||||||
|
|
||||||
expect(validateCommitMessage(
|
expectValidationResult(
|
||||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}))
|
validateCommitMessage(
|
||||||
.toBe(VALID);
|
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}),
|
||||||
expect(validateCommitMessage(
|
VALID);
|
||||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}))
|
expectValidationResult(
|
||||||
.toBe(VALID);
|
validateCommitMessage(
|
||||||
expect(validateCommitMessage(
|
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}),
|
||||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}))
|
VALID);
|
||||||
.toBe(VALID);
|
expectValidationResult(
|
||||||
|
validateCommitMessage(
|
||||||
|
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}),
|
||||||
|
VALID);
|
||||||
|
|
||||||
expect(validateCommitMessage(
|
expectValidationResult(
|
||||||
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}))
|
validateCommitMessage(
|
||||||
.toBe(INVALID);
|
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}),
|
||||||
expect(lastError).toContain(
|
INVALID,
|
||||||
'Unable to find match for fixup commit among prior commits: \n' +
|
['Unable to find match for fixup commit among prior commits: \n' +
|
||||||
' qux\n' +
|
' qux\n' +
|
||||||
' quux\n' +
|
' quux\n' +
|
||||||
' quuux');
|
' quuux']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if `nonFixupCommitHeaders` is empty', () => {
|
it('should fail if `nonFixupCommitHeaders` is empty', () => {
|
||||||
expect(validateCommitMessage('refactor(core): make reactive', {
|
expectValidationResult(
|
||||||
disallowSquash: false,
|
validateCommitMessage(
|
||||||
nonFixupCommitHeaders: []
|
'refactor(core): make reactive',
|
||||||
})).toBe(VALID);
|
{disallowSquash: false, nonFixupCommitHeaders: []}),
|
||||||
expect(validateCommitMessage(
|
VALID);
|
||||||
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}))
|
expectValidationResult(
|
||||||
.toBe(INVALID);
|
validateCommitMessage(
|
||||||
expect(lastError).toContain(
|
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}),
|
||||||
`Unable to find match for fixup commit among prior commits: -`);
|
INVALID, [`Unable to find match for fixup commit among prior commits: -`]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -246,24 +240,27 @@ describe('validate-commit-message.js', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fail validation if the body is shorter than `minBodyLength`', () => {
|
it('should fail validation if the body is shorter than `minBodyLength`', () => {
|
||||||
expect(validateCommitMessage(
|
expectValidationResult(
|
||||||
'fix(core): something\n\n Explanation of the motivation behind this change'))
|
validateCommitMessage(
|
||||||
.toBe(VALID);
|
'fix(core): something\n\n Explanation of the motivation behind this change'),
|
||||||
expect(validateCommitMessage('fix(core): something\n\n too short')).toBe(INVALID);
|
VALID);
|
||||||
expect(lastError).toContain(
|
expectValidationResult(
|
||||||
'The commit message body does not meet the minimum length of 30 characters');
|
validateCommitMessage('fix(core): something\n\n too short'), INVALID,
|
||||||
expect(validateCommitMessage('fix(core): something')).toBe(INVALID);
|
['The commit message body does not meet the minimum length of 30 characters']);
|
||||||
expect(lastError).toContain(
|
expectValidationResult(validateCommitMessage('fix(core): something'), INVALID, [
|
||||||
'The commit message body does not meet the minimum length of 30 characters');
|
|
||||||
|
'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',
|
it('should pass validation if the body is shorter than `minBodyLength` but the commit type is in the `minBodyLengthTypeExclusions` list',
|
||||||
() => {
|
() => {
|
||||||
expect(validateCommitMessage('docs: just fixing a typo')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('docs: just fixing a typo'), VALID);
|
||||||
expect(validateCommitMessage('docs(core): just fixing a typo')).toBe(VALID);
|
expectValidationResult(validateCommitMessage('docs(core): just fixing a typo'), VALID);
|
||||||
expect(validateCommitMessage(
|
expectValidationResult(
|
||||||
'docs(core): just fixing a typo\n\nThis was just a silly typo.'))
|
validateCommitMessage(
|
||||||
.toBe(VALID);
|
'docs(core): just fixing a typo\n\nThis was just a silly typo.'),
|
||||||
|
VALID);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import {error} from '../utils/console';
|
import {error} from '../utils/console';
|
||||||
|
|
||||||
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
|
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
|
||||||
import {parseCommitMessage} from './parse';
|
import {parseCommitMessage, ParsedCommitMessage} from './parse';
|
||||||
|
|
||||||
/** Options for commit message validation. */
|
/** Options for commit message validation. */
|
||||||
export interface ValidateCommitMessageOptions {
|
export interface ValidateCommitMessageOptions {
|
||||||
@ -16,27 +16,26 @@ export interface ValidateCommitMessageOptions {
|
|||||||
nonFixupCommitHeaders?: string[];
|
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. */
|
/** Regex matching a URL for an entire commit body line. */
|
||||||
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
|
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
|
||||||
|
|
||||||
/** Validate a commit message against using the local repo's config. */
|
/** Validate a commit message against using the local repo's config. */
|
||||||
export function validateCommitMessage(
|
export function validateCommitMessage(
|
||||||
commitMsg: string, options: ValidateCommitMessageOptions = {}) {
|
commitMsg: string, options: ValidateCommitMessageOptions = {}): ValidateCommitMessageResult {
|
||||||
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 config = getCommitMessageConfig().commitMessage;
|
||||||
const commit = parseCommitMessage(commitMsg);
|
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 //
|
||||||
@ -51,7 +50,7 @@ export function validateCommitMessage(
|
|||||||
// the git history anyway, unless the options provided to not allow squash commits.
|
// the git history anyway, unless the options provided to not allow squash commits.
|
||||||
if (commit.isSquash) {
|
if (commit.isSquash) {
|
||||||
if (options.disallowSquash) {
|
if (options.disallowSquash) {
|
||||||
printError('The commit must be manually squashed into the target commit');
|
errors.push('The commit must be manually squashed into the target commit');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -64,7 +63,7 @@ export function validateCommitMessage(
|
|||||||
// check.
|
// check.
|
||||||
if (commit.isFixup) {
|
if (commit.isFixup) {
|
||||||
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
|
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
|
||||||
printError(
|
errors.push(
|
||||||
'Unable to find match for fixup commit among prior commits: ' +
|
'Unable to find match for fixup commit among prior commits: ' +
|
||||||
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
|
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
|
||||||
return false;
|
return false;
|
||||||
@ -77,19 +76,17 @@ export function validateCommitMessage(
|
|||||||
// Checking commit header //
|
// Checking commit header //
|
||||||
////////////////////////////
|
////////////////////////////
|
||||||
if (commit.header.length > config.maxLineLength) {
|
if (commit.header.length > config.maxLineLength) {
|
||||||
printError(`The commit message header is longer than ${config.maxLineLength} characters`);
|
errors.push(`The commit message header is longer than ${config.maxLineLength} characters`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!commit.type) {
|
if (!commit.type) {
|
||||||
printError(`The commit message header does not match the expected format.`);
|
errors.push(`The commit message header does not match the expected format.`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (COMMIT_TYPES[commit.type] === undefined) {
|
if (COMMIT_TYPES[commit.type] === undefined) {
|
||||||
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${
|
errors.push(`'${commit.type}' is not an allowed type.\n => TYPES: ${
|
||||||
Object.keys(COMMIT_TYPES).join(', ')}`);
|
Object.keys(COMMIT_TYPES).join(', ')}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -98,19 +95,19 @@ export function validateCommitMessage(
|
|||||||
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
|
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
|
||||||
|
|
||||||
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
|
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
|
||||||
printError(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
|
errors.push(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
|
||||||
commit.scope}' was provided.`);
|
commit.scope}' was provided.`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
|
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
|
||||||
printError(
|
errors.push(
|
||||||
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
|
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commit.scope && !config.scopes.includes(commit.scope)) {
|
if (commit.scope && !config.scopes.includes(commit.scope)) {
|
||||||
printError(
|
errors.push(
|
||||||
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
|
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -126,7 +123,7 @@ export function validateCommitMessage(
|
|||||||
|
|
||||||
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
|
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
|
||||||
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
||||||
printError(`The commit message body does not meet the minimum length of ${
|
errors.push(`The commit message body does not meet the minimum length of ${
|
||||||
config.minBodyLength} characters`);
|
config.minBodyLength} characters`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -139,10 +136,27 @@ export function validateCommitMessage(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (lineExceedsMaxLength) {
|
if (lineExceedsMaxLength) {
|
||||||
printError(
|
errors.push(
|
||||||
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
|
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {valid: validateCommitAndCollectErrors(), errors, commit};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 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();
|
||||||
|
}
|
||||||
|
54
dev-infra/commit-message/wizard/cli.ts
Normal file
54
dev-infra/commit-message/wizard/cli.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* @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,
|
||||||
|
};
|
@ -7,15 +7,12 @@
|
|||||||
*/
|
*/
|
||||||
import {writeFileSync} from 'fs';
|
import {writeFileSync} from 'fs';
|
||||||
|
|
||||||
import {info} from '../utils/console';
|
import {getUserConfig} from '../../utils/config';
|
||||||
|
import {debug, info} from '../../utils/console';
|
||||||
|
|
||||||
import {buildCommitMessage} from './builder';
|
import {buildCommitMessage} from '../builder';
|
||||||
|
import {CommitMsgSource} from '../commit-message-source';
|
||||||
|
|
||||||
/**
|
|
||||||
* 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. */
|
/** The default commit message used if the wizard does not procude a commit message. */
|
||||||
const defaultCommitMessage = `<type>(<scope>): <summary>
|
const defaultCommitMessage = `<type>(<scope>): <summary>
|
||||||
@ -24,11 +21,16 @@ const defaultCommitMessage = `<type>(<scope>): <summary>
|
|||||||
# lines at 100 characters.>\n\n`;
|
# lines at 100 characters.>\n\n`;
|
||||||
|
|
||||||
export async function runWizard(
|
export async function runWizard(
|
||||||
args: {filePath: string, source?: PrepareCommitMsgHookSource, commitSha?: string}) {
|
args: {filePath: string, source?: CommitMsgSource, commitSha?: string}) {
|
||||||
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
|
if (getUserConfig().commitMessage?.disableWizard) {
|
||||||
|
debug('Skipping commit message wizard due to enabled `commitMessage.disableWizard` option in');
|
||||||
|
debug('user config.');
|
||||||
|
process.exitCode = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.source !== undefined) {
|
if (args.source !== undefined) {
|
||||||
info(`Skipping commit message wizard due because the commit was created via '${
|
info(`Skipping commit message wizard because the commit was created via '${
|
||||||
args.source}' source`);
|
args.source}' source`);
|
||||||
process.exitCode = 0;
|
process.exitCode = 0;
|
||||||
return;
|
return;
|
@ -3,6 +3,7 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
|||||||
ts_library(
|
ts_library(
|
||||||
name = "common",
|
name = "common",
|
||||||
srcs = glob(["*.ts"]),
|
srcs = glob(["*.ts"]),
|
||||||
|
module_name = "@angular/dev-infra-private/pr/common",
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
"//dev-infra/utils",
|
"//dev-infra/utils",
|
||||||
|
@ -12,11 +12,13 @@
|
|||||||
"@angular/benchpress": "0.2.1",
|
"@angular/benchpress": "0.2.1",
|
||||||
"@octokit/graphql": "<from-root>",
|
"@octokit/graphql": "<from-root>",
|
||||||
"@octokit/types": "<from-root>",
|
"@octokit/types": "<from-root>",
|
||||||
|
"@octokit/rest": "<from-root>",
|
||||||
"brotli": "<from-root>",
|
"brotli": "<from-root>",
|
||||||
"chalk": "<from-root>",
|
"chalk": "<from-root>",
|
||||||
"cli-progress": "<from-root>",
|
"cli-progress": "<from-root>",
|
||||||
"glob": "<from-root>",
|
"glob": "<from-root>",
|
||||||
"inquirer": "<from-root>",
|
"inquirer": "<from-root>",
|
||||||
|
"inquirer-autocomplete-prompt": "<from-root>",
|
||||||
"minimatch": "<from-root>",
|
"minimatch": "<from-root>",
|
||||||
"multimatch": "<from-root>",
|
"multimatch": "<from-root>",
|
||||||
"node-fetch": "<from-root>",
|
"node-fetch": "<from-root>",
|
||||||
@ -26,9 +28,7 @@
|
|||||||
"tslib": "<from-root>",
|
"tslib": "<from-root>",
|
||||||
"typed-graphqlify": "<from-root>",
|
"typed-graphqlify": "<from-root>",
|
||||||
"yaml": "<from-root>",
|
"yaml": "<from-root>",
|
||||||
"yargs": "<from-root>"
|
"yargs": "<from-root>",
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@bazel/buildifier": "<from-root>",
|
"@bazel/buildifier": "<from-root>",
|
||||||
"clang-format": "<from-root>",
|
"clang-format": "<from-root>",
|
||||||
"protractor": "<from-root>",
|
"protractor": "<from-root>",
|
||||||
|
@ -12,13 +12,11 @@ ts_library(
|
|||||||
"@npm//@octokit/graphql",
|
"@npm//@octokit/graphql",
|
||||||
"@npm//@octokit/rest",
|
"@npm//@octokit/rest",
|
||||||
"@npm//@octokit/types",
|
"@npm//@octokit/types",
|
||||||
"@npm//@types/fs-extra",
|
|
||||||
"@npm//@types/inquirer",
|
"@npm//@types/inquirer",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
"@npm//@types/shelljs",
|
"@npm//@types/shelljs",
|
||||||
"@npm//@types/yargs",
|
"@npm//@types/yargs",
|
||||||
"@npm//chalk",
|
"@npm//chalk",
|
||||||
"@npm//fs-extra",
|
|
||||||
"@npm//inquirer",
|
"@npm//inquirer",
|
||||||
"@npm//inquirer-autocomplete-prompt",
|
"@npm//inquirer-autocomplete-prompt",
|
||||||
"@npm//shelljs",
|
"@npm//shelljs",
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import {existsSync} from 'fs';
|
import {existsSync} from 'fs';
|
||||||
import {dirname, join} from 'path';
|
import {dirname, join} from 'path';
|
||||||
|
|
||||||
import {error} from './console';
|
import {debug, error} from './console';
|
||||||
import {exec} from './shelljs';
|
import {exec} from './shelljs';
|
||||||
import {isTsNodeAvailable} from './ts-node';
|
import {isTsNodeAvailable} from './ts-node';
|
||||||
|
|
||||||
@ -49,7 +49,16 @@ export type NgDevConfig<T = {}> = CommonConfig&T;
|
|||||||
const CONFIG_FILE_PATH = '.ng-dev/config';
|
const CONFIG_FILE_PATH = '.ng-dev/config';
|
||||||
|
|
||||||
/** The configuration for ng-dev. */
|
/** The configuration for ng-dev. */
|
||||||
let CONFIG: {}|null = null;
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the configuration from the file system, returning the already loaded
|
* Get the configuration from the file system, returning the already loaded
|
||||||
@ -57,15 +66,15 @@ let CONFIG: {}|null = null;
|
|||||||
*/
|
*/
|
||||||
export function getConfig(): NgDevConfig {
|
export function getConfig(): NgDevConfig {
|
||||||
// If the global config is not defined, load it from the file system.
|
// If the global config is not defined, load it from the file system.
|
||||||
if (CONFIG === null) {
|
if (cachedConfig === null) {
|
||||||
// The full path to the configuration file.
|
// The full path to the configuration file.
|
||||||
const configPath = join(getRepoBaseDir(), CONFIG_FILE_PATH);
|
const configPath = join(getRepoBaseDir(), CONFIG_FILE_PATH);
|
||||||
// Set the global config object.
|
// Read the configuration and validate it before caching it for the future.
|
||||||
CONFIG = readConfigFile(configPath);
|
cachedConfig = validateCommonConfig(readConfigFile(configPath));
|
||||||
}
|
}
|
||||||
// Return a clone of the global config to ensure that a new instance of the config is returned
|
// Return a clone of the cached global config to ensure that a new instance of the config
|
||||||
// each time, preventing unexpected effects of modifications to the config object.
|
// is returned each time, preventing unexpected effects of modifications to the config object.
|
||||||
return validateCommonConfig({...CONFIG});
|
return {...cachedConfig};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Validate the common configuration has been met for the ng-dev command. */
|
/** Validate the common configuration has been met for the ng-dev command. */
|
||||||
@ -86,8 +95,11 @@ function validateCommonConfig(config: Partial<NgDevConfig>) {
|
|||||||
return config as NgDevConfig;
|
return config as NgDevConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolves and reads the specified configuration file. */
|
/**
|
||||||
function readConfigFile(configPath: string): object {
|
* 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 {
|
||||||
// If the the `.ts` extension has not been set up already, and a TypeScript based
|
// 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.
|
// version of the given configuration seems to exist, set up `ts-node` if available.
|
||||||
if (require.extensions['.ts'] === undefined && existsSync(`${configPath}.ts`) &&
|
if (require.extensions['.ts'] === undefined && existsSync(`${configPath}.ts`) &&
|
||||||
@ -103,7 +115,12 @@ function readConfigFile(configPath: string): object {
|
|||||||
try {
|
try {
|
||||||
return require(configPath);
|
return require(configPath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error('Could not read configuration file.');
|
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(e);
|
error(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@ -135,3 +152,23 @@ export function getRepoBaseDir() {
|
|||||||
}
|
}
|
||||||
return baseRepoDir.trim();
|
return baseRepoDir.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the local user configuration from the file system, returning the already loaded copy if it is
|
||||||
|
* defined.
|
||||||
|
*
|
||||||
|
* @returns The user configuration object, or an empty object if no user configuration file is
|
||||||
|
* present. The object is an untyped object as there are no required user configurations.
|
||||||
|
*/
|
||||||
|
export function getUserConfig() {
|
||||||
|
// If the global config is not defined, load it from the file system.
|
||||||
|
if (userConfig === null) {
|
||||||
|
// The full path to the configuration file.
|
||||||
|
const configPath = join(getRepoBaseDir(), USER_CONFIG_FILE_PATH);
|
||||||
|
// Set the global config object.
|
||||||
|
userConfig = readConfigFile(configPath, true);
|
||||||
|
}
|
||||||
|
// Return a clone of the user config to ensure that a new instance of the config is returned
|
||||||
|
// each time, preventing unexpected effects of modifications to the config object.
|
||||||
|
return {...userConfig};
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import {writeFileSync} from 'fs-extra';
|
import {writeFileSync} from 'fs';
|
||||||
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
|
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
|
||||||
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
||||||
import {join} from 'path';
|
import {join} from 'path';
|
||||||
@ -196,6 +196,9 @@ export function captureLogOutputForCommand(argv: Arguments) {
|
|||||||
/** Path to the log file location. */
|
/** Path to the log file location. */
|
||||||
const logFilePath = join(getRepoBaseDir(), '.ng-dev.log');
|
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);
|
writeFileSync(logFilePath, LOGGED_TEXT);
|
||||||
|
|
||||||
// For failure codes greater than 1, the new logged lines should be written to a specific log
|
// For failure codes greater than 1, the new logged lines should be written to a specific log
|
||||||
|
@ -34,7 +34,7 @@ export class GitCommandError extends Error {
|
|||||||
/**
|
/**
|
||||||
* Common client for performing Git interactions.
|
* Common client for performing Git interactions.
|
||||||
*
|
*
|
||||||
* Takes in two optional arguements:
|
* Takes in two optional arguments:
|
||||||
* _githubToken: the token used for authentifation in github interactions, by default empty
|
* _githubToken: the token used for authentifation in github interactions, by default empty
|
||||||
* allowing readonly actions.
|
* allowing readonly actions.
|
||||||
* _config: The dev-infra configuration containing GitClientConfig information, by default
|
* _config: The dev-infra configuration containing GitClientConfig information, by default
|
||||||
|
@ -4,71 +4,23 @@ Caretaker is responsible for merging PRs into the individual branches and intern
|
|||||||
|
|
||||||
## Responsibilities
|
## Responsibilities
|
||||||
|
|
||||||
- Draining the queue of PRs ready to be merged. (PRs with [`PR action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22PR+action%3A+merge%22) label)
|
- Draining the queue of PRs ready to be merged. (PRs with [`action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22action%3A+merge%22) label)
|
||||||
- Assigning [new issues](https://github.com/angular/angular/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) to individual component authors.
|
- Assigning [new issues](https://github.com/angular/angular/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) to individual component authors.
|
||||||
|
|
||||||
## Merging the PR
|
## Merging the PR
|
||||||
|
|
||||||
A PR needs to have `PR action: merge` and `PR target: *` labels to be considered
|
A PR needs to have `action: merge` and `target: *` labels to be considered
|
||||||
ready to merge. Merging is performed by running `merge-pr` with a PR number to merge.
|
ready to merge. Merging is performed by running `ng-dev pr merge` with a PR number to merge.
|
||||||
|
|
||||||
|
The tooling automatically verifies the given PR is ready for merge. If the PR passes the tests, the
|
||||||
|
tool will automatically merge it based on the applied target label.
|
||||||
|
|
||||||
To merge a PR run:
|
To merge a PR run:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ ./scripts/github/merge-pr 1234
|
$ yarn ng-dev pr merge <pr number>
|
||||||
```
|
```
|
||||||
|
|
||||||
The `merge-pr` script will:
|
|
||||||
- Ensure that all appropriate labels are on the PR.
|
|
||||||
- Fetches the latest PR code from the `angular/angular` repo.
|
|
||||||
- It will `cherry-pick` all of the SHAs from the PR into the current corresponding branches `master` and or `?.?.x` (patch).
|
|
||||||
- It will rewrite commit history by automatically adding `Close #1234` and `(#1234)` into the commit message.
|
|
||||||
|
|
||||||
NOTE: The `merge-pr` will land the PR on `master` and or `?.?.x` (patch) as described by `PR target: *` label.
|
|
||||||
|
|
||||||
### Recovering from failed `merge-pr` due to conflicts
|
### Recovering from failed `merge-pr` due to conflicts
|
||||||
|
|
||||||
When running `merge-pr` the script will output the commands which it is about to run.
|
The `ng-dev pr merge` tool will automatically restore to the previous git state when a merge fails.
|
||||||
|
|
||||||
```
|
|
||||||
$ ./scripts/github/merge-pr 1234
|
|
||||||
======================
|
|
||||||
GitHub Merge PR Steps
|
|
||||||
======================
|
|
||||||
git cherry-pick angular/pr/1234~1..angular/pr/1234
|
|
||||||
git filter-branch -f --msg-filter "/home/misko/angular/scripts/github/utils/github.closes 1234" HEAD~1..HEAD
|
|
||||||
```
|
|
||||||
|
|
||||||
If the `cherry-pick` command fails than resolve conflicts and use `git cherry-pick --continue` once ready. After the `cherry-pick` is done cut&paste and run the `filter-branch` command to properly rewrite the messages
|
|
||||||
|
|
||||||
## Cherry-picking PRs into patch branch
|
|
||||||
|
|
||||||
In addition to merging PRs into the master branch, many PRs need to be also merged into a patch branch.
|
|
||||||
Follow these steps to get patch branch up to date.
|
|
||||||
|
|
||||||
1. Check out the most recent patch branch: `git checkout 4.3.x`
|
|
||||||
2. Get a list of PRs merged into master: `git log master --oneline -n10`
|
|
||||||
3. For each PR number in the commit message run: `./scripts/github/merge-pr 1234`
|
|
||||||
- The PR will only merge if the `PR target:` matches the branch.
|
|
||||||
|
|
||||||
Once all of the PRs are in patch branch, push the all branches and tags to github using `push-upstream` script.
|
|
||||||
|
|
||||||
|
|
||||||
## Pushing merged PRs into github
|
|
||||||
|
|
||||||
Use `push-upstream` script to push all of the branch and tags to github.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ./scripts/github/push-upstream
|
|
||||||
git push git@github.com:angular/angular.git master:master 4.3.x:4.3.x
|
|
||||||
Counting objects: 25, done.
|
|
||||||
Delta compression using up to 6 threads.
|
|
||||||
Compressing objects: 100% (17/17), done.
|
|
||||||
Writing objects: 100% (25/25), 2.22 KiB | 284.00 KiB/s, done.
|
|
||||||
Total 25 (delta 22), reused 8 (delta 7)
|
|
||||||
remote: Resolving deltas: 100% (22/22), completed with 18 local objects.
|
|
||||||
To github.com:angular/angular.git
|
|
||||||
079d884b6..d1c4a94bb master -> master
|
|
||||||
git push --tags -f git@github.com:angular/angular.git patch_sync:patch_sync
|
|
||||||
Everything up-to-date
|
|
||||||
```
|
|
||||||
|
@ -12,7 +12,7 @@ Change approvals in our monorepo are managed via [PullApprove](https://docs.pull
|
|||||||
# Merging
|
# Merging
|
||||||
|
|
||||||
Once a change has all of the required approvals, either the last approver or the PR author (if PR author has the project collaborator status)
|
Once a change has all of the required approvals, either the last approver or the PR author (if PR author has the project collaborator status)
|
||||||
should mark the PR with the `PR action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
|
should mark the PR with the `action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
|
||||||
This signals to the caretaker that the PR should be merged. See [merge instructions](CARETAKER.md).
|
This signals to the caretaker that the PR should be merged. See [merge instructions](CARETAKER.md).
|
||||||
|
|
||||||
# Who is the Caretaker?
|
# Who is the Caretaker?
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Triage Process and GitHub Labels for Angular
|
# 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 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.
|
The owner of the component is then responsible for the secondary / component-level triage.
|
||||||
|
|
||||||
@ -125,32 +125,32 @@ Triaging PRs is the same as triaging issues, except that the labels `frequency:
|
|||||||
|
|
||||||
PRs also have additional label categories that should be used to signal their state.
|
PRs also have additional label categories that should be used to signal their state.
|
||||||
|
|
||||||
Every triaged PR must have a `PR action` label assigned to it:
|
Every triaged PR must have a `action: *` label assigned to it:
|
||||||
|
|
||||||
* `PR action: discuss`: Discussion is needed, to be led by the author.
|
* `action: discuss`: Discussion is needed, to be led by the author.
|
||||||
* _**Who adds it:** Typically the PR author._
|
* _**Who adds it:** Typically the PR author._
|
||||||
* _**Who removes it:** Whoever added it._
|
* _**Who removes it:** Whoever added it._
|
||||||
* `PR action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
|
* `action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
|
||||||
* _**Who adds it:** Any team member. The caretaker can use it to differentiate PRs pending review from merge-ready PRs._
|
* _**Who adds it:** Any team member. The caretaker can use it to differentiate PRs pending review from merge-ready PRs._
|
||||||
* _**Who removes it:** Whoever added it or the reviewer adding the last missing review._
|
* _**Who removes it:** Whoever added it or the reviewer adding the last missing review._
|
||||||
* `PR action: cleanup`: More work is needed from the author.
|
* `action: cleanup`: More work is needed from the author.
|
||||||
* _**Who adds it:** The reviewer requesting changes to the PR._
|
* _**Who adds it:** The reviewer requesting changes to the PR._
|
||||||
* _**Who removes it:** Either the author (after implementing the requested changes) or the reviewer (after confirming the requested changes have been implemented)._
|
* _**Who removes it:** Either the author (after implementing the requested changes) or the reviewer (after confirming the requested changes have been implemented)._
|
||||||
* `PR action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
|
* `action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
|
||||||
* _**Who adds it:** Typically the PR author._
|
* _**Who adds it:** Typically the PR author._
|
||||||
* _**Who removes it:** Whoever added it._
|
* _**Who removes it:** Whoever added it._
|
||||||
|
|
||||||
|
|
||||||
In addition, PRs can have the following states:
|
In addition, PRs can have the following states:
|
||||||
|
|
||||||
* `PR state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
|
* `state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
|
||||||
* _**Who adds it:** The PR author._
|
* _**Who adds it:** The PR author._
|
||||||
* _**Who removes it:** Whoever added it._
|
* _**Who removes it:** Whoever added it._
|
||||||
* `PR state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
|
* `state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
|
||||||
* _**Who adds it:** Any team member._
|
* _**Who adds it:** Any team member._
|
||||||
* _**Who removes 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
|
## PR Target
|
||||||
@ -160,15 +160,29 @@ 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.
|
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.
|
This decision is then honored when the PR is being merged by the caretaker.
|
||||||
|
|
||||||
To communicate the target we use the following labels:
|
To communicate the target we use GitHub labels and only one target label may be applied to a PR.
|
||||||
|
|
||||||
* `PR target: master & patch`: the PR should me merged into the master branch and cherry-picked into the most recent patch branch. All PRs with fixes, docs and refactorings should use this target.
|
Targeting an active release train:
|
||||||
* `PR target: master-only`: the PR should be merged only into the `master` branch. All PRs with new features, API changes or high-risk changes should use this target.
|
|
||||||
* `PR target: patch-only`: the PR should be merged only into the most recent patch branch (e.g. 5.0.x). This target is useful if a `master & patch` PR can't be cleanly cherry-picked into the stable branch and a new PR is needed.
|
|
||||||
* `PR target: LTS-only`: the PR should be merged only into the active LTS branch(es). Only security and critical fixes are allowed in these branches. Always send a new PR targeting just the LTS branch and request review approval from @IgorMinar.
|
|
||||||
* `PR target: TBD`: the target is yet to be determined.
|
|
||||||
|
|
||||||
If a PR is missing the `PR target: *` label, or if the label is set to "TBD" when the PR is sent to the caretaker, the caretaker should reject the PR and request the appropriate target label to be applied before the PR is merged.
|
* `target: major`: Any breaking change
|
||||||
|
* `target: minor`: Any new feature
|
||||||
|
* `target: patch`: Bug fixes, refactorings, documentation changes, etc. that pose no or very low risk of adversely
|
||||||
|
affecting existing applications.
|
||||||
|
|
||||||
|
Special Cases:
|
||||||
|
* `target: rc`: A critical fix for an active release-train while it is in a feature freeze or RC phase
|
||||||
|
* `target: lts`: A criticial fix for a specific release-train that is still within the long term support phase
|
||||||
|
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- To land a change only in a patch/RC branch, without landing it in any other active release-train branch (such
|
||||||
|
as `master`), the patch/RC branch can be targeted in the GitHub UI with the appropriate
|
||||||
|
`target: patch`/`target: rc` label.
|
||||||
|
- `target: lts` PRs must target the specific LTS branch they would need to merge into in the GitHub UI, in
|
||||||
|
cases which a change is desired in multiple LTS branches, individual PRs for each LTS branch must be created
|
||||||
|
|
||||||
|
|
||||||
|
If a PR is missing the `target:*` label, it will be marked as pending by the angular robot status checks.
|
||||||
|
|
||||||
|
|
||||||
## PR Approvals
|
## PR Approvals
|
||||||
@ -182,7 +196,7 @@ In any case, the reviewer should actually look through the code and provide feed
|
|||||||
|
|
||||||
Note that approved state does not mean a PR is ready to be merged.
|
Note that approved state does not mean a PR is ready to be merged.
|
||||||
For example, a reviewer might approve the PR but request a minor tweak that doesn't need further review, e.g., a rebase or small uncontroversial change.
|
For example, a reviewer might approve the PR but request a minor tweak that doesn't need further review, e.g., a rebase or small uncontroversial change.
|
||||||
Only the `PR action: merge` label means that the PR is ready for merging.
|
Only the `action: merge` label means that the PR is ready for merging.
|
||||||
|
|
||||||
|
|
||||||
## Special Labels
|
## Special Labels
|
||||||
@ -201,7 +215,7 @@ Only issues with `cla:yes` should be merged into master.
|
|||||||
|
|
||||||
Applying this label to a PR makes the angular.io preview available regardless of the author. [More info](../aio/aio-builds-setup/docs/overview--security-model.md)
|
Applying this label to a PR makes the angular.io preview available regardless of the author. [More info](../aio/aio-builds-setup/docs/overview--security-model.md)
|
||||||
|
|
||||||
### `PR action: merge-assistance`
|
### `action: merge-assistance`
|
||||||
* _**Who adds it:** Any team member._
|
* _**Who adds it:** Any team member._
|
||||||
* _**Who removes it:** Any team member._
|
* _**Who removes it:** Any team member._
|
||||||
|
|
||||||
@ -211,7 +225,7 @@ The comment should be formatted like this: `merge-assistance: <explain what kind
|
|||||||
|
|
||||||
For example, the PR owner might not be a Googler and needs help to run g3sync; or one of the checks is failing due to external causes and the PR should still be merged.
|
For example, the PR owner might not be a Googler and needs help to run g3sync; or one of the checks is failing due to external causes and the PR should still be merged.
|
||||||
|
|
||||||
### `PR action: rerun CI at HEAD`
|
### `action: rerun CI at HEAD`
|
||||||
* _**Who adds it:** Any team member._
|
* _**Who adds it:** Any team member._
|
||||||
* _**Who removes it:** The Angular Bot, once it triggers the CI rerun._
|
* _**Who removes it:** The Angular Bot, once it triggers the CI rerun._
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ export declare enum ErrorCode {
|
|||||||
CONFIG_FLAT_MODULE_NO_INDEX = 4001,
|
CONFIG_FLAT_MODULE_NO_INDEX = 4001,
|
||||||
CONFIG_STRICT_TEMPLATES_IMPLIES_FULL_TEMPLATE_TYPECHECK = 4002,
|
CONFIG_STRICT_TEMPLATES_IMPLIES_FULL_TEMPLATE_TYPECHECK = 4002,
|
||||||
HOST_BINDING_PARSE_ERROR = 5001,
|
HOST_BINDING_PARSE_ERROR = 5001,
|
||||||
|
TEMPLATE_PARSE_ERROR = 5002,
|
||||||
NGMODULE_INVALID_DECLARATION = 6001,
|
NGMODULE_INVALID_DECLARATION = 6001,
|
||||||
NGMODULE_INVALID_IMPORT = 6002,
|
NGMODULE_INVALID_IMPORT = 6002,
|
||||||
NGMODULE_INVALID_EXPORT = 6003,
|
NGMODULE_INVALID_EXPORT = 6003,
|
||||||
|
2
goldens/public-api/router/router.d.ts
vendored
2
goldens/public-api/router/router.d.ts
vendored
@ -432,7 +432,7 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
|
|||||||
constructor(router: Router, route: ActivatedRoute, locationStrategy: LocationStrategy);
|
constructor(router: Router, route: ActivatedRoute, locationStrategy: LocationStrategy);
|
||||||
ngOnChanges(changes: SimpleChanges): any;
|
ngOnChanges(changes: SimpleChanges): any;
|
||||||
ngOnDestroy(): any;
|
ngOnDestroy(): any;
|
||||||
onClick(button: number, ctrlKey: boolean, metaKey: boolean, shiftKey: boolean): boolean;
|
onClick(button: number, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class RouterModule {
|
export declare class RouterModule {
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 1485,
|
"runtime-es2015": 1485,
|
||||||
"main-es2015": 147573,
|
"main-es2015": 146989,
|
||||||
"polyfills-es2015": 36571
|
"polyfills-es2015": 36571
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "angular-srcs",
|
"name": "angular-srcs",
|
||||||
"version": "10.1.0",
|
"version": "10.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Angular - a web framework for modern web apps",
|
"description": "Angular - a web framework for modern web apps",
|
||||||
"homepage": "https://github.com/angular/angular",
|
"homepage": "https://github.com/angular/angular",
|
||||||
@ -35,6 +35,8 @@
|
|||||||
"tslint": "tsc -p tools/tsconfig.json && tslint -c tslint.json \"+(dev-infra|packages|modules|scripts|tools)/**/*.+(js|ts)\"",
|
"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:check": "node goldens/public-api/manage.js test",
|
||||||
"public-api:update": "node goldens/public-api/manage.js accept",
|
"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": "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:check": "yarn -s ts-circular-deps check",
|
||||||
"ts-circular-deps:approve": "yarn -s ts-circular-deps approve",
|
"ts-circular-deps:approve": "yarn -s ts-circular-deps approve",
|
||||||
|
@ -14,6 +14,7 @@ import {Logger} from '../../../src/ngtsc/logging';
|
|||||||
import {ParsedConfiguration} from '../../../src/perform_compile';
|
import {ParsedConfiguration} from '../../../src/perform_compile';
|
||||||
import {getEntryPointFormat} from '../packages/entry_point';
|
import {getEntryPointFormat} from '../packages/entry_point';
|
||||||
import {makeEntryPointBundle} from '../packages/entry_point_bundle';
|
import {makeEntryPointBundle} from '../packages/entry_point_bundle';
|
||||||
|
import {createModuleResolutionCache, SharedFileCache} from '../packages/source_file_cache';
|
||||||
import {PathMappings} from '../path_mappings';
|
import {PathMappings} from '../path_mappings';
|
||||||
import {FileWriter} from '../writing/file_writer';
|
import {FileWriter} from '../writing/file_writer';
|
||||||
|
|
||||||
@ -30,6 +31,8 @@ export function getCreateCompileFn(
|
|||||||
return (beforeWritingFiles, onTaskCompleted) => {
|
return (beforeWritingFiles, onTaskCompleted) => {
|
||||||
const {Transformer} = require('../packages/transformer');
|
const {Transformer} = require('../packages/transformer');
|
||||||
const transformer = new Transformer(fileSystem, logger, tsConfig);
|
const transformer = new Transformer(fileSystem, logger, tsConfig);
|
||||||
|
const sharedFileCache = new SharedFileCache(fileSystem);
|
||||||
|
const moduleResolutionCache = createModuleResolutionCache(fileSystem);
|
||||||
|
|
||||||
return (task: Task) => {
|
return (task: Task) => {
|
||||||
const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task;
|
const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task;
|
||||||
@ -54,8 +57,8 @@ export function getCreateCompileFn(
|
|||||||
logger.info(`Compiling ${entryPoint.name} : ${formatProperty} as ${format}`);
|
logger.info(`Compiling ${entryPoint.name} : ${formatProperty} as ${format}`);
|
||||||
|
|
||||||
const bundle = makeEntryPointBundle(
|
const bundle = makeEntryPointBundle(
|
||||||
fileSystem, entryPoint, formatPath, isCore, format, processDts, pathMappings, true,
|
fileSystem, entryPoint, sharedFileCache, moduleResolutionCache, formatPath, isCore,
|
||||||
enableI18nLegacyMessageIdFormat);
|
format, processDts, pathMappings, true, enableI18nLegacyMessageIdFormat);
|
||||||
|
|
||||||
const result = transformer.transform(bundle);
|
const result = transformer.transform(bundle);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
|
|
||||||
|
|
||||||
|
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
|
||||||
import {Logger} from '../../../src/ngtsc/logging';
|
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 {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';
|
import {isWithinPackage} from '../analysis/util';
|
||||||
@ -1593,35 +1593,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
|||||||
constructorParamInfo[index] :
|
constructorParamInfo[index] :
|
||||||
{decorators: null, typeExpression: null};
|
{decorators: null, typeExpression: null};
|
||||||
const nameNode = node.name;
|
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 {
|
return {
|
||||||
name: getNameText(nameNode),
|
name: getNameText(nameNode),
|
||||||
@ -1633,6 +1605,29 @@ 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,
|
* Get the parameter type and decorators for the constructor of a class,
|
||||||
* where the information is stored on a static property of the class.
|
* where the information is stored on a static property of the class.
|
||||||
|
@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
/// <reference types="node" />
|
/// <reference types="node" />
|
||||||
|
|
||||||
import * as os from 'os';
|
|
||||||
|
|
||||||
import {AbsoluteFsPath, FileSystem, resolve} from '../../src/ngtsc/file_system';
|
import {AbsoluteFsPath, FileSystem, resolve} from '../../src/ngtsc/file_system';
|
||||||
import {Logger} from '../../src/ngtsc/logging';
|
import {Logger} from '../../src/ngtsc/logging';
|
||||||
import {ParsedConfiguration} from '../../src/perform_compile';
|
import {ParsedConfiguration} from '../../src/perform_compile';
|
||||||
@ -35,7 +33,7 @@ import {composeTaskCompletedCallbacks, createLogErrorHandler, createMarkAsProces
|
|||||||
import {AsyncLocker} from './locking/async_locker';
|
import {AsyncLocker} from './locking/async_locker';
|
||||||
import {LockFileWithChildProcess} from './locking/lock_file_with_child_process';
|
import {LockFileWithChildProcess} from './locking/lock_file_with_child_process';
|
||||||
import {SyncLocker} from './locking/sync_locker';
|
import {SyncLocker} from './locking/sync_locker';
|
||||||
import {AsyncNgccOptions, getSharedSetup, SyncNgccOptions} from './ngcc_options';
|
import {AsyncNgccOptions, getMaxNumberOfWorkers, getSharedSetup, SyncNgccOptions} from './ngcc_options';
|
||||||
import {NgccConfiguration} from './packages/configuration';
|
import {NgccConfiguration} from './packages/configuration';
|
||||||
import {EntryPointJsonProperty, SUPPORTED_FORMAT_PROPERTIES} from './packages/entry_point';
|
import {EntryPointJsonProperty, SUPPORTED_FORMAT_PROPERTIES} from './packages/entry_point';
|
||||||
import {EntryPointManifest, InvalidatingEntryPointManifest} from './packages/entry_point_manifest';
|
import {EntryPointManifest, InvalidatingEntryPointManifest} from './packages/entry_point_manifest';
|
||||||
@ -92,10 +90,9 @@ export function mainNgcc(options: AsyncNgccOptions|SyncNgccOptions): void|Promis
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute in parallel, if async execution is acceptable and there are more than 2 CPU cores.
|
// Determine the number of workers to use and whether ngcc should run in parallel.
|
||||||
// (One CPU core is always reserved for the master process and we need at least 2 worker processes
|
const workerCount = async ? getMaxNumberOfWorkers() : 1;
|
||||||
// in order to run tasks in parallel.)
|
const inParallel = workerCount > 1;
|
||||||
const inParallel = async && (os.cpus().length > 2);
|
|
||||||
|
|
||||||
const analyzeEntryPoints = getAnalyzeEntryPointsFn(
|
const analyzeEntryPoints = getAnalyzeEntryPointsFn(
|
||||||
logger, finder, fileSystem, supportedPropertiesToConsider, compileAllFormats,
|
logger, finder, fileSystem, supportedPropertiesToConsider, compileAllFormats,
|
||||||
@ -113,7 +110,7 @@ export function mainNgcc(options: AsyncNgccOptions|SyncNgccOptions): void|Promis
|
|||||||
const createTaskCompletedCallback =
|
const createTaskCompletedCallback =
|
||||||
getCreateTaskCompletedCallback(pkgJsonUpdater, errorOnFailedEntryPoint, logger, fileSystem);
|
getCreateTaskCompletedCallback(pkgJsonUpdater, errorOnFailedEntryPoint, logger, fileSystem);
|
||||||
const executor = getExecutor(
|
const executor = getExecutor(
|
||||||
async, inParallel, logger, fileWriter, pkgJsonUpdater, fileSystem, config,
|
async, workerCount, logger, fileWriter, pkgJsonUpdater, fileSystem, config,
|
||||||
createTaskCompletedCallback);
|
createTaskCompletedCallback);
|
||||||
|
|
||||||
return executor.execute(analyzeEntryPoints, createCompileFn);
|
return executor.execute(analyzeEntryPoints, createCompileFn);
|
||||||
@ -153,7 +150,7 @@ function getCreateTaskCompletedCallback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getExecutor(
|
function getExecutor(
|
||||||
async: boolean, inParallel: boolean, logger: Logger, fileWriter: FileWriter,
|
async: boolean, workerCount: number, logger: Logger, fileWriter: FileWriter,
|
||||||
pkgJsonUpdater: PackageJsonUpdater, fileSystem: FileSystem, config: NgccConfiguration,
|
pkgJsonUpdater: PackageJsonUpdater, fileSystem: FileSystem, config: NgccConfiguration,
|
||||||
createTaskCompletedCallback: CreateTaskCompletedCallback): Executor {
|
createTaskCompletedCallback: CreateTaskCompletedCallback): Executor {
|
||||||
const lockFile = new LockFileWithChildProcess(fileSystem, logger);
|
const lockFile = new LockFileWithChildProcess(fileSystem, logger);
|
||||||
@ -161,9 +158,8 @@ function getExecutor(
|
|||||||
// Execute asynchronously (either serially or in parallel)
|
// Execute asynchronously (either serially or in parallel)
|
||||||
const {retryAttempts, retryDelay} = config.getLockingConfig();
|
const {retryAttempts, retryDelay} = config.getLockingConfig();
|
||||||
const locker = new AsyncLocker(lockFile, logger, retryDelay, retryAttempts);
|
const locker = new AsyncLocker(lockFile, logger, retryDelay, retryAttempts);
|
||||||
if (inParallel) {
|
if (workerCount > 1) {
|
||||||
// Execute in parallel. Use up to 8 CPU cores for workers, always reserving one for master.
|
// Execute in parallel.
|
||||||
const workerCount = Math.min(8, os.cpus().length - 1);
|
|
||||||
return new ClusterExecutor(
|
return new ClusterExecutor(
|
||||||
workerCount, fileSystem, logger, fileWriter, pkgJsonUpdater, locker,
|
workerCount, fileSystem, logger, fileWriter, pkgJsonUpdater, locker,
|
||||||
createTaskCompletedCallback);
|
createTaskCompletedCallback);
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '../../src/ngtsc/file_system';
|
||||||
import {ConsoleLogger, Logger, LogLevel} from '../../src/ngtsc/logging';
|
import {ConsoleLogger, Logger, LogLevel} from '../../src/ngtsc/logging';
|
||||||
import {ParsedConfiguration, readConfiguration} from '../../src/perform_compile';
|
import {ParsedConfiguration, readConfiguration} from '../../src/perform_compile';
|
||||||
@ -254,3 +256,26 @@ function checkForSolutionStyleTsConfig(
|
|||||||
` ngcc ... --tsconfig "${fileSystem.relative(projectPath, tsConfig.project)}"`);
|
` ngcc ... --tsconfig "${fileSystem.relative(projectPath, tsConfig.project)}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the maximum number of workers to use for parallel execution. This can be set using the
|
||||||
|
* NGCC_MAX_WORKERS environment variable, or is computed based on the number of available CPUs. One
|
||||||
|
* CPU core is always reserved for the master process, so we take the number of CPUs minus one, with
|
||||||
|
* a maximum of 4 workers. We don't scale the number of workers beyond 4 by default, as it takes
|
||||||
|
* considerably more memory and CPU cycles while not offering a substantial improvement in time.
|
||||||
|
*/
|
||||||
|
export function getMaxNumberOfWorkers(): number {
|
||||||
|
const maxWorkers = process.env.NGCC_MAX_WORKERS;
|
||||||
|
if (maxWorkers === undefined) {
|
||||||
|
// Use up to 4 CPU cores for workers, always reserving one for master.
|
||||||
|
return Math.max(1, Math.min(4, os.cpus().length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericMaxWorkers = +maxWorkers.trim();
|
||||||
|
if (!Number.isInteger(numericMaxWorkers)) {
|
||||||
|
throw new Error('NGCC_MAX_WORKERS should be an integer.');
|
||||||
|
} else if (numericMaxWorkers < 1) {
|
||||||
|
throw new Error('NGCC_MAX_WORKERS should be at least 1.');
|
||||||
|
}
|
||||||
|
return numericMaxWorkers;
|
||||||
|
}
|
||||||
|
@ -6,11 +6,12 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
|
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
|
||||||
import {PathMappings} from '../path_mappings';
|
import {PathMappings} from '../path_mappings';
|
||||||
import {BundleProgram, makeBundleProgram} from './bundle_program';
|
import {BundleProgram, makeBundleProgram} from './bundle_program';
|
||||||
import {EntryPoint, EntryPointFormat} from './entry_point';
|
import {EntryPoint, EntryPointFormat} from './entry_point';
|
||||||
import {NgccSourcesCompilerHost} from './ngcc_compiler_host';
|
import {NgccDtsCompilerHost, NgccSourcesCompilerHost} from './ngcc_compiler_host';
|
||||||
|
import {EntryPointFileCache, SharedFileCache} from './source_file_cache';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A bundle of files and paths (and TS programs) that correspond to a particular
|
* A bundle of files and paths (and TS programs) that correspond to a particular
|
||||||
@ -31,6 +32,8 @@ export interface EntryPointBundle {
|
|||||||
* Get an object that describes a formatted bundle for an entry-point.
|
* Get an object that describes a formatted bundle for an entry-point.
|
||||||
* @param fs The current file-system being used.
|
* @param fs The current file-system being used.
|
||||||
* @param entryPoint The entry-point that contains the bundle.
|
* @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 formatPath The path to the source files for this bundle.
|
||||||
* @param isCore This entry point is the Angular core package.
|
* @param isCore This entry point is the Angular core package.
|
||||||
* @param format The underlying format of the bundle.
|
* @param format The underlying format of the bundle.
|
||||||
@ -42,7 +45,8 @@ export interface EntryPointBundle {
|
|||||||
* component templates.
|
* component templates.
|
||||||
*/
|
*/
|
||||||
export function makeEntryPointBundle(
|
export function makeEntryPointBundle(
|
||||||
fs: FileSystem, entryPoint: EntryPoint, formatPath: string, isCore: boolean,
|
fs: FileSystem, entryPoint: EntryPoint, sharedFileCache: SharedFileCache,
|
||||||
|
moduleResolutionCache: ts.ModuleResolutionCache, formatPath: string, isCore: boolean,
|
||||||
format: EntryPointFormat, transformDts: boolean, pathMappings?: PathMappings,
|
format: EntryPointFormat, transformDts: boolean, pathMappings?: PathMappings,
|
||||||
mirrorDtsFromSrc: boolean = false,
|
mirrorDtsFromSrc: boolean = false,
|
||||||
enableI18nLegacyMessageIdFormat: boolean = true): EntryPointBundle {
|
enableI18nLegacyMessageIdFormat: boolean = true): EntryPointBundle {
|
||||||
@ -50,8 +54,10 @@ export function makeEntryPointBundle(
|
|||||||
const rootDir = entryPoint.packagePath;
|
const rootDir = entryPoint.packagePath;
|
||||||
const options: ts
|
const options: ts
|
||||||
.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings};
|
.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings};
|
||||||
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.packagePath);
|
const entryPointCache = new EntryPointFileCache(fs, sharedFileCache);
|
||||||
const dtsHost = new NgtscCompilerHost(fs, options);
|
const dtsHost = new NgccDtsCompilerHost(fs, options, entryPointCache, moduleResolutionCache);
|
||||||
|
const srcHost = new NgccSourcesCompilerHost(
|
||||||
|
fs, options, entryPointCache, moduleResolutionCache, entryPoint.packagePath);
|
||||||
|
|
||||||
// Create the bundle programs, as necessary.
|
// Create the bundle programs, as necessary.
|
||||||
const absFormatPath = fs.resolve(entryPoint.path, formatPath);
|
const absFormatPath = fs.resolve(entryPoint.path, formatPath);
|
||||||
|
@ -10,6 +10,7 @@ import * as ts from 'typescript';
|
|||||||
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
|
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
|
||||||
import {isWithinPackage} from '../analysis/util';
|
import {isWithinPackage} from '../analysis/util';
|
||||||
import {isRelativePath} from '../utils';
|
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
|
* Represents a compiler host that resolves a module import as a JavaScript source file if
|
||||||
@ -18,19 +19,24 @@ import {isRelativePath} from '../utils';
|
|||||||
* would otherwise let TypeScript prefer the .d.ts file instead of the JavaScript source file.
|
* would otherwise let TypeScript prefer the .d.ts file instead of the JavaScript source file.
|
||||||
*/
|
*/
|
||||||
export class NgccSourcesCompilerHost extends NgtscCompilerHost {
|
export class NgccSourcesCompilerHost extends NgtscCompilerHost {
|
||||||
private cache = ts.createModuleResolutionCache(
|
constructor(
|
||||||
this.getCurrentDirectory(), file => this.getCanonicalFileName(file));
|
fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointFileCache,
|
||||||
|
private moduleResolutionCache: ts.ModuleResolutionCache,
|
||||||
constructor(fs: FileSystem, options: ts.CompilerOptions, protected packagePath: AbsoluteFsPath) {
|
protected packagePath: AbsoluteFsPath) {
|
||||||
super(fs, options);
|
super(fs, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
|
||||||
|
return this.cache.getCachedSourceFile(fileName, languageVersion);
|
||||||
|
}
|
||||||
|
|
||||||
resolveModuleNames(
|
resolveModuleNames(
|
||||||
moduleNames: string[], containingFile: string, reusedNames?: string[],
|
moduleNames: string[], containingFile: string, reusedNames?: string[],
|
||||||
redirectedReference?: ts.ResolvedProjectReference): Array<ts.ResolvedModule|undefined> {
|
redirectedReference?: ts.ResolvedProjectReference): Array<ts.ResolvedModule|undefined> {
|
||||||
return moduleNames.map(moduleName => {
|
return moduleNames.map(moduleName => {
|
||||||
const {resolvedModule} = ts.resolveModuleName(
|
const {resolvedModule} = ts.resolveModuleName(
|
||||||
moduleName, containingFile, this.options, this, this.cache, redirectedReference);
|
moduleName, containingFile, this.options, this, this.moduleResolutionCache,
|
||||||
|
redirectedReference);
|
||||||
|
|
||||||
// If the module request originated from a relative import in a JavaScript source file,
|
// 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
|
// TypeScript may have resolved the module to its .d.ts declaration file if the .js source
|
||||||
@ -59,3 +65,31 @@ 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
197
packages/compiler-cli/ngcc/src/packages/source_file_cache.ts
Normal file
197
packages/compiler-cli/ngcc/src/packages/source_file_cache.ts
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache that holds on to source files that can be shared for processing all entry-points in a
|
||||||
|
* single invocation of ngcc. In particular, the following files are shared across all entry-points
|
||||||
|
* through this cache:
|
||||||
|
*
|
||||||
|
* 1. Default library files such as `lib.dom.d.ts` and `lib.es5.d.ts`. These files don't change
|
||||||
|
* and some are very large, so parsing is expensive. Therefore, the parsed `ts.SourceFile`s for
|
||||||
|
* the default library files are cached.
|
||||||
|
* 2. The typings of @angular scoped packages. The typing files for @angular packages are typically
|
||||||
|
* used in the entry-points that ngcc processes, so benefit from a single source file cache.
|
||||||
|
* Especially `@angular/core/core.d.ts` is large and expensive to parse repeatedly. In contrast
|
||||||
|
* to default library files, we have to account for these files to be invalidated during a single
|
||||||
|
* invocation of ngcc, as ngcc will overwrite the .d.ts files during its processing.
|
||||||
|
*
|
||||||
|
* The lifecycle of this cache corresponds with a single invocation of ngcc. Separate invocations,
|
||||||
|
* e.g. the CLI's synchronous module resolution fallback will therefore all have their own cache.
|
||||||
|
* This allows for the source file cache to be garbage collected once ngcc processing has completed.
|
||||||
|
*/
|
||||||
|
export class SharedFileCache {
|
||||||
|
private sfCache = new Map<AbsoluteFsPath, ts.SourceFile>();
|
||||||
|
|
||||||
|
constructor(private fs: FileSystem) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a `ts.SourceFile` if the provided `fileName` is deemed appropriate to be cached. To
|
||||||
|
* optimize for memory usage, only files that are generally used in all entry-points are cached.
|
||||||
|
* If `fileName` is not considered to benefit from caching or the requested file does not exist,
|
||||||
|
* then `undefined` is returned.
|
||||||
|
*/
|
||||||
|
getCachedSourceFile(fileName: string): ts.SourceFile|undefined {
|
||||||
|
const absPath = this.fs.resolve(fileName);
|
||||||
|
if (isDefaultLibrary(absPath, this.fs)) {
|
||||||
|
return this.getStableCachedFile(absPath);
|
||||||
|
} else if (isAngularDts(absPath, this.fs)) {
|
||||||
|
return this.getVolatileCachedFile(absPath);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to load the source file from the cache, or parses the file into a `ts.SourceFile` if
|
||||||
|
* it's not yet cached. This method assumes that the file will not be modified for the duration
|
||||||
|
* that this cache is valid for. If that assumption does not hold, the `getVolatileCachedFile`
|
||||||
|
* method is to be used instead.
|
||||||
|
*/
|
||||||
|
private getStableCachedFile(absPath: AbsoluteFsPath): ts.SourceFile|undefined {
|
||||||
|
if (!this.sfCache.has(absPath)) {
|
||||||
|
const content = readFile(absPath, this.fs);
|
||||||
|
if (content === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const sf = ts.createSourceFile(absPath, content, ts.ScriptTarget.ES2015);
|
||||||
|
this.sfCache.set(absPath, sf);
|
||||||
|
}
|
||||||
|
return this.sfCache.get(absPath)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In contrast to `getStableCachedFile`, this method always verifies that the cached source file
|
||||||
|
* is the same as what's stored on disk. This is done for files that are expected to change during
|
||||||
|
* ngcc's processing, such as @angular scoped packages for which the .d.ts files are overwritten
|
||||||
|
* by ngcc. If the contents on disk have changed compared to a previously cached source file, the
|
||||||
|
* content from disk is re-parsed and the cache entry is replaced.
|
||||||
|
*/
|
||||||
|
private getVolatileCachedFile(absPath: AbsoluteFsPath): ts.SourceFile|undefined {
|
||||||
|
const content = readFile(absPath, this.fs);
|
||||||
|
if (content === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!this.sfCache.has(absPath) || this.sfCache.get(absPath)!.text !== content) {
|
||||||
|
const sf = ts.createSourceFile(absPath, content, ts.ScriptTarget.ES2015);
|
||||||
|
this.sfCache.set(absPath, sf);
|
||||||
|
}
|
||||||
|
return this.sfCache.get(absPath)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_LIB_PATTERN = ['node_modules', 'typescript', 'lib', /^lib\..+\.d\.ts$/];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the provided path corresponds with a default library file inside of the
|
||||||
|
* typescript package.
|
||||||
|
*
|
||||||
|
* @param absPath The path for which to determine if it corresponds with a default library file.
|
||||||
|
* @param fs The filesystem to use for inspecting the path.
|
||||||
|
*/
|
||||||
|
export function isDefaultLibrary(absPath: AbsoluteFsPath, fs: FileSystem): boolean {
|
||||||
|
return isFile(absPath, DEFAULT_LIB_PATTERN, fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANGULAR_DTS_PATTERN = ['node_modules', '@angular', /./, /\.d\.ts$/];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the provided path corresponds with a .d.ts file inside of an @angular
|
||||||
|
* scoped package. This logic only accounts for the .d.ts files in the root, which is sufficient
|
||||||
|
* to find the large, flattened entry-point files that benefit from caching.
|
||||||
|
*
|
||||||
|
* @param absPath The path for which to determine if it corresponds with an @angular .d.ts file.
|
||||||
|
* @param fs The filesystem to use for inspecting the path.
|
||||||
|
*/
|
||||||
|
export function isAngularDts(absPath: AbsoluteFsPath, fs: FileSystem): boolean {
|
||||||
|
return isFile(absPath, ANGULAR_DTS_PATTERN, fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to determine whether a file corresponds with a given pattern of segments.
|
||||||
|
*
|
||||||
|
* @param path The path for which to determine if it corresponds with the provided segments.
|
||||||
|
* @param segments Array of segments; the `path` must have ending segments that match the
|
||||||
|
* patterns in this array.
|
||||||
|
* @param fs The filesystem to use for inspecting the path.
|
||||||
|
*/
|
||||||
|
function isFile(
|
||||||
|
path: AbsoluteFsPath, segments: ReadonlyArray<string|RegExp>, fs: FileSystem): boolean {
|
||||||
|
for (let i = segments.length - 1; i >= 0; i--) {
|
||||||
|
const pattern = segments[i];
|
||||||
|
const segment = fs.basename(path);
|
||||||
|
if (typeof pattern === 'string') {
|
||||||
|
if (pattern !== segment) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!pattern.test(segment)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path = fs.dirname(path);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache for processing a single entry-point. This exists to share `ts.SourceFile`s between the
|
||||||
|
* source and typing programs that are created for a single program.
|
||||||
|
*/
|
||||||
|
export class EntryPointFileCache {
|
||||||
|
private readonly sfCache = new Map<AbsoluteFsPath, ts.SourceFile>();
|
||||||
|
|
||||||
|
constructor(private fs: FileSystem, private sharedFileCache: SharedFileCache) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns and caches a parsed `ts.SourceFile` for the provided `fileName`. If the `fileName` is
|
||||||
|
* cached in the shared file cache, that result is used. Otherwise, the source file is cached
|
||||||
|
* internally. This method returns `undefined` if the requested file does not exist.
|
||||||
|
*
|
||||||
|
* @param fileName The path of the file to retrieve a source file for.
|
||||||
|
* @param languageVersion The language version to use for parsing the file.
|
||||||
|
*/
|
||||||
|
getCachedSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
|
||||||
|
const staticSf = this.sharedFileCache.getCachedSourceFile(fileName);
|
||||||
|
if (staticSf !== undefined) {
|
||||||
|
return staticSf;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absPath = this.fs.resolve(fileName);
|
||||||
|
if (this.sfCache.has(absPath)) {
|
||||||
|
return this.sfCache.get(absPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = readFile(absPath, this.fs);
|
||||||
|
if (content === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const sf = ts.createSourceFile(fileName, content, languageVersion);
|
||||||
|
this.sfCache.set(absPath, sf);
|
||||||
|
return sf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFile(absPath: AbsoluteFsPath, fs: FileSystem): string|undefined {
|
||||||
|
if (!fs.exists(absPath) || !fs.stat(absPath).isFile()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return fs.readFile(absPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a `ts.ModuleResolutionCache` that uses the provided filesystem for path operations.
|
||||||
|
*
|
||||||
|
* @param fs The filesystem to use for path operations.
|
||||||
|
*/
|
||||||
|
export function createModuleResolutionCache(fs: FileSystem): ts.ModuleResolutionCache {
|
||||||
|
return ts.createModuleResolutionCache(fs.pwd(), fileName => {
|
||||||
|
return fs.isCaseSensitive() ? fileName : fileName.toLowerCase();
|
||||||
|
});
|
||||||
|
}
|
@ -14,6 +14,7 @@ import {NgccEntryPointConfig} from '../../src/packages/configuration';
|
|||||||
import {EntryPoint, EntryPointFormat} from '../../src/packages/entry_point';
|
import {EntryPoint, EntryPointFormat} from '../../src/packages/entry_point';
|
||||||
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
|
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
|
||||||
import {NgccSourcesCompilerHost} from '../../src/packages/ngcc_compiler_host';
|
import {NgccSourcesCompilerHost} from '../../src/packages/ngcc_compiler_host';
|
||||||
|
import {createModuleResolutionCache, EntryPointFileCache, SharedFileCache} from '../../src/packages/source_file_cache';
|
||||||
|
|
||||||
export type TestConfig = Pick<NgccEntryPointConfig, 'generateDeepReexports'>;
|
export type TestConfig = Pick<NgccEntryPointConfig, 'generateDeepReexports'>;
|
||||||
|
|
||||||
@ -68,7 +69,10 @@ export function makeTestBundleProgram(
|
|||||||
const rootDir = fs.dirname(entryPointPath);
|
const rootDir = fs.dirname(entryPointPath);
|
||||||
const options: ts.CompilerOptions =
|
const options: ts.CompilerOptions =
|
||||||
{allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]};
|
{allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]};
|
||||||
const host = new NgccSourcesCompilerHost(fs, options, rootDir);
|
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||||
|
const entryPointFileCache = new EntryPointFileCache(fs, new SharedFileCache(fs));
|
||||||
|
const host =
|
||||||
|
new NgccSourcesCompilerHost(fs, options, entryPointFileCache, moduleResolutionCache, rootDir);
|
||||||
return makeBundleProgram(
|
return makeBundleProgram(
|
||||||
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
|
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
|
||||||
}
|
}
|
||||||
|
@ -1211,6 +1211,69 @@ exports.MissingClass2 = MissingClass2;
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getConstructorParameters', () => {
|
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', () => {
|
it('should find the decorated constructor parameters', () => {
|
||||||
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
||||||
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
|
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
|
||||||
|
@ -1140,6 +1140,57 @@ runInEachFileSystem(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getConstructorParameters()', () => {
|
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', () => {
|
it('should find the decorated constructor parameters', () => {
|
||||||
loadFakeCore(getFileSystem());
|
loadFakeCore(getFileSystem());
|
||||||
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
||||||
@ -1154,7 +1205,7 @@ runInEachFileSystem(() => {
|
|||||||
'_viewContainer', '_template', 'injected'
|
'_viewContainer', '_template', 'injected'
|
||||||
]);
|
]);
|
||||||
expectTypeValueReferencesForParameters(
|
expectTypeValueReferencesForParameters(
|
||||||
parameters, ['ViewContainerRef', 'TemplateRef', null], '@angular/core');
|
parameters, ['ViewContainerRef', 'TemplateRef', null]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept `ctorParameters` as an array', () => {
|
it('should accept `ctorParameters` as an array', () => {
|
||||||
|
@ -1252,6 +1252,67 @@ runInEachFileSystem(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getConstructorParameters()', () => {
|
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', () => {
|
it('should find the decorated constructor parameters', () => {
|
||||||
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
||||||
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
|
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
|
||||||
|
@ -1332,6 +1332,78 @@ runInEachFileSystem(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getConstructorParameters', () => {
|
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', () => {
|
it('should find the decorated constructor parameters', () => {
|
||||||
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
loadTestFiles([SOME_DIRECTIVE_FILE]);
|
||||||
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
|
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
|
||||||
|
@ -13,26 +13,36 @@ import {CtorParameter, TypeValueReferenceKind} from '../../../src/ngtsc/reflecti
|
|||||||
* names.
|
* names.
|
||||||
*/
|
*/
|
||||||
export function expectTypeValueReferencesForParameters(
|
export function expectTypeValueReferencesForParameters(
|
||||||
parameters: CtorParameter[], expectedParams: (string|null)[], fromModule: string|null = null) {
|
parameters: CtorParameter[], expectedParams: (string|null)[],
|
||||||
|
fromModule: (string|null)[] = []) {
|
||||||
parameters!.forEach((param, idx) => {
|
parameters!.forEach((param, idx) => {
|
||||||
const expected = expectedParams[idx];
|
const expected = expectedParams[idx];
|
||||||
if (expected !== null) {
|
if (expected !== null) {
|
||||||
if (param.typeValueReference.kind === TypeValueReferenceKind.UNAVAILABLE) {
|
if (param.typeValueReference.kind === TypeValueReferenceKind.UNAVAILABLE) {
|
||||||
fail(`Incorrect typeValueReference generated, expected ${expected}`);
|
fail(`Incorrect typeValueReference generated for ${param.name}, expected "${
|
||||||
|
expected}" because "${param.typeValueReference.reason}"`);
|
||||||
} else if (
|
} else if (
|
||||||
param.typeValueReference.kind === TypeValueReferenceKind.LOCAL && fromModule !== null) {
|
param.typeValueReference.kind === TypeValueReferenceKind.LOCAL &&
|
||||||
fail(`Incorrect typeValueReference generated, expected non-local`);
|
fromModule[idx] != null) {
|
||||||
|
fail(`Incorrect typeValueReference generated for ${param.name}, expected non-LOCAL (from ${
|
||||||
|
fromModule[idx]}) but was marked LOCAL`);
|
||||||
} else if (
|
} else if (
|
||||||
param.typeValueReference.kind !== TypeValueReferenceKind.LOCAL && fromModule === null) {
|
param.typeValueReference.kind !== TypeValueReferenceKind.LOCAL &&
|
||||||
fail(`Incorrect typeValueReference generated, expected local`);
|
fromModule[idx] == null) {
|
||||||
|
fail(`Incorrect typeValueReference generated for ${
|
||||||
|
param.name}, expected LOCAL but was imported from ${
|
||||||
|
param.typeValueReference.moduleName}`);
|
||||||
} else if (param.typeValueReference.kind === TypeValueReferenceKind.LOCAL) {
|
} else if (param.typeValueReference.kind === TypeValueReferenceKind.LOCAL) {
|
||||||
if (!ts.isIdentifier(param.typeValueReference.expression)) {
|
if (!ts.isIdentifier(param.typeValueReference.expression) &&
|
||||||
fail(`Incorrect typeValueReference generated, expected identifier`);
|
!ts.isPropertyAccessExpression(param.typeValueReference.expression)) {
|
||||||
|
fail(`Incorrect typeValueReference generated for ${
|
||||||
|
param.name}, expected an identifier but got "${
|
||||||
|
param.typeValueReference.expression.getText()}"`);
|
||||||
} else {
|
} else {
|
||||||
expect(param.typeValueReference.expression.text).toEqual(expected);
|
expect(param.typeValueReference.expression.getText()).toEqual(expected);
|
||||||
}
|
}
|
||||||
} else if (param.typeValueReference.kind === TypeValueReferenceKind.IMPORTED) {
|
} else if (param.typeValueReference.kind === TypeValueReferenceKind.IMPORTED) {
|
||||||
expect(param.typeValueReference.moduleName).toBe(fromModule!);
|
expect(param.typeValueReference.moduleName).toBe(fromModule[idx]!);
|
||||||
expect(param.typeValueReference.importedName).toBe(expected);
|
expect(param.typeValueReference.importedName).toBe(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,13 +213,15 @@ export function compileIntoApf(
|
|||||||
fs.resolve(`/node_modules/${pkgName}/package.json`), JSON.stringify(pkgJson, null, 2));
|
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
|
* Prepares a mock filesystem that contains all provided source files, which can be used to emit
|
||||||
* compiled code into.
|
* compiled code into.
|
||||||
*/
|
*/
|
||||||
function setupCompileFs(sources: PackageSources): {rootNames: string[], compileFs: FileSystem} {
|
function setupCompileFs(sources: PackageSources): {rootNames: string[], compileFs: FileSystem} {
|
||||||
const compileFs = new MockFileSystemPosix(true);
|
const compileFs = new MockFileSystemPosix(true);
|
||||||
compileFs.init(loadStandardTestFiles({fakeCore: false}));
|
compileFs.init(stdFiles);
|
||||||
|
|
||||||
const rootNames = Object.keys(sources);
|
const rootNames = Object.keys(sources);
|
||||||
|
|
||||||
|
@ -5,12 +5,13 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '../../src/ngtsc/file_system';
|
||||||
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
|
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
|
||||||
import {MockLogger} from '../../src/ngtsc/logging/testing';
|
import {MockLogger} from '../../src/ngtsc/logging/testing';
|
||||||
|
|
||||||
import {clearTsConfigCache, getSharedSetup, NgccOptions} from '../src/ngcc_options';
|
import {clearTsConfigCache, getMaxNumberOfWorkers, getSharedSetup, NgccOptions} from '../src/ngcc_options';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -100,6 +101,67 @@ 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.
|
* This function creates an object that contains the minimal required properties for NgccOptions.
|
||||||
*/
|
*/
|
||||||
|
@ -10,6 +10,7 @@ import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
|||||||
import {loadTestFiles} from '../../../test/helpers';
|
import {loadTestFiles} from '../../../test/helpers';
|
||||||
import {EntryPoint} from '../../src/packages/entry_point';
|
import {EntryPoint} from '../../src/packages/entry_point';
|
||||||
import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
|
import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
|
||||||
|
import {createModuleResolutionCache, SharedFileCache} from '../../src/packages/source_file_cache';
|
||||||
|
|
||||||
runInEachFileSystem(() => {
|
runInEachFileSystem(() => {
|
||||||
describe('entry point bundle', () => {
|
describe('entry point bundle', () => {
|
||||||
@ -180,7 +181,10 @@ runInEachFileSystem(() => {
|
|||||||
ignoreMissingDependencies: false,
|
ignoreMissingDependencies: false,
|
||||||
generateDeepReexports: false,
|
generateDeepReexports: false,
|
||||||
};
|
};
|
||||||
const esm5bundle = makeEntryPointBundle(fs, entryPoint, './index.js', false, 'esm5', true);
|
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||||
|
const esm5bundle = makeEntryPointBundle(
|
||||||
|
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
|
||||||
|
'esm5', true);
|
||||||
|
|
||||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
||||||
.toEqual(jasmine.arrayWithExactContents([
|
.toEqual(jasmine.arrayWithExactContents([
|
||||||
@ -291,8 +295,11 @@ runInEachFileSystem(() => {
|
|||||||
ignoreMissingDependencies: false,
|
ignoreMissingDependencies: false,
|
||||||
generateDeepReexports: false,
|
generateDeepReexports: false,
|
||||||
};
|
};
|
||||||
|
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||||
const esm5bundle = makeEntryPointBundle(
|
const esm5bundle = makeEntryPointBundle(
|
||||||
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
|
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
|
||||||
|
'esm5',
|
||||||
|
/* transformDts */ true,
|
||||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
||||||
|
|
||||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => _(sf.fileName)))
|
expect(esm5bundle.src.program.getSourceFiles().map(sf => _(sf.fileName)))
|
||||||
@ -328,8 +335,11 @@ runInEachFileSystem(() => {
|
|||||||
ignoreMissingDependencies: false,
|
ignoreMissingDependencies: false,
|
||||||
generateDeepReexports: false,
|
generateDeepReexports: false,
|
||||||
};
|
};
|
||||||
|
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||||
const esm5bundle = makeEntryPointBundle(
|
const esm5bundle = makeEntryPointBundle(
|
||||||
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
|
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
|
||||||
|
'esm5',
|
||||||
|
/* transformDts */ true,
|
||||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
||||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
||||||
.toContain(absoluteFrom('/node_modules/test/internal.js'));
|
.toContain(absoluteFrom('/node_modules/test/internal.js'));
|
||||||
@ -351,8 +361,11 @@ runInEachFileSystem(() => {
|
|||||||
ignoreMissingDependencies: false,
|
ignoreMissingDependencies: false,
|
||||||
generateDeepReexports: false,
|
generateDeepReexports: false,
|
||||||
};
|
};
|
||||||
|
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||||
const esm5bundle = makeEntryPointBundle(
|
const esm5bundle = makeEntryPointBundle(
|
||||||
fs, entryPoint, './esm2015/index.js', false, 'esm2015', /* transformDts */ true,
|
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache,
|
||||||
|
'./esm2015/index.js', false, 'esm2015',
|
||||||
|
/* transformDts */ true,
|
||||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
||||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
||||||
.toContain(absoluteFrom('/node_modules/internal/esm2015/src/internal.js'));
|
.toContain(absoluteFrom('/node_modules/internal/esm2015/src/internal.js'));
|
||||||
@ -374,8 +387,11 @@ runInEachFileSystem(() => {
|
|||||||
ignoreMissingDependencies: false,
|
ignoreMissingDependencies: false,
|
||||||
generateDeepReexports: false,
|
generateDeepReexports: false,
|
||||||
};
|
};
|
||||||
|
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||||
const esm5bundle = makeEntryPointBundle(
|
const esm5bundle = makeEntryPointBundle(
|
||||||
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
|
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
|
||||||
|
'esm5',
|
||||||
|
/* transformDts */ true,
|
||||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ false);
|
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ false);
|
||||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
|
||||||
.toContain(absoluteFrom('/node_modules/test/internal.js'));
|
.toContain(absoluteFrom('/node_modules/test/internal.js'));
|
||||||
@ -398,8 +414,11 @@ runInEachFileSystem(() => {
|
|||||||
ignoreMissingDependencies: false,
|
ignoreMissingDependencies: false,
|
||||||
generateDeepReexports: false,
|
generateDeepReexports: false,
|
||||||
};
|
};
|
||||||
|
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||||
const bundle = makeEntryPointBundle(
|
const bundle = makeEntryPointBundle(
|
||||||
fs, entryPoint, './index.js', false, 'esm2015', /* transformDts */ true,
|
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
|
||||||
|
'esm2015',
|
||||||
|
/* transformDts */ true,
|
||||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
||||||
expect(bundle.rootDirs).toEqual([absoluteFrom('/node_modules/primary')]);
|
expect(bundle.rootDirs).toEqual([absoluteFrom('/node_modules/primary')]);
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {absoluteFrom, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
|
||||||
|
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||||
|
import {loadTestFiles} from '../../../test/helpers';
|
||||||
|
import {EntryPointFileCache, isAngularDts, isDefaultLibrary, SharedFileCache} from '../../src/packages/source_file_cache';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
|
describe('caching', () => {
|
||||||
|
let _: typeof absoluteFrom;
|
||||||
|
let fs: FileSystem;
|
||||||
|
beforeEach(() => {
|
||||||
|
_ = absoluteFrom;
|
||||||
|
fs = getFileSystem();
|
||||||
|
loadTestFiles([
|
||||||
|
{
|
||||||
|
name: _('/node_modules/typescript/lib/lib.es5.d.ts'),
|
||||||
|
contents: `export declare interface Array {}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/node_modules/typescript/lib/lib.dom.d.ts'),
|
||||||
|
contents: `export declare interface Window {}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/node_modules/@angular/core/core.d.ts'),
|
||||||
|
contents: `export declare interface Component {}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/node_modules/@angular/common/common.d.ts'),
|
||||||
|
contents: `export declare interface NgIf {}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/index.ts'),
|
||||||
|
contents: `export const index = true;`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/main.ts'),
|
||||||
|
contents: `export const main = true;`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SharedFileCache', () => {
|
||||||
|
it('should cache a parsed source file for default libraries', () => {
|
||||||
|
const cache = new SharedFileCache(fs);
|
||||||
|
|
||||||
|
const libEs5 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.es5.d.ts')!;
|
||||||
|
expect(libEs5).not.toBeUndefined();
|
||||||
|
expect(libEs5.text).toContain('Array');
|
||||||
|
|
||||||
|
const libDom = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.dom.d.ts')!;
|
||||||
|
expect(libDom).not.toBeUndefined();
|
||||||
|
expect(libDom.text).toContain('Window');
|
||||||
|
|
||||||
|
const libEs5_2 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.es5.d.ts')!;
|
||||||
|
expect(libEs5_2).toBe(libEs5);
|
||||||
|
|
||||||
|
const libDom_2 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.dom.d.ts')!;
|
||||||
|
expect(libDom_2).toBe(libDom);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cache a parsed source file for @angular scoped packages', () => {
|
||||||
|
const cache = new SharedFileCache(fs);
|
||||||
|
|
||||||
|
const core = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
|
||||||
|
expect(core).not.toBeUndefined();
|
||||||
|
expect(core.text).toContain('Component');
|
||||||
|
|
||||||
|
const common = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
|
||||||
|
expect(common).not.toBeUndefined();
|
||||||
|
expect(common.text).toContain('NgIf');
|
||||||
|
|
||||||
|
const core_2 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
|
||||||
|
expect(core_2).toBe(core);
|
||||||
|
|
||||||
|
const common_2 = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
|
||||||
|
expect(common_2).toBe(common);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reparse @angular d.ts files when they change', () => {
|
||||||
|
const cache = new SharedFileCache(fs);
|
||||||
|
|
||||||
|
const core = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
|
||||||
|
expect(core).not.toBeUndefined();
|
||||||
|
expect(core.text).toContain('Component');
|
||||||
|
|
||||||
|
const common = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
|
||||||
|
expect(common).not.toBeUndefined();
|
||||||
|
expect(common.text).toContain('NgIf');
|
||||||
|
|
||||||
|
fs.writeFile(
|
||||||
|
_('/node_modules/@angular/core/core.d.ts'), `export declare interface Directive {}`);
|
||||||
|
|
||||||
|
const core_2 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
|
||||||
|
expect(core_2).not.toBe(core);
|
||||||
|
expect(core_2.text).toContain('Directive');
|
||||||
|
|
||||||
|
const core_3 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
|
||||||
|
expect(core_3).toBe(core_2);
|
||||||
|
|
||||||
|
const common_2 = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
|
||||||
|
expect(common_2).toBe(common);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not cache files that are not default library files inside of the typescript package',
|
||||||
|
() => {
|
||||||
|
const cache = new SharedFileCache(fs);
|
||||||
|
|
||||||
|
expect(cache.getCachedSourceFile('/node_modules/typescript/lib/typescript.d.ts'))
|
||||||
|
.toBeUndefined();
|
||||||
|
expect(cache.getCachedSourceFile('/typescript/lib.es5.d.ts')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isDefaultLibrary()', () => {
|
||||||
|
it('should accept lib files inside of the typescript package', () => {
|
||||||
|
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es5.d.ts'), fs)).toBe(true);
|
||||||
|
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.dom.d.ts'), fs)).toBe(true);
|
||||||
|
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es2015.core.d.ts'), fs))
|
||||||
|
.toBe(true);
|
||||||
|
expect(isDefaultLibrary(_('/root/node_modules/typescript/lib/lib.es5.d.ts'), fs))
|
||||||
|
.toBe(true);
|
||||||
|
});
|
||||||
|
it('should reject non lib files inside of the typescript package', () => {
|
||||||
|
expect(isDefaultLibrary(_('/node_modules/typescript/lib/typescript.d.ts'), fs)).toBe(false);
|
||||||
|
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es5.ts'), fs)).toBe(false);
|
||||||
|
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.d.ts'), fs)).toBe(false);
|
||||||
|
expect(isDefaultLibrary(_('/node_modules/typescript/lib.es5.d.ts'), fs)).toBe(false);
|
||||||
|
});
|
||||||
|
it('should reject lib files outside of the typescript package', () => {
|
||||||
|
expect(isDefaultLibrary(_('/node_modules/ttypescript/lib/lib.es5.d.ts'), fs)).toBe(false);
|
||||||
|
expect(isDefaultLibrary(_('/node_modules/ttypescript/lib/lib.es5.d.ts'), fs)).toBe(false);
|
||||||
|
expect(isDefaultLibrary(_('/typescript/lib/lib.es5.d.ts'), fs)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isAngularDts()', () => {
|
||||||
|
it('should accept .d.ts files inside of the @angular scope', () => {
|
||||||
|
expect(isAngularDts(_('/node_modules/@angular/core/core.d.ts'), fs)).toBe(true);
|
||||||
|
expect(isAngularDts(_('/node_modules/@angular/common/common.d.ts'), fs)).toBe(true);
|
||||||
|
});
|
||||||
|
it('should reject non-.d.ts files inside @angular scoped packages', () => {
|
||||||
|
expect(isAngularDts(_('/node_modules/@angular/common/src/common.ts'), fs)).toBe(false);
|
||||||
|
});
|
||||||
|
it('should reject .d.ts files nested deeply inside @angular scoped packages', () => {
|
||||||
|
expect(isAngularDts(_('/node_modules/@angular/common/src/common.d.ts'), fs)).toBe(false);
|
||||||
|
});
|
||||||
|
it('should reject .d.ts files directly inside the @angular scope', () => {
|
||||||
|
expect(isAngularDts(_('/node_modules/@angular/common.d.ts'), fs)).toBe(false);
|
||||||
|
});
|
||||||
|
it('should reject files that are not inside node_modules', () => {
|
||||||
|
expect(isAngularDts(_('/@angular/core/core.d.ts'), fs)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('EntryPointFileCache', () => {
|
||||||
|
let sharedFileCache: SharedFileCache;
|
||||||
|
beforeEach(() => {
|
||||||
|
sharedFileCache = new SharedFileCache(fs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer source files cached in SharedFileCache', () => {
|
||||||
|
const cache1 = new EntryPointFileCache(fs, sharedFileCache);
|
||||||
|
const libEs5_1 = cache1.getCachedSourceFile(
|
||||||
|
'/node_modules/typescript/lib/lib.es5.d.ts', ts.ScriptTarget.ESNext)!;
|
||||||
|
expect(libEs5_1).not.toBeUndefined();
|
||||||
|
expect(libEs5_1.text).toContain('Array');
|
||||||
|
expect(libEs5_1.languageVersion).toBe(ts.ScriptTarget.ES2015);
|
||||||
|
|
||||||
|
const cache2 = new EntryPointFileCache(fs, sharedFileCache);
|
||||||
|
const libEs5_2 = cache2.getCachedSourceFile(
|
||||||
|
'/node_modules/typescript/lib/lib.es5.d.ts', ts.ScriptTarget.ESNext)!;
|
||||||
|
expect(libEs5_1).toBe(libEs5_2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cache source files that are not default library files', () => {
|
||||||
|
const cache = new EntryPointFileCache(fs, sharedFileCache);
|
||||||
|
const index = cache.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
|
||||||
|
expect(index).not.toBeUndefined();
|
||||||
|
expect(index.text).toContain('index');
|
||||||
|
expect(index.languageVersion).toBe(ts.ScriptTarget.ESNext);
|
||||||
|
|
||||||
|
const main = cache.getCachedSourceFile('/main.ts', ts.ScriptTarget.ESNext)!;
|
||||||
|
expect(main).not.toBeUndefined();
|
||||||
|
expect(main.text).toContain('main');
|
||||||
|
expect(main.languageVersion).toBe(ts.ScriptTarget.ESNext);
|
||||||
|
|
||||||
|
const index_2 = cache.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
|
||||||
|
expect(index_2).toBe(index);
|
||||||
|
|
||||||
|
const main_2 = cache.getCachedSourceFile('/main.ts', ts.ScriptTarget.ESNext)!;
|
||||||
|
expect(main_2).toBe(main);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not share non-library files across multiple cache instances', () => {
|
||||||
|
const cache1 = new EntryPointFileCache(fs, sharedFileCache);
|
||||||
|
const cache2 = new EntryPointFileCache(fs, sharedFileCache);
|
||||||
|
|
||||||
|
const index1 = cache1.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
|
||||||
|
const index2 = cache2.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
|
||||||
|
expect(index1).not.toBe(index2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if the file does not exist', () => {
|
||||||
|
const cache = new EntryPointFileCache(fs, sharedFileCache);
|
||||||
|
expect(cache.getCachedSourceFile('/nonexistent.ts', ts.ScriptTarget.ESNext))
|
||||||
|
.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if the path is a directory', () => {
|
||||||
|
const cache = new EntryPointFileCache(fs, sharedFileCache);
|
||||||
|
expect(cache.getCachedSourceFile('/node_modules', ts.ScriptTarget.ESNext)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -12,6 +12,7 @@ import {loadTestFiles} from '../../../test/helpers';
|
|||||||
import {NgccConfiguration} from '../../src/packages/configuration';
|
import {NgccConfiguration} from '../../src/packages/configuration';
|
||||||
import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointInfo, isEntryPoint} from '../../src/packages/entry_point';
|
import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointInfo, isEntryPoint} from '../../src/packages/entry_point';
|
||||||
import {EntryPointBundle, makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
|
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 {FileWriter} from '../../src/writing/file_writer';
|
||||||
import {NewEntryPointFileWriter} from '../../src/writing/new_entry_point_file_writer';
|
import {NewEntryPointFileWriter} from '../../src/writing/new_entry_point_file_writer';
|
||||||
import {DirectPackageJsonUpdater} from '../../src/writing/package_json_updater';
|
import {DirectPackageJsonUpdater} from '../../src/writing/package_json_updater';
|
||||||
@ -634,7 +635,9 @@ runInEachFileSystem(() => {
|
|||||||
function makeTestBundle(
|
function makeTestBundle(
|
||||||
fs: FileSystem, entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty,
|
fs: FileSystem, entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty,
|
||||||
format: EntryPointFormat): EntryPointBundle {
|
format: EntryPointFormat): EntryPointBundle {
|
||||||
|
const moduleResolutionCache = createModuleResolutionCache(fs);
|
||||||
return makeEntryPointBundle(
|
return makeEntryPointBundle(
|
||||||
fs, entryPoint, entryPoint.packageJson[formatProperty]!, false, format, true);
|
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache,
|
||||||
|
entryPoint.packageJson[formatProperty]!, false, format, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -23,6 +23,7 @@ ts_library(
|
|||||||
"//packages/compiler-cli/src/ngtsc/shims:api",
|
"//packages/compiler-cli/src/ngtsc/shims:api",
|
||||||
"//packages/compiler-cli/src/ngtsc/transform",
|
"//packages/compiler-cli/src/ngtsc/transform",
|
||||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
|
||||||
"//packages/compiler-cli/src/ngtsc/util",
|
"//packages/compiler-cli/src/ngtsc/util",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
"@npm//typescript",
|
"@npm//typescript",
|
||||||
|
@ -10,18 +10,18 @@ import {compileComponentFromMetadata, ConstantPool, CssSelector, DEFAULT_INTERPO
|
|||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {CycleAnalyzer} from '../../cycles';
|
import {CycleAnalyzer} from '../../cycles';
|
||||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
import {ErrorCode, FatalDiagnosticError, ngErrorCode} from '../../diagnostics';
|
||||||
import {absoluteFrom, relative} from '../../file_system';
|
import {absoluteFrom, relative} from '../../file_system';
|
||||||
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
|
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
|
||||||
import {DependencyTracker} from '../../incremental/api';
|
import {DependencyTracker} from '../../incremental/api';
|
||||||
import {IndexingContext} from '../../indexer';
|
import {IndexingContext} from '../../indexer';
|
||||||
import {DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
import {ClassPropertyMapping, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
||||||
import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance';
|
|
||||||
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||||
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
||||||
import {ComponentScopeReader, LocalModuleScopeRegistry} from '../../scope';
|
import {ComponentScopeReader, LocalModuleScopeRegistry} from '../../scope';
|
||||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform';
|
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform';
|
||||||
import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck/api';
|
import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck/api';
|
||||||
|
import {getTemplateId, makeTemplateDiagnostic} from '../../typecheck/diagnostics';
|
||||||
import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
|
import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
|
||||||
import {SubsetOfKeys} from '../../util/src/typescript';
|
import {SubsetOfKeys} from '../../util/src/typescript';
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagno
|
|||||||
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
|
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
|
||||||
import {compileNgFactoryDefField} from './factory';
|
import {compileNgFactoryDefField} from './factory';
|
||||||
import {generateSetClassMetadataCall} from './metadata';
|
import {generateSetClassMetadataCall} from './metadata';
|
||||||
|
import {TypeCheckScopes} from './typecheck_scopes';
|
||||||
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, makeDuplicateDeclarationError, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
|
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, makeDuplicateDeclarationError, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
|
||||||
|
|
||||||
const EMPTY_MAP = new Map<string, Expression>();
|
const EMPTY_MAP = new Map<string, Expression>();
|
||||||
@ -55,6 +56,9 @@ export interface ComponentAnalysisData {
|
|||||||
template: ParsedTemplateWithSource;
|
template: ParsedTemplateWithSource;
|
||||||
metadataStmt: Statement|null;
|
metadataStmt: Statement|null;
|
||||||
|
|
||||||
|
inputs: ClassPropertyMapping;
|
||||||
|
outputs: ClassPropertyMapping;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Providers extracted from the `providers` field of the component annotation which will require
|
* Providers extracted from the `providers` field of the component annotation which will require
|
||||||
* an Angular factory definition at runtime.
|
* an Angular factory definition at runtime.
|
||||||
@ -91,6 +95,7 @@ export class ComponentDecoratorHandler implements
|
|||||||
|
|
||||||
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
|
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
|
||||||
private elementSchemaRegistry = new DomElementSchemaRegistry();
|
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
|
* During the asynchronous preanalyze phase, it's necessary to parse the template to extract
|
||||||
@ -190,7 +195,7 @@ export class ComponentDecoratorHandler implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Next, read the `@Component`-specific fields.
|
// Next, read the `@Component`-specific fields.
|
||||||
const {decorator: component, metadata} = directiveResult;
|
const {decorator: component, metadata, inputs, outputs} = directiveResult;
|
||||||
|
|
||||||
// Go through the root directories for this project, and select the one with the smallest
|
// Go through the root directories for this project, and select the one with the smallest
|
||||||
// relative path representation.
|
// relative path representation.
|
||||||
@ -254,9 +259,26 @@ export class ComponentDecoratorHandler implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let diagnostics: ts.Diagnostic[]|undefined = undefined;
|
||||||
|
|
||||||
if (template.errors !== undefined) {
|
if (template.errors !== undefined) {
|
||||||
throw new Error(
|
// If there are any template parsing errors, convert them to `ts.Diagnostic`s for display.
|
||||||
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Figure out the set of styles. The ordering here is important: external resources (styleUrls)
|
// Figure out the set of styles. The ordering here is important: external resources (styleUrls)
|
||||||
@ -310,6 +332,8 @@ export class ComponentDecoratorHandler implements
|
|||||||
const output: AnalysisOutput<ComponentAnalysisData> = {
|
const output: AnalysisOutput<ComponentAnalysisData> = {
|
||||||
analysis: {
|
analysis: {
|
||||||
baseClass: readBaseClass(node, this.reflector, this.evaluator),
|
baseClass: readBaseClass(node, this.reflector, this.evaluator),
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
meta: {
|
meta: {
|
||||||
...metadata,
|
...metadata,
|
||||||
template: {
|
template: {
|
||||||
@ -327,7 +351,7 @@ export class ComponentDecoratorHandler implements
|
|||||||
i18nUseExternalIds: this.i18nUseExternalIds,
|
i18nUseExternalIds: this.i18nUseExternalIds,
|
||||||
relativeContextFilePath,
|
relativeContextFilePath,
|
||||||
},
|
},
|
||||||
typeCheckMeta: extractDirectiveTypeCheckMeta(node, metadata.inputs, this.reflector),
|
typeCheckMeta: extractDirectiveTypeCheckMeta(node, inputs, this.reflector),
|
||||||
metadataStmt: generateSetClassMetadataCall(
|
metadataStmt: generateSetClassMetadataCall(
|
||||||
node, this.reflector, this.defaultImportRecorder, this.isCore,
|
node, this.reflector, this.defaultImportRecorder, this.isCore,
|
||||||
this.annotateForClosureCompiler),
|
this.annotateForClosureCompiler),
|
||||||
@ -335,6 +359,7 @@ export class ComponentDecoratorHandler implements
|
|||||||
providersRequiringFactory,
|
providersRequiringFactory,
|
||||||
viewProvidersRequiringFactory,
|
viewProvidersRequiringFactory,
|
||||||
},
|
},
|
||||||
|
diagnostics,
|
||||||
};
|
};
|
||||||
if (changeDetection !== null) {
|
if (changeDetection !== null) {
|
||||||
output.analysis!.meta.changeDetection = changeDetection;
|
output.analysis!.meta.changeDetection = changeDetection;
|
||||||
@ -351,8 +376,8 @@ export class ComponentDecoratorHandler implements
|
|||||||
name: node.name.text,
|
name: node.name.text,
|
||||||
selector: analysis.meta.selector,
|
selector: analysis.meta.selector,
|
||||||
exportAs: analysis.meta.exportAs,
|
exportAs: analysis.meta.exportAs,
|
||||||
inputs: analysis.meta.inputs,
|
inputs: analysis.inputs,
|
||||||
outputs: analysis.meta.outputs,
|
outputs: analysis.outputs,
|
||||||
queries: analysis.meta.queries.map(query => query.propertyName),
|
queries: analysis.meta.queries.map(query => query.propertyName),
|
||||||
isComponent: true,
|
isComponent: true,
|
||||||
baseClass: analysis.baseClass,
|
baseClass: analysis.baseClass,
|
||||||
@ -399,36 +424,15 @@ export class ComponentDecoratorHandler implements
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matcher = new SelectorMatcher<DirectiveMeta>();
|
const scope = this.typeCheckScopes.getTypeCheckScope(node);
|
||||||
const pipes = new Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>();
|
|
||||||
let schemas: SchemaMetadata[] = [];
|
|
||||||
|
|
||||||
const scope = this.scopeReader.getScopeForComponent(node);
|
|
||||||
if (scope === 'error') {
|
if (scope === 'error') {
|
||||||
// Don't type-check components that had errors in their scopes.
|
// Don't type-check components that had errors in their scopes.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scope !== null) {
|
const binder = new R3TargetBinder(scope.matcher);
|
||||||
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(
|
ctx.addTemplate(
|
||||||
new Reference(node), binder, meta.template.diagNodes, pipes, schemas,
|
new Reference(node), binder, meta.template.diagNodes, scope.pipes, scope.schemas,
|
||||||
meta.template.sourceMapping, meta.template.file);
|
meta.template.sourceMapping, meta.template.file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,36 +475,49 @@ export class ComponentDecoratorHandler implements
|
|||||||
// Set up the R3TargetBinder, as well as a 'directives' array and a 'pipes' map that are later
|
// 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
|
// fed to the TemplateDefinitionBuilder. First, a SelectorMatcher is constructed to match
|
||||||
// directives that are in scope.
|
// directives that are in scope.
|
||||||
const matcher = new SelectorMatcher<DirectiveMeta&{expression: Expression}>();
|
type MatchedDirective = DirectiveMeta&{selector: string};
|
||||||
const directives: {selector: string, expression: Expression}[] = [];
|
const matcher = new SelectorMatcher<MatchedDirective>();
|
||||||
|
|
||||||
for (const dir of scope.compilation.directives) {
|
for (const dir of scope.compilation.directives) {
|
||||||
const {ref, selector} = dir;
|
if (dir.selector !== null) {
|
||||||
if (selector !== null) {
|
matcher.addSelectables(CssSelector.parse(dir.selector), dir as MatchedDirective);
|
||||||
const expression = this.refEmitter.emit(ref, context);
|
|
||||||
directives.push({selector, expression});
|
|
||||||
matcher.addSelectables(CssSelector.parse(selector), {...dir, expression});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const pipes = new Map<string, Expression>();
|
const pipes = new Map<string, Reference<ClassDeclaration>>();
|
||||||
for (const pipe of scope.compilation.pipes) {
|
for (const pipe of scope.compilation.pipes) {
|
||||||
pipes.set(pipe.name, this.refEmitter.emit(pipe.ref, context));
|
pipes.set(pipe.name, pipe.ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next, the component template AST is bound using the R3TargetBinder. This produces an
|
// Next, the component template AST is bound using the R3TargetBinder. This produces a
|
||||||
// BoundTarget, which is similar to a ts.TypeChecker.
|
// BoundTarget, which is similar to a ts.TypeChecker.
|
||||||
const binder = new R3TargetBinder(matcher);
|
const binder = new R3TargetBinder(matcher);
|
||||||
const bound = binder.bind({template: metadata.template.nodes});
|
const bound = binder.bind({template: metadata.template.nodes});
|
||||||
|
|
||||||
// The BoundTarget knows which directives and pipes matched the template.
|
// The BoundTarget knows which directives and pipes matched the template.
|
||||||
const usedDirectives = bound.getUsedDirectives();
|
const usedDirectives = bound.getUsedDirectives().map(directive => {
|
||||||
const usedPipes = bound.getUsedPipes().map(name => pipes.get(name)!);
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Scan through the directives/pipes actually used in the template and check whether any
|
// Scan through the directives/pipes actually used in the template and check whether any
|
||||||
// import which needs to be generated would create a cycle.
|
// import which needs to be generated would create a cycle.
|
||||||
const cycleDetected =
|
const cycleDetected =
|
||||||
usedDirectives.some(dir => this._isCyclicImport(dir.expression, context)) ||
|
usedDirectives.some(dir => this._isCyclicImport(dir.expression, context)) ||
|
||||||
usedPipes.some(pipe => this._isCyclicImport(pipe, context));
|
usedPipes.some(pipe => this._isCyclicImport(pipe.expression, context));
|
||||||
|
|
||||||
if (!cycleDetected) {
|
if (!cycleDetected) {
|
||||||
// No cycle was detected. Record the imports that need to be created in the cycle detector
|
// No cycle was detected. Record the imports that need to be created in the cycle detector
|
||||||
@ -508,8 +525,8 @@ export class ComponentDecoratorHandler implements
|
|||||||
for (const {expression} of usedDirectives) {
|
for (const {expression} of usedDirectives) {
|
||||||
this._recordSyntheticImport(expression, context);
|
this._recordSyntheticImport(expression, context);
|
||||||
}
|
}
|
||||||
for (const pipe of usedPipes) {
|
for (const {expression} of usedPipes) {
|
||||||
this._recordSyntheticImport(pipe, context);
|
this._recordSyntheticImport(expression, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures.
|
// Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures.
|
||||||
@ -518,16 +535,11 @@ export class ComponentDecoratorHandler implements
|
|||||||
const wrapDirectivesAndPipesInClosure =
|
const wrapDirectivesAndPipesInClosure =
|
||||||
usedDirectives.some(
|
usedDirectives.some(
|
||||||
dir => isExpressionForwardReference(dir.expression, node.name, context)) ||
|
dir => isExpressionForwardReference(dir.expression, node.name, context)) ||
|
||||||
usedPipes.some(pipe => isExpressionForwardReference(pipe, node.name, context));
|
usedPipes.some(
|
||||||
|
pipe => isExpressionForwardReference(pipe.expression, node.name, context));
|
||||||
|
|
||||||
// Actual compilation still uses the full scope, not the narrowed scope determined by
|
data.directives = usedDirectives;
|
||||||
// R3TargetBinder. This is a hedge against potential issues with the R3TargetBinder - right
|
data.pipes = new Map(usedPipes.map(pipe => [pipe.pipeName, pipe.expression]));
|
||||||
// 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;
|
data.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure;
|
||||||
} else {
|
} else {
|
||||||
// Declaring the directiveDefs/pipeDefs arrays directly would require imports that would
|
// Declaring the directiveDefs/pipeDefs arrays directly would require imports that would
|
||||||
|
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
|||||||
|
|
||||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||||
import {DefaultImportRecorder, Reference} from '../../imports';
|
import {DefaultImportRecorder, Reference} from '../../imports';
|
||||||
import {DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
import {ClassPropertyMapping, DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
||||||
import {extractDirectiveTypeCheckMeta} from '../../metadata/src/util';
|
import {extractDirectiveTypeCheckMeta} from '../../metadata/src/util';
|
||||||
import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||||
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
||||||
@ -39,6 +39,8 @@ export interface DirectiveHandlerData {
|
|||||||
meta: R3DirectiveMetadata;
|
meta: R3DirectiveMetadata;
|
||||||
metadataStmt: Statement|null;
|
metadataStmt: Statement|null;
|
||||||
providersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
|
providersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
|
||||||
|
inputs: ClassPropertyMapping;
|
||||||
|
outputs: ClassPropertyMapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DirectiveDecoratorHandler implements
|
export class DirectiveDecoratorHandler implements
|
||||||
@ -83,11 +85,10 @@ export class DirectiveDecoratorHandler implements
|
|||||||
const directiveResult = extractDirectiveMetadata(
|
const directiveResult = extractDirectiveMetadata(
|
||||||
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
|
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
|
||||||
flags, this.annotateForClosureCompiler);
|
flags, this.annotateForClosureCompiler);
|
||||||
const analysis = directiveResult && directiveResult.metadata;
|
if (directiveResult === undefined) {
|
||||||
|
|
||||||
if (analysis === undefined) {
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
const analysis = directiveResult.metadata;
|
||||||
|
|
||||||
let providersRequiringFactory: Set<Reference<ClassDeclaration>>|null = null;
|
let providersRequiringFactory: Set<Reference<ClassDeclaration>>|null = null;
|
||||||
if (directiveResult !== undefined && directiveResult.decorator.has('providers')) {
|
if (directiveResult !== undefined && directiveResult.decorator.has('providers')) {
|
||||||
@ -97,12 +98,14 @@ export class DirectiveDecoratorHandler implements
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
analysis: {
|
analysis: {
|
||||||
|
inputs: directiveResult.inputs,
|
||||||
|
outputs: directiveResult.outputs,
|
||||||
meta: analysis,
|
meta: analysis,
|
||||||
metadataStmt: generateSetClassMetadataCall(
|
metadataStmt: generateSetClassMetadataCall(
|
||||||
node, this.reflector, this.defaultImportRecorder, this.isCore,
|
node, this.reflector, this.defaultImportRecorder, this.isCore,
|
||||||
this.annotateForClosureCompiler),
|
this.annotateForClosureCompiler),
|
||||||
baseClass: readBaseClass(node, this.reflector, this.evaluator),
|
baseClass: readBaseClass(node, this.reflector, this.evaluator),
|
||||||
typeCheckMeta: extractDirectiveTypeCheckMeta(node, analysis.inputs, this.reflector),
|
typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector),
|
||||||
providersRequiringFactory
|
providersRequiringFactory
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -117,8 +120,8 @@ export class DirectiveDecoratorHandler implements
|
|||||||
name: node.name.text,
|
name: node.name.text,
|
||||||
selector: analysis.meta.selector,
|
selector: analysis.meta.selector,
|
||||||
exportAs: analysis.meta.exportAs,
|
exportAs: analysis.meta.exportAs,
|
||||||
inputs: analysis.meta.inputs,
|
inputs: analysis.inputs,
|
||||||
outputs: analysis.meta.outputs,
|
outputs: analysis.outputs,
|
||||||
queries: analysis.meta.queries.map(query => query.propertyName),
|
queries: analysis.meta.queries.map(query => query.propertyName),
|
||||||
isComponent: false,
|
isComponent: false,
|
||||||
baseClass: analysis.baseClass,
|
baseClass: analysis.baseClass,
|
||||||
@ -199,8 +202,13 @@ export class DirectiveDecoratorHandler implements
|
|||||||
export function extractDirectiveMetadata(
|
export function extractDirectiveMetadata(
|
||||||
clazz: ClassDeclaration, decorator: Readonly<Decorator|null>, reflector: ReflectionHost,
|
clazz: ClassDeclaration, decorator: Readonly<Decorator|null>, reflector: ReflectionHost,
|
||||||
evaluator: PartialEvaluator, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
|
evaluator: PartialEvaluator, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
|
||||||
flags: HandlerFlags, annotateForClosureCompiler: boolean, defaultSelector: string|null = null):
|
flags: HandlerFlags, annotateForClosureCompiler: boolean,
|
||||||
{decorator: Map<string, ts.Expression>, metadata: R3DirectiveMetadata}|undefined {
|
defaultSelector: string|null = null): {
|
||||||
|
decorator: Map<string, ts.Expression>,
|
||||||
|
metadata: R3DirectiveMetadata,
|
||||||
|
inputs: ClassPropertyMapping,
|
||||||
|
outputs: ClassPropertyMapping,
|
||||||
|
}|undefined {
|
||||||
let directive: Map<string, ts.Expression>;
|
let directive: Map<string, ts.Expression>;
|
||||||
if (decorator === null || decorator.args === null || decorator.args.length === 0) {
|
if (decorator === null || decorator.args === null || decorator.args.length === 0) {
|
||||||
directive = new Map<string, ts.Expression>();
|
directive = new Map<string, ts.Expression>();
|
||||||
@ -331,6 +339,9 @@ export function extractDirectiveMetadata(
|
|||||||
const type = wrapTypeReference(reflector, clazz);
|
const type = wrapTypeReference(reflector, clazz);
|
||||||
const internalType = new WrappedNodeExpr(reflector.getInternalNameOfClass(clazz));
|
const internalType = new WrappedNodeExpr(reflector.getInternalNameOfClass(clazz));
|
||||||
|
|
||||||
|
const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields});
|
||||||
|
const outputs = ClassPropertyMapping.fromMappedObject({...outputsFromMeta, ...outputsFromFields});
|
||||||
|
|
||||||
const metadata: R3DirectiveMetadata = {
|
const metadata: R3DirectiveMetadata = {
|
||||||
name: clazz.name.text,
|
name: clazz.name.text,
|
||||||
deps: ctorDeps,
|
deps: ctorDeps,
|
||||||
@ -338,8 +349,8 @@ export function extractDirectiveMetadata(
|
|||||||
lifecycle: {
|
lifecycle: {
|
||||||
usesOnChanges,
|
usesOnChanges,
|
||||||
},
|
},
|
||||||
inputs: {...inputsFromMeta, ...inputsFromFields},
|
inputs: inputs.toJointMappedObject(),
|
||||||
outputs: {...outputsFromMeta, ...outputsFromFields},
|
outputs: outputs.toDirectMappedObject(),
|
||||||
queries,
|
queries,
|
||||||
viewQueries,
|
viewQueries,
|
||||||
selector,
|
selector,
|
||||||
@ -352,7 +363,12 @@ export function extractDirectiveMetadata(
|
|||||||
exportAs,
|
exportAs,
|
||||||
providers
|
providers
|
||||||
};
|
};
|
||||||
return {decorator: directive, metadata};
|
return {
|
||||||
|
decorator: directive,
|
||||||
|
metadata,
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractQueryMetadata(
|
export function extractQueryMetadata(
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {CssSelector, SchemaMetadata, SelectorMatcher} from '@angular/compiler';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {Reference} from '../../imports';
|
||||||
|
import {DirectiveMeta, flattenInheritedDirectiveMetadata, MetadataReader} from '../../metadata';
|
||||||
|
import {ClassDeclaration} from '../../reflection';
|
||||||
|
import {ComponentScopeReader} from '../../scope';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scope that is used for type-check code generation of a component template.
|
||||||
|
*/
|
||||||
|
export interface TypeCheckScope {
|
||||||
|
/**
|
||||||
|
* A `SelectorMatcher` instance that contains the flattened directive metadata of all directives
|
||||||
|
* that are in the compilation scope of the declaring NgModule.
|
||||||
|
*/
|
||||||
|
matcher: SelectorMatcher<DirectiveMeta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pipes that are available in the compilation scope.
|
||||||
|
*/
|
||||||
|
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The schemas that are used in this scope.
|
||||||
|
*/
|
||||||
|
schemas: SchemaMetadata[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes scope information to be used in template type checking.
|
||||||
|
*/
|
||||||
|
export class TypeCheckScopes {
|
||||||
|
/**
|
||||||
|
* Cache of flattened directive metadata. Because flattened metadata is scope-invariant it's
|
||||||
|
* cached individually, such that all scopes refer to the same flattened metadata.
|
||||||
|
*/
|
||||||
|
private flattenedDirectiveMetaCache = new Map<ClassDeclaration, DirectiveMeta>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of the computed type check scope per NgModule declaration.
|
||||||
|
*/
|
||||||
|
private scopeCache = new Map<ClassDeclaration, TypeCheckScope>();
|
||||||
|
|
||||||
|
constructor(private scopeReader: ComponentScopeReader, private metaReader: MetadataReader) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the type-check scope information for the component declaration. If the NgModule
|
||||||
|
* contains an error, then 'error' is returned. If the component is not declared in any NgModule,
|
||||||
|
* an empty type-check scope is returned.
|
||||||
|
*/
|
||||||
|
getTypeCheckScope(node: ClassDeclaration): TypeCheckScope|'error' {
|
||||||
|
const matcher = new SelectorMatcher<DirectiveMeta>();
|
||||||
|
const pipes = new Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>();
|
||||||
|
|
||||||
|
const scope = this.scopeReader.getScopeForComponent(node);
|
||||||
|
if (scope === null) {
|
||||||
|
return {matcher, pipes, schemas: []};
|
||||||
|
} else if (scope === 'error') {
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.scopeCache.has(scope.ngModule)) {
|
||||||
|
return this.scopeCache.get(scope.ngModule)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const meta of scope.compilation.directives) {
|
||||||
|
if (meta.selector !== null) {
|
||||||
|
const extMeta = this.getInheritedDirectiveMetadata(meta.ref);
|
||||||
|
matcher.addSelectables(CssSelector.parse(meta.selector), extMeta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const {name, ref} of scope.compilation.pipes) {
|
||||||
|
if (!ts.isClassDeclaration(ref.node)) {
|
||||||
|
throw new Error(`Unexpected non-class declaration ${
|
||||||
|
ts.SyntaxKind[ref.node.kind]} for pipe ${ref.debugName}`);
|
||||||
|
}
|
||||||
|
pipes.set(name, ref as Reference<ClassDeclaration<ts.ClassDeclaration>>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeCheckScope: TypeCheckScope = {matcher, pipes, schemas: scope.schemas};
|
||||||
|
this.scopeCache.set(scope.ngModule, typeCheckScope);
|
||||||
|
return typeCheckScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getInheritedDirectiveMetadata(ref: Reference<ClassDeclaration>): DirectiveMeta {
|
||||||
|
const clazz = ref.node;
|
||||||
|
if (this.flattenedDirectiveMetaCache.has(clazz)) {
|
||||||
|
return this.flattenedDirectiveMetaCache.get(clazz)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = flattenInheritedDirectiveMetadata(this.metaReader, ref);
|
||||||
|
this.flattenedDirectiveMetaCache.set(clazz, meta);
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
}
|
@ -215,8 +215,10 @@ function createUnsuitableInjectionTokenError(
|
|||||||
makeRelatedInformation(
|
makeRelatedInformation(
|
||||||
reason.typeNode,
|
reason.typeNode,
|
||||||
'This type does not have a value, so it cannot be used as injection token.'),
|
'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;
|
break;
|
||||||
case ValueUnavailableKind.TYPE_ONLY_IMPORT:
|
case ValueUnavailableKind.TYPE_ONLY_IMPORT:
|
||||||
chainMessage =
|
chainMessage =
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 * as ts from 'typescript';
|
||||||
|
|
||||||
import {absoluteFrom} from '../../file_system';
|
import {absoluteFrom} from '../../file_system';
|
||||||
@ -73,6 +74,49 @@ runInEachFileSystem(() => {
|
|||||||
expect(span.start.toString()).toContain('/entry.ts@5:22');
|
expect(span.start.toString()).toContain('/entry.ts@5:22');
|
||||||
expect(span.end.toString()).toContain('/entry.ts@5:29');
|
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
|
// Helpers
|
||||||
|
@ -34,6 +34,7 @@ ts_library(
|
|||||||
"//packages/compiler-cli/src/ngtsc/transform",
|
"//packages/compiler-cli/src/ngtsc/transform",
|
||||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
|
||||||
"//packages/compiler-cli/src/ngtsc/util",
|
"//packages/compiler-cli/src/ngtsc/util",
|
||||||
"@npm//typescript",
|
"@npm//typescript",
|
||||||
],
|
],
|
||||||
|
@ -28,8 +28,9 @@ import {ComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeRe
|
|||||||
import {generatedFactoryTransform} from '../../shims';
|
import {generatedFactoryTransform} from '../../shims';
|
||||||
import {ivySwitchTransform} from '../../switch';
|
import {ivySwitchTransform} from '../../switch';
|
||||||
import {aliasTransformFactory, declarationTransformFactory, DecoratorHandler, DtsTransformRegistry, ivyTransformFactory, TraitCompiler} from '../../transform';
|
import {aliasTransformFactory, declarationTransformFactory, DecoratorHandler, DtsTransformRegistry, ivyTransformFactory, TraitCompiler} from '../../transform';
|
||||||
import {isTemplateDiagnostic, TemplateTypeCheckerImpl} from '../../typecheck';
|
import {TemplateTypeCheckerImpl} from '../../typecheck';
|
||||||
import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy} from '../../typecheck/api';
|
import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy} from '../../typecheck/api';
|
||||||
|
import {isTemplateDiagnostic} from '../../typecheck/diagnostics';
|
||||||
import {getSourceFileOrNull, isDtsPath, resolveModuleName} from '../../util/src/typescript';
|
import {getSourceFileOrNull, isDtsPath, resolveModuleName} from '../../util/src/typescript';
|
||||||
import {LazyRoute, NgCompilerAdapter, NgCompilerOptions} from '../api';
|
import {LazyRoute, NgCompilerAdapter, NgCompilerOptions} from '../api';
|
||||||
|
|
||||||
|
@ -56,6 +56,11 @@ export enum ErrorCode {
|
|||||||
*/
|
*/
|
||||||
HOST_BINDING_PARSE_ERROR = 5001,
|
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`.
|
* Raised when an NgModule contains an invalid reference in `declarations`.
|
||||||
*/
|
*/
|
||||||
|
@ -22,10 +22,10 @@ export class InvalidFileSystem implements FileSystem {
|
|||||||
readFile(path: AbsoluteFsPath): string {
|
readFile(path: AbsoluteFsPath): string {
|
||||||
throw makeError();
|
throw makeError();
|
||||||
}
|
}
|
||||||
readFileBuffer(path: AbsoluteFsPath): Buffer {
|
readFileBuffer(path: AbsoluteFsPath): Uint8Array {
|
||||||
throw makeError();
|
throw makeError();
|
||||||
}
|
}
|
||||||
writeFile(path: AbsoluteFsPath, data: string|Buffer, exclusive?: boolean): void {
|
writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive?: boolean): void {
|
||||||
throw makeError();
|
throw makeError();
|
||||||
}
|
}
|
||||||
removeFile(path: AbsoluteFsPath): void {
|
removeFile(path: AbsoluteFsPath): void {
|
||||||
|
@ -23,10 +23,10 @@ export class NodeJSFileSystem implements FileSystem {
|
|||||||
readFile(path: AbsoluteFsPath): string {
|
readFile(path: AbsoluteFsPath): string {
|
||||||
return fs.readFileSync(path, 'utf8');
|
return fs.readFileSync(path, 'utf8');
|
||||||
}
|
}
|
||||||
readFileBuffer(path: AbsoluteFsPath): Buffer {
|
readFileBuffer(path: AbsoluteFsPath): Uint8Array {
|
||||||
return fs.readFileSync(path);
|
return fs.readFileSync(path);
|
||||||
}
|
}
|
||||||
writeFile(path: AbsoluteFsPath, data: string|Buffer, exclusive: boolean = false): void {
|
writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive: boolean = false): void {
|
||||||
fs.writeFileSync(path, data, exclusive ? {flag: 'wx'} : undefined);
|
fs.writeFileSync(path, data, exclusive ? {flag: 'wx'} : undefined);
|
||||||
}
|
}
|
||||||
removeFile(path: AbsoluteFsPath): void {
|
removeFile(path: AbsoluteFsPath): void {
|
||||||
|
@ -37,8 +37,8 @@ export type PathSegment = BrandedPath<'PathSegment'>;
|
|||||||
export interface FileSystem {
|
export interface FileSystem {
|
||||||
exists(path: AbsoluteFsPath): boolean;
|
exists(path: AbsoluteFsPath): boolean;
|
||||||
readFile(path: AbsoluteFsPath): string;
|
readFile(path: AbsoluteFsPath): string;
|
||||||
readFileBuffer(path: AbsoluteFsPath): Buffer;
|
readFileBuffer(path: AbsoluteFsPath): Uint8Array;
|
||||||
writeFile(path: AbsoluteFsPath, data: string|Buffer, exclusive?: boolean): void;
|
writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive?: boolean): void;
|
||||||
removeFile(path: AbsoluteFsPath): void;
|
removeFile(path: AbsoluteFsPath): void;
|
||||||
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void;
|
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void;
|
||||||
readdir(path: AbsoluteFsPath): PathSegment[];
|
readdir(path: AbsoluteFsPath): PathSegment[];
|
||||||
|
@ -38,16 +38,16 @@ export abstract class MockFileSystem implements FileSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readFileBuffer(path: AbsoluteFsPath): Buffer {
|
readFileBuffer(path: AbsoluteFsPath): Uint8Array {
|
||||||
const {entity} = this.findFromPath(path);
|
const {entity} = this.findFromPath(path);
|
||||||
if (isFile(entity)) {
|
if (isFile(entity)) {
|
||||||
return Buffer.isBuffer(entity) ? entity : new Buffer(entity);
|
return entity instanceof Uint8Array ? entity : new Buffer(entity);
|
||||||
} else {
|
} else {
|
||||||
throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`);
|
throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFile(path: AbsoluteFsPath, data: string|Buffer, exclusive: boolean = false): void {
|
writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive: boolean = false): void {
|
||||||
const [folderPath, basename] = this.splitIntoFolderAndFile(path);
|
const [folderPath, basename] = this.splitIntoFolderAndFile(path);
|
||||||
const {entity} = this.findFromPath(folderPath);
|
const {entity} = this.findFromPath(folderPath);
|
||||||
if (entity === null || !isFolder(entity)) {
|
if (entity === null || !isFolder(entity)) {
|
||||||
@ -295,7 +295,7 @@ export type Entity = Folder|File|SymLink;
|
|||||||
export interface Folder {
|
export interface Folder {
|
||||||
[pathSegments: string]: Entity;
|
[pathSegments: string]: Entity;
|
||||||
}
|
}
|
||||||
export type File = string|Buffer;
|
export type File = string|Uint8Array;
|
||||||
export class SymLink {
|
export class SymLink {
|
||||||
constructor(public path: AbsoluteFsPath) {}
|
constructor(public path: AbsoluteFsPath) {}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import * as ts from 'typescript';
|
|||||||
|
|
||||||
import {absoluteFrom, AbsoluteFsPath} from '../../file_system';
|
import {absoluteFrom, AbsoluteFsPath} from '../../file_system';
|
||||||
import {Reference} from '../../imports';
|
import {Reference} from '../../imports';
|
||||||
|
import {ClassPropertyMapping} from '../../metadata';
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
import {getDeclaration, makeProgram} from '../../testing';
|
import {getDeclaration, makeProgram} from '../../testing';
|
||||||
import {ComponentMeta} from '../src/context';
|
import {ComponentMeta} from '../src/context';
|
||||||
@ -50,8 +51,8 @@ export function getBoundTemplate(
|
|||||||
selector,
|
selector,
|
||||||
name: declaration.name.getText(),
|
name: declaration.name.getText(),
|
||||||
isComponent: true,
|
isComponent: true,
|
||||||
inputs: {},
|
inputs: ClassPropertyMapping.fromMappedObject({}),
|
||||||
outputs: {},
|
outputs: ClassPropertyMapping.fromMappedObject({}),
|
||||||
exportAs: null,
|
exportAs: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,5 +8,7 @@
|
|||||||
|
|
||||||
export * from './src/api';
|
export * from './src/api';
|
||||||
export {DtsMetadataReader} from './src/dts';
|
export {DtsMetadataReader} from './src/dts';
|
||||||
|
export {flattenInheritedDirectiveMetadata} from './src/inheritance';
|
||||||
export {CompoundMetadataRegistry, LocalMetadataRegistry, InjectableClassRegistry} from './src/registry';
|
export {CompoundMetadataRegistry, LocalMetadataRegistry, InjectableClassRegistry} from './src/registry';
|
||||||
export {extractDirectiveTypeCheckMeta, CompoundMetadataReader} from './src/util';
|
export {extractDirectiveTypeCheckMeta, CompoundMetadataReader} from './src/util';
|
||||||
|
export {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, InputOrOutput} from './src/property_mapping';
|
||||||
|
@ -12,6 +12,8 @@ import * as ts from 'typescript';
|
|||||||
import {Reference} from '../../imports';
|
import {Reference} from '../../imports';
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
|
|
||||||
|
import {ClassPropertyMapping, ClassPropertyName} from './property_mapping';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata collected for an `NgModule`.
|
* Metadata collected for an `NgModule`.
|
||||||
@ -52,25 +54,25 @@ export interface DirectiveTypeCheckMeta {
|
|||||||
* Directive's class. This allows inputs to accept a wider range of types and coerce the input to
|
* Directive's class. This allows inputs to accept a wider range of types and coerce the input to
|
||||||
* a narrower type with a getter/setter. See https://angular.io/guide/template-typecheck.
|
* a narrower type with a getter/setter. See https://angular.io/guide/template-typecheck.
|
||||||
*/
|
*/
|
||||||
coercedInputFields: Set<string>;
|
coercedInputFields: Set<ClassPropertyName>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The set of input fields which map to `readonly`, `private`, or `protected` members in the
|
* The set of input fields which map to `readonly`, `private`, or `protected` members in the
|
||||||
* Directive's class.
|
* Directive's class.
|
||||||
*/
|
*/
|
||||||
restrictedInputFields: Set<string>;
|
restrictedInputFields: Set<ClassPropertyName>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The set of input fields which are declared as string literal members in the Directive's class.
|
* The set of input fields which are declared as string literal members in the Directive's class.
|
||||||
* We need to track these separately because these fields may not be valid JS identifiers so
|
* We need to track these separately because these fields may not be valid JS identifiers so
|
||||||
* we cannot use them with property access expressions when assigning inputs.
|
* we cannot use them with property access expressions when assigning inputs.
|
||||||
*/
|
*/
|
||||||
stringLiteralInputFields: Set<string>;
|
stringLiteralInputFields: Set<ClassPropertyName>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The set of input fields which do not have corresponding members in the Directive's class.
|
* The set of input fields which do not have corresponding members in the Directive's class.
|
||||||
*/
|
*/
|
||||||
undeclaredInputFields: Set<string>;
|
undeclaredInputFields: Set<ClassPropertyName>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the Directive's class is generic, i.e. `class MyDir<T> {...}`.
|
* Whether the Directive's class is generic, i.e. `class MyDir<T> {...}`.
|
||||||
@ -89,6 +91,16 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta {
|
|||||||
selector: string|null;
|
selector: string|null;
|
||||||
queries: string[];
|
queries: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping of input field names to the property names.
|
||||||
|
*/
|
||||||
|
inputs: ClassPropertyMapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping of output field names to the property names.
|
||||||
|
*/
|
||||||
|
outputs: ClassPropertyMapping;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A `Reference` to the base class for the directive, if one was detected.
|
* A `Reference` to the base class for the directive, if one was detected.
|
||||||
*
|
*
|
||||||
|
@ -12,6 +12,7 @@ import {Reference} from '../../imports';
|
|||||||
import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
|
import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
|
||||||
|
|
||||||
import {DirectiveMeta, MetadataReader, NgModuleMeta, PipeMeta} from './api';
|
import {DirectiveMeta, MetadataReader, NgModuleMeta, PipeMeta} from './api';
|
||||||
|
import {ClassPropertyMapping} from './property_mapping';
|
||||||
import {extractDirectiveTypeCheckMeta, extractReferencesFromType, readStringArrayType, readStringMapType, readStringType} from './util';
|
import {extractDirectiveTypeCheckMeta, extractReferencesFromType, readStringArrayType, readStringMapType, readStringType} from './util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,7 +77,10 @@ export class DtsMetadataReader implements MetadataReader {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputs = readStringMapType(def.type.typeArguments[3]);
|
const inputs =
|
||||||
|
ClassPropertyMapping.fromMappedObject(readStringMapType(def.type.typeArguments[3]));
|
||||||
|
const outputs =
|
||||||
|
ClassPropertyMapping.fromMappedObject(readStringMapType(def.type.typeArguments[4]));
|
||||||
return {
|
return {
|
||||||
ref,
|
ref,
|
||||||
name: clazz.name.text,
|
name: clazz.name.text,
|
||||||
@ -84,7 +88,7 @@ export class DtsMetadataReader implements MetadataReader {
|
|||||||
selector: readStringType(def.type.typeArguments[1]),
|
selector: readStringType(def.type.typeArguments[1]),
|
||||||
exportAs: readStringArrayType(def.type.typeArguments[2]),
|
exportAs: readStringArrayType(def.type.typeArguments[2]),
|
||||||
inputs,
|
inputs,
|
||||||
outputs: readStringMapType(def.type.typeArguments[4]),
|
outputs,
|
||||||
queries: readStringArrayType(def.type.typeArguments[5]),
|
queries: readStringArrayType(def.type.typeArguments[5]),
|
||||||
...extractDirectiveTypeCheckMeta(clazz, inputs, this.reflector),
|
...extractDirectiveTypeCheckMeta(clazz, inputs, this.reflector),
|
||||||
baseClass: readBaseClass(clazz, this.checker, this.reflector),
|
baseClass: readBaseClass(clazz, this.checker, this.reflector),
|
||||||
|
@ -7,9 +7,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Reference} from '../../imports';
|
import {Reference} from '../../imports';
|
||||||
import {DirectiveMeta, MetadataReader} from '../../metadata';
|
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
|
|
||||||
|
import {DirectiveMeta, MetadataReader} from './api';
|
||||||
|
import {ClassPropertyMapping, ClassPropertyName} from './property_mapping';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a reference to a directive, return a flattened version of its `DirectiveMeta` metadata
|
* Given a reference to a directive, return a flattened version of its `DirectiveMeta` metadata
|
||||||
* which includes metadata from its entire inheritance chain.
|
* which includes metadata from its entire inheritance chain.
|
||||||
@ -24,14 +26,17 @@ export function flattenInheritedDirectiveMetadata(
|
|||||||
if (topMeta === null) {
|
if (topMeta === null) {
|
||||||
throw new Error(`Metadata not found for directive: ${dir.debugName}`);
|
throw new Error(`Metadata not found for directive: ${dir.debugName}`);
|
||||||
}
|
}
|
||||||
|
if (topMeta.baseClass === null) {
|
||||||
|
return topMeta;
|
||||||
|
}
|
||||||
|
|
||||||
let inputs: {[key: string]: string|[string, string]} = {};
|
const coercedInputFields = new Set<ClassPropertyName>();
|
||||||
let outputs: {[key: string]: string} = {};
|
const undeclaredInputFields = new Set<ClassPropertyName>();
|
||||||
const coercedInputFields = new Set<string>();
|
const restrictedInputFields = new Set<ClassPropertyName>();
|
||||||
const undeclaredInputFields = new Set<string>();
|
const stringLiteralInputFields = new Set<ClassPropertyName>();
|
||||||
const restrictedInputFields = new Set<string>();
|
|
||||||
const stringLiteralInputFields = new Set<string>();
|
|
||||||
let isDynamic = false;
|
let isDynamic = false;
|
||||||
|
let inputs = ClassPropertyMapping.empty();
|
||||||
|
let outputs = ClassPropertyMapping.empty();
|
||||||
|
|
||||||
const addMetadata = (meta: DirectiveMeta): void => {
|
const addMetadata = (meta: DirectiveMeta): void => {
|
||||||
if (meta.baseClass === 'dynamic') {
|
if (meta.baseClass === 'dynamic') {
|
||||||
@ -45,8 +50,9 @@ export function flattenInheritedDirectiveMetadata(
|
|||||||
isDynamic = true;
|
isDynamic = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inputs = {...inputs, ...meta.inputs};
|
|
||||||
outputs = {...outputs, ...meta.outputs};
|
inputs = ClassPropertyMapping.merge(inputs, meta.inputs);
|
||||||
|
outputs = ClassPropertyMapping.merge(outputs, meta.outputs);
|
||||||
|
|
||||||
for (const coercedInputField of meta.coercedInputFields) {
|
for (const coercedInputField of meta.coercedInputFields) {
|
||||||
coercedInputFields.add(coercedInputField);
|
coercedInputFields.add(coercedInputField);
|
||||||
|
200
packages/compiler-cli/src/ngtsc/metadata/src/property_mapping.ts
Normal file
200
packages/compiler-cli/src/ngtsc/metadata/src/property_mapping.ts
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* @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 {InputOutputPropertySet} from '@angular/compiler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of a class property that backs an input or output declared by a directive or component.
|
||||||
|
*
|
||||||
|
* This type exists for documentation only.
|
||||||
|
*/
|
||||||
|
export type ClassPropertyName = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name by which an input or output of a directive or component is bound in an Angular template.
|
||||||
|
*
|
||||||
|
* This type exists for documentation only.
|
||||||
|
*/
|
||||||
|
export type BindingPropertyName = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An input or output of a directive that has both a named JavaScript class property on a component
|
||||||
|
* or directive class, as well as an Angular template property name used for binding.
|
||||||
|
*/
|
||||||
|
export interface InputOrOutput {
|
||||||
|
/**
|
||||||
|
* The name of the JavaScript property on the component or directive instance for this input or
|
||||||
|
* output.
|
||||||
|
*/
|
||||||
|
readonly classPropertyName: ClassPropertyName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The property name used to bind this input or output in an Angular template.
|
||||||
|
*/
|
||||||
|
readonly bindingPropertyName: BindingPropertyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping of component property and template binding property names, for example containing the
|
||||||
|
* inputs of a particular directive or component.
|
||||||
|
*
|
||||||
|
* A single component property has exactly one input/output annotation (and therefore one binding
|
||||||
|
* property name) associated with it, but the same binding property name may be shared across many
|
||||||
|
* component property names.
|
||||||
|
*
|
||||||
|
* Allows bidirectional querying of the mapping - looking up all inputs/outputs with a given
|
||||||
|
* property name, or mapping from a specific class property to its binding property name.
|
||||||
|
*/
|
||||||
|
export class ClassPropertyMapping implements InputOutputPropertySet {
|
||||||
|
/**
|
||||||
|
* Mapping from class property names to the single `InputOrOutput` for that class property.
|
||||||
|
*/
|
||||||
|
private forwardMap: Map<ClassPropertyName, InputOrOutput>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping from property names to one or more `InputOrOutput`s which share that name.
|
||||||
|
*/
|
||||||
|
private reverseMap: Map<BindingPropertyName, InputOrOutput[]>;
|
||||||
|
|
||||||
|
private constructor(forwardMap: Map<ClassPropertyName, InputOrOutput>) {
|
||||||
|
this.forwardMap = forwardMap;
|
||||||
|
this.reverseMap = reverseMapFromForwardMap(forwardMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a `ClassPropertyMapping` with no entries.
|
||||||
|
*/
|
||||||
|
static empty(): ClassPropertyMapping {
|
||||||
|
return new ClassPropertyMapping(new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a `ClassPropertyMapping` from a primitive JS object which maps class property names
|
||||||
|
* to either binding property names or an array that contains both names, which is used in on-disk
|
||||||
|
* metadata formats (e.g. in .d.ts files).
|
||||||
|
*/
|
||||||
|
static fromMappedObject(obj: {
|
||||||
|
[classPropertyName: string]: BindingPropertyName|[ClassPropertyName, BindingPropertyName]
|
||||||
|
}): ClassPropertyMapping {
|
||||||
|
const forwardMap = new Map<ClassPropertyName, InputOrOutput>();
|
||||||
|
|
||||||
|
for (const classPropertyName of Object.keys(obj)) {
|
||||||
|
const value = obj[classPropertyName];
|
||||||
|
const bindingPropertyName = Array.isArray(value) ? value[0] : value;
|
||||||
|
const inputOrOutput: InputOrOutput = {classPropertyName, bindingPropertyName};
|
||||||
|
forwardMap.set(classPropertyName, inputOrOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClassPropertyMapping(forwardMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge two mappings into one, with class properties from `b` taking precedence over class
|
||||||
|
* properties from `a`.
|
||||||
|
*/
|
||||||
|
static merge(a: ClassPropertyMapping, b: ClassPropertyMapping): ClassPropertyMapping {
|
||||||
|
const forwardMap = new Map<ClassPropertyName, InputOrOutput>(a.forwardMap.entries());
|
||||||
|
for (const [classPropertyName, inputOrOutput] of b.forwardMap) {
|
||||||
|
forwardMap.set(classPropertyName, inputOrOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClassPropertyMapping(forwardMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All class property names mapped in this mapping.
|
||||||
|
*/
|
||||||
|
get classPropertyNames(): ClassPropertyName[] {
|
||||||
|
return Array.from(this.forwardMap.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All binding property names mapped in this mapping.
|
||||||
|
*/
|
||||||
|
get propertyNames(): BindingPropertyName[] {
|
||||||
|
return Array.from(this.reverseMap.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a mapping for the given property name exists.
|
||||||
|
*/
|
||||||
|
hasBindingPropertyName(propertyName: BindingPropertyName): boolean {
|
||||||
|
return this.reverseMap.has(propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup all `InputOrOutput`s that use this `propertyName`.
|
||||||
|
*/
|
||||||
|
getByBindingPropertyName(propertyName: string): ReadonlyArray<InputOrOutput>|null {
|
||||||
|
return this.reverseMap.has(propertyName) ? this.reverseMap.get(propertyName)! : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup the `InputOrOutput` associated with a `classPropertyName`.
|
||||||
|
*/
|
||||||
|
getByClassPropertyName(classPropertyName: string): InputOrOutput|null {
|
||||||
|
return this.forwardMap.has(classPropertyName) ? this.forwardMap.get(classPropertyName)! : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this mapping to a primitive JS object which maps each class property directly to the
|
||||||
|
* binding property name associated with it.
|
||||||
|
*/
|
||||||
|
toDirectMappedObject(): {[classPropertyName: string]: BindingPropertyName} {
|
||||||
|
const obj: {[classPropertyName: string]: BindingPropertyName} = {};
|
||||||
|
for (const [classPropertyName, inputOrOutput] of this.forwardMap) {
|
||||||
|
obj[classPropertyName] = inputOrOutput.bindingPropertyName;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this mapping to a primitive JS object which maps each class property either to itself
|
||||||
|
* (for cases where the binding property name is the same) or to an array which contains both
|
||||||
|
* names if they differ.
|
||||||
|
*
|
||||||
|
* This object format is used when mappings are serialized (for example into .d.ts files).
|
||||||
|
*/
|
||||||
|
toJointMappedObject():
|
||||||
|
{[classPropertyName: string]: BindingPropertyName|[BindingPropertyName, ClassPropertyName]} {
|
||||||
|
const obj: {
|
||||||
|
[classPropertyName: string]: BindingPropertyName|[BindingPropertyName, ClassPropertyName]
|
||||||
|
} = {};
|
||||||
|
for (const [classPropertyName, inputOrOutput] of this.forwardMap) {
|
||||||
|
if (inputOrOutput.bindingPropertyName as string === classPropertyName as string) {
|
||||||
|
obj[classPropertyName] = inputOrOutput.bindingPropertyName;
|
||||||
|
} else {
|
||||||
|
obj[classPropertyName] = [inputOrOutput.bindingPropertyName, classPropertyName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement the iterator protocol and return entry objects which contain the class and binding
|
||||||
|
* property names (and are useful for destructuring).
|
||||||
|
*/
|
||||||
|
* [Symbol.iterator](): IterableIterator<[ClassPropertyName, BindingPropertyName]> {
|
||||||
|
for (const [classPropertyName, inputOrOutput] of this.forwardMap.entries()) {
|
||||||
|
yield [classPropertyName, inputOrOutput.bindingPropertyName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reverseMapFromForwardMap(forwardMap: Map<ClassPropertyName, InputOrOutput>):
|
||||||
|
Map<BindingPropertyName, InputOrOutput[]> {
|
||||||
|
const reverseMap = new Map<BindingPropertyName, InputOrOutput[]>();
|
||||||
|
for (const [_, inputOrOutput] of forwardMap) {
|
||||||
|
if (!reverseMap.has(inputOrOutput.bindingPropertyName)) {
|
||||||
|
reverseMap.set(inputOrOutput.bindingPropertyName, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
reverseMap.get(inputOrOutput.bindingPropertyName)!.push(inputOrOutput);
|
||||||
|
}
|
||||||
|
return reverseMap;
|
||||||
|
}
|
@ -13,6 +13,7 @@ import {ClassDeclaration, ClassMember, ClassMemberKind, isNamedClassDeclaration,
|
|||||||
import {nodeDebugInfo} from '../../util/src/typescript';
|
import {nodeDebugInfo} from '../../util/src/typescript';
|
||||||
|
|
||||||
import {DirectiveMeta, DirectiveTypeCheckMeta, MetadataReader, NgModuleMeta, PipeMeta, TemplateGuardMeta} from './api';
|
import {DirectiveMeta, DirectiveTypeCheckMeta, MetadataReader, NgModuleMeta, PipeMeta, TemplateGuardMeta} from './api';
|
||||||
|
import {ClassPropertyMapping, ClassPropertyName} from './property_mapping';
|
||||||
|
|
||||||
export function extractReferencesFromType(
|
export function extractReferencesFromType(
|
||||||
checker: ts.TypeChecker, def: ts.TypeNode, ngModuleImportedFrom: string|null,
|
checker: ts.TypeChecker, def: ts.TypeNode, ngModuleImportedFrom: string|null,
|
||||||
@ -91,7 +92,7 @@ export function readStringArrayType(type: ts.TypeNode): string[] {
|
|||||||
* making this metadata invariant to changes of inherited classes.
|
* making this metadata invariant to changes of inherited classes.
|
||||||
*/
|
*/
|
||||||
export function extractDirectiveTypeCheckMeta(
|
export function extractDirectiveTypeCheckMeta(
|
||||||
node: ClassDeclaration, inputs: {[fieldName: string]: string|[string, string]},
|
node: ClassDeclaration, inputs: ClassPropertyMapping,
|
||||||
reflector: ReflectionHost): DirectiveTypeCheckMeta {
|
reflector: ReflectionHost): DirectiveTypeCheckMeta {
|
||||||
const members = reflector.getMembersOfClass(node);
|
const members = reflector.getMembersOfClass(node);
|
||||||
const staticMembers = members.filter(member => member.isStatic);
|
const staticMembers = members.filter(member => member.isStatic);
|
||||||
@ -102,23 +103,23 @@ export function extractDirectiveTypeCheckMeta(
|
|||||||
|
|
||||||
const coercedInputFields =
|
const coercedInputFields =
|
||||||
new Set(staticMembers.map(extractCoercedInput)
|
new Set(staticMembers.map(extractCoercedInput)
|
||||||
.filter((inputName): inputName is string => inputName !== null));
|
.filter((inputName): inputName is ClassPropertyName => inputName !== null));
|
||||||
|
|
||||||
const restrictedInputFields = new Set<string>();
|
const restrictedInputFields = new Set<ClassPropertyName>();
|
||||||
const stringLiteralInputFields = new Set<string>();
|
const stringLiteralInputFields = new Set<ClassPropertyName>();
|
||||||
const undeclaredInputFields = new Set<string>();
|
const undeclaredInputFields = new Set<ClassPropertyName>();
|
||||||
|
|
||||||
for (const fieldName of Object.keys(inputs)) {
|
for (const classPropertyName of inputs.classPropertyNames) {
|
||||||
const field = members.find(member => member.name === fieldName);
|
const field = members.find(member => member.name === classPropertyName);
|
||||||
if (field === undefined || field.node === null) {
|
if (field === undefined || field.node === null) {
|
||||||
undeclaredInputFields.add(fieldName);
|
undeclaredInputFields.add(classPropertyName);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isRestricted(field.node)) {
|
if (isRestricted(field.node)) {
|
||||||
restrictedInputFields.add(fieldName);
|
restrictedInputFields.add(classPropertyName);
|
||||||
}
|
}
|
||||||
if (field.nameNode !== null && ts.isStringLiteral(field.nameNode)) {
|
if (field.nameNode !== null && ts.isStringLiteral(field.nameNode)) {
|
||||||
stringLiteralInputFields.add(fieldName);
|
stringLiteralInputFields.add(classPropertyName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,7 +336,7 @@ export interface UnsupportedType {
|
|||||||
export interface NoValueDeclaration {
|
export interface NoValueDeclaration {
|
||||||
kind: ValueUnavailableKind.NO_VALUE_DECLARATION;
|
kind: ValueUnavailableKind.NO_VALUE_DECLARATION;
|
||||||
typeNode: ts.TypeNode;
|
typeNode: ts.TypeNode;
|
||||||
decl: ts.Declaration;
|
decl: ts.Declaration|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TypeOnlyImport {
|
export interface TypeOnlyImport {
|
||||||
|
@ -38,7 +38,11 @@ export function typeToValue(
|
|||||||
// has a value declaration associated with it. Note that const enums are an exception,
|
// has a value declaration associated with it. Note that const enums are an exception,
|
||||||
// because while they do have a value declaration, they don't exist at runtime.
|
// because while they do have a value declaration, they don't exist at runtime.
|
||||||
if (decl.valueDeclaration === undefined || decl.flags & ts.SymbolFlags.ConstEnum) {
|
if (decl.valueDeclaration === undefined || decl.flags & ts.SymbolFlags.ConstEnum) {
|
||||||
return noValueDeclaration(typeNode, decl.declarations[0]);
|
let typeOnlyDecl: ts.Declaration|null = null;
|
||||||
|
if (decl.declarations !== undefined && decl.declarations.length > 0) {
|
||||||
|
typeOnlyDecl = decl.declarations[0];
|
||||||
|
}
|
||||||
|
return noValueDeclaration(typeNode, typeOnlyDecl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The type points to a valid value declaration. Rewrite the TypeReference into an
|
// The type points to a valid value declaration. Rewrite the TypeReference into an
|
||||||
@ -140,7 +144,7 @@ function unsupportedType(typeNode: ts.TypeNode): UnavailableTypeValueReference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function noValueDeclaration(
|
function noValueDeclaration(
|
||||||
typeNode: ts.TypeNode, decl: ts.Declaration): UnavailableTypeValueReference {
|
typeNode: ts.TypeNode, decl: ts.Declaration|null): UnavailableTypeValueReference {
|
||||||
return {
|
return {
|
||||||
kind: TypeValueReferenceKind.UNAVAILABLE,
|
kind: TypeValueReferenceKind.UNAVAILABLE,
|
||||||
reason: {kind: ValueUnavailableKind.NO_VALUE_DECLARATION, typeNode, decl},
|
reason: {kind: ValueUnavailableKind.NO_VALUE_DECLARATION, typeNode, decl},
|
||||||
|
@ -26,6 +26,7 @@ export interface LocalNgModuleData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalModuleScope extends ExportScope {
|
export interface LocalModuleScope extends ExportScope {
|
||||||
|
ngModule: ClassDeclaration;
|
||||||
compilation: ScopeData;
|
compilation: ScopeData;
|
||||||
reexports: Reexport[]|null;
|
reexports: Reexport[]|null;
|
||||||
schemas: SchemaMetadata[];
|
schemas: SchemaMetadata[];
|
||||||
@ -433,7 +434,8 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Finally, produce the `LocalModuleScope` with both the compilation and export scopes.
|
// Finally, produce the `LocalModuleScope` with both the compilation and export scopes.
|
||||||
const scope = {
|
const scope: LocalModuleScope = {
|
||||||
|
ngModule: ngModule.ref.node,
|
||||||
compilation: {
|
compilation: {
|
||||||
directives: Array.from(compilationDirectives.values()),
|
directives: Array.from(compilationDirectives.values()),
|
||||||
pipes: Array.from(compilationPipes.values()),
|
pipes: Array.from(compilationPipes.values()),
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {Reference, ReferenceEmitter} from '../../imports';
|
import {Reference, ReferenceEmitter} from '../../imports';
|
||||||
import {CompoundMetadataRegistry, DirectiveMeta, LocalMetadataRegistry, MetadataRegistry, PipeMeta} from '../../metadata';
|
import {ClassPropertyMapping, CompoundMetadataRegistry, DirectiveMeta, LocalMetadataRegistry, MetadataRegistry, PipeMeta} from '../../metadata';
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
import {ScopeData} from '../src/api';
|
import {ScopeData} from '../src/api';
|
||||||
import {DtsModuleScopeResolver} from '../src/dependency';
|
import {DtsModuleScopeResolver} from '../src/dependency';
|
||||||
@ -236,8 +236,8 @@ function fakeDirective(ref: Reference<ClassDeclaration>): DirectiveMeta {
|
|||||||
name,
|
name,
|
||||||
selector: `[${ref.debugName}]`,
|
selector: `[${ref.debugName}]`,
|
||||||
isComponent: name.startsWith('Cmp'),
|
isComponent: name.startsWith('Cmp'),
|
||||||
inputs: {},
|
inputs: ClassPropertyMapping.fromMappedObject({}),
|
||||||
outputs: {},
|
outputs: ClassPropertyMapping.fromMappedObject({}),
|
||||||
exportAs: null,
|
exportAs: null,
|
||||||
queries: [],
|
queries: [],
|
||||||
hasNgTemplateContextGuard: false,
|
hasNgTemplateContextGuard: false,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user