Compare commits
93 Commits
master
...
10.1.0-nex
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d36828a7a1 | ||
![]() |
f18e2d5898 | ||
![]() |
375f0a6f67 | ||
![]() |
281b647f15 | ||
![]() |
0fc44e0436 | ||
![]() |
201a546af8 | ||
![]() |
6e643d9874 | ||
![]() |
4985267211 | ||
![]() |
b48cc6ead5 | ||
![]() |
874792dc43 | ||
![]() |
e7da4040d6 | ||
![]() |
2a643e1ab6 | ||
![]() |
364284b0dc | ||
![]() |
956b25a100 | ||
![]() |
8017ca4db3 | ||
![]() |
22f1ac3e37 | ||
![]() |
4ee5e730ab | ||
![]() |
6442875c99 | ||
![]() |
8f24bc9443 | ||
![]() |
ac461e1efd | ||
![]() |
f245c6bb15 | ||
![]() |
68a9a01a64 | ||
![]() |
8cd4099db9 | ||
![]() |
0b54c0c6b4 | ||
![]() |
1ec609946f | ||
![]() |
7ad32649c0 | ||
![]() |
9ad69c1503 | ||
![]() |
9af2de821c | ||
![]() |
0270020ac2 | ||
![]() |
6b662d10c1 | ||
![]() |
55fd725e74 | ||
![]() |
f77fd5e02a | ||
![]() |
63ba74fe4e | ||
![]() |
aaa1d8e2fe | ||
![]() |
dbfb50e9f4 | ||
![]() |
bee44b3359 | ||
![]() |
723a9ff095 | ||
![]() |
e472f5f688 | ||
![]() |
8373b720f3 | ||
![]() |
301513311e | ||
![]() |
64cf087ae5 | ||
![]() |
fec9dcbeb0 | ||
![]() |
e0e5c9f195 | ||
![]() |
cfe424e875 | ||
![]() |
3b9c802dee | ||
![]() |
ca07da4563 | ||
![]() |
81c3e809aa | ||
![]() |
be96510ce9 | ||
![]() |
cb05c0102f | ||
![]() |
5f90b64328 | ||
![]() |
71079ce47e | ||
![]() |
ca798804b2 | ||
![]() |
b071495f92 | ||
![]() |
d5f819ebc1 | ||
![]() |
1388c1761f | ||
![]() |
fb8f4b4d72 | ||
![]() |
f42e6ce917 | ||
![]() |
175c79d1d8 | ||
![]() |
945751e2e8 | ||
![]() |
b769771d60 | ||
![]() |
a80f654af9 | ||
![]() |
aa847cb014 | ||
![]() |
8763d8201c | ||
![]() |
9f7a37b4e9 | ||
![]() |
773f7908c0 | ||
![]() |
f4ced74e3a | ||
![]() |
8366effeec | ||
![]() |
5f2e475abf | ||
![]() |
aa3520eb7d | ||
![]() |
823dd5b341 | ||
![]() |
d6d7caa2a8 | ||
![]() |
dcf7baf3d1 | ||
![]() |
4d17418569 | ||
![]() |
a2e069fdda | ||
![]() |
28534d83ee | ||
![]() |
a6292faa97 | ||
![]() |
e2e5f83869 | ||
![]() |
71138f6004 | ||
![]() |
fa0104017a | ||
![]() |
80b67e02b7 | ||
![]() |
18098d38b8 | ||
![]() |
9514fd9080 | ||
![]() |
2e9fdbde9e | ||
![]() |
df76a2048b | ||
![]() |
3d156162af | ||
![]() |
5dc8d287aa | ||
![]() |
6da9e5851a | ||
![]() |
250e299dc3 | ||
![]() |
8f708b561c | ||
![]() |
e34c33cd46 | ||
![]() |
6d8c73a4d6 | ||
![]() |
6ff28ac944 | ||
![]() |
0de93fd402 |
@ -656,6 +656,18 @@ jobs:
|
||||
- run: yarn tsc -p packages
|
||||
- run: yarn tsc -p modules
|
||||
- run: yarn bazel build //packages/zone.js:npm_package
|
||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
||||
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
|
||||
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
|
||||
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
|
||||
# test in all supported browsers on Saucelabs anyway until this job can be removed.
|
||||
- run:
|
||||
name: Preparing Bazel-generated fixtures required in legacy tests
|
||||
command: |
|
||||
yarn bazel build //packages/core/test:downleveled_es5_fixture
|
||||
# Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`.
|
||||
cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \
|
||||
dist/all/@angular/core/test/reflection/es5_downleveled_inheritance_fixture.js
|
||||
- run:
|
||||
# Waiting on ready ensures that we don't run tests too early without Saucelabs not being ready.
|
||||
name: Waiting for Saucelabs tunnel to connect
|
||||
|
@ -7,18 +7,6 @@ export const commitMessage: CommitMessageConfig = {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 20,
|
||||
minBodyLengthTypeExcludes: ['docs'],
|
||||
types: [
|
||||
'build',
|
||||
'ci',
|
||||
'docs',
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'refactor',
|
||||
'release',
|
||||
'style',
|
||||
'test',
|
||||
],
|
||||
scopes: [
|
||||
'animations',
|
||||
'bazel',
|
||||
|
@ -509,8 +509,8 @@ groups:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'packages/core/src/i18n/**',
|
||||
'packages/core/src/render3/i18n.ts',
|
||||
'packages/core/src/render3/i18n.md',
|
||||
'packages/core/src/render3/i18n/**',
|
||||
'packages/core/src/render3/instructions/i18n.ts',
|
||||
'packages/core/src/render3/interfaces/i18n.ts',
|
||||
'packages/common/locales/**',
|
||||
'packages/common/src/i18n/**',
|
||||
|
140
CHANGELOG.md
140
CHANGELOG.md
@ -1,3 +1,143 @@
|
||||
<a name="10.1.0-next.8"></a>
|
||||
# 10.1.0-next.8 (2020-08-24)
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0-next.8"></a>
|
||||
# 10.1.0-next.8 (2020-08-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **localize:** extract the correct message ids ([#38498](https://github.com/angular/angular/issues/38498)) ([ac461e1](https://github.com/angular/angular/commit/ac461e1))
|
||||
* **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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** support unary operators for more accurate type checking ([#37918](https://github.com/angular/angular/issues/37918)) ([874792d](https://github.com/angular/angular/commit/874792d)), closes [#20845](https://github.com/angular/angular/issues/20845) [#36178](https://github.com/angular/angular/issues/36178)
|
||||
* **compiler-cli:** add support for TypeScript 4.0 ([#38076](https://github.com/angular/angular/issues/38076)) ([0fc44e0](https://github.com/angular/angular/commit/0fc44e0))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **forms:** use internal `ngDevMode` flag to tree-shake error messages in prod builds ([#37821](https://github.com/angular/angular/issues/37821)) ([201a546](https://github.com/angular/angular/commit/201a546)), closes [#37697](https://github.com/angular/angular/issues/37697)
|
||||
|
||||
|
||||
|
||||
<a name="10.0.12"></a>
|
||||
## 10.0.12 (2020-08-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** adding references to const enums in runtime code ([#38542](https://github.com/angular/angular/issues/38542)) ([814b436](https://github.com/angular/angular/commit/814b436)), closes [#38513](https://github.com/angular/angular/issues/38513)
|
||||
* **core:** remove closing body tag from inert DOM builder ([#38454](https://github.com/angular/angular/issues/38454)) ([5528536](https://github.com/angular/angular/commit/5528536))
|
||||
* **localize:** include the last placeholder in parsed translation text ([#38452](https://github.com/angular/angular/issues/38452)) ([57d1a48](https://github.com/angular/angular/commit/57d1a48))
|
||||
* **localize:** parse all parts of a translation with nested HTML ([#38452](https://github.com/angular/angular/issues/38452)) ([07b99f5](https://github.com/angular/angular/commit/07b99f5)), closes [#38422](https://github.com/angular/angular/issues/38422)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **language-service:** introduce hybrid visitor to locate AST node ([#38540](https://github.com/angular/angular/issues/38540)) ([66d8c22](https://github.com/angular/angular/commit/66d8c22))
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0-next.7"></a>
|
||||
# 10.1.0-next.7 (2020-08-19)
|
||||
|
||||
This release contains various API docs improvements.
|
||||
|
||||
|
||||
<a name="10.0.11"></a>
|
||||
## 10.0.11 (2020-08-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **router:** ensure routerLinkActive updates when associated routerLinks change (resubmit of [#38349](https://github.com/angular/angular/issues/38349)) ([#38511](https://github.com/angular/angular/issues/38511)) ([0af9533](https://github.com/angular/angular/commit/0af9533)), closes [#18469](https://github.com/angular/angular/issues/18469)
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0-next.6"></a>
|
||||
# 10.1.0-next.6 (2020-08-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** detect DI parameters in JIT mode for downleveled ES2015 classes ([#38463](https://github.com/angular/angular/issues/38463)) ([ca07da4](https://github.com/angular/angular/commit/ca07da4)), closes [#38453](https://github.com/angular/angular/issues/38453)
|
||||
* **core:** move generated i18n statements to the `consts` field of ComponentDef ([#38404](https://github.com/angular/angular/issues/38404)) ([cb05c01](https://github.com/angular/angular/commit/cb05c01))
|
||||
* **localize:** render ICU placeholders in extracted translation files ([#38484](https://github.com/angular/angular/issues/38484)) ([81c3e80](https://github.com/angular/angular/commit/81c3e80))
|
||||
* **ngcc:** detect synthesized delegate constructors for downleveled ES2015 classes ([#38463](https://github.com/angular/angular/issues/38463)) ([3b9c802](https://github.com/angular/angular/commit/3b9c802)), closes [#38453](https://github.com/angular/angular/issues/38453) [#38453](https://github.com/angular/angular/issues/38453)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **compiler-cli:** don't emit template guards when child scope is empty ([#38418](https://github.com/angular/angular/issues/38418)) ([1388c17](https://github.com/angular/angular/commit/1388c17))
|
||||
* **compiler-cli:** only generate directive declarations when used ([#38418](https://github.com/angular/angular/issues/38418)) ([fb8f4b4](https://github.com/angular/angular/commit/fb8f4b4))
|
||||
* **compiler-cli:** only generate type-check code for referenced DOM elements ([#38418](https://github.com/angular/angular/issues/38418)) ([f42e6ce](https://github.com/angular/angular/commit/f42e6ce))
|
||||
|
||||
### 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))
|
||||
|
||||
|
||||
|
||||
<a name="10.0.10"></a>
|
||||
## 10.0.10 (2020-08-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** Allow scrolling when browser supports scrollTo ([#38468](https://github.com/angular/angular/issues/38468)) ([b32126c](https://github.com/angular/angular/commit/b32126c)), closes [#30630](https://github.com/angular/angular/issues/30630)
|
||||
* **core:** detect DI parameters in JIT mode for downleveled ES2015 classes ([#38500](https://github.com/angular/angular/issues/38500)) ([863acb6](https://github.com/angular/angular/commit/863acb6)), closes [#38453](https://github.com/angular/angular/issues/38453)
|
||||
* **core:** error if CSS custom property in host binding has number in name ([#38432](https://github.com/angular/angular/issues/38432)) ([cb83b8a](https://github.com/angular/angular/commit/cb83b8a)), closes [#37292](https://github.com/angular/angular/issues/37292)
|
||||
* **core:** fix multiple nested views removal from ViewContainerRef ([#38317](https://github.com/angular/angular/issues/38317)) ([d5e09f4](https://github.com/angular/angular/commit/d5e09f4)), closes [#38201](https://github.com/angular/angular/issues/38201)
|
||||
* **ngcc:** detect synthesized delegate constructors for downleveled ES2015 classes ([#38500](https://github.com/angular/angular/issues/38500)) ([f3dd6c2](https://github.com/angular/angular/commit/f3dd6c2)), closes [#38453](https://github.com/angular/angular/issues/38453) [#38453](https://github.com/angular/angular/issues/38453)
|
||||
* **router:** ensure routerLinkActive updates when associated routerLinks change ([#38349](https://github.com/angular/angular/issues/38349)) ([989e8a1](https://github.com/angular/angular/commit/989e8a1)), closes [#18469](https://github.com/angular/angular/issues/18469)
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0-next.5"></a>
|
||||
# 10.1.0-next.5 (2020-08-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** avoid creating value expressions for symbols from type-only imports ([#37912](https://github.com/angular/angular/issues/37912)) ([18098d3](https://github.com/angular/angular/commit/18098d3)), closes [#37900](https://github.com/angular/angular/issues/37900)
|
||||
* **compiler-cli:** type-check inputs that include undefined when there's coercion members ([#38273](https://github.com/angular/angular/issues/38273)) ([7525f3a](https://github.com/angular/angular/commit/7525f3a))
|
||||
* **router:** defer loading of wildcard module until needed ([#38348](https://github.com/angular/angular/issues/38348)) ([8f708b5](https://github.com/angular/angular/commit/8f708b5)), closes [#25494](https://github.com/angular/angular/issues/25494)
|
||||
* **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))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler-cli:** Add compiler option to report errors when assigning to restricted input fields ([#38249](https://github.com/angular/angular/issues/38249)) ([71138f6](https://github.com/angular/angular/commit/71138f6))
|
||||
* **router:** better warning message when a router outlet has not been instantiated ([#30246](https://github.com/angular/angular/issues/30246)) ([1609815](https://github.com/angular/angular/commit/1609815))
|
||||
|
||||
|
||||
|
||||
<a name="10.0.9"></a>
|
||||
## 10.0.9 (2020-08-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** ensure scrollRestoration is writable ([#30630](https://github.com/angular/angular/issues/30630)) ([#38357](https://github.com/angular/angular/issues/38357)) ([58f4b3a](https://github.com/angular/angular/commit/58f4b3a)), closes [#30629](https://github.com/angular/angular/issues/30629)
|
||||
* **compiler:** evaluate safe navigation expressions in correct binding order ([#37911](https://github.com/angular/angular/issues/37911)) ([f5b9d87](https://github.com/angular/angular/commit/f5b9d87)), closes [#37194](https://github.com/angular/angular/issues/37194)
|
||||
* **compiler-cli:** avoid creating value expressions for symbols from type-only imports ([#38415](https://github.com/angular/angular/issues/38415)) ([ca2b4bc](https://github.com/angular/angular/commit/ca2b4bc)), closes [#37912](https://github.com/angular/angular/issues/37912)
|
||||
* **compiler-cli:** infer quote expressions as any type in type checker ([#37917](https://github.com/angular/angular/issues/37917)) ([5b87c67](https://github.com/angular/angular/commit/5b87c67)), closes [#36568](https://github.com/angular/angular/issues/36568)
|
||||
* **compiler-cli:** mark eager `NgModuleFactory` construction as not side effectful ([#38320](https://github.com/angular/angular/issues/38320)) ([016a41b](https://github.com/angular/angular/commit/016a41b)), closes [#38147](https://github.com/angular/angular/issues/38147)
|
||||
* **compiler-cli:** match wrapHost parameter types within plugin interface ([#38004](https://github.com/angular/angular/issues/38004)) ([df01a82](https://github.com/angular/angular/commit/df01a82))
|
||||
* **compiler-cli:** preserve quotes in class member names ([#38387](https://github.com/angular/angular/issues/38387)) ([c9acb7b](https://github.com/angular/angular/commit/c9acb7b)), closes [#38311](https://github.com/angular/angular/issues/38311)
|
||||
* **core:** prevent NgModule scope being overwritten in JIT compiler ([#37795](https://github.com/angular/angular/issues/37795)) ([3acebdc](https://github.com/angular/angular/commit/3acebdc)), closes [#37105](https://github.com/angular/angular/issues/37105)
|
||||
* **core:** queries not matching string injection tokens ([#38321](https://github.com/angular/angular/issues/38321)) ([32109dc](https://github.com/angular/angular/commit/32109dc)), closes [#38313](https://github.com/angular/angular/issues/38313) [#38315](https://github.com/angular/angular/issues/38315)
|
||||
* **core:** Store the currently selected ICU in `LView` ([#38345](https://github.com/angular/angular/issues/38345)) ([ee5123f](https://github.com/angular/angular/commit/ee5123f))
|
||||
* **platform-server:** remove styles added by ServerStylesHost on destruction ([#38367](https://github.com/angular/angular/issues/38367)) ([7f11149](https://github.com/angular/angular/commit/7f11149))
|
||||
* **router:** prevent calling unsubscribe on undefined subscription in RouterPreloader ([#38344](https://github.com/angular/angular/issues/38344)) ([4151314](https://github.com/angular/angular/commit/4151314))
|
||||
* **service-worker:** fix the chrome debugger syntax highlighter ([#38332](https://github.com/angular/angular/issues/38332)) ([f5d5bac](https://github.com/angular/angular/commit/f5d5bac))
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0-next.4"></a>
|
||||
# 10.1.0-next.4 (2020-08-04)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
@ -11,7 +11,7 @@ import { HeroService } from '../hero.service';
|
||||
styleUrls: [ './hero-detail.component.css' ]
|
||||
})
|
||||
export class HeroDetailComponent implements OnInit {
|
||||
@Input() hero: Hero;
|
||||
hero: Hero;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
|
@ -31,7 +31,7 @@ For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
...
|
||||
|
@ -154,7 +154,7 @@ Attributes can be changed by `setAttribute()`, which re-initializes correspondin
|
||||
</div>
|
||||
|
||||
For more information, see the [MDN Interfaces documentation](https://developer.mozilla.org/en-US/docs/Web/API#Interfaces) which has API docs for all the standard DOM elements and their properties.
|
||||
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) attributes to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
|
||||
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
|
||||
In particular, you can navigate from the attributes page to the properties via "DOM interface" link, and navigate the inheritance hierarchy up to `HTMLTableCellElement`.
|
||||
|
||||
|
||||
@ -195,7 +195,7 @@ To control the state of the button, set the `disabled` *property*,
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
|
||||
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to be a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
|
||||
|
||||
```html
|
||||
<input [disabled]="condition ? true : false">
|
||||
|
@ -40,8 +40,7 @@ The top level of the workspace contains workspace-wide configuration files, conf
|
||||
| `package-lock.json` | Provides version information for all packages installed into `node_modules` by the npm client. See [npm documentation](https://docs.npmjs.com/files/package-lock.json) for details. If you use the yarn client, this file will be [yarn.lock](https://yarnpkg.com/lang/en/docs/yarn-lock/) instead. |
|
||||
| `src/` | Source files for the root-level application project. |
|
||||
| `node_modules/` | Provides [npm packages](guide/npm-packages) to the entire workspace. Workspace-wide `node_modules` dependencies are visible to all projects. |
|
||||
| `tsconfig.json` | The `tsconfig.json` file is a ["Solution Style"](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#support-for-solution-style-tsconfigjson-files) TypeScript configuration file. Code editors and TypeScript’s language server use this file to improve development experience. Compilers do not use this file. |
|
||||
| `tsconfig.base.json` | The base [TypeScript](https://www.typescriptlang.org/) configuration for projects in the workspace. All other configuration files inherit from this base file. For more information, see the [Configuration inheritance with extends](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends) section of the TypeScript documentation.|
|
||||
| `tsconfig.json` | The base [TypeScript](https://www.typescriptlang.org/) configuration for projects in the workspace. All other configuration files inherit from this base file. For more information, see the [Configuration inheritance with extends](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends) section of the TypeScript documentation.|
|
||||
| `tslint.json` | Default [TSLint](https://palantir.github.io/tslint/) configuration for projects in the workspace. |
|
||||
|
||||
|
||||
@ -103,7 +102,7 @@ Angular components, templates, and styles go here.
|
||||
The application-specific configuration files for the root application reside at the workspace root level.
|
||||
For a multi-project workspace, project-specific configuration files are in the project root, under `projects/project-name/`.
|
||||
|
||||
Project-specific [TypeScript](https://www.typescriptlang.org/) configuration files inherit from the workspace-wide `tsconfig.base.json`, and project-specific [TSLint](https://palantir.github.io/tslint/) configuration files inherit from the workspace-wide `tslint.json`.
|
||||
Project-specific [TypeScript](https://www.typescriptlang.org/) configuration files inherit from the workspace-wide `tsconfig.json`, and project-specific [TSLint](https://palantir.github.io/tslint/) configuration files inherit from the workspace-wide `tslint.json`.
|
||||
|
||||
| APPLICATION-SPECIFIC CONFIG FILES | PURPOSE |
|
||||
| :--------------------- | :------------------------------------------|
|
||||
|
@ -766,8 +766,10 @@ The HTML `base` tag with the `href` attribute specifies the base URI, or URL, fo
|
||||
"i18n": {
|
||||
"sourceLocale": "en-US",
|
||||
"locales": {
|
||||
"fr": "src/locale/messages.fr.xlf"
|
||||
"baseHref": ""
|
||||
"fr": {
|
||||
"translation": "src/locale/messages.fr.xlf",
|
||||
"baseHref": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
|
@ -208,7 +208,7 @@ about the event and gives that data to the parent.
|
||||
The child's template has two controls. The first is an HTML `<input>` with a
|
||||
[template reference variable](guide/template-reference-variables) , `#newItem`,
|
||||
where the user types in an item name. Whatever the user types
|
||||
into the `<input>` gets stored in the `#newItem` variable.
|
||||
into the `<input>` gets stored in the `value` property of the `#newItem` variable.
|
||||
|
||||
<code-example path="inputs-outputs/src/app/item-output/item-output.component.html" region="child-output" header="src/app/item-output/item-output.component.html"></code-example>
|
||||
|
||||
@ -218,7 +218,7 @@ an event binding because the part to the left of the equal
|
||||
sign is in parentheses, `(click)`.
|
||||
|
||||
The `(click)` event is bound to the `addNewItem()` method in the child component class which
|
||||
takes as its argument whatever the value of `#newItem` is.
|
||||
takes as its argument whatever the value of `#newItem.value` property is.
|
||||
|
||||
Now the child component has an `@Output()`
|
||||
for sending data to the parent and a method for raising an event.
|
||||
|
@ -11,11 +11,11 @@ That said, some applications will likely need to apply some manual updates.
|
||||
In version 10, [a few deprecated APIs have been removed](guide/updating-to-version-10#removals) and there are a [few breaking changes](guide/updating-to-version-10#breaking-changes) unrelated to Ivy.
|
||||
If you're seeing errors after updating to version 9, you'll first want to rule those changes out.
|
||||
|
||||
To do so, temporarily [turn off Ivy](guide/ivy#opting-out-of-angular-ivy) in your `tsconfig.base.json` and re-start your app.
|
||||
To do so, temporarily [turn off Ivy](guide/ivy#opting-out-of-angular-ivy) in your `tsconfig.json` and re-start your app.
|
||||
|
||||
If you're still seeing the errors, they are not specific to Ivy. In this case, you may want to consult the [general version 10 guide](guide/updating-to-version-10). If you've opted into any of the new, stricter type-checking settings, you may also want to check out the [template type-checking guide](guide/template-typecheck).
|
||||
|
||||
If the errors are gone, switch back to Ivy by removing the changes to the `tsconfig.base.json` and review the list of expected changes below.
|
||||
If the errors are gone, switch back to Ivy by removing the changes to the `tsconfig.json` and review the list of expected changes below.
|
||||
|
||||
{@a payload-size-debugging}
|
||||
### Payload size debugging
|
||||
|
@ -1,55 +0,0 @@
|
||||
# Solution-style `tsconfig.json` migration
|
||||
|
||||
## What does this migration do?
|
||||
|
||||
This migration adds support to existing projects for TypeScript's new ["solution-style" tsconfig feature](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig).
|
||||
|
||||
Support is added by making two changes:
|
||||
|
||||
1. Renaming the workspace-level `tsconfig.json` to `tsconfig.base.json`.
|
||||
All project [TypeScript configuration files](guide/typescript-configuration) will extend from this base which contains the common options used throughout the workspace.
|
||||
|
||||
2. Adding the solution `tsconfig.json` file at the root of the workspace.
|
||||
This `tsconfig.json` file will only contain references to project-level TypeScript configuration files and is only used by editors/IDEs.
|
||||
|
||||
As an example, the solution `tsconfig.json` for a new project is as follows:
|
||||
```json
|
||||
// This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience.
|
||||
// It is not intended to be used to perform a compilation.
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"path": "./e2e/tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Why is this migration necessary?
|
||||
|
||||
Solution-style `tsconfig.json` files provide an improved editing experience and fix several long-standing defects when editing files in an IDE.
|
||||
IDEs that leverage the TypeScript language service (for example, [Visual Studio Code](https://code.visualstudio.com)), will only use TypeScript configuration files that are named `tsconfig.json`.
|
||||
In complex projects, there may be more than one compilation unit and each of these units may have different settings and options.
|
||||
|
||||
With the Angular CLI, a project will have application code that will target a browser.
|
||||
It will also have unit tests that should not be included within the built application and that also need additional type information present (`jasmine` in this case).
|
||||
Both parts of the project also share some but not all of the code within the project.
|
||||
As a result, two separate TypeScript configuration files (`tsconfig.app.json` and `tsconfig.spec.json`) are needed to ensure that each part of the application is configured properly and that the right types are used for each part.
|
||||
Also if web workers are used within a project, an additional tsconfig (`tsconfig.worker.json`) is needed.
|
||||
Web workers use similar but incompatible types to the main browser application.
|
||||
This requires the additional configuration file to ensure that the web worker files use the appropriate types and will build successfully.
|
||||
|
||||
While the Angular build system knows about all of these TypeScript configuration files, an IDE using TypeScript's language service does not.
|
||||
Because of this, an IDE will not be able to properly analyze the code from each part of the project and may generate false errors or make suggestions that are incorrect for certain files.
|
||||
By leveraging the new solution-style tsconfig, the IDE can now be aware of the configuration of each part of a project.
|
||||
This allows each file to be treated appropriately based on its tsconfig.
|
||||
IDE features such as error/warning reporting and auto-suggestion will operate more effectively as well.
|
||||
|
||||
The TypeScript 3.9 release [blog post](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig) also contains some additional information regarding this new feature.
|
@ -9,7 +9,7 @@ This process helps ensure that intentional changes to the options are kept in pl
|
||||
|
||||
TypeScript Configuration File(s) | Changed Property | Existing Value | New Value
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
`<workspace base>/tsconfig.base.json` | `"module"` | `"esnext"` | `"es2020"`
|
||||
`<workspace base>/tsconfig.json` | `"module"` | `"esnext"` | `"es2020"`
|
||||
Used in `browser` builder options (`ng build` for applications) | `"module"` | `"esnext"` | `"es2020"`
|
||||
Used in `ng-packgr` builder options (`ng build` for libraries) | `"module"` | `"esnext"` | `"es2020"`
|
||||
Used in `karma` builder options (`ng test` for applications) | `"module"` | `"esnext"` | `"es2020"`
|
||||
|
@ -2,10 +2,9 @@
|
||||
|
||||
In a single-page app, you change what the user sees by showing or hiding portions of the display that correspond to particular components, rather than going out to the server to get a new page.
|
||||
As users perform application tasks, they need to move between the different [views](guide/glossary#view "Definition of view") that you have defined.
|
||||
To implement this kind of navigation within the single page of your app, you use the Angular **`Router`**.
|
||||
|
||||
To handle the navigation from one [view](guide/glossary#view) to the next, you use the Angular _router_.
|
||||
The router enables navigation by interpreting a browser URL as an instruction to change the view.
|
||||
To handle the navigation from one [view](guide/glossary#view) to the next, you use the Angular **`Router`**.
|
||||
The **`Router`** enables navigation by interpreting a browser URL as an instruction to change the view.
|
||||
|
||||
To explore a sample app featuring the router's primary features, see the <live-example></live-example>.
|
||||
|
||||
|
@ -37,9 +37,9 @@ by HTML.
|
||||
|
||||
<code-example path="template-reference-variables/src/app/app.component.html" region="ngForm" header="src/app/hero-form.component.html"></code-example>
|
||||
|
||||
The reference value of itemForm, without the ngForm attribute value, would be
|
||||
The reference value of `itemForm`, without the `ngForm` attribute value, would be
|
||||
the [HTMLFormElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement).
|
||||
There is, however, a difference between a Component and a Directive in that a `Component`
|
||||
There is, however, a difference between a `Component` and a `Directive` in that a `Component`
|
||||
will be referenced without specifying the attribute value, and a `Directive` will not
|
||||
change the implicit reference (that is, the element).
|
||||
|
||||
|
@ -114,6 +114,7 @@ In case of a false positive like these, there are a few options:
|
||||
|Strictness flag|Effect|
|
||||
|-|-|
|
||||
|`strictInputTypes`|Whether the assignability of a binding expression to the `@Input()` field is checked. Also affects the inference of directive generic types. |
|
||||
|`strictInputAccessModifiers`|Whether access modifiers such as `private`/`protected`/`readonly` are honored when assigning a binding expression to an `@Input()`. If disabled, the access modifiers of the `@Input` are ignored; only the type is checked.|
|
||||
|`strictNullInputTypes`|Whether `strictNullChecks` is honored when checking `@Input()` bindings (per `strictInputTypes`). Turning this off can be useful when using a library that was not built with `strictNullChecks` in mind.|
|
||||
|`strictAttributeTypes`|Whether to check `@Input()` bindings that are made using text attributes (for example, `<mat-tab label="Step 1">` vs `<mat-tab [label]="'Step 1'">`).
|
||||
|`strictSafeNavigationTypes`|Whether the return type of safe navigation operations (for example, `user?.name`) will be correctly inferred based on the type of `user`). If disabled, `user?.name` will be of type `any`.
|
||||
|
@ -19,7 +19,7 @@ Here's a summary of the stand-alone functions, in order of likely utility:
|
||||
|
||||
<tr>
|
||||
<td style="vertical-align: top">
|
||||
<code>async</code>
|
||||
<code>waitForAsync</code>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
|
@ -18,32 +18,7 @@ that are important to Angular developers, including details about the following
|
||||
## Configuration files
|
||||
|
||||
A given Angular workspace contains several TypeScript configuration files.
|
||||
At the root level, there are two main TypeScript configuration files: a `tsconfig.json` file and a `tsconfig.base.json` file.
|
||||
|
||||
The `tsconfig.json` file is a ["Solution Style"](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#support-for-solution-style-tsconfigjson-files) TypeScript configuration file.
|
||||
Code editors and TypeScript’s language server use this file to improve development experience.
|
||||
Compilers do not use this file.
|
||||
|
||||
The `tsconfig.json` file contains a list of paths to the other TypeScript configuration files used in the workspace.
|
||||
|
||||
<code-example lang="json" header="tsconfig.json" linenums="false">
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"path": "./projects/my-lib/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
</code-example>
|
||||
|
||||
The `tsconfig.base.json` file specifies the base TypeScript and Angular compiler options that all projects in the workspace inherit.
|
||||
At the root `tsconfig.json` file specifies the base TypeScript and Angular compiler options that all projects in the workspace inherit.
|
||||
|
||||
The TypeScript and Angular have a wide range of options which can be used to configure type-checking features and generated output.
|
||||
For more information, see the [Configuration inheritance with extends](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends) section of the TypeScript documentation.
|
||||
@ -55,9 +30,9 @@ For details about configuration inheritance, see the [Configuration inheritance
|
||||
|
||||
</div>
|
||||
|
||||
The initial `tsconfig.base.json` for an Angular workspace typically looks like the following example.
|
||||
The initial `tsconfig.json` for an Angular workspace typically looks like the following example.
|
||||
|
||||
<code-example lang="json" header="tsconfig.base.json" linenums="false">
|
||||
<code-example lang="json" header="tsconfig.json" linenums="false">
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
|
@ -48,8 +48,7 @@ src/
|
||||
app/ ... <i>application code</i>
|
||||
app.server.module.ts <i>* server-side application module</i>
|
||||
server.ts <i>* express web server</i>
|
||||
tsconfig.json <i>TypeScript solution style configuration</i>
|
||||
tsconfig.base.json <i>TypeScript base configuration</i>
|
||||
tsconfig.json <i>TypeScript base configuration</i>
|
||||
tsconfig.app.json <i>TypeScript browser application configuration</i>
|
||||
tsconfig.server.json <i>TypeScript server application configuration</i>
|
||||
tsconfig.spec.json <i>TypeScript tests configuration</i>
|
||||
|
@ -77,6 +77,5 @@ Read about the migrations the CLI handles for you automatically:
|
||||
|
||||
* [Migrating missing `@Directive()`/`@Component()` decorators](guide/migration-undecorated-classes)
|
||||
* [Migrating `ModuleWithProviders`](guide/migration-module-with-providers)
|
||||
* [Solution-style `tsconfig.json` migration](guide/migration-solution-style-tsconfig)
|
||||
* [`tslib` direct dependency migration](guide/migration-update-libraries-tslib)
|
||||
* [Update `module` and `target` compiler options migration](guide/migration-update-module-and-target-compiler-options)
|
||||
|
@ -14,12 +14,16 @@ The CLI does not support running Angular itself in a web worker.
|
||||
|
||||
To add a web worker to an existing project, use the Angular CLI `ng generate` command.
|
||||
|
||||
`ng generate web-worker` *location*
|
||||
```bash
|
||||
ng generate web-worker <location>
|
||||
```
|
||||
|
||||
You can add a web worker anywhere in your application.
|
||||
For example, to add a web worker to the root component, `src/app/app.component.ts`, run the following command.
|
||||
|
||||
`ng generate web-worker app`
|
||||
```bash
|
||||
ng generate web-worker app
|
||||
```
|
||||
|
||||
The command performs the following actions.
|
||||
|
||||
|
@ -86,24 +86,6 @@
|
||||
"groups": ["Angular"],
|
||||
"lead": "kara"
|
||||
},
|
||||
"matsko": {
|
||||
"name": "Matias Niemela",
|
||||
"picture": "matias.jpg",
|
||||
"twitter": "yearofmoo",
|
||||
"website": "http://yearofmoo.com",
|
||||
"bio": "Matias Niemela is a fullstack web developer who has been programming & building websites for over 10 years, and a core team member of AngularJS for two years. In the spring of 2015 Matias joined Angular full time at Google. In his free time Matias loves to build complex things and is always up for public speaking, travelling and tweaking his current Vim setup.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "kara"
|
||||
},
|
||||
"kara": {
|
||||
"name": "Kara Erickson",
|
||||
"picture": "kara-erickson.jpg",
|
||||
"twitter": "karaforthewin",
|
||||
"website": "https://github.com/kara",
|
||||
"bio": "Kara is a software engineer on the Angular team at Google and a co-organizer of the Angular-SF Meetup. Prior to Google, she helped build UI components in Angular for guest management systems at OpenTable. She enjoys snacking indiscriminately and probably other things too.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "igorminar"
|
||||
},
|
||||
"pkozlowski-opensource": {
|
||||
"name": "Pawel Kozlowski",
|
||||
"picture": "pawel.jpg",
|
||||
@ -618,26 +600,6 @@
|
||||
"bio": "Justin (aka Schwarty) is a Google Developer Expert in Web Technologies and Angular, the host and maintainer of the weekly AngularAir live video broadcast, educator, writer and content creator. He has Angular courses available on LinkedIn Learning and Pluralsight and loves passing on years of full stack development knowledge to help empower others to find their inner awesomeness!",
|
||||
"groups": ["GDE"]
|
||||
},
|
||||
"dennispbrown": {
|
||||
"name": "Denny Brown",
|
||||
"picture": "denny.jpg",
|
||||
"bio": "Denny is founder of Expert Support, a professional services firm specializing in technical communication, and leads the Angular technical writing team. His lifelong passion has been to reduce the time and effort required to understand complex technical information. Early on, he was Associate Chairman of the Computer Science Department at Stanford, where he taught introductory courses in programming. He also plays old-timers baseball in local leagues and national tournaments.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "aikidave"
|
||||
},
|
||||
"jbogarthyde": {
|
||||
"name": "Judy Bogart",
|
||||
"picture": "judy.png",
|
||||
"groups": ["Angular"],
|
||||
"lead": "dennispbrown"
|
||||
},
|
||||
"rockument69": {
|
||||
"name": "Tony Bove",
|
||||
"picture": "rockument69.jpg",
|
||||
"bio": "Tony is a technical writer with Expert Support. His lifelong passions are helping people use technology, writing fiction, and playing music. When he's not working or playing the harmonica with friends in a bluegrass band, he's swimming and snorkeling on a Kauai beach and playing ball with his Irish Wolfhound. He's worked at home for decades before it became a thing.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "aikidave"
|
||||
},
|
||||
"kapunahelewong": {
|
||||
"name": "Kapunahele Wong",
|
||||
"picture": "kapunahele.jpg",
|
||||
@ -751,9 +713,9 @@
|
||||
"santosh": {
|
||||
"name": "Santosh Yadav",
|
||||
"picture": "santoshyadav.jpg",
|
||||
"twitter": "Santosh19742211",
|
||||
"twitter": "SantoshYadavDev",
|
||||
"website": "https://www.santoshyadav.dev",
|
||||
"bio": "Santosh is a GDE for Angular and Web Technologies and loves to contribute to Open Source. He is the creator of ng deploy for netlify and core team member for NestJS Addons. He writes for AngularInDepth, mentors for DotNetTricks, organizes Pune Tech Meetup, and conducts free workshops on Angular.",
|
||||
"bio": "Santosh is a GDE for Angular and loves to contribute to Open Source. He is the creator of ng deploy for netlify and core team member for NestJS Addons. He writes for AngularInDepth, mentors for DotNetTricks, organizes Pune Tech Meetup, and conducts free workshops on Angular.",
|
||||
"groups": ["GDE"]
|
||||
},
|
||||
"josephperrott": {
|
||||
|
@ -858,11 +858,6 @@
|
||||
"title": "Missing @Injectable() Decorators",
|
||||
"tooltip": "Migration to add missing @Injectable() decorators and incomplete provider definitions."
|
||||
},
|
||||
{
|
||||
"url": "guide/migration-solution-style-tsconfig",
|
||||
"title": "Solution-style `tsconfig.json`",
|
||||
"tooltip": "Migration to create a solution-style `tsconfig.json`."
|
||||
},
|
||||
{
|
||||
"url": "guide/migration-update-libraries-tslib",
|
||||
"title": "`tslib` direct dependency",
|
||||
|
@ -385,7 +385,6 @@ next section on [Routing](tutorial/toh-pt5).
|
||||
path="toh-pt4/src/app/heroes/heroes.component.ts">
|
||||
</code-example>
|
||||
|
||||
The browser refreshes and the page displays the list of heroes.
|
||||
Refresh the browser to see the list of heroes, and scroll to the bottom to see the
|
||||
messages from the HeroService. Each time you click a hero, a new message appears to record
|
||||
the selection. Use the "clear" button to clear the message history.
|
||||
|
@ -23,7 +23,7 @@
|
||||
"build-local-with-viewengine": "yarn ~~build",
|
||||
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
|
||||
"build-local-with-viewengine-ci": "yarn ~~build --progress=false",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js b0b27361d",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js ef770f1cb",
|
||||
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
|
||||
"test": "yarn check-env && ng test",
|
||||
"pree2e": "yarn check-env && yarn update-webdriver",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
|
||||
@ -12,20 +12,22 @@ import { LocationService } from 'app/shared/location.service';
|
||||
describe('ContributorListComponent', () => {
|
||||
|
||||
let component: ContributorListComponent;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let contributorService: TestContributorService;
|
||||
let locationService: TestLocationService;
|
||||
let contributorGroups: ContributorGroup[];
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
ContributorListComponent,
|
||||
{provide: ContributorService, useClass: TestContributorService },
|
||||
{provide: LocationService, useClass: TestLocationService }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{provide: ContributorListComponent, deps: [ContributorService, LocationService] },
|
||||
{provide: ContributorService, useClass: TestContributorService, deps: [] },
|
||||
{provide: LocationService, useClass: TestLocationService, deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
locationService = injector.get(LocationService);
|
||||
contributorService = injector.get(ContributorService);
|
||||
locationService = injector.get(LocationService) as unknown as TestLocationService;
|
||||
contributorService = injector.get(ContributorService) as unknown as TestContributorService;
|
||||
contributorGroups = contributorService.testContributors;
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
|
||||
@ -12,20 +12,22 @@ import { Category } from './resource.model';
|
||||
describe('ResourceListComponent', () => {
|
||||
|
||||
let component: ResourceListComponent;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let resourceService: TestResourceService;
|
||||
let locationService: TestLocationService;
|
||||
let categories: Category[];
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
ResourceListComponent,
|
||||
{provide: ResourceService, useClass: TestResourceService },
|
||||
{provide: LocationService, useClass: TestLocationService }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{provide: ResourceListComponent, deps: [ResourceService, LocationService] },
|
||||
{provide: ResourceService, useClass: TestResourceService, deps: [] },
|
||||
{provide: LocationService, useClass: TestLocationService, deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
locationService = injector.get(LocationService);
|
||||
resourceService = injector.get(ResourceService);
|
||||
locationService = injector.get(LocationService) as unknown as TestLocationService;
|
||||
resourceService = injector.get(ResourceService) as unknown as TestResourceService;
|
||||
categories = resourceService.testCategories;
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector, NgZone } from '@angular/core';
|
||||
import { Injector, NgZone } from '@angular/core';
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { SearchService } from './search.service';
|
||||
@ -6,7 +6,7 @@ import { WebWorkerClient } from 'app/shared/web-worker';
|
||||
|
||||
describe('SearchService', () => {
|
||||
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let service: SearchService;
|
||||
let sendMessageSpy: jasmine.Spy;
|
||||
let mockWorker: WebWorkerClient;
|
||||
@ -16,10 +16,13 @@ describe('SearchService', () => {
|
||||
mockWorker = { sendMessage: sendMessageSpy } as any;
|
||||
spyOn(WebWorkerClient, 'create').and.returnValue(mockWorker);
|
||||
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
SearchService,
|
||||
{ provide: NgZone, useFactory: () => new NgZone({ enableLongStackTrace: false }) }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: SearchService, deps: [NgZone]},
|
||||
{ provide: NgZone, useFactory: () => new NgZone({ enableLongStackTrace: false }), deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
service = injector.get(SearchService);
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { environment } from 'environments/environment';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { MockLocationService } from 'testing/location.service';
|
||||
@ -15,7 +15,7 @@ describe('Deployment service', () => {
|
||||
it('should get the mode from the `mode` query parameter if available', () => {
|
||||
const injector = getInjector();
|
||||
|
||||
const locationService: MockLocationService = injector.get(LocationService);
|
||||
const locationService = injector.get(LocationService) as unknown as MockLocationService;
|
||||
locationService.search.and.returnValue({ mode: 'bar' });
|
||||
|
||||
const deployment = injector.get(Deployment);
|
||||
@ -25,8 +25,8 @@ describe('Deployment service', () => {
|
||||
});
|
||||
|
||||
function getInjector() {
|
||||
return ReflectiveInjector.resolveAndCreate([
|
||||
Deployment,
|
||||
{ provide: LocationService, useFactory: () => new MockLocationService('') }
|
||||
]);
|
||||
return Injector.create({providers: [
|
||||
{ provide: Deployment, deps: [LocationService] },
|
||||
{ provide: LocationService, useFactory: () => new MockLocationService(''), deps: [] }
|
||||
]});
|
||||
}
|
||||
|
@ -1,18 +1,23 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
|
||||
import { GaService } from 'app/shared/ga.service';
|
||||
import { WindowToken } from 'app/shared/window';
|
||||
|
||||
describe('GaService', () => {
|
||||
let gaService: GaService;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let gaSpy: jasmine.Spy;
|
||||
let mockWindow: any;
|
||||
|
||||
beforeEach(() => {
|
||||
gaSpy = jasmine.createSpy('ga');
|
||||
mockWindow = { ga: gaSpy };
|
||||
injector = ReflectiveInjector.resolveAndCreate([GaService, { provide: WindowToken, useFactory: () => mockWindow }]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: GaService, deps: [WindowToken] },
|
||||
{ provide: WindowToken, useFactory: () => mockWindow, deps: [] }
|
||||
]});
|
||||
|
||||
gaService = injector.get(GaService);
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { Location, LocationStrategy, PlatformLocation } from '@angular/common';
|
||||
import { MockLocationStrategy } from '@angular/common/testing';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -9,26 +9,28 @@ import { LocationService } from './location.service';
|
||||
import { ScrollService } from './scroll.service';
|
||||
|
||||
describe('LocationService', () => {
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let location: MockLocationStrategy;
|
||||
let service: LocationService;
|
||||
let swUpdates: MockSwUpdatesService;
|
||||
let scrollService: MockScrollService;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
LocationService,
|
||||
Location,
|
||||
{ provide: GaService, useClass: TestGaService },
|
||||
{ provide: LocationStrategy, useClass: MockLocationStrategy },
|
||||
{ provide: PlatformLocation, useClass: MockPlatformLocation },
|
||||
{ provide: SwUpdatesService, useClass: MockSwUpdatesService },
|
||||
{ provide: ScrollService, useClass: MockScrollService }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: LocationService, deps: [GaService, Location, ScrollService, PlatformLocation, SwUpdatesService] },
|
||||
{ provide: Location, deps: [LocationStrategy, PlatformLocation] },
|
||||
{ provide: GaService, useClass: TestGaService, deps: [] },
|
||||
{ provide: LocationStrategy, useClass: MockLocationStrategy, deps: [] },
|
||||
{ provide: PlatformLocation, useClass: MockPlatformLocation, deps: [] },
|
||||
{ provide: SwUpdatesService, useClass: MockSwUpdatesService, deps: [] },
|
||||
{ provide: ScrollService, useClass: MockScrollService, deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
location = injector.get(LocationStrategy);
|
||||
service = injector.get(LocationService);
|
||||
swUpdates = injector.get(SwUpdatesService);
|
||||
location = injector.get(LocationStrategy) as unknown as MockLocationStrategy;
|
||||
service = injector.get(LocationService);
|
||||
swUpdates = injector.get(SwUpdatesService) as unknown as MockSwUpdatesService;
|
||||
scrollService = injector.get(ScrollService);
|
||||
});
|
||||
|
||||
@ -380,7 +382,7 @@ describe('LocationService', () => {
|
||||
let platformLocation: MockPlatformLocation;
|
||||
|
||||
beforeEach(() => {
|
||||
platformLocation = injector.get(PlatformLocation);
|
||||
platformLocation = injector.get(PlatformLocation) as unknown as MockPlatformLocation;
|
||||
});
|
||||
|
||||
it('should call replaceState on PlatformLocation', () => {
|
||||
@ -577,7 +579,7 @@ describe('LocationService', () => {
|
||||
let gaLocationChanged: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
const gaService = injector.get(GaService);
|
||||
const gaService = injector.get(GaService) as unknown as TestGaService;
|
||||
gaLocationChanged = gaService.locationChanged;
|
||||
// execute currentPath observable so that gaLocationChanged is called
|
||||
service.currentPath.subscribe();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ErrorHandler, ReflectiveInjector } from '@angular/core';
|
||||
import { ErrorHandler, Injector } from '@angular/core';
|
||||
import { Logger } from './logger.service';
|
||||
|
||||
describe('logger service', () => {
|
||||
@ -10,10 +10,10 @@ describe('logger service', () => {
|
||||
beforeEach(() => {
|
||||
logSpy = spyOn(console, 'log');
|
||||
warnSpy = spyOn(console, 'warn');
|
||||
const injector = ReflectiveInjector.resolveAndCreate([
|
||||
Logger,
|
||||
{ provide: ErrorHandler, useClass: MockErrorHandler }
|
||||
]);
|
||||
const injector = Injector.create({providers: [
|
||||
{ provide: Logger, deps: [ErrorHandler] },
|
||||
{ provide: ErrorHandler, useClass: MockErrorHandler, deps: [] }
|
||||
]});
|
||||
logger = injector.get(Logger);
|
||||
errorHandler = injector.get(ErrorHandler);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ErrorHandler, ReflectiveInjector } from '@angular/core';
|
||||
import { ErrorHandler, Injector } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { WindowToken } from 'app/shared/window';
|
||||
import { AppModule } from 'app/app.module';
|
||||
@ -14,11 +14,12 @@ describe('ReportingErrorHandler service', () => {
|
||||
onerrorSpy = jasmine.createSpy('onerror');
|
||||
superHandler = spyOn(ErrorHandler.prototype, 'handleError');
|
||||
|
||||
const injector = ReflectiveInjector.resolveAndCreate([
|
||||
{ provide: ErrorHandler, useClass: ReportingErrorHandler },
|
||||
{ provide: WindowToken, useFactory: () => ({ onerror: onerrorSpy }) }
|
||||
]);
|
||||
handler = injector.get(ErrorHandler);
|
||||
const injector = Injector.create({providers: [
|
||||
{ provide: ErrorHandler, useClass: ReportingErrorHandler, deps: [WindowToken] },
|
||||
{ provide: WindowToken, useFactory: () => ({ onerror: onerrorSpy }), deps: [] }
|
||||
]});
|
||||
|
||||
handler = injector.get(ErrorHandler) as unknown as ReportingErrorHandler;
|
||||
});
|
||||
|
||||
it('should be registered on the AppModule', () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Injector, ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
|
||||
@ -151,11 +151,11 @@ describe('ScrollSpyService', () => {
|
||||
let scrollSpyService: ScrollSpyService;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
injector = Injector.create({providers: [
|
||||
{ provide: DOCUMENT, useValue: { body: {} } },
|
||||
{ provide: ScrollService, useValue: { topOffset: 50 } },
|
||||
ScrollSpyService
|
||||
]);
|
||||
{ provide: ScrollSpyService, deps: [DOCUMENT, ScrollService] }
|
||||
]});
|
||||
|
||||
scrollSpyService = injector.get(ScrollSpyService);
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Location, LocationStrategy, PlatformLocation, ViewportScroller} from '@angular/common';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {MockLocationStrategy, SpyLocation} from '@angular/common/testing';
|
||||
import {ReflectiveInjector} from '@angular/core';
|
||||
import {Injector} from '@angular/core';
|
||||
import {fakeAsync, tick} from '@angular/core/testing';
|
||||
|
||||
import {ScrollService, topMargin} from './scroll.service';
|
||||
@ -15,7 +15,7 @@ describe('ScrollService', () => {
|
||||
};
|
||||
|
||||
const topOfPageElem = {} as Element;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let document: MockDocument;
|
||||
let platformLocation: MockPlatformLocation;
|
||||
let scrollService: ScrollService;
|
||||
@ -41,21 +41,25 @@ describe('ScrollService', () => {
|
||||
jasmine.createSpyObj('viewportScroller', ['getScrollPosition', 'scrollToPosition']);
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{
|
||||
provide: ScrollService,
|
||||
useFactory: createScrollService,
|
||||
deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
|
||||
},
|
||||
{provide: Location, useClass: SpyLocation}, {provide: DOCUMENT, useClass: MockDocument},
|
||||
{provide: PlatformLocation, useClass: MockPlatformLocation},
|
||||
{provide: ViewportScroller, useValue: viewportScrollerStub},
|
||||
{provide: LocationStrategy, useClass: MockLocationStrategy}
|
||||
]);
|
||||
injector = Injector.create( {
|
||||
providers: [
|
||||
{
|
||||
provide: ScrollService,
|
||||
useFactory: createScrollService,
|
||||
deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
|
||||
},
|
||||
{provide: Location, useClass: SpyLocation, deps: [] },
|
||||
{provide: DOCUMENT, useClass: MockDocument, deps: []},
|
||||
{provide: PlatformLocation, useClass: MockPlatformLocation, deps: []},
|
||||
{provide: ViewportScroller, useValue: viewportScrollerStub},
|
||||
{provide: LocationStrategy, useClass: MockLocationStrategy, deps: []}
|
||||
]
|
||||
});
|
||||
|
||||
platformLocation = injector.get(PlatformLocation);
|
||||
document = injector.get(DOCUMENT);
|
||||
document = injector.get(DOCUMENT) as unknown as MockDocument;
|
||||
scrollService = injector.get(ScrollService);
|
||||
location = injector.get(Location);
|
||||
location = injector.get(Location) as unknown as SpyLocation;
|
||||
|
||||
spyOn(window, 'scrollBy');
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@ -7,7 +7,7 @@ import { ScrollItem, ScrollSpyInfo, ScrollSpyService } from 'app/shared/scroll-s
|
||||
import { TocItem, TocService } from './toc.service';
|
||||
|
||||
describe('TocService', () => {
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let scrollSpyService: MockScrollSpyService;
|
||||
let tocService: TocService;
|
||||
let lastTocList: TocItem[];
|
||||
@ -21,13 +21,14 @@ describe('TocService', () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{ provide: DomSanitizer, useClass: TestDomSanitizer },
|
||||
injector = Injector.create({providers: [
|
||||
{ provide: DomSanitizer, useClass: TestDomSanitizer, deps: [] },
|
||||
{ provide: DOCUMENT, useValue: document },
|
||||
{ provide: ScrollSpyService, useClass: MockScrollSpyService },
|
||||
TocService,
|
||||
]);
|
||||
scrollSpyService = injector.get(ScrollSpyService);
|
||||
{ provide: ScrollSpyService, useClass: MockScrollSpyService, deps: [] },
|
||||
{ provide: TocService, deps: [DOCUMENT, DomSanitizer, ScrollSpyService] },
|
||||
]});
|
||||
|
||||
scrollSpyService = injector.get(ScrollSpyService) as unknown as MockScrollSpyService;
|
||||
tocService = injector.get(TocService);
|
||||
tocService.tocList.subscribe(tocList => lastTocList = tocList);
|
||||
});
|
||||
@ -330,7 +331,7 @@ describe('TocService', () => {
|
||||
});
|
||||
|
||||
it('should have bypassed HTML sanitizing of heading\'s innerHTML ', () => {
|
||||
const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer);
|
||||
const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer) as unknown as TestDomSanitizer;
|
||||
expect(domSanitizer.bypassSecurityTrustHtml)
|
||||
.toHaveBeenCalledWith('Setup to develop <i>locally</i>.');
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ApplicationRef, ReflectiveInjector } from '@angular/core';
|
||||
import { ApplicationRef, Injector } from '@angular/core';
|
||||
import { discardPeriodicTasks, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -8,7 +8,7 @@ import { SwUpdatesService } from './sw-updates.service';
|
||||
|
||||
|
||||
describe('SwUpdatesService', () => {
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let appRef: MockApplicationRef;
|
||||
let service: SwUpdatesService;
|
||||
let swu: MockSwUpdate;
|
||||
@ -21,16 +21,16 @@ describe('SwUpdatesService', () => {
|
||||
// run `setup()`/`tearDown()` in `beforeEach()`/`afterEach()` blocks. We use the `run()` helper
|
||||
// to call them inside each test's zone.
|
||||
const setup = (isSwUpdateEnabled: boolean) => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{ provide: ApplicationRef, useClass: MockApplicationRef },
|
||||
{ provide: Logger, useClass: MockLogger },
|
||||
{ provide: SwUpdate, useFactory: () => new MockSwUpdate(isSwUpdateEnabled) },
|
||||
SwUpdatesService
|
||||
]);
|
||||
injector = Injector.create({providers: [
|
||||
{ provide: ApplicationRef, useClass: MockApplicationRef, deps: [] },
|
||||
{ provide: Logger, useClass: MockLogger, deps: [] },
|
||||
{ provide: SwUpdate, useFactory: () => new MockSwUpdate(isSwUpdateEnabled), deps: [] },
|
||||
{ provide: SwUpdatesService, deps: [ApplicationRef, Logger, SwUpdate] }
|
||||
]});
|
||||
|
||||
appRef = injector.get(ApplicationRef);
|
||||
appRef = injector.get(ApplicationRef) as unknown as MockApplicationRef;
|
||||
service = injector.get(SwUpdatesService);
|
||||
swu = injector.get(SwUpdate);
|
||||
swu = injector.get(SwUpdate) as unknown as MockSwUpdate;
|
||||
checkInterval = (service as any).checkInterval;
|
||||
};
|
||||
const tearDown = () => service.ngOnDestroy();
|
||||
|
@ -214,7 +214,7 @@ code {
|
||||
margin-left: 2px;
|
||||
position: relative;
|
||||
@include line-height(24);
|
||||
vertical-align: bottom;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,8 +12,10 @@
|
||||
// If a category becomes empty (e.g. BS and required), then the corresponding job must be commented
|
||||
// out in the CI configuration.
|
||||
var CIconfiguration = {
|
||||
'Chrome': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Firefox': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
// Chrome and Firefox run as part of the Bazel browser tests, so we do not run them as
|
||||
// part of the legacy Saucelabs tests.
|
||||
'Chrome': {unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
|
||||
'Firefox': {unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
|
||||
// Set ESR as a not required browser as it fails for Ivy acceptance tests.
|
||||
'FirefoxESR': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
// Disabled because using the "beta" channel of Chrome can cause non-deterministic CI results.
|
||||
|
@ -4,28 +4,39 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
ts_library(
|
||||
name = "commit-message",
|
||||
srcs = [
|
||||
"builder.ts",
|
||||
"cli.ts",
|
||||
"commit-message-draft.ts",
|
||||
"config.ts",
|
||||
"parse.ts",
|
||||
"restore-commit-message.ts",
|
||||
"validate.ts",
|
||||
"validate-file.ts",
|
||||
"validate-range.ts",
|
||||
"wizard.ts",
|
||||
],
|
||||
module_name = "@angular/dev-infra-private/commit-message",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/shelljs",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//inquirer",
|
||||
"@npm//shelljs",
|
||||
"@npm//yargs",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "validate-test",
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = ["validate.spec.ts"],
|
||||
srcs = [
|
||||
"builder.spec.ts",
|
||||
"parse.spec.ts",
|
||||
"validate.spec.ts",
|
||||
],
|
||||
deps = [
|
||||
":commit-message",
|
||||
"//dev-infra/utils",
|
||||
@ -40,7 +51,6 @@ jasmine_node_test(
|
||||
name = "test",
|
||||
bootstrap = ["//tools/testing:node_no_angular_es5"],
|
||||
deps = [
|
||||
":commit-message",
|
||||
":validate-test",
|
||||
"test_lib",
|
||||
],
|
||||
)
|
||||
|
46
dev-infra/commit-message/builder.spec.ts
Normal file
46
dev-infra/commit-message/builder.spec.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @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 config from '../utils/config';
|
||||
import * as console from '../utils/console';
|
||||
|
||||
import {buildCommitMessage} from './builder';
|
||||
|
||||
|
||||
describe('commit message building:', () => {
|
||||
beforeEach(() => {
|
||||
// stub logging calls to prevent noise in test log
|
||||
spyOn(console, 'info').and.stub();
|
||||
// provide a configuration for DevInfra when loaded
|
||||
spyOn(config, 'getConfig').and.returnValue({
|
||||
commitMessage: {
|
||||
scopes: ['core'],
|
||||
}
|
||||
} as any);
|
||||
});
|
||||
|
||||
it('creates a commit message with a scope', async () => {
|
||||
buildPromptResponseSpies('fix', 'core', 'This is a summary');
|
||||
|
||||
expect(await buildCommitMessage()).toMatch(/^fix\(core\): This is a summary/);
|
||||
});
|
||||
|
||||
it('creates a commit message without a scope', async () => {
|
||||
buildPromptResponseSpies('build', false, 'This is a summary');
|
||||
|
||||
expect(await buildCommitMessage()).toMatch(/^build: This is a summary/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/** Create spies to return the mocked selections from prompts. */
|
||||
function buildPromptResponseSpies(type: string, scope: string|false, summary: string) {
|
||||
spyOn(console, 'promptAutocomplete')
|
||||
.and.returnValues(Promise.resolve(type), Promise.resolve(scope));
|
||||
spyOn(console, 'promptInput').and.returnValue(Promise.resolve(summary));
|
||||
}
|
70
dev-infra/commit-message/builder.ts
Normal file
70
dev-infra/commit-message/builder.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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 {ListChoiceOptions} from 'inquirer';
|
||||
|
||||
import {info, promptAutocomplete, promptInput} from '../utils/console';
|
||||
|
||||
import {COMMIT_TYPES, CommitType, getCommitMessageConfig, ScopeRequirement} from './config';
|
||||
|
||||
/** Validate commit message at the provided file path. */
|
||||
export async function buildCommitMessage() {
|
||||
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
|
||||
// TODO(josephperrott): Add default commit message information/commenting into generated messages
|
||||
info('Just a few questions to start building the commit message!');
|
||||
|
||||
/** The commit message type. */
|
||||
const type = await promptForCommitMessageType();
|
||||
/** The commit message scope. */
|
||||
const scope = await promptForCommitMessageScopeForType(type);
|
||||
/** The commit message summary. */
|
||||
const summary = await promptForCommitMessageSummary();
|
||||
|
||||
return `${type.name}${scope ? '(' + scope + ')' : ''}: ${summary}\n\n`;
|
||||
}
|
||||
|
||||
/** Prompts in the terminal for the commit message's type. */
|
||||
async function promptForCommitMessageType(): Promise<CommitType> {
|
||||
info('The type of change in the commit. Allows a reader to know the effect of the change,');
|
||||
info('whether it brings a new feature, adds additional testing, documents the `project, etc.');
|
||||
|
||||
/** List of commit type options for the autocomplete prompt. */
|
||||
const typeOptions: ListChoiceOptions[] =
|
||||
Object.values(COMMIT_TYPES).map(({description, name}) => {
|
||||
return {
|
||||
name: `${name} - ${description}`,
|
||||
value: name,
|
||||
short: name,
|
||||
};
|
||||
});
|
||||
/** The key of a commit message type, selected by the user via prompt. */
|
||||
const typeName = await promptAutocomplete('Select a type for the commit:', typeOptions);
|
||||
|
||||
return COMMIT_TYPES[typeName];
|
||||
}
|
||||
|
||||
/** Prompts in the terminal for the commit message's scope. */
|
||||
async function promptForCommitMessageScopeForType(type: CommitType): Promise<string|false> {
|
||||
// If the commit type's scope requirement is forbidden, return early.
|
||||
if (type.scope === ScopeRequirement.Forbidden) {
|
||||
info(`Skipping scope selection as the '${type.name}' type does not allow scopes`);
|
||||
return false;
|
||||
}
|
||||
/** Commit message configuration */
|
||||
const config = getCommitMessageConfig();
|
||||
|
||||
info('The area of the repository the changes in this commit most affects.');
|
||||
return await promptAutocomplete(
|
||||
'Select a scope for the commit:', config.commitMessage.scopes,
|
||||
type.scope === ScopeRequirement.Optional ? '<no scope>' : '');
|
||||
}
|
||||
|
||||
/** Prompts in the terminal for the commit message's summary. */
|
||||
async function promptForCommitMessageSummary(): Promise<string> {
|
||||
info('Provide a short summary of what the changes in the commit do');
|
||||
return await promptInput('Provide a short summary of the commit');
|
||||
}
|
@ -9,13 +9,56 @@ import * as yargs from 'yargs';
|
||||
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {restoreCommitMessage} from './restore-commit-message';
|
||||
import {validateFile} from './validate-file';
|
||||
import {validateCommitRange} from './validate-range';
|
||||
import {runWizard} from './wizard';
|
||||
|
||||
/** Build the parser for the commit-message commands. */
|
||||
export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||
return localYargs.help()
|
||||
.strict()
|
||||
.command(
|
||||
'restore-commit-message-draft', false,
|
||||
args => {
|
||||
return args.option('file-env-variable', {
|
||||
type: 'string',
|
||||
array: true,
|
||||
conflicts: ['file'],
|
||||
required: true,
|
||||
description:
|
||||
'The key for the environment variable which holds the arguments for the\n' +
|
||||
'prepare-commit-msg hook as described here:\n' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
|
||||
coerce: arg => {
|
||||
const [file, source] = (process.env[arg] || '').split(' ');
|
||||
if (!file) {
|
||||
throw new Error(`Provided environment variable "${arg}" was not found.`);
|
||||
}
|
||||
return [file, source];
|
||||
},
|
||||
});
|
||||
},
|
||||
args => {
|
||||
restoreCommitMessage(args['file-env-variable'][0], args['file-env-variable'][1] as any);
|
||||
})
|
||||
.command(
|
||||
'wizard <filePath> [source] [commitSha]', '', ((args: any) => {
|
||||
return args
|
||||
.positional(
|
||||
'filePath',
|
||||
{description: 'The file path to write the generated commit message into'})
|
||||
.positional('source', {
|
||||
choices: ['message', 'template', 'merge', 'squash', 'commit'],
|
||||
description: 'The source of the commit message as described here: ' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
|
||||
})
|
||||
.positional(
|
||||
'commitSha', {description: 'The commit sha if source is set to `commit`'});
|
||||
}),
|
||||
async (args: any) => {
|
||||
await runWizard(args);
|
||||
})
|
||||
.command(
|
||||
'pre-commit-validate', 'Validate the most recent commit message', {
|
||||
'file': {
|
||||
@ -38,7 +81,7 @@ export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||
}
|
||||
},
|
||||
args => {
|
||||
const file = args.file || args.fileEnvVariable || '.git/COMMIT_EDITMSG';
|
||||
const file = args.file || args['file-env-variable'] || '.git/COMMIT_EDITMSG';
|
||||
validateFile(file);
|
||||
})
|
||||
.command(
|
||||
|
30
dev-infra/commit-message/commit-message-draft.ts
Normal file
30
dev-infra/commit-message/commit-message-draft.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {existsSync, readFileSync, unlinkSync, writeFileSync} from 'fs';
|
||||
|
||||
/** Load the commit message draft from the file system if it exists. */
|
||||
export function loadCommitMessageDraft(basePath: string) {
|
||||
const commitMessageDraftPath = `${basePath}.ngDevSave`;
|
||||
if (existsSync(commitMessageDraftPath)) {
|
||||
return readFileSync(commitMessageDraftPath).toString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Remove the commit message draft from the file system. */
|
||||
export function deleteCommitMessageDraft(basePath: string) {
|
||||
const commitMessageDraftPath = `${basePath}.ngDevSave`;
|
||||
if (existsSync(commitMessageDraftPath)) {
|
||||
unlinkSync(commitMessageDraftPath);
|
||||
}
|
||||
}
|
||||
|
||||
/** Save the commit message draft to the file system for later retrieval. */
|
||||
export function saveCommitMessageDraft(basePath: string, commitMessage: string) {
|
||||
writeFileSync(`${basePath}.ngDevSave`, commitMessage);
|
||||
}
|
@ -12,7 +12,6 @@ export interface CommitMessageConfig {
|
||||
maxLineLength: number;
|
||||
minBodyLength: number;
|
||||
minBodyLengthTypeExcludes?: string[];
|
||||
types: string[];
|
||||
scopes: string[];
|
||||
}
|
||||
|
||||
@ -30,3 +29,66 @@ export function getCommitMessageConfig() {
|
||||
assertNoErrors(errors);
|
||||
return config as Required<typeof config>;
|
||||
}
|
||||
|
||||
/** Scope requirement level to be set for each commit type. */
|
||||
export enum ScopeRequirement {
|
||||
Required,
|
||||
Optional,
|
||||
Forbidden,
|
||||
}
|
||||
|
||||
/** A commit type */
|
||||
export interface CommitType {
|
||||
description: string;
|
||||
name: string;
|
||||
scope: ScopeRequirement;
|
||||
}
|
||||
|
||||
/** The valid commit types for Angular commit messages. */
|
||||
export const COMMIT_TYPES: {[key: string]: CommitType} = {
|
||||
build: {
|
||||
name: 'build',
|
||||
description: 'Changes to local repository build system and tooling',
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
ci: {
|
||||
name: 'ci',
|
||||
description: 'Changes to CI configuration and CI specific tooling',
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
docs: {
|
||||
name: 'docs',
|
||||
description: 'Changes which exclusively affects documentation.',
|
||||
scope: ScopeRequirement.Optional,
|
||||
},
|
||||
feat: {
|
||||
name: 'feat',
|
||||
description: 'Creates a new feature',
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
fix: {
|
||||
name: 'fix',
|
||||
description: 'Fixes a previously discovered failure/bug',
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
perf: {
|
||||
name: 'perf',
|
||||
description: 'Improves performance without any change in functionality or API',
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
refactor: {
|
||||
name: 'refactor',
|
||||
description: 'Refactor without any change in functionality or API (includes style changes)',
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
release: {
|
||||
name: 'release',
|
||||
description: 'A release point in the repository',
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
test: {
|
||||
name: 'test',
|
||||
description: 'Improvements or corrections made to the project\'s test suite',
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
};
|
||||
|
105
dev-infra/commit-message/parse.spec.ts
Normal file
105
dev-infra/commit-message/parse.spec.ts
Normal file
@ -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 {parseCommitMessage, ParsedCommitMessage} from './parse';
|
||||
|
||||
|
||||
const commitValues = {
|
||||
prefix: '',
|
||||
type: 'fix',
|
||||
scope: 'changed-area',
|
||||
summary: 'This is a short summary of the change',
|
||||
body: 'This is a longer description of the change Closes #1',
|
||||
};
|
||||
|
||||
function buildCommitMessage(params = {}) {
|
||||
const {prefix, type, scope, summary, body} = {...commitValues, ...params};
|
||||
return `${prefix}${type}${scope ? '(' + scope + ')' : ''}: ${summary}\n\n${body}`;
|
||||
}
|
||||
|
||||
|
||||
describe('commit message parsing:', () => {
|
||||
it('parses the scope', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).scope).toBe(commitValues.scope);
|
||||
});
|
||||
|
||||
it('parses the type', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).type).toBe(commitValues.type);
|
||||
});
|
||||
|
||||
it('parses the header', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).header)
|
||||
.toBe(`${commitValues.type}(${commitValues.scope}): ${commitValues.summary}`);
|
||||
});
|
||||
|
||||
it('parses the body', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).body).toBe(commitValues.body);
|
||||
});
|
||||
|
||||
it('parses the body without Github linking', () => {
|
||||
const body = 'This has linking\nCloses #1';
|
||||
const message = buildCommitMessage({body});
|
||||
expect(parseCommitMessage(message).bodyWithoutLinking).toBe('This has linking\n');
|
||||
});
|
||||
|
||||
it('parses the subject', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).subject).toBe(commitValues.summary);
|
||||
});
|
||||
|
||||
it('identifies if a commit is a fixup', () => {
|
||||
const message1 = buildCommitMessage();
|
||||
expect(parseCommitMessage(message1).isFixup).toBe(false);
|
||||
|
||||
const message2 = buildCommitMessage({prefix: 'fixup! '});
|
||||
expect(parseCommitMessage(message2).isFixup).toBe(true);
|
||||
});
|
||||
|
||||
it('identifies if a commit is a revert', () => {
|
||||
const message1 = buildCommitMessage();
|
||||
expect(parseCommitMessage(message1).isRevert).toBe(false);
|
||||
|
||||
const message2 = buildCommitMessage({prefix: 'revert: '});
|
||||
expect(parseCommitMessage(message2).isRevert).toBe(true);
|
||||
|
||||
const message3 = buildCommitMessage({prefix: 'revert '});
|
||||
expect(parseCommitMessage(message3).isRevert).toBe(true);
|
||||
});
|
||||
|
||||
it('identifies if a commit is a squash', () => {
|
||||
const message1 = buildCommitMessage();
|
||||
expect(parseCommitMessage(message1).isSquash).toBe(false);
|
||||
|
||||
const message2 = buildCommitMessage({prefix: 'squash! '});
|
||||
expect(parseCommitMessage(message2).isSquash).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores comment lines', () => {
|
||||
const message = buildCommitMessage({
|
||||
prefix: '# This is a comment line before the header.\n' +
|
||||
'## This is another comment line before the headers.\n',
|
||||
body: '# This is a comment line befor the body.\n' +
|
||||
'This is line 1 of the actual body.\n' +
|
||||
'## This is another comment line inside the body.\n' +
|
||||
'This is line 2 of the actual body (and it also contains a # but it not a comment).\n' +
|
||||
'### This is yet another comment line after the body.\n',
|
||||
});
|
||||
const parsedMessage = parseCommitMessage(message);
|
||||
|
||||
expect(parsedMessage.header)
|
||||
.toBe(`${commitValues.type}(${commitValues.scope}): ${commitValues.summary}`);
|
||||
expect(parsedMessage.body)
|
||||
.toBe(
|
||||
'This is line 1 of the actual body.\n' +
|
||||
'This is line 2 of the actual body (and it also contains a # but it not a comment).\n');
|
||||
});
|
||||
});
|
77
dev-infra/commit-message/parse.ts
Normal file
77
dev-infra/commit-message/parse.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/** A parsed commit message. */
|
||||
export interface ParsedCommitMessage {
|
||||
header: string;
|
||||
body: string;
|
||||
bodyWithoutLinking: string;
|
||||
type: string;
|
||||
scope: string;
|
||||
subject: string;
|
||||
isFixup: boolean;
|
||||
isSquash: boolean;
|
||||
isRevert: boolean;
|
||||
}
|
||||
|
||||
/** Regex determining if a commit is a fixup. */
|
||||
const FIXUP_PREFIX_RE = /^fixup! /i;
|
||||
/** Regex finding all github keyword links. */
|
||||
const GITHUB_LINKING_RE = /((closed?s?)|(fix(es)?(ed)?)|(resolved?s?))\s\#(\d+)/ig;
|
||||
/** Regex determining if a commit is a squash. */
|
||||
const SQUASH_PREFIX_RE = /^squash! /i;
|
||||
/** Regex determining if a commit is a revert. */
|
||||
const REVERT_PREFIX_RE = /^revert:? /i;
|
||||
/** Regex determining the scope of a commit if provided. */
|
||||
const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/;
|
||||
/** Regex determining the entire header line of the commit. */
|
||||
const COMMIT_HEADER_RE = /^(.*)/i;
|
||||
/** Regex determining the body of the commit. */
|
||||
const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/;
|
||||
|
||||
/** Parse a full commit message into its composite parts. */
|
||||
export function parseCommitMessage(commitMsg: string): ParsedCommitMessage {
|
||||
// Ignore comments (i.e. lines starting with `#`). Comments are automatically removed by git and
|
||||
// should not be considered part of the final commit message.
|
||||
commitMsg = commitMsg.split('\n').filter(line => !line.startsWith('#')).join('\n');
|
||||
|
||||
let header = '';
|
||||
let body = '';
|
||||
let bodyWithoutLinking = '';
|
||||
let type = '';
|
||||
let scope = '';
|
||||
let subject = '';
|
||||
|
||||
if (COMMIT_HEADER_RE.test(commitMsg)) {
|
||||
header = COMMIT_HEADER_RE.exec(commitMsg)![1]
|
||||
.replace(FIXUP_PREFIX_RE, '')
|
||||
.replace(SQUASH_PREFIX_RE, '');
|
||||
}
|
||||
if (COMMIT_BODY_RE.test(commitMsg)) {
|
||||
body = COMMIT_BODY_RE.exec(commitMsg)![1];
|
||||
bodyWithoutLinking = body.replace(GITHUB_LINKING_RE, '');
|
||||
}
|
||||
|
||||
if (TYPE_SCOPE_RE.test(header)) {
|
||||
const parsedCommitHeader = TYPE_SCOPE_RE.exec(header)!;
|
||||
type = parsedCommitHeader[1];
|
||||
scope = parsedCommitHeader[2];
|
||||
subject = parsedCommitHeader[3];
|
||||
}
|
||||
return {
|
||||
header,
|
||||
body,
|
||||
bodyWithoutLinking,
|
||||
type,
|
||||
scope,
|
||||
subject,
|
||||
isFixup: FIXUP_PREFIX_RE.test(commitMsg),
|
||||
isSquash: SQUASH_PREFIX_RE.test(commitMsg),
|
||||
isRevert: REVERT_PREFIX_RE.test(commitMsg),
|
||||
};
|
||||
}
|
48
dev-infra/commit-message/restore-commit-message.ts
Normal file
48
dev-infra/commit-message/restore-commit-message.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @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 {info} from 'console';
|
||||
import {writeFileSync} from 'fs';
|
||||
|
||||
import {loadCommitMessageDraft} from './commit-message-draft';
|
||||
|
||||
/**
|
||||
* Restore the commit message draft to the git to be used as the default commit message.
|
||||
*
|
||||
* The source provided may be one of the sources described in
|
||||
* https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||
*/
|
||||
export function restoreCommitMessage(
|
||||
filePath: string, source?: 'message'|'template'|'squash'|'commit') {
|
||||
if (!!source) {
|
||||
info('Skipping commit message restoration attempt');
|
||||
if (source === 'message') {
|
||||
info('A commit message was already provided via the command with a -m or -F flag');
|
||||
}
|
||||
if (source === 'template') {
|
||||
info('A commit message was already provided via the -t flag or config.template setting');
|
||||
}
|
||||
if (source === 'squash') {
|
||||
info('A commit message was already provided as a merge action or via .git/MERGE_MSG');
|
||||
}
|
||||
if (source === 'commit') {
|
||||
info('A commit message was already provided through a revision specified via --fixup, -c,');
|
||||
info('-C or --amend flag');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
/** A draft of a commit message. */
|
||||
const commitMessage = loadCommitMessageDraft(filePath);
|
||||
|
||||
// If the commit message draft has content, restore it into the provided filepath.
|
||||
if (commitMessage) {
|
||||
writeFileSync(filePath, commitMessage);
|
||||
}
|
||||
// Exit the process
|
||||
process.exit(0);
|
||||
}
|
@ -11,6 +11,7 @@ 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. */
|
||||
@ -18,8 +19,12 @@ 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);
|
||||
}
|
||||
|
@ -8,7 +8,8 @@
|
||||
import {info} from '../utils/console';
|
||||
import {exec} from '../utils/shelljs';
|
||||
|
||||
import {parseCommitMessage, validateCommitMessage, ValidateCommitMessageOptions} from './validate';
|
||||
import {parseCommitMessage} from './parse';
|
||||
import {validateCommitMessage, ValidateCommitMessageOptions} from './validate';
|
||||
|
||||
// Whether the provided commit is a fixup commit.
|
||||
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
|
||||
|
@ -18,13 +18,6 @@ const config: {commitMessage: CommitMessageConfig} = {
|
||||
commitMessage: {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 0,
|
||||
types: [
|
||||
'feat',
|
||||
'fix',
|
||||
'refactor',
|
||||
'release',
|
||||
'style',
|
||||
],
|
||||
scopes: [
|
||||
'common',
|
||||
'compiler',
|
||||
@ -33,7 +26,7 @@ const config: {commitMessage: CommitMessageConfig} = {
|
||||
]
|
||||
}
|
||||
};
|
||||
const TYPES = config.commitMessage.types.join(', ');
|
||||
const TYPES = Object.keys(validateConfig.COMMIT_TYPES).join(', ');
|
||||
const SCOPES = config.commitMessage.scopes.join(', ');
|
||||
const INVALID = false;
|
||||
const VALID = true;
|
||||
@ -47,7 +40,8 @@ describe('validate-commit-message.js', () => {
|
||||
lastError = '';
|
||||
|
||||
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
|
||||
spyOn(validateConfig, 'getCommitMessageConfig').and.returnValue(config);
|
||||
spyOn(validateConfig, 'getCommitMessageConfig')
|
||||
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
|
||||
});
|
||||
|
||||
describe('validateMessage()', () => {
|
||||
@ -55,16 +49,16 @@ describe('validate-commit-message.js', () => {
|
||||
expect(validateCommitMessage('feat(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('release(packaging): something')).toBe(VALID);
|
||||
expect(validateCommitMessage('fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('fixup! release(packaging): something')).toBe(VALID);
|
||||
expect(validateCommitMessage('fixup! fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('squash! release(packaging): something')).toBe(VALID);
|
||||
expect(validateCommitMessage('squash! fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('Revert: "release(packaging): something"')).toBe(VALID);
|
||||
expect(validateCommitMessage('Revert: "fix(packaging): something"')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
});
|
||||
|
||||
@ -110,8 +104,8 @@ describe('validate-commit-message.js', () => {
|
||||
expect(validateCommitMessage('feat(bah): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('bah', 'feat(bah): something'));
|
||||
|
||||
expect(validateCommitMessage('style(webworker): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('webworker', 'style(webworker): something'));
|
||||
expect(validateCommitMessage('fix(webworker): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('webworker', 'fix(webworker): something'));
|
||||
|
||||
expect(validateCommitMessage('refactor(security): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('security', 'refactor(security): something'));
|
||||
@ -119,12 +113,12 @@ describe('validate-commit-message.js', () => {
|
||||
expect(validateCommitMessage('refactor(docs): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('docs', 'refactor(docs): something'));
|
||||
|
||||
expect(validateCommitMessage('release(angular): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('angular', 'release(angular): something'));
|
||||
expect(validateCommitMessage('feat(angular): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('angular', 'feat(angular): something'));
|
||||
});
|
||||
|
||||
it('should allow empty scope', () => {
|
||||
expect(validateCommitMessage('fix: blablabla')).toBe(VALID);
|
||||
expect(validateCommitMessage('build: blablabla')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
});
|
||||
|
||||
@ -243,7 +237,6 @@ describe('validate-commit-message.js', () => {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 30,
|
||||
minBodyLengthTypeExcludes: ['docs'],
|
||||
types: ['fix', 'docs'],
|
||||
scopes: ['core']
|
||||
}
|
||||
};
|
||||
|
@ -7,7 +7,8 @@
|
||||
*/
|
||||
import {error} from '../utils/console';
|
||||
|
||||
import {getCommitMessageConfig} from './config';
|
||||
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
|
||||
import {parseCommitMessage} from './parse';
|
||||
|
||||
/** Options for commit message validation. */
|
||||
export interface ValidateCommitMessageOptions {
|
||||
@ -15,53 +16,9 @@ export interface ValidateCommitMessageOptions {
|
||||
nonFixupCommitHeaders?: string[];
|
||||
}
|
||||
|
||||
const FIXUP_PREFIX_RE = /^fixup! /i;
|
||||
const GITHUB_LINKING_RE = /((closed?s?)|(fix(es)?(ed)?)|(resolved?s?))\s\#(\d+)/ig;
|
||||
const SQUASH_PREFIX_RE = /^squash! /i;
|
||||
const REVERT_PREFIX_RE = /^revert:? /i;
|
||||
const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/;
|
||||
const COMMIT_HEADER_RE = /^(.*)/i;
|
||||
const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/;
|
||||
/** Regex matching a URL for an entire commit body line. */
|
||||
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
|
||||
|
||||
/** Parse a full commit message into its composite parts. */
|
||||
export function parseCommitMessage(commitMsg: string) {
|
||||
let header = '';
|
||||
let body = '';
|
||||
let bodyWithoutLinking = '';
|
||||
let type = '';
|
||||
let scope = '';
|
||||
let subject = '';
|
||||
|
||||
if (COMMIT_HEADER_RE.test(commitMsg)) {
|
||||
header = COMMIT_HEADER_RE.exec(commitMsg)![1]
|
||||
.replace(FIXUP_PREFIX_RE, '')
|
||||
.replace(SQUASH_PREFIX_RE, '');
|
||||
}
|
||||
if (COMMIT_BODY_RE.test(commitMsg)) {
|
||||
body = COMMIT_BODY_RE.exec(commitMsg)![1];
|
||||
bodyWithoutLinking = body.replace(GITHUB_LINKING_RE, '');
|
||||
}
|
||||
|
||||
if (TYPE_SCOPE_RE.test(header)) {
|
||||
const parsedCommitHeader = TYPE_SCOPE_RE.exec(header)!;
|
||||
type = parsedCommitHeader[1];
|
||||
scope = parsedCommitHeader[2];
|
||||
subject = parsedCommitHeader[3];
|
||||
}
|
||||
return {
|
||||
header,
|
||||
body,
|
||||
bodyWithoutLinking,
|
||||
type,
|
||||
scope,
|
||||
subject,
|
||||
isFixup: FIXUP_PREFIX_RE.test(commitMsg),
|
||||
isSquash: SQUASH_PREFIX_RE.test(commitMsg),
|
||||
isRevert: REVERT_PREFIX_RE.test(commitMsg),
|
||||
};
|
||||
}
|
||||
|
||||
/** Validate a commit message against using the local repo's config. */
|
||||
export function validateCommitMessage(
|
||||
commitMsg: string, options: ValidateCommitMessageOptions = {}) {
|
||||
@ -129,8 +86,26 @@ export function validateCommitMessage(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.types.includes(commit.type)) {
|
||||
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${config.types.join(', ')}`);
|
||||
|
||||
|
||||
if (COMMIT_TYPES[commit.type] === undefined) {
|
||||
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${
|
||||
Object.keys(COMMIT_TYPES).join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The scope requirement level for the provided type of the commit message. */
|
||||
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
|
||||
printError(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
|
||||
commit.scope}' was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
|
||||
printError(
|
||||
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
43
dev-infra/commit-message/wizard.ts
Normal file
43
dev-infra/commit-message/wizard.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @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 {writeFileSync} from 'fs';
|
||||
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {buildCommitMessage} from './builder';
|
||||
|
||||
/**
|
||||
* The source triggering the git commit message creation.
|
||||
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||
*/
|
||||
export type PrepareCommitMsgHookSource = 'message'|'template'|'merge'|'squash'|'commit';
|
||||
|
||||
/** The default commit message used if the wizard does not procude a commit message. */
|
||||
const defaultCommitMessage = `<type>(<scope>): <summary>
|
||||
|
||||
# <Describe the motivation behind this change - explain WHY you are making this change. Wrap all
|
||||
# lines at 100 characters.>\n\n`;
|
||||
|
||||
export async function runWizard(
|
||||
args: {filePath: string, source?: PrepareCommitMsgHookSource, commitSha?: string}) {
|
||||
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
|
||||
|
||||
if (args.source !== undefined) {
|
||||
info(`Skipping commit message wizard due because the commit was created via '${
|
||||
args.source}' source`);
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the default commit message to be updated if the user cancels out of the wizard in progress
|
||||
writeFileSync(args.filePath, defaultCommitMessage);
|
||||
|
||||
/** The generated commit message. */
|
||||
const commitMessage = await buildCommitMessage();
|
||||
writeFileSync(args.filePath, commitMessage);
|
||||
}
|
@ -10,12 +10,10 @@ ts_library(
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/cli-progress",
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/shelljs",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//cli-progress",
|
||||
"@npm//inquirer",
|
||||
"@npm//multimatch",
|
||||
"@npm//shelljs",
|
||||
"@npm//yargs",
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {allChangedFilesSince, allFiles} from '../utils/repo-files';
|
||||
import {allChangedFilesSince, allFiles, allStagedFiles} from '../utils/repo-files';
|
||||
|
||||
import {checkFiles, formatFiles} from './format';
|
||||
|
||||
@ -22,22 +22,31 @@ export function buildFormatParser(localYargs: yargs.Argv) {
|
||||
description: 'Run the formatter to check formatting rather than updating code format'
|
||||
})
|
||||
.command(
|
||||
'all', 'Run the formatter on all files in the repository', {},
|
||||
'all', 'Run the formatter on all files in the repository', args => args,
|
||||
({check}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allFiles());
|
||||
})
|
||||
.command(
|
||||
'changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref', {},
|
||||
'changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref',
|
||||
args => args.positional('shaOrRef', {type: 'string'}),
|
||||
({shaOrRef, check}) => {
|
||||
const sha = shaOrRef || 'master';
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allChangedFilesSince(sha));
|
||||
})
|
||||
.command('files <files..>', 'Run the formatter on provided files', {}, ({check, files}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(files);
|
||||
});
|
||||
.command(
|
||||
'staged', 'Run the formatter on all staged files', args => args,
|
||||
({check}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allStagedFiles());
|
||||
})
|
||||
.command(
|
||||
'files <files..>', 'Run the formatter on provided files',
|
||||
args => args.positional('files', {array: true, type: 'string'}), ({check, files}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(files!);
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
|
@ -6,9 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {prompt} from 'inquirer';
|
||||
|
||||
import {error, info} from '../utils/console';
|
||||
import {error, info, promptConfirm} from '../utils/console';
|
||||
|
||||
import {runFormatterInParallel} from './run-commands-parallel';
|
||||
|
||||
@ -57,11 +55,7 @@ export async function checkFiles(files: string[]) {
|
||||
// If the command is run in a non-CI environment, prompt to format the files immediately.
|
||||
let runFormatter = false;
|
||||
if (!process.env['CI']) {
|
||||
runFormatter = (await prompt({
|
||||
type: 'confirm',
|
||||
name: 'runFormatter',
|
||||
message: 'Format the files now?',
|
||||
})).runFormatter;
|
||||
runFormatter = await promptConfirm('Format the files now?', true);
|
||||
}
|
||||
|
||||
if (runFormatter) {
|
||||
|
@ -6,6 +6,7 @@ ts_library(
|
||||
module_name = "@angular/dev-infra-private/pr",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/pr/checkout",
|
||||
"//dev-infra/pr/discover-new-conflicts",
|
||||
"//dev-infra/pr/merge",
|
||||
"//dev-infra/pr/rebase",
|
||||
|
13
dev-infra/pr/checkout/BUILD.bazel
Normal file
13
dev-infra/pr/checkout/BUILD.bazel
Normal file
@ -0,0 +1,13 @@
|
||||
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "checkout",
|
||||
srcs = glob(["*.ts"]),
|
||||
module_name = "@angular/dev-infra-private/pr/checkout",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/pr/common",
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/yargs",
|
||||
],
|
||||
)
|
50
dev-infra/pr/checkout/cli.ts
Normal file
50
dev-infra/pr/checkout/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 {error} from '../../utils/console';
|
||||
import {checkOutPullRequestLocally} from '../common/checkout-pr';
|
||||
|
||||
export interface CheckoutOptions {
|
||||
prNumber: number;
|
||||
'github-token'?: string;
|
||||
}
|
||||
|
||||
/** URL to the Github page where personal access tokens can be generated. */
|
||||
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
|
||||
|
||||
/** Builds the checkout pull request command. */
|
||||
function builder(yargs: Argv) {
|
||||
return yargs.positional('prNumber', {type: 'number', demandOption: true}).option('github-token', {
|
||||
type: 'string',
|
||||
description: 'Github token. If not set, token is retrieved from the environment variables.'
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the checkout pull request command. */
|
||||
async function handler({prNumber, 'github-token': token}: Arguments<CheckoutOptions>) {
|
||||
const githubToken = token || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
if (!githubToken) {
|
||||
error('No Github token set. Please set the `GITHUB_TOKEN` environment variable.');
|
||||
error('Alternatively, pass the `--github-token` command line flag.');
|
||||
error(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const prCheckoutOptions = {allowIfMaintainerCannotModify: true, branchName: `pr-${prNumber}`};
|
||||
await checkOutPullRequestLocally(prNumber, githubToken, prCheckoutOptions);
|
||||
}
|
||||
|
||||
/** yargs command module for checking out a PR */
|
||||
export const CheckoutCommandModule: CommandModule<{}, CheckoutOptions> = {
|
||||
handler,
|
||||
builder,
|
||||
command: 'checkout <pr-number>',
|
||||
describe: 'Checkout a PR from the upstream repo',
|
||||
};
|
@ -8,6 +8,7 @@
|
||||
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {CheckoutCommandModule} from './checkout/cli';
|
||||
import {buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand} from './discover-new-conflicts/cli';
|
||||
import {buildMergeCommand, handleMergeCommand} from './merge/cli';
|
||||
import {buildRebaseCommand, handleRebaseCommand} from './rebase/cli';
|
||||
@ -24,7 +25,8 @@ export function buildPrParser(localYargs: yargs.Argv) {
|
||||
buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand)
|
||||
.command(
|
||||
'rebase <pr-number>', 'Rebase a pending PR and push the rebased commits back to Github',
|
||||
buildRebaseCommand, handleRebaseCommand);
|
||||
buildRebaseCommand, handleRebaseCommand)
|
||||
.command(CheckoutCommandModule);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
|
12
dev-infra/pr/common/BUILD.bazel
Normal file
12
dev-infra/pr/common/BUILD.bazel
Normal file
@ -0,0 +1,12 @@
|
||||
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "common",
|
||||
srcs = glob(["*.ts"]),
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/node",
|
||||
"@npm//typed-graphqlify",
|
||||
],
|
||||
)
|
135
dev-infra/pr/common/checkout-pr.ts
Normal file
135
dev-infra/pr/common/checkout-pr.ts
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @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 {types as graphQLTypes} from 'typed-graphqlify';
|
||||
import {URL} from 'url';
|
||||
|
||||
import {info} from '../../utils/console';
|
||||
import {GitClient} from '../../utils/git';
|
||||
import {getPr} from '../../utils/github';
|
||||
|
||||
/* GraphQL schema for the response body for a pending PR. */
|
||||
const PR_SCHEMA = {
|
||||
state: graphQLTypes.string,
|
||||
maintainerCanModify: graphQLTypes.boolean,
|
||||
viewerDidAuthor: graphQLTypes.boolean,
|
||||
headRefOid: graphQLTypes.string,
|
||||
headRef: {
|
||||
name: graphQLTypes.string,
|
||||
repository: {
|
||||
url: graphQLTypes.string,
|
||||
nameWithOwner: graphQLTypes.string,
|
||||
},
|
||||
},
|
||||
baseRef: {
|
||||
name: graphQLTypes.string,
|
||||
repository: {
|
||||
url: graphQLTypes.string,
|
||||
nameWithOwner: graphQLTypes.string,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export class UnexpectedLocalChangesError extends Error {
|
||||
constructor(m: string) {
|
||||
super(m);
|
||||
Object.setPrototypeOf(this, UnexpectedLocalChangesError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class MaintainerModifyAccessError extends Error {
|
||||
constructor(m: string) {
|
||||
super(m);
|
||||
Object.setPrototypeOf(this, MaintainerModifyAccessError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
/** Options for checking out a PR */
|
||||
export interface PullRequestCheckoutOptions {
|
||||
/** Whether the PR should be checked out if the maintainer cannot modify. */
|
||||
allowIfMaintainerCannotModify?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebase the provided PR onto its merge target branch, and push up the resulting
|
||||
* commit to the PRs repository.
|
||||
*/
|
||||
export async function checkOutPullRequestLocally(
|
||||
prNumber: number, githubToken: string, opts: PullRequestCheckoutOptions = {}) {
|
||||
/** Authenticated Git client for git and Github interactions. */
|
||||
const git = new GitClient(githubToken);
|
||||
|
||||
// In order to preserve local changes, checkouts cannot occur if local changes are present in the
|
||||
// git environment. Checked before retrieving the PR to fail fast.
|
||||
if (git.hasLocalChanges()) {
|
||||
throw new UnexpectedLocalChangesError('Unable to checkout PR due to uncommitted changes.');
|
||||
}
|
||||
|
||||
/**
|
||||
* The branch or revision originally checked out before this method performed
|
||||
* any Git operations that may change the working branch.
|
||||
*/
|
||||
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
||||
/* The PR information from Github. */
|
||||
const pr = await getPr(PR_SCHEMA, prNumber, git);
|
||||
/** The branch name of the PR from the repository the PR came from. */
|
||||
const headRefName = pr.headRef.name;
|
||||
/** The full ref for the repository and branch the PR came from. */
|
||||
const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`;
|
||||
/** The full URL path of the repository the PR came from with github token as authentication. */
|
||||
const headRefUrl = addAuthenticationToUrl(pr.headRef.repository.url, githubToken);
|
||||
// Note: Since we use a detached head for rebasing the PR and therefore do not have
|
||||
// remote-tracking branches configured, we need to set our expected ref and SHA. This
|
||||
// allows us to use `--force-with-lease` for the detached head while ensuring that we
|
||||
// never accidentally override upstream changes that have been pushed in the meanwhile.
|
||||
// See:
|
||||
// https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt
|
||||
/** Flag for a force push with leage back to upstream. */
|
||||
const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`;
|
||||
|
||||
// If the PR does not allow maintainers to modify it, exit as the rebased PR cannot
|
||||
// be pushed up.
|
||||
if (!pr.maintainerCanModify && !pr.viewerDidAuthor && !opts.allowIfMaintainerCannotModify) {
|
||||
throw new MaintainerModifyAccessError('PR is not set to allow maintainers to modify the PR');
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch the branch at the commit of the PR, and check it out in a detached state.
|
||||
info(`Checking out PR #${prNumber} from ${fullHeadRef}`);
|
||||
git.run(['fetch', headRefUrl, headRefName]);
|
||||
git.run(['checkout', '--detach', 'FETCH_HEAD']);
|
||||
} catch (e) {
|
||||
git.checkout(previousBranchOrRevision, true);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Pushes the current local branch to the PR on the upstream repository.
|
||||
*
|
||||
* @returns true If the command did not fail causing a GitCommandError to be thrown.
|
||||
* @throws GitCommandError Thrown when the push back to upstream fails.
|
||||
*/
|
||||
pushToUpstream: (): true => {
|
||||
git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]);
|
||||
return true;
|
||||
},
|
||||
/** Restores the state of the local repository to before the PR checkout occured. */
|
||||
resetGitState: (): boolean => {
|
||||
return git.checkout(previousBranchOrRevision, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Adds the provided token as username to the provided url. */
|
||||
function addAuthenticationToUrl(urlString: string, token: string) {
|
||||
const url = new URL(urlString);
|
||||
url.username = token;
|
||||
return url.toString();
|
||||
}
|
@ -12,18 +12,28 @@ import {error} from '../../utils/console';
|
||||
|
||||
import {discoverNewConflictsForPr} from './index';
|
||||
|
||||
/** The options available to the discover-new-conflicts command via CLI. */
|
||||
export interface DiscoverNewConflictsCommandOptions {
|
||||
date: number;
|
||||
'pr-number': number;
|
||||
}
|
||||
|
||||
/** Builds the discover-new-conflicts pull request command. */
|
||||
export function buildDiscoverNewConflictsCommand(yargs: Argv) {
|
||||
return yargs.option('date', {
|
||||
description: 'Only consider PRs updated since provided date',
|
||||
defaultDescription: '30 days ago',
|
||||
coerce: Date.parse,
|
||||
default: getThirtyDaysAgoDate,
|
||||
});
|
||||
export function buildDiscoverNewConflictsCommand(yargs: Argv):
|
||||
Argv<DiscoverNewConflictsCommandOptions> {
|
||||
return yargs
|
||||
.option('date', {
|
||||
description: 'Only consider PRs updated since provided date',
|
||||
defaultDescription: '30 days ago',
|
||||
coerce: (date) => typeof date === 'number' ? date : Date.parse(date),
|
||||
default: getThirtyDaysAgoDate(),
|
||||
})
|
||||
.positional('pr-number', {demandOption: true, type: 'number'});
|
||||
}
|
||||
|
||||
/** Handles the discover-new-conflicts pull request command. */
|
||||
export async function handleDiscoverNewConflictsCommand({prNumber, date}: Arguments) {
|
||||
export async function handleDiscoverNewConflictsCommand(
|
||||
{'pr-number': prNumber, date}: Arguments<DiscoverNewConflictsCommandOptions>) {
|
||||
// If a provided date is not able to be parsed, yargs provides it as NaN.
|
||||
if (isNaN(date)) {
|
||||
error('Unable to parse the value provided via --date flag');
|
||||
@ -33,11 +43,11 @@ export async function handleDiscoverNewConflictsCommand({prNumber, date}: Argume
|
||||
}
|
||||
|
||||
/** Gets a date object 30 days ago from today. */
|
||||
function getThirtyDaysAgoDate(): Date {
|
||||
function getThirtyDaysAgoDate() {
|
||||
const date = new Date();
|
||||
// Set the hours, minutes and seconds to 0 to only consider date.
|
||||
date.setHours(0, 0, 0, 0);
|
||||
// Set the date to 30 days in the past.
|
||||
date.setDate(date.getDate() - 30);
|
||||
return date;
|
||||
return date.getTime();
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ export async function discoverNewConflictsForPr(
|
||||
|
||||
info(`Requesting pending PRs from Github`);
|
||||
/** List of PRs from github currently known as mergable. */
|
||||
const allPendingPRs = (await getPendingPrs(PR_SCHEMA, config.github)).map(processPr);
|
||||
const allPendingPRs = (await getPendingPrs(PR_SCHEMA, git)).map(processPr);
|
||||
/** The PR which is being checked against. */
|
||||
const requestedPr = allPendingPRs.find(pr => pr.number === newPrNumber);
|
||||
if (requestedPr === undefined) {
|
||||
|
@ -12,17 +12,26 @@ import {error, red, yellow} from '../../utils/console';
|
||||
|
||||
import {GITHUB_TOKEN_GENERATE_URL, mergePullRequest} from './index';
|
||||
|
||||
/** The options available to the merge command via CLI. */
|
||||
export interface MergeCommandOptions {
|
||||
'github-token'?: string;
|
||||
'pr-number': number;
|
||||
}
|
||||
|
||||
/** Builds the options for the merge command. */
|
||||
export function buildMergeCommand(yargs: Argv) {
|
||||
return yargs.help().strict().option('github-token', {
|
||||
type: 'string',
|
||||
description: 'Github token. If not set, token is retrieved from the environment variables.'
|
||||
});
|
||||
export function buildMergeCommand(yargs: Argv): Argv<MergeCommandOptions> {
|
||||
return yargs.help()
|
||||
.strict()
|
||||
.positional('pr-number', {demandOption: true, type: 'number'})
|
||||
.option('github-token', {
|
||||
type: 'string',
|
||||
description: 'Github token. If not set, token is retrieved from the environment variables.'
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the merge command. i.e. performs the merge of a specified pull request. */
|
||||
export async function handleMergeCommand(args: Arguments) {
|
||||
const githubToken = args.githubToken || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
export async function handleMergeCommand(args: Arguments<MergeCommandOptions>) {
|
||||
const githubToken = args['github-token'] || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
if (!githubToken) {
|
||||
error(red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.'));
|
||||
error(red('Alternatively, pass the `--github-token` command line flag.'));
|
||||
@ -30,5 +39,5 @@ export async function handleMergeCommand(args: Arguments) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await mergePullRequest(args.prNumber, githubToken);
|
||||
await mergePullRequest(args['pr-number'], githubToken);
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {PullsListCommitsResponse, PullsMergeParams} from '@octokit/rest';
|
||||
import {prompt} from 'inquirer';
|
||||
|
||||
import {parseCommitMessage} from '../../../commit-message/validate';
|
||||
import {parseCommitMessage} from '../../../commit-message/parse';
|
||||
import {GitClient} from '../../../utils/git';
|
||||
import {GithubApiMergeMethod} from '../config';
|
||||
import {PullRequestFailure} from '../failures';
|
||||
|
@ -15,17 +15,26 @@ import {rebasePr} from './index';
|
||||
/** URL to the Github page where personal access tokens can be generated. */
|
||||
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
|
||||
|
||||
/** Builds the rebase pull request command. */
|
||||
export function buildRebaseCommand(yargs: Argv) {
|
||||
return yargs.option('github-token', {
|
||||
type: 'string',
|
||||
description: 'Github token. If not set, token is retrieved from the environment variables.'
|
||||
});
|
||||
/** The options available to the rebase command via CLI. */
|
||||
export interface RebaseCommandOptions {
|
||||
'github-token'?: string;
|
||||
prNumber: number;
|
||||
}
|
||||
|
||||
/** Builds the rebase pull request command. */
|
||||
export function buildRebaseCommand(yargs: Argv): Argv<RebaseCommandOptions> {
|
||||
return yargs
|
||||
.option('github-token', {
|
||||
type: 'string',
|
||||
description: 'Github token. If not set, token is retrieved from the environment variables.'
|
||||
})
|
||||
.positional('prNumber', {type: 'number', demandOption: true});
|
||||
}
|
||||
|
||||
|
||||
/** Handles the rebase pull request command. */
|
||||
export async function handleRebaseCommand(args: Arguments) {
|
||||
const githubToken = args.githubToken || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
export async function handleRebaseCommand(args: Arguments<RebaseCommandOptions>) {
|
||||
const githubToken = args['github-token'] || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
if (!githubToken) {
|
||||
error('No Github token set. Please set the `GITHUB_TOKEN` environment variable.');
|
||||
error('Alternatively, pass the `--github-token` command line flag.');
|
||||
|
@ -55,7 +55,7 @@ export async function rebasePr(
|
||||
*/
|
||||
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
||||
/* Get the PR information from Github. */
|
||||
const pr = await getPr(PR_SCHEMA, prNumber, config.github);
|
||||
const pr = await getPr(PR_SCHEMA, prNumber, git);
|
||||
|
||||
const headRefName = pr.headRef.name;
|
||||
const baseRefName = pr.baseRef.name;
|
||||
|
@ -9,7 +9,7 @@
|
||||
"ts-circular-deps": "./ts-circular-dependencies/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/benchpress": "0.2.0",
|
||||
"@angular/benchpress": "0.2.1",
|
||||
"@octokit/graphql": "<from-root>",
|
||||
"@octokit/types": "<from-root>",
|
||||
"brotli": "<from-root>",
|
||||
|
@ -30,20 +30,19 @@ export function tsCircularDependenciesBuilder(localYargs: yargs.Argv) {
|
||||
{type: 'string', demandOption: true, description: 'Path to the configuration file.'})
|
||||
.option('warnings', {type: 'boolean', description: 'Prints all warnings.'})
|
||||
.command(
|
||||
'check', 'Checks if the circular dependencies have changed.', {},
|
||||
(argv: yargs.Arguments) => {
|
||||
'check', 'Checks if the circular dependencies have changed.', args => args,
|
||||
argv => {
|
||||
const {config: configArg, warnings} = argv;
|
||||
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
|
||||
const config = loadTestConfig(configPath);
|
||||
process.exit(main(false, config, warnings));
|
||||
process.exit(main(false, config, !!warnings));
|
||||
})
|
||||
.command(
|
||||
'approve', 'Approves the current circular dependencies.', {}, (argv: yargs.Arguments) => {
|
||||
const {config: configArg, warnings} = argv;
|
||||
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
|
||||
const config = loadTestConfig(configPath);
|
||||
process.exit(main(true, config, warnings));
|
||||
});
|
||||
.command('approve', 'Approves the current circular dependencies.', args => args, argv => {
|
||||
const {config: configArg, warnings} = argv;
|
||||
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
|
||||
const config = loadTestConfig(configPath);
|
||||
process.exit(main(true, config, !!warnings));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,6 +17,7 @@ ts_library(
|
||||
"@npm//@types/shelljs",
|
||||
"@npm//chalk",
|
||||
"@npm//inquirer",
|
||||
"@npm//inquirer-autocomplete-prompt",
|
||||
"@npm//shelljs",
|
||||
"@npm//tslib",
|
||||
"@npm//typed-graphqlify",
|
||||
|
@ -7,7 +7,8 @@
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {prompt} from 'inquirer';
|
||||
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
|
||||
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
||||
|
||||
|
||||
/** Reexport of chalk colors for convenient access. */
|
||||
@ -26,6 +27,52 @@ export async function promptConfirm(message: string, defaultValue = false): Prom
|
||||
.result;
|
||||
}
|
||||
|
||||
/** Prompts the user to select an option from a filterable autocomplete list. */
|
||||
export async function promptAutocomplete(
|
||||
message: string, choices: (string|ListChoiceOptions)[]): Promise<string>;
|
||||
/**
|
||||
* Prompts the user to select an option from a filterable autocomplete list, with an option to
|
||||
* choose no value.
|
||||
*/
|
||||
export async function promptAutocomplete(
|
||||
message: string, choices: (string|ListChoiceOptions)[],
|
||||
noChoiceText?: string): Promise<string|false>;
|
||||
export async function promptAutocomplete(
|
||||
message: string, choices: (string|ListChoiceOptions)[],
|
||||
noChoiceText?: string): Promise<string|false> {
|
||||
// Creates a local prompt module with an autocomplete prompt type.
|
||||
const prompt = createPromptModule({}).registerPrompt('autocomplete', inquirerAutocomplete);
|
||||
if (noChoiceText) {
|
||||
choices = [noChoiceText, ...choices];
|
||||
}
|
||||
// `prompt` must be cast as `any` as the autocomplete typings are not available.
|
||||
const result = (await (prompt as any)({
|
||||
type: 'autocomplete',
|
||||
name: 'result',
|
||||
message,
|
||||
source: (_: any, input: string) => {
|
||||
if (!input) {
|
||||
return Promise.resolve(choices);
|
||||
}
|
||||
return Promise.resolve(choices.filter(choice => {
|
||||
if (typeof choice === 'string') {
|
||||
return choice.includes(input);
|
||||
}
|
||||
return choice.name!.includes(input);
|
||||
}));
|
||||
}
|
||||
})).result;
|
||||
if (result === noChoiceText) {
|
||||
return false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Prompts the user for one line of input. */
|
||||
export async function promptInput(message: string): Promise<string> {
|
||||
return (await prompt<{result: string}>({type: 'input', name: 'result', message})).result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported levels for logging functions.
|
||||
*
|
||||
|
@ -26,7 +26,7 @@ export class GithubApiRequestError extends Error {
|
||||
**/
|
||||
export class GithubClient extends Octokit {
|
||||
/** The Github GraphQL (v4) API. */
|
||||
graqhql: GithubGraphqlClient;
|
||||
graphql: GithubGraphqlClient;
|
||||
|
||||
/** The current user based on checking against the Github API. */
|
||||
private _currentUser: string|null = null;
|
||||
@ -42,7 +42,7 @@ export class GithubClient extends Octokit {
|
||||
});
|
||||
|
||||
// Create authenticated graphql client.
|
||||
this.graqhql = new GithubGraphqlClient(token);
|
||||
this.graphql = new GithubGraphqlClient(token);
|
||||
}
|
||||
|
||||
/** Retrieve the login of the current user from Github. */
|
||||
@ -51,7 +51,7 @@ export class GithubClient extends Octokit {
|
||||
if (this._currentUser !== null) {
|
||||
return this._currentUser;
|
||||
}
|
||||
const result = await this.graqhql.query({
|
||||
const result = await this.graphql.query({
|
||||
viewer: {
|
||||
login: types.string,
|
||||
}
|
||||
@ -80,7 +80,7 @@ class GithubGraphqlClient {
|
||||
// Set the default headers to include authorization with the provided token for all
|
||||
// graphQL calls.
|
||||
if (token) {
|
||||
this.graqhql.defaults({headers: {authorization: `token ${token}`}});
|
||||
this.graqhql = this.graqhql.defaults({headers: {authorization: `token ${token}`}});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,6 +150,25 @@ export class GitClient {
|
||||
return value.replace(this._githubTokenRegex, '<TOKEN>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks out a requested branch or revision, optionally cleaning the state of the repository
|
||||
* before attempting the checking. Returns a boolean indicating whether the branch or revision
|
||||
* was cleanly checked out.
|
||||
*/
|
||||
checkout(branchOrRevision: string, cleanState: boolean): boolean {
|
||||
if (cleanState) {
|
||||
// Abort any outstanding ams.
|
||||
this.runGraceful(['am', '--abort'], {stdio: 'ignore'});
|
||||
// Abort any outstanding cherry-picks.
|
||||
this.runGraceful(['cherry-pick', '--abort'], {stdio: 'ignore'});
|
||||
// Abort any outstanding rebases.
|
||||
this.runGraceful(['rebase', '--abort'], {stdio: 'ignore'});
|
||||
// Clear any changes in the current repo.
|
||||
this.runGraceful(['reset', '--hard'], {stdio: 'ignore'});
|
||||
}
|
||||
return this.runGraceful(['checkout', branchOrRevision], {stdio: 'ignore'}).status === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the GitClient instance is using a token with permissions for the all of the
|
||||
* provided OAuth scopes.
|
||||
|
@ -6,29 +6,15 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {graphql as unauthenticatedGraphql} from '@octokit/graphql';
|
||||
import {params, types} from 'typed-graphqlify';
|
||||
|
||||
import {params, query as graphqlQuery, types} from 'typed-graphqlify';
|
||||
import {NgDevConfig} from './config';
|
||||
|
||||
/** The configuration required for github interactions. */
|
||||
type GithubConfig = NgDevConfig['github'];
|
||||
|
||||
/**
|
||||
* Authenticated instance of Github GraphQl API service, relies on a
|
||||
* personal access token being available in the TOKEN environment variable.
|
||||
*/
|
||||
const graphql = unauthenticatedGraphql.defaults({
|
||||
headers: {
|
||||
// TODO(josephperrott): Remove reference to TOKEN environment variable as part of larger
|
||||
// effort to migrate to expecting tokens via GITHUB_ACCESS_TOKEN environment variables.
|
||||
authorization: `token ${process.env.TOKEN || process.env.GITHUB_ACCESS_TOKEN}`,
|
||||
}
|
||||
});
|
||||
import {GitClient} from './git';
|
||||
|
||||
/** Get a PR from github */
|
||||
export async function getPr<PrSchema>(
|
||||
prSchema: PrSchema, prNumber: number, {owner, name}: GithubConfig) {
|
||||
export async function getPr<PrSchema>(prSchema: PrSchema, prNumber: number, git: GitClient) {
|
||||
/** The owner and name of the repository */
|
||||
const {owner, name} = git.remoteConfig;
|
||||
/** The GraphQL query object to get a the PR */
|
||||
const PR_QUERY = params(
|
||||
{
|
||||
$number: 'Int!', // The PR number
|
||||
@ -41,14 +27,15 @@ export async function getPr<PrSchema>(
|
||||
})
|
||||
});
|
||||
|
||||
const result =
|
||||
await graphql(graphqlQuery(PR_QUERY), {number: prNumber, owner, name}) as typeof PR_QUERY;
|
||||
const result = (await git.github.graphql.query(PR_QUERY, {number: prNumber, owner, name}));
|
||||
return result.repository.pullRequest;
|
||||
}
|
||||
|
||||
/** Get all pending PRs from github */
|
||||
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, {owner, name}: GithubConfig) {
|
||||
// The GraphQL query object to get a page of pending PRs
|
||||
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, git: GitClient) {
|
||||
/** The owner and name of the repository */
|
||||
const {owner, name} = git.remoteConfig;
|
||||
/** The GraphQL query object to get a page of pending PRs */
|
||||
const PRS_QUERY = params(
|
||||
{
|
||||
$first: 'Int', // How many entries to get with each request
|
||||
@ -73,36 +60,22 @@ export async function getPendingPrs<PrSchema>(prSchema: PrSchema, {owner, name}:
|
||||
}),
|
||||
})
|
||||
});
|
||||
const query = graphqlQuery('members', PRS_QUERY);
|
||||
|
||||
/**
|
||||
* Gets the query and queryParams for a specific page of entries.
|
||||
*/
|
||||
const queryBuilder = (count: number, cursor?: string) => {
|
||||
return {
|
||||
query,
|
||||
params: {
|
||||
after: cursor || null,
|
||||
first: count,
|
||||
owner,
|
||||
name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// The current cursor
|
||||
/** The current cursor */
|
||||
let cursor: string|undefined;
|
||||
// If an additional page of members is expected
|
||||
/** If an additional page of members is expected */
|
||||
let hasNextPage = true;
|
||||
// Array of pending PRs
|
||||
/** Array of pending PRs */
|
||||
const prs: Array<PrSchema> = [];
|
||||
|
||||
// For each page of the response, get the page and add it to the
|
||||
// list of PRs
|
||||
// For each page of the response, get the page and add it to the list of PRs
|
||||
while (hasNextPage) {
|
||||
const {query, params} = queryBuilder(100, cursor);
|
||||
const results = await graphql(query, params) as typeof PRS_QUERY;
|
||||
|
||||
const params = {
|
||||
after: cursor || null,
|
||||
first: 100,
|
||||
owner,
|
||||
name,
|
||||
};
|
||||
const results = await git.github.graphql.query(PRS_QUERY, params) as typeof PRS_QUERY;
|
||||
prs.push(...results.repository.pullRequests.nodes);
|
||||
hasNextPage = results.repository.pullRequests.pageInfo.hasNextPage;
|
||||
cursor = results.repository.pullRequests.pageInfo.endCursor;
|
||||
|
17
dev-infra/utils/inquirer-autocomplete-typings.d.ts
vendored
Normal file
17
dev-infra/utils/inquirer-autocomplete-typings.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
// inquirer-autocomplete-prompt doesn't provide types and no types are made available via
|
||||
// DefinitelyTyped.
|
||||
declare module "inquirer-autocomplete-prompt" {
|
||||
|
||||
import {registerPrompt} from 'inquirer';
|
||||
|
||||
let AutocompletePrompt: Parameters<typeof registerPrompt>[1];
|
||||
export = AutocompletePrompt;
|
||||
}
|
@ -27,6 +27,18 @@ export function allChangedFilesSince(sha = 'HEAD') {
|
||||
return Array.from(new Set([...diffFiles, ...untrackedFiles]));
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all staged files which have been modified.
|
||||
*
|
||||
* Only added, created and modified files are listed as others (deleted, renamed, etc) aren't
|
||||
* changed or available as content to act upon.
|
||||
*/
|
||||
export function allStagedFiles() {
|
||||
return gitOutputAsArray(`git diff --staged --name-only --diff-filter=ACM`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function allFiles() {
|
||||
return gitOutputAsArray(`git ls-files`);
|
||||
}
|
||||
|
@ -1758,9 +1758,10 @@
|
||||
"packages/core/src/render3/index.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/i18n.ts",
|
||||
"packages/core/src/render3/i18n/i18n_apply.ts",
|
||||
"packages/core/src/render3/interfaces/type_checks.ts",
|
||||
"packages/core/src/render3/index.ts"
|
||||
"packages/core/src/render3/index.ts",
|
||||
"packages/core/src/render3/instructions/i18n.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
|
2
goldens/public-api/common/common.d.ts
vendored
2
goldens/public-api/common/common.d.ts
vendored
@ -216,7 +216,7 @@ export declare class NgComponentOutlet implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
export declare class NgForOf<T, U extends NgIterable<T> = NgIterable<T>> implements DoCheck {
|
||||
set ngForOf(ngForOf: (U & NgIterable<T>) | undefined | null);
|
||||
set ngForOf(ngForOf: U & NgIterable<T> | undefined | null);
|
||||
set ngForTemplate(value: TemplateRef<NgForOfContext<T, U>>);
|
||||
set ngForTrackBy(fn: TrackByFunction<T>);
|
||||
get ngForTrackBy(): TrackByFunction<T>;
|
||||
|
@ -35,6 +35,7 @@ export interface StrictTemplateOptions {
|
||||
strictContextGenerics?: boolean;
|
||||
strictDomEventTypes?: boolean;
|
||||
strictDomLocalRefTypes?: boolean;
|
||||
strictInputAccessModifiers?: boolean;
|
||||
strictInputTypes?: boolean;
|
||||
strictLiteralTypes?: boolean;
|
||||
strictNullInputTypes?: boolean;
|
||||
|
2
goldens/public-api/elements/elements.d.ts
vendored
2
goldens/public-api/elements/elements.d.ts
vendored
@ -2,7 +2,7 @@ export declare function createCustomElement<P>(component: Type<any>, config: NgE
|
||||
|
||||
export declare abstract class NgElement extends HTMLElement {
|
||||
protected ngElementEventsSubscription: Subscription | null;
|
||||
protected ngElementStrategy: NgElementStrategy;
|
||||
protected abstract ngElementStrategy: NgElementStrategy;
|
||||
abstract attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string, namespace?: string): void;
|
||||
abstract connectedCallback(): void;
|
||||
abstract disconnectedCallback(): void;
|
||||
|
13
goldens/public-api/router/router.d.ts
vendored
13
goldens/public-api/router/router.d.ts
vendored
@ -51,6 +51,14 @@ export declare class ActivationStart {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export declare abstract class BaseRouteReuseStrategy implements RouteReuseStrategy {
|
||||
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null;
|
||||
shouldAttach(route: ActivatedRouteSnapshot): boolean;
|
||||
shouldDetach(route: ActivatedRouteSnapshot): boolean;
|
||||
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
|
||||
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void;
|
||||
}
|
||||
|
||||
export declare interface CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
}
|
||||
@ -370,7 +378,7 @@ export declare class RouterEvent {
|
||||
url: string);
|
||||
}
|
||||
|
||||
export declare class RouterLink {
|
||||
export declare class RouterLink implements OnChanges {
|
||||
fragment: string;
|
||||
preserveFragment: boolean;
|
||||
/** @deprecated */ set preserveQueryParams(value: boolean);
|
||||
@ -386,6 +394,7 @@ export declare class RouterLink {
|
||||
};
|
||||
get urlTree(): UrlTree;
|
||||
constructor(router: Router, route: ActivatedRoute, tabIndex: string, renderer: Renderer2, el: ElementRef);
|
||||
ngOnChanges(changes: SimpleChanges): void;
|
||||
onClick(): boolean;
|
||||
}
|
||||
|
||||
@ -421,7 +430,7 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
|
||||
target: string;
|
||||
get urlTree(): UrlTree;
|
||||
constructor(router: Router, route: ActivatedRoute, locationStrategy: LocationStrategy);
|
||||
ngOnChanges(changes: {}): any;
|
||||
ngOnChanges(changes: SimpleChanges): any;
|
||||
ngOnDestroy(): any;
|
||||
onClick(button: number, ctrlKey: boolean, metaKey: boolean, shiftKey: boolean): boolean;
|
||||
}
|
||||
|
@ -62,7 +62,7 @@
|
||||
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
|
||||
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
|
||||
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
|
||||
"bundle": 1213130
|
||||
"bundle": 1214857
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,10 @@ INTEGRATION_TESTS = {
|
||||
},
|
||||
"dynamic-compiler": {"tags": ["no-ivy-aot"]},
|
||||
"hello_world__closure": {
|
||||
"commands": "payload_size_tracking",
|
||||
# TODO: Re-enable the payload_size_tracking command:
|
||||
# We should define ngDevMode to false in Closure, but --define only works in the global scope.
|
||||
# With ngDevMode not being set to false, this size tracking test provides little value but a lot of
|
||||
# headache to continue updating the size.
|
||||
"tags": ["no-ivy-aot"],
|
||||
},
|
||||
"hello_world__systemjs_umd": {
|
||||
@ -82,6 +85,12 @@ INTEGRATION_TESTS = {
|
||||
# root @npm//typescript package.
|
||||
"pinned_npm_packages": ["typescript"],
|
||||
},
|
||||
"typings_test_ts40": {
|
||||
# Special case for `typings_test_ts40` test as we want to pin
|
||||
# `typescript` at version 4.0.x for that test and not link to the
|
||||
# root @npm//typescript package.
|
||||
"pinned_npm_packages": ["typescript"],
|
||||
},
|
||||
}
|
||||
|
||||
[
|
||||
|
71
integration/typings_test_ts40/include-all.ts
Normal file
71
integration/typings_test_ts40/include-all.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @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 animations from '@angular/animations';
|
||||
import * as animationsBrowser from '@angular/animations/browser';
|
||||
import * as animationsBrowserTesting from '@angular/animations/browser/testing';
|
||||
import * as common from '@angular/common';
|
||||
import * as commonHttp from '@angular/common/http';
|
||||
import * as commonTesting from '@angular/common/testing';
|
||||
import * as commonHttpTesting from '@angular/common/testing';
|
||||
import * as compiler from '@angular/compiler';
|
||||
import * as compilerTesting from '@angular/compiler/testing';
|
||||
import * as core from '@angular/core';
|
||||
import * as coreTesting from '@angular/core/testing';
|
||||
import * as elements from '@angular/elements';
|
||||
import * as forms from '@angular/forms';
|
||||
import * as platformBrowser from '@angular/platform-browser';
|
||||
import * as platformBrowserDynamic from '@angular/platform-browser-dynamic';
|
||||
import * as platformBrowserDynamicTesting from '@angular/platform-browser-dynamic/testing';
|
||||
import * as platformBrowserAnimations from '@angular/platform-browser/animations';
|
||||
import * as platformBrowserTesting from '@angular/platform-browser/testing';
|
||||
import * as platformServer from '@angular/platform-server';
|
||||
import * as platformServerTesting from '@angular/platform-server/testing';
|
||||
import * as platformWebworker from '@angular/platform-webworker';
|
||||
import * as platformWebworkerDynamic from '@angular/platform-webworker-dynamic';
|
||||
import * as router from '@angular/router';
|
||||
import * as routerTesting from '@angular/router/testing';
|
||||
import * as routerUpgrade from '@angular/router/upgrade';
|
||||
import * as serviceWorker from '@angular/service-worker';
|
||||
import * as upgrade from '@angular/upgrade';
|
||||
import * as upgradeStatic from '@angular/upgrade/static';
|
||||
import * as upgradeTesting from '@angular/upgrade/static/testing';
|
||||
|
||||
export default {
|
||||
animations,
|
||||
animationsBrowser,
|
||||
animationsBrowserTesting,
|
||||
common,
|
||||
commonTesting,
|
||||
commonHttp,
|
||||
commonHttpTesting,
|
||||
compiler,
|
||||
compilerTesting,
|
||||
core,
|
||||
coreTesting,
|
||||
elements,
|
||||
forms,
|
||||
platformBrowser,
|
||||
platformBrowserTesting,
|
||||
platformBrowserDynamic,
|
||||
platformBrowserDynamicTesting,
|
||||
platformBrowserAnimations,
|
||||
platformServer,
|
||||
platformServerTesting,
|
||||
platformWebworker,
|
||||
platformWebworkerDynamic,
|
||||
router,
|
||||
routerTesting,
|
||||
routerUpgrade,
|
||||
serviceWorker,
|
||||
upgrade,
|
||||
upgradeStatic,
|
||||
upgradeTesting,
|
||||
};
|
30
integration/typings_test_ts40/package.json
Normal file
30
integration/typings_test_ts40/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "angular-integration",
|
||||
"description": "Assert that users with TypeScript 4.0 can type-check an Angular application",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular/animations": "file:../../dist/packages-dist/animations",
|
||||
"@angular/common": "file:../../dist/packages-dist/common",
|
||||
"@angular/compiler": "file:../../dist/packages-dist/compiler",
|
||||
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
|
||||
"@angular/core": "file:../../dist/packages-dist/core",
|
||||
"@angular/elements": "file:../../dist/packages-dist/elements",
|
||||
"@angular/forms": "file:../../dist/packages-dist/forms",
|
||||
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
|
||||
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
|
||||
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
|
||||
"@angular/platform-webworker": "file:../../dist/packages-dist/platform-webworker",
|
||||
"@angular/platform-webworker-dynamic": "file:../../dist/packages-dist/platform-webworker-dynamic",
|
||||
"@angular/router": "file:../../dist/packages-dist/router",
|
||||
"@angular/service-worker": "file:../../dist/packages-dist/service-worker",
|
||||
"@angular/upgrade": "file:../../dist/packages-dist/upgrade",
|
||||
"@types/jasmine": "file:../../node_modules/@types/jasmine",
|
||||
"rxjs": "file:../../node_modules/rxjs",
|
||||
"typescript": "4.0.2",
|
||||
"zone.js": "file:../../dist/zone.js-dist/zone.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tsc"
|
||||
}
|
||||
}
|
26
integration/typings_test_ts40/tsconfig.json
Normal file
26
integration/typings_test_ts40/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"rootDir": ".",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection",
|
||||
"es2015.iterable",
|
||||
"es2015.promise"
|
||||
],
|
||||
"types": [],
|
||||
},
|
||||
"files": [
|
||||
"include-all.ts",
|
||||
"node_modules/@types/jasmine/index.d.ts"
|
||||
]
|
||||
}
|
@ -37,6 +37,9 @@ module.exports = function(config) {
|
||||
|
||||
'node_modules/core-js/client/core.js',
|
||||
'node_modules/jasmine-ajax/lib/mock-ajax.js',
|
||||
|
||||
// Dependencies built by Bazel. See `config.yml` for steps running before
|
||||
// the legacy Saucelabs tests run.
|
||||
'dist/bin/packages/zone.js/npm_package/bundles/zone.umd.js',
|
||||
'dist/bin/packages/zone.js/npm_package/bundles/zone-testing.umd.js',
|
||||
'dist/bin/packages/zone.js/npm_package/bundles/task-tracking.umd.js',
|
||||
|
21
package.json
21
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "10.1.0-next.4",
|
||||
"version": "10.1.0-next.8",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@ -76,7 +76,7 @@
|
||||
"@types/diff": "^3.5.1",
|
||||
"@types/fs-extra": "4.0.2",
|
||||
"@types/hammerjs": "2.0.35",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/inquirer": "^7.3.0",
|
||||
"@types/jasmine": "3.5.10",
|
||||
"@types/jasmine-ajax": "^3.3.1",
|
||||
"@types/jasminewd2": "^2.0.8",
|
||||
@ -88,7 +88,7 @@
|
||||
"@types/shelljs": "^0.8.6",
|
||||
"@types/systemjs": "0.19.32",
|
||||
"@types/yaml": "^1.2.0",
|
||||
"@types/yargs": "^11.1.1",
|
||||
"@types/yargs": "^15.0.5",
|
||||
"@webcomponents/custom-elements": "^1.1.0",
|
||||
"angular": "npm:angular@1.7",
|
||||
"angular-1.5": "npm:angular@1.5",
|
||||
@ -149,11 +149,11 @@
|
||||
"terser": "^4.4.0",
|
||||
"tsickle": "0.38.1",
|
||||
"tslib": "^2.0.0",
|
||||
"tslint": "6.0.0",
|
||||
"typescript": "~3.9.5",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "~4.0.2",
|
||||
"xhr2": "0.2.0",
|
||||
"yaml": "^1.7.2",
|
||||
"yargs": "15.3.0"
|
||||
"yargs": "^15.4.1"
|
||||
},
|
||||
"// 2": "devDependencies are not used under Bazel. Many can be removed after test.sh is deleted.",
|
||||
"devDependencies": {
|
||||
@ -179,8 +179,9 @@
|
||||
"glob": "7.1.2",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-conventional-changelog": "^2.0.3",
|
||||
"husky": "^4.2.3",
|
||||
"inquirer": "^7.1.0",
|
||||
"husky": "^4.2.5",
|
||||
"inquirer": "^7.3.3",
|
||||
"inquirer-autocomplete-prompt": "^1.0.2",
|
||||
"jpm": "1.3.1",
|
||||
"karma-browserstack-launcher": "^1.3.0",
|
||||
"karma-sauce-launcher": "^2.0.2",
|
||||
@ -209,7 +210,9 @@
|
||||
"cldr-data-coverage": "full",
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS"
|
||||
"pre-commit": "yarn -s ng-dev format staged",
|
||||
"commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS",
|
||||
"prepare-commit-msg": "yarn -s ng-dev commit-message restore-commit-message-draft --file-env-variable HUSKY_GIT_PARAMS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@
|
||||
"@angular/compiler-cli": "0.0.0-PLACEHOLDER",
|
||||
"@bazel/typescript": ">=1.0.0",
|
||||
"terser": "^4.3.1",
|
||||
"typescript": ">=3.9 <4.0",
|
||||
"typescript": ">=3.9 <4.1",
|
||||
"rollup": ">=1.20.0",
|
||||
"rollup-plugin-commonjs": ">=9.0.0",
|
||||
"rollup-plugin-node-resolve": ">=4.2.0",
|
||||
|
@ -155,7 +155,7 @@ export class NgForOf<T, U extends NgIterable<T> = NgIterable<T>> implements DoCh
|
||||
* rather than the identity of the object itself.
|
||||
*
|
||||
* The function receives two inputs,
|
||||
* the iteration index and the node object ID.
|
||||
* the iteration index and the associated node data.
|
||||
*/
|
||||
@Input()
|
||||
set ngForTrackBy(fn: TrackByFunction<T>) {
|
||||
|
@ -88,7 +88,7 @@ export class BrowserViewportScroller implements ViewportScroller {
|
||||
* @returns The position in screen coordinates.
|
||||
*/
|
||||
getScrollPosition(): [number, number] {
|
||||
if (this.supportScrollRestoration()) {
|
||||
if (this.supportsScrolling()) {
|
||||
return [this.window.scrollX, this.window.scrollY];
|
||||
} else {
|
||||
return [0, 0];
|
||||
@ -100,7 +100,7 @@ export class BrowserViewportScroller implements ViewportScroller {
|
||||
* @param position The new position in screen coordinates.
|
||||
*/
|
||||
scrollToPosition(position: [number, number]): void {
|
||||
if (this.supportScrollRestoration()) {
|
||||
if (this.supportsScrolling()) {
|
||||
this.window.scrollTo(position[0], position[1]);
|
||||
}
|
||||
}
|
||||
@ -110,7 +110,7 @@ export class BrowserViewportScroller implements ViewportScroller {
|
||||
* @param anchor The ID of the anchor element.
|
||||
*/
|
||||
scrollToAnchor(anchor: string): void {
|
||||
if (this.supportScrollRestoration()) {
|
||||
if (this.supportsScrolling()) {
|
||||
const elSelected =
|
||||
this.document.getElementById(anchor) || this.document.getElementsByName(anchor)[0];
|
||||
if (elSelected) {
|
||||
@ -163,6 +163,14 @@ export class BrowserViewportScroller implements ViewportScroller {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private supportsScrolling(): boolean {
|
||||
try {
|
||||
return !!this.window.scrollTo;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getScrollRestorationProperty(obj: any): PropertyDescriptor|undefined {
|
||||
|
@ -15,21 +15,30 @@ describe('BrowserViewportScroller', () => {
|
||||
let windowSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
windowSpy = jasmine.createSpyObj('window', ['history']);
|
||||
windowSpy.scrollTo = 1;
|
||||
windowSpy = jasmine.createSpyObj('window', ['history', 'scrollTo']);
|
||||
windowSpy.history.scrollRestoration = 'auto';
|
||||
documentSpy = jasmine.createSpyObj('document', ['getElementById', 'getElementsByName']);
|
||||
scroller = new BrowserViewportScroller(documentSpy, windowSpy, null!);
|
||||
});
|
||||
|
||||
describe('setHistoryScrollRestoration', () => {
|
||||
it('should not crash when scrollRestoration is not writable', () => {
|
||||
function createNonWritableScrollRestoration() {
|
||||
Object.defineProperty(windowSpy.history, 'scrollRestoration', {
|
||||
value: 'auto',
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
it('should not crash when scrollRestoration is not writable', () => {
|
||||
createNonWritableScrollRestoration();
|
||||
expect(() => scroller.setHistoryScrollRestoration('manual')).not.toThrow();
|
||||
});
|
||||
|
||||
it('should still allow scrolling if scrollRestoration is not writable', () => {
|
||||
createNonWritableScrollRestoration();
|
||||
scroller.scrollToPosition([10, 10]);
|
||||
expect(windowSpy.scrollTo as jasmine.Spy).toHaveBeenCalledWith(10, 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrollToAnchor', () => {
|
||||
|
@ -19,9 +19,10 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
alias: 'source',
|
||||
describe:
|
||||
'A path (relative to the working directory) of the `node_modules` folder to process.',
|
||||
default: './node_modules'
|
||||
default: './node_modules',
|
||||
type: 'string',
|
||||
})
|
||||
.option('f', {alias: 'formats', hidden: true, array: true})
|
||||
.option('f', {alias: 'formats', hidden: true, array: true, type: 'string'})
|
||||
.option('p', {
|
||||
alias: 'properties',
|
||||
array: true,
|
||||
@ -29,7 +30,8 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
'An array of names of properties in package.json to compile (e.g. `module` or `main`)\n' +
|
||||
'Each of these properties should hold the path to a bundle-format.\n' +
|
||||
'If provided, only the specified properties are considered for processing.\n' +
|
||||
'If not provided, all the supported format properties (e.g. fesm2015, fesm5, es2015, esm2015, esm5, main, module) in the package.json are considered.'
|
||||
'If not provided, all the supported format properties (e.g. fesm2015, fesm5, es2015, esm2015, esm5, main, module) in the package.json are considered.',
|
||||
type: 'string',
|
||||
})
|
||||
.option('t', {
|
||||
alias: 'target',
|
||||
@ -37,6 +39,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
'A relative path (from the `source` path) to a single entry-point to process (plus its dependencies).\n' +
|
||||
'If this property is provided then `error-on-failed-entry-point` is forced to true.\n' +
|
||||
'This option overrides the `--use-program-dependencies` option.',
|
||||
type: 'string',
|
||||
})
|
||||
.option('use-program-dependencies', {
|
||||
type: 'boolean',
|
||||
@ -47,7 +50,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
.option('first-only', {
|
||||
describe:
|
||||
'If specified then only the first matching package.json property will be compiled.',
|
||||
type: 'boolean'
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('create-ivy-entry-points', {
|
||||
describe:
|
||||
@ -78,6 +81,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
alias: 'loglevel',
|
||||
describe: 'The lowest severity logging message that should be output.',
|
||||
choices: ['debug', 'info', 'warn', 'error'],
|
||||
type: 'string',
|
||||
})
|
||||
.option('invalidate-entry-point-manifest', {
|
||||
describe:
|
||||
@ -105,7 +109,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
.help()
|
||||
.parse(args);
|
||||
|
||||
if (options['f'] && options['f'].length) {
|
||||
if (options.f?.length) {
|
||||
console.error(
|
||||
'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.');
|
||||
process.exit(1);
|
||||
@ -113,12 +117,12 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
|
||||
setFileSystem(new NodeJSFileSystem());
|
||||
|
||||
const baseSourcePath = resolve(options['s'] || './node_modules');
|
||||
const propertiesToConsider: string[] = options['p'];
|
||||
const targetEntryPointPath = options['t'] ? options['t'] : undefined;
|
||||
const baseSourcePath = resolve(options.s || './node_modules');
|
||||
const propertiesToConsider = options.p;
|
||||
const targetEntryPointPath = options.t;
|
||||
const compileAllFormats = !options['first-only'];
|
||||
const createNewEntryPointFormats = options['create-ivy-entry-points'];
|
||||
const logLevel = options['l'] as keyof typeof LogLevel | undefined;
|
||||
const logLevel = options.l as keyof typeof LogLevel | undefined;
|
||||
const enableI18nLegacyMessageIdFormat = options['legacy-message-ids'];
|
||||
const invalidateEntryPointManifest = options['invalidate-entry-point-manifest'];
|
||||
const errorOnFailedEntryPoint = options['error-on-failed-entry-point'];
|
||||
@ -126,7 +130,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
// yargs is not so great at mixed string+boolean types, so we have to test tsconfig against a
|
||||
// string "false" to capture the `tsconfig=false` option.
|
||||
// And we have to convert the option to a string to handle `no-tsconfig`, which will be `false`.
|
||||
const tsConfigPath = `${options['tsconfig']}` === 'false' ? null : options['tsconfig'];
|
||||
const tsConfigPath = `${options.tsconfig}` === 'false' ? null : options.tsconfig;
|
||||
|
||||
const logger = logLevel && new ConsoleLogger(LogLevel[logLevel]);
|
||||
|
||||
@ -138,7 +142,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
createNewEntryPointFormats,
|
||||
logger,
|
||||
enableI18nLegacyMessageIdFormat,
|
||||
async: options['async'],
|
||||
async: options.async,
|
||||
invalidateEntryPointManifest,
|
||||
errorOnFailedEntryPoint,
|
||||
tsConfigPath,
|
||||
|
@ -10,7 +10,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
|
||||
|
||||
import {Logger} from '../../../src/ngtsc/logging';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} 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 {BundleProgram} from '../packages/bundle_program';
|
||||
import {findAll, getNameText, hasNameIdentifier, isDefined, stripDollarSuffix} from '../utils';
|
||||
@ -1594,7 +1594,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
{decorators: null, typeExpression: null};
|
||||
const nameNode = node.name;
|
||||
|
||||
let typeValueReference: TypeValueReference|null = null;
|
||||
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
|
||||
@ -1603,7 +1603,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
if (decl !== null && decl.node !== null && decl.viaModule !== null &&
|
||||
isNamedDeclaration(decl.node)) {
|
||||
typeValueReference = {
|
||||
local: false,
|
||||
kind: TypeValueReferenceKind.IMPORTED,
|
||||
valueDeclaration: decl.node,
|
||||
moduleName: decl.viaModule,
|
||||
importedName: decl.node.name.text,
|
||||
@ -1611,11 +1611,16 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
};
|
||||
} else {
|
||||
typeValueReference = {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: typeExpression,
|
||||
defaultImportStatement: null,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
typeValueReference = {
|
||||
kind: TypeValueReferenceKind.UNAVAILABLE,
|
||||
reason: {kind: ValueUnavailableKind.MISSING_TYPE},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, KnownDeclaration, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils';
|
||||
|
||||
import {Esm2015ReflectionHost, getClassDeclarationFromInnerDeclaration, getPropertyValueFromSymbol, isAssignmentStatement, ParamInfo} from './esm2015_host';
|
||||
@ -219,7 +219,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||
return Array.from(constructor.parameters);
|
||||
}
|
||||
|
||||
if (isSynthesizedConstructor(constructor)) {
|
||||
if (this.isSynthesizedConstructor(constructor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -352,6 +352,219 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||
const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent;
|
||||
return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
|
||||
}
|
||||
|
||||
///////////// Host Private Helpers /////////////
|
||||
|
||||
/**
|
||||
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
|
||||
* in the case no user-defined constructor exists and e.g. property initializers are used.
|
||||
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
|
||||
* compiler generates a synthetic constructor.
|
||||
*
|
||||
* We need to identify such constructors as ngcc needs to be able to tell if a class did
|
||||
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
|
||||
* empty constructor apart from a synthesized constructor, but fortunately that does not
|
||||
* matter for the code generated by ngtsc.
|
||||
*
|
||||
* When a class has a superclass however, a synthesized constructor must not be considered
|
||||
* as a user-defined constructor as that prevents a base factory call from being created by
|
||||
* ngtsc, resulting in a factory function that does not inject the dependencies of the
|
||||
* superclass. Hence, we identify a default synthesized super call in the constructor body,
|
||||
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
|
||||
*
|
||||
* Additionally, we handle synthetic delegate constructors that are emitted when TypeScript
|
||||
* downlevel's ES2015 synthetically generated to ES5. These vary slightly from the default
|
||||
* structure mentioned above because the ES2015 output uses a spread operator, for delegating
|
||||
* to the parent constructor, that is preserved through a TypeScript helper in ES5. e.g.
|
||||
*
|
||||
* ```
|
||||
* return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* Such constructs can be still considered as synthetic delegate constructors as they are
|
||||
* the product of a common TypeScript to ES5 synthetic constructor, just being downleveled
|
||||
* to ES5 using `tsc`. See: https://github.com/angular/angular/issues/38453.
|
||||
*
|
||||
*
|
||||
* @param constructor a constructor function to test
|
||||
* @returns true if the constructor appears to have been synthesized
|
||||
*/
|
||||
private isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
|
||||
if (!constructor.body) return false;
|
||||
|
||||
const firstStatement = constructor.body.statements[0];
|
||||
if (!firstStatement) return false;
|
||||
|
||||
return this.isSynthesizedSuperThisAssignment(firstStatement) ||
|
||||
this.isSynthesizedSuperReturnStatement(firstStatement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies synthesized super calls which pass-through function arguments directly and are
|
||||
* being assigned to a common `_this` variable. The following patterns we intend to match:
|
||||
*
|
||||
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
|
||||
* ```
|
||||
* var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
|
||||
* ```
|
||||
* var _this = _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
private isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
|
||||
if (!ts.isVariableStatement(statement)) return false;
|
||||
|
||||
const variableDeclarations = statement.declarationList.declarations;
|
||||
if (variableDeclarations.length !== 1) return false;
|
||||
|
||||
const variableDeclaration = variableDeclarations[0];
|
||||
if (!ts.isIdentifier(variableDeclaration.name) ||
|
||||
!variableDeclaration.name.text.startsWith('_this'))
|
||||
return false;
|
||||
|
||||
const initializer = variableDeclaration.initializer;
|
||||
if (!initializer) return false;
|
||||
|
||||
return this.isSynthesizedDefaultSuperCall(initializer);
|
||||
}
|
||||
/**
|
||||
* Identifies synthesized super calls which pass-through function arguments directly and
|
||||
* are being returned. The following patterns correspond to synthetic super return calls:
|
||||
*
|
||||
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
|
||||
* ```
|
||||
* return _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
|
||||
* ```
|
||||
* return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
private isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
|
||||
if (!ts.isReturnStatement(statement)) return false;
|
||||
|
||||
const expression = statement.expression;
|
||||
if (!expression) return false;
|
||||
|
||||
return this.isSynthesizedDefaultSuperCall(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies synthesized super calls which pass-through function arguments directly. The
|
||||
* synthetic delegate super call match the following patterns we intend to match:
|
||||
*
|
||||
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
|
||||
* ```
|
||||
* _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
|
||||
* ```
|
||||
* _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
private isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
|
||||
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
|
||||
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const left = expression.left;
|
||||
if (isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) {
|
||||
return isSuperNotNull(left.left) && this.isSuperApplyCall(left.right);
|
||||
} else {
|
||||
return this.isSuperApplyCall(left);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the expression corresponds to a `super` call passing through
|
||||
* function arguments without any modification. e.g.
|
||||
*
|
||||
* ```
|
||||
* _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
|
||||
*
|
||||
* Additionally, we also handle cases where `arguments` are wrapped by a TypeScript spread helper.
|
||||
* This can happen if ES2015 class output contain auto-generated constructors due to class
|
||||
* members. The ES2015 output will be using `super(...arguments)` to delegate to the superclass,
|
||||
* but once downleveled to ES5, the spread operator will be persisted through a TypeScript spread
|
||||
* helper. For example:
|
||||
*
|
||||
* ```
|
||||
* _super.apply(this, __spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* More details can be found in: https://github.com/angular/angular/issues/38453.
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
private isSuperApplyCall(expression: ts.Expression): boolean {
|
||||
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
|
||||
|
||||
const targetFn = expression.expression;
|
||||
if (!ts.isPropertyAccessExpression(targetFn)) return false;
|
||||
if (!isSuperIdentifier(targetFn.expression)) return false;
|
||||
if (targetFn.name.text !== 'apply') return false;
|
||||
|
||||
const thisArgument = expression.arguments[0];
|
||||
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const argumentsExpr = expression.arguments[1];
|
||||
|
||||
// If the super is directly invoked with `arguments`, return `true`. This represents the
|
||||
// common TypeScript output where the delegate constructor super call matches the following
|
||||
// pattern: `super.apply(this, arguments)`.
|
||||
if (isArgumentsIdentifier(argumentsExpr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The other scenario we intend to detect: The `arguments` variable might be wrapped with the
|
||||
// TypeScript spread helper (either through tslib or inlined). This can happen if an explicit
|
||||
// delegate constructor uses `super(...arguments)` in ES2015 and is downleveled to ES5 using
|
||||
// `--downlevelIteration`. The output in such cases would not directly pass the function
|
||||
// `arguments` to the `super` call, but wrap it in a TS spread helper. The output would match
|
||||
// the following pattern: `super.apply(this, tslib.__spread(arguments))`. We check for such
|
||||
// constructs below, but perform the detection of the call expression definition as last as
|
||||
// that is the most expensive operation here.
|
||||
if (!ts.isCallExpression(argumentsExpr) || argumentsExpr.arguments.length !== 1 ||
|
||||
!isArgumentsIdentifier(argumentsExpr.arguments[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const argumentsCallExpr = argumentsExpr.expression;
|
||||
let argumentsCallDeclaration: Declaration|null = null;
|
||||
|
||||
// The `__spread` helper could be globally available, or accessed through a namespaced
|
||||
// import. Hence we support a property access here as long as it resolves to the actual
|
||||
// known TypeScript spread helper.
|
||||
if (ts.isIdentifier(argumentsCallExpr)) {
|
||||
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr);
|
||||
} else if (
|
||||
ts.isPropertyAccessExpression(argumentsCallExpr) &&
|
||||
ts.isIdentifier(argumentsCallExpr.name)) {
|
||||
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr.name);
|
||||
}
|
||||
|
||||
return argumentsCallDeclaration !== null &&
|
||||
argumentsCallDeclaration.known === KnownDeclaration.TsHelperSpread;
|
||||
}
|
||||
}
|
||||
|
||||
///////////// Internal Helpers /////////////
|
||||
@ -422,103 +635,8 @@ function reflectArrayElement(element: ts.Expression) {
|
||||
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
|
||||
* in the case no user-defined constructor exists and e.g. property initializers are used.
|
||||
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
|
||||
* compiler generates a synthetic constructor.
|
||||
*
|
||||
* We need to identify such constructors as ngcc needs to be able to tell if a class did
|
||||
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
|
||||
* empty constructor apart from a synthesized constructor, but fortunately that does not
|
||||
* matter for the code generated by ngtsc.
|
||||
*
|
||||
* When a class has a superclass however, a synthesized constructor must not be considered
|
||||
* as a user-defined constructor as that prevents a base factory call from being created by
|
||||
* ngtsc, resulting in a factory function that does not inject the dependencies of the
|
||||
* superclass. Hence, we identify a default synthesized super call in the constructor body,
|
||||
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
|
||||
*
|
||||
* @param constructor a constructor function to test
|
||||
* @returns true if the constructor appears to have been synthesized
|
||||
*/
|
||||
function isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
|
||||
if (!constructor.body) return false;
|
||||
|
||||
const firstStatement = constructor.body.statements[0];
|
||||
if (!firstStatement) return false;
|
||||
|
||||
return isSynthesizedSuperThisAssignment(firstStatement) ||
|
||||
isSynthesizedSuperReturnStatement(firstStatement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies a synthesized super call of the form:
|
||||
*
|
||||
* ```
|
||||
* var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
function isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
|
||||
if (!ts.isVariableStatement(statement)) return false;
|
||||
|
||||
const variableDeclarations = statement.declarationList.declarations;
|
||||
if (variableDeclarations.length !== 1) return false;
|
||||
|
||||
const variableDeclaration = variableDeclarations[0];
|
||||
if (!ts.isIdentifier(variableDeclaration.name) ||
|
||||
!variableDeclaration.name.text.startsWith('_this'))
|
||||
return false;
|
||||
|
||||
const initializer = variableDeclaration.initializer;
|
||||
if (!initializer) return false;
|
||||
|
||||
return isSynthesizedDefaultSuperCall(initializer);
|
||||
}
|
||||
/**
|
||||
* Identifies a synthesized super call of the form:
|
||||
*
|
||||
* ```
|
||||
* return _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
function isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
|
||||
if (!ts.isReturnStatement(statement)) return false;
|
||||
|
||||
const expression = statement.expression;
|
||||
if (!expression) return false;
|
||||
|
||||
return isSynthesizedDefaultSuperCall(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the expression is of the form:
|
||||
*
|
||||
* ```
|
||||
* _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
function isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
|
||||
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
|
||||
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const left = expression.left;
|
||||
if (!isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) return false;
|
||||
|
||||
return isSuperNotNull(left.left) && isSuperApplyCall(left.right);
|
||||
function isArgumentsIdentifier(expression: ts.Expression): boolean {
|
||||
return ts.isIdentifier(expression) && expression.text === 'arguments';
|
||||
}
|
||||
|
||||
function isSuperNotNull(expression: ts.Expression): boolean {
|
||||
@ -526,31 +644,6 @@ function isSuperNotNull(expression: ts.Expression): boolean {
|
||||
isSuperIdentifier(expression.left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the expression is of the form
|
||||
*
|
||||
* ```
|
||||
* _super.apply(this, arguments)
|
||||
* ```
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
function isSuperApplyCall(expression: ts.Expression): boolean {
|
||||
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
|
||||
|
||||
const targetFn = expression.expression;
|
||||
if (!ts.isPropertyAccessExpression(targetFn)) return false;
|
||||
if (!isSuperIdentifier(targetFn.expression)) return false;
|
||||
if (targetFn.name.text !== 'apply') return false;
|
||||
|
||||
const thisArgument = expression.arguments[0];
|
||||
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const argumentsArgument = expression.arguments[1];
|
||||
return ts.isIdentifier(argumentsArgument) && argumentsArgument.text === 'arguments';
|
||||
}
|
||||
|
||||
function isBinaryExpr(
|
||||
expression: ts.Expression, operator: ts.BinaryOperator): expression is ts.BinaryExpression {
|
||||
return ts.isBinaryExpression(expression) && expression.operatorToken.kind === operator;
|
||||
|
@ -10,7 +10,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
|
||||
import {MockLogger} from '../../../src/ngtsc/logging/testing';
|
||||
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
|
||||
import {getDeclaration} from '../../../src/ngtsc/testing';
|
||||
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
|
||||
import {CommonJsReflectionHost} from '../../src/host/commonjs_host';
|
||||
@ -1456,6 +1456,210 @@ exports.MissingClass2 = MissingClass2;
|
||||
expect(decorators[0].args).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function getConstructorParameters(
|
||||
constructor: string, mode?: 'inlined'|'inlined_with_suffix'|'imported') {
|
||||
let fileHeader = '';
|
||||
|
||||
switch (mode) {
|
||||
case 'imported':
|
||||
fileHeader = `const tslib = require('tslib');`;
|
||||
break;
|
||||
case 'inlined':
|
||||
fileHeader =
|
||||
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
case 'inlined_with_suffix':
|
||||
fileHeader =
|
||||
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
}
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
${fileHeader}
|
||||
|
||||
var TestClass = /** @class */ (function (_super) {
|
||||
__extends(TestClass, _super);
|
||||
${constructor}
|
||||
return TestClass;
|
||||
}(null));
|
||||
|
||||
exports.TestClass = TestClass;`,
|
||||
};
|
||||
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host =
|
||||
createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
|
||||
describe('TS -> ES5: synthesized constructors', () => {
|
||||
it('recognizes _this assignment from super call', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes super call as return statement', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass(arg) {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not consider manual super calls as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super.call(this) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not consider empty constructors as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`function TestClass() {}`);
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// See: https://github.com/angular/angular/issues/38453.
|
||||
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread$1(arguments)) || this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
describe('with class member assignment', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread$1(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass(arg) {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefinitionOfFunction()', () => {
|
||||
@ -1599,7 +1803,7 @@ exports.MissingClass2 = MissingClass2;
|
||||
isNamedVariableDeclaration);
|
||||
const ctrDecorators = host.getConstructorParameters(classNode)!;
|
||||
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: ts.Identifier,
|
||||
defaultImportStatement: null,
|
||||
}).expression;
|
||||
|
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
|
||||
import {MockLogger} from '../../../src/ngtsc/logging/testing';
|
||||
import {ClassMemberKind, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
|
||||
import {ClassMemberKind, isNamedVariableDeclaration, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
|
||||
import {getDeclaration} from '../../../src/ngtsc/testing';
|
||||
import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers';
|
||||
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
|
||||
@ -484,7 +484,7 @@ runInEachFileSystem(() => {
|
||||
isNamedVariableDeclaration);
|
||||
const ctrDecorators = host.getConstructorParameters(classNode)!;
|
||||
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: ts.Identifier,
|
||||
defaultImportStatement: null,
|
||||
}).expression;
|
||||
|
@ -10,7 +10,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
|
||||
import {MockLogger} from '../../../src/ngtsc/logging/testing';
|
||||
import {ClassMemberKind, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
|
||||
import {ClassMemberKind, isNamedFunctionDeclaration, isNamedVariableDeclaration, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
|
||||
import {getDeclaration} from '../../../src/ngtsc/testing';
|
||||
import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers';
|
||||
import {getIifeBody} from '../../src/host/esm2015_host';
|
||||
@ -544,7 +544,7 @@ export { AliasedDirective$1 };
|
||||
isNamedVariableDeclaration);
|
||||
const ctrDecorators = host.getConstructorParameters(classNode)!;
|
||||
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: ts.Identifier,
|
||||
defaultImportStatement: null,
|
||||
}).expression;
|
||||
|
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
|
||||
import {MockLogger} from '../../../src/ngtsc/logging/testing';
|
||||
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, Decorator, DownleveledEnum, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, Decorator, DownleveledEnum, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
|
||||
import {getDeclaration} from '../../../src/ngtsc/testing';
|
||||
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
|
||||
import {DelegatingReflectionHost} from '../../src/host/delegating_host';
|
||||
@ -1417,86 +1417,236 @@ runInEachFileSystem(() => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('synthesized constructors', () => {
|
||||
function getConstructorParameters(constructor: string) {
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
function getConstructorParameters(
|
||||
constructor: string,
|
||||
mode?: 'inlined'|'inlined_with_suffix'|'imported'|'imported_namespace') {
|
||||
let fileHeader = '';
|
||||
|
||||
switch (mode) {
|
||||
case 'imported':
|
||||
fileHeader = `import {__spread} from 'tslib';`;
|
||||
break;
|
||||
case 'imported_namespace':
|
||||
fileHeader = `import * as tslib from 'tslib';`;
|
||||
break;
|
||||
case 'inlined':
|
||||
fileHeader =
|
||||
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
case 'inlined_with_suffix':
|
||||
fileHeader =
|
||||
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
}
|
||||
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
${fileHeader}
|
||||
var TestClass = /** @class */ (function (_super) {
|
||||
__extends(TestClass, _super);
|
||||
${constructor}
|
||||
return TestClass;
|
||||
}(null));
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
|
||||
describe('TS -> ES5: synthesized constructors', () => {
|
||||
it('recognizes _this assignment from super call', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`);
|
||||
function TestClass() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes super call as return statement', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}`);
|
||||
function TestClass() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}`);
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass(arg) {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}`);
|
||||
function TestClass(arg) {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not consider manual super calls as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super.call(this) || this;
|
||||
}`);
|
||||
function TestClass() {
|
||||
return _super.call(this) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not consider empty constructors as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
}`);
|
||||
|
||||
const parameters = getConstructorParameters(`function TestClass() {}`);
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// See: https://github.com/angular/angular/issues/38453.
|
||||
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread$1(arguments)) || this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using namespace imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
}`,
|
||||
'imported_namespace');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
describe('with class member assignment', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread$1(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using namespace imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'imported_namespace');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass(arg) {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(returned parameters `decorators.args`)', () => {
|
||||
it('should be an empty array if param decorator has no `args` property', () => {
|
||||
loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]);
|
||||
@ -1670,7 +1820,7 @@ runInEachFileSystem(() => {
|
||||
bundle.program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration);
|
||||
const ctrDecorators = host.getConstructorParameters(classNode)!;
|
||||
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: ts.Identifier,
|
||||
defaultImportStatement: null,
|
||||
}).expression;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user