Compare commits
144 Commits
zone.js-0.
...
11.0.0-nex
Author | SHA1 | Date | |
---|---|---|---|
a69507a0ad | |||
886f58d4fe | |||
18f84a0328 | |||
b0ca3cd0c4 | |||
3d77b64fc3 | |||
f645d26e3f | |||
6d9bfb8368 | |||
7997fc5f97 | |||
3cb2a79399 | |||
d896c33b0e | |||
19a484302d | |||
ded075a79c | |||
ba95b79a21 | |||
4faac78e32 | |||
4360eed9b7 | |||
c880e393e9 | |||
b0c79f2373 | |||
a32a317ea1 | |||
bfb7eec698 | |||
7e0b3fd953 | |||
7a6a061a9e | |||
109555b33a | |||
bf31ef29f6 | |||
40096bee00 | |||
45a73dddfd | |||
687477279b | |||
4007422cc6 | |||
1150649139 | |||
7869de6136 | |||
2c4a98a285 | |||
92ff6d93eb | |||
83ace4ed30 | |||
926ffcd8ac | |||
97475d7408 | |||
a29f9a9fb3 | |||
9f28f82598 | |||
261f689e9b | |||
1d9873c44c | |||
d9da7e5a18 | |||
79d8795633 | |||
3e57ca1d98 | |||
c2d017de83 | |||
7baa7ebfc4 | |||
4e5286180b | |||
73001b42fe | |||
c90eb5450d | |||
3e97435f1c | |||
1c7e5cef3e | |||
2cb3d58b42 | |||
44bb85ade4 | |||
50f4d8a1ce | |||
fdea1804d6 | |||
1d8c5d88cd | |||
a68f1a78a7 | |||
86e11f1110 | |||
5da1934115 | |||
86e7cd8117 | |||
e6ee7c2aeb | |||
54687f7765 | |||
59c234cfb4 | |||
a6f3cd93a9 | |||
d9fea857db | |||
03dbcc7a56 | |||
c142b071eb | |||
71acf9dd49 | |||
f5a148b1b7 | |||
4f28192d62 | |||
0fc2bef0cd | |||
f5d1e9a2d1 | |||
036a2faf02 | |||
5be4edfa17 | |||
38d6596742 | |||
0a7a5e3aff | |||
d5fabc303d | |||
ebc0e46501 | |||
3487b549fd | |||
52c7a4bfc6 | |||
827ba05914 | |||
b2857b4e3a | |||
5d5caf21b8 | |||
c1bc070b40 | |||
930eeaf177 | |||
2dd29fbae7 | |||
9613660fee | |||
c0523fc3b4 | |||
de1cffb23b | |||
31f4557621 | |||
7723bfd9ba | |||
e8ea839df8 | |||
90cec40cce | |||
4036281007 | |||
164cd274a4 | |||
fedcfec346 | |||
618cb32407 | |||
4aee0087ea | |||
0681a20d28 | |||
fb06903237 | |||
ecc6fd0d28 | |||
80cab26023 | |||
841dfa68f9 | |||
f0af387f6c | |||
14e90bef58 | |||
db3a21b382 | |||
b8351f3b10 | |||
81053d3160 | |||
bdba1a062d | |||
23f855b300 | |||
94a3e0e81d | |||
9cbde86534 | |||
18e474f522 | |||
cb3db0d31b | |||
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 |
@ -653,8 +653,10 @@ jobs:
|
|||||||
name: Starting Saucelabs tunnel service
|
name: Starting Saucelabs tunnel service
|
||||||
command: ./tools/saucelabs/sauce-service.sh run
|
command: ./tools/saucelabs/sauce-service.sh run
|
||||||
background: true
|
background: true
|
||||||
- run: yarn tsc -p packages
|
# add module umd tsc compile option so the test can work
|
||||||
- run: yarn tsc -p modules
|
# properly in the legacy browsers
|
||||||
|
- run: yarn tsc -p packages --module UMD
|
||||||
|
- run: yarn tsc -p modules --module UMD
|
||||||
- run: yarn bazel build //packages/zone.js:npm_package
|
- run: yarn bazel build //packages/zone.js:npm_package
|
||||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
# 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
|
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
|
||||||
|
28
.github/angular-robot.yml
vendored
28
.github/angular-robot.yml
vendored
@ -73,15 +73,14 @@ merge:
|
|||||||
- "packages/zone.js/scripts/**"
|
- "packages/zone.js/scripts/**"
|
||||||
|
|
||||||
# comment that will be added to a PR when there is a conflict, leave empty or set to false to disable
|
# comment that will be added to a PR when there is a conflict, leave empty or set to false to disable
|
||||||
mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges.
|
mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges.\nPlease help to unblock it by resolving these conflicts. Thanks!"
|
||||||
\nPlease help to unblock it by resolving these conflicts. Thanks!"
|
|
||||||
|
|
||||||
# label to monitor
|
# label to monitor
|
||||||
mergeLabel: "PR action: merge"
|
mergeLabel: "action: merge"
|
||||||
|
|
||||||
# adding any of these labels will also add the merge label
|
# adding any of these labels will also add the merge label
|
||||||
mergeLinkedLabels:
|
mergeLinkedLabels:
|
||||||
- "PR action: merge-assistance"
|
- "action: merge-assistance"
|
||||||
|
|
||||||
# list of checks that will determine if the merge label can be added
|
# list of checks that will determine if the merge label can be added
|
||||||
checks:
|
checks:
|
||||||
@ -94,17 +93,17 @@ merge:
|
|||||||
|
|
||||||
# whether the PR shouldn't have a conflict with the base branch
|
# whether the PR shouldn't have a conflict with the base branch
|
||||||
noConflict: true
|
noConflict: true
|
||||||
# list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master")
|
# list of labels that a PR needs to have, checked with a regexp (e.g. "target:" will work for the label "target: master")
|
||||||
requiredLabels:
|
requiredLabels:
|
||||||
- "PR target: *"
|
- "target: *"
|
||||||
- "cla: yes"
|
- "cla: yes"
|
||||||
|
|
||||||
# list of labels that a PR shouldn't have, checked after the required labels with a regexp
|
# list of labels that a PR shouldn't have, checked after the required labels with a regexp
|
||||||
forbiddenLabels:
|
forbiddenLabels:
|
||||||
- "PR target: TBD"
|
- "target: TBD"
|
||||||
- "PR action: cleanup"
|
- "action: cleanup"
|
||||||
- "PR action: review"
|
- "action: review"
|
||||||
- "PR state: blocked"
|
- "state: blocked"
|
||||||
- "cla: no"
|
- "cla: no"
|
||||||
|
|
||||||
# list of PR statuses that need to be successful
|
# list of PR statuses that need to be successful
|
||||||
@ -121,12 +120,7 @@ merge:
|
|||||||
# the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable
|
# the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable
|
||||||
# {{MERGE_LABEL}} will be replaced by the value of the mergeLabel option
|
# {{MERGE_LABEL}} will be replaced by the value of the mergeLabel option
|
||||||
# {{PLACEHOLDER}} will be replaced by the list of failing checks
|
# {{PLACEHOLDER}} will be replaced by the list of failing checks
|
||||||
mergeRemovedComment: "I see that you just added the `{{MERGE_LABEL}}` label, but the following checks are still failing:
|
mergeRemovedComment: "I see that you just added the `{{MERGE_LABEL}}` label, but the following checks are still failing:\n{{PLACEHOLDER}}\n\n**If you want your PR to be merged, it has to pass all the CI checks.**\n\nIf you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help."
|
||||||
\n{{PLACEHOLDER}}
|
|
||||||
\n
|
|
||||||
\n**If you want your PR to be merged, it has to pass all the CI checks.**
|
|
||||||
\n
|
|
||||||
\nIf you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help."
|
|
||||||
|
|
||||||
# options for the triage plugin
|
# options for the triage plugin
|
||||||
triage:
|
triage:
|
||||||
@ -186,4 +180,4 @@ rerunCircleCI:
|
|||||||
# set to true to disable
|
# set to true to disable
|
||||||
disabled: false
|
disabled: false
|
||||||
# the label which when added triggers a rerun of the default CircleCI workflow
|
# the label which when added triggers a rerun of the default CircleCI workflow
|
||||||
triggerRerunLabel: "PR action: rerun CI at HEAD"
|
triggerRerunLabel: "action: rerun CI at HEAD"
|
||||||
|
2
.github/workflows/lock-closed.yml
vendored
2
.github/workflows/lock-closed.yml
vendored
@ -10,6 +10,6 @@ jobs:
|
|||||||
if: github.repository == 'angular/angular'
|
if: github.repository == 'angular/angular'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: angular/dev-infra/github-actions/lock-closed@66462f6
|
- uses: angular/dev-infra/github-actions/lock-closed@414834b2b24dd2df37c6ed00808387ee6fd91b66
|
||||||
with:
|
with:
|
||||||
lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }}
|
lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }}
|
||||||
|
19
.ng-dev/caretaker.ts
Normal file
19
.ng-dev/caretaker.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {CaretakerConfig} from '../dev-infra/caretaker/config';
|
||||||
|
|
||||||
|
/** The configuration for `ng-dev caretaker` commands. */
|
||||||
|
export const caretaker: CaretakerConfig = {
|
||||||
|
githubQueries: [
|
||||||
|
{
|
||||||
|
name: 'Merge Queue',
|
||||||
|
query: `is:pr is:open status:success label:"action: merge"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Merge Assistance Queue',
|
||||||
|
query: `is:pr is:open status:success label:"action: merge-assistance"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Primary Triage Queue',
|
||||||
|
query: `is:open is:issue no:milestone`,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
|
import {caretaker} from './caretaker';
|
||||||
import {commitMessage} from './commit-message';
|
import {commitMessage} from './commit-message';
|
||||||
import {format} from './format';
|
import {format} from './format';
|
||||||
import {github} from './github';
|
import {github} from './github';
|
||||||
@ -8,4 +9,5 @@ module.exports = {
|
|||||||
format,
|
format,
|
||||||
github,
|
github,
|
||||||
merge,
|
merge,
|
||||||
|
caretaker,
|
||||||
};
|
};
|
||||||
|
@ -1,38 +1,25 @@
|
|||||||
import {MergeConfig} from '../dev-infra/pr/merge/config';
|
import {DevInfraMergeConfig} from '../dev-infra/pr/merge/config';
|
||||||
|
import {getDefaultTargetLabelConfiguration} from '../dev-infra/pr/merge/defaults';
|
||||||
|
import {github} from './github';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for the merge tool in `ng-dev`. This sets up the labels which
|
* Configuration for the merge tool in `ng-dev`. This sets up the labels which
|
||||||
* are respected by the merge script (e.g. the target labels).
|
* are respected by the merge script (e.g. the target labels).
|
||||||
*/
|
*/
|
||||||
export const merge = (): MergeConfig => {
|
export const merge: DevInfraMergeConfig['merge'] = async api => {
|
||||||
// TODO: resume dynamically determining patch branch
|
|
||||||
const patch = '10.0.x';
|
|
||||||
return {
|
return {
|
||||||
githubApiMerge: false,
|
githubApiMerge: false,
|
||||||
claSignedLabel: 'cla: yes',
|
claSignedLabel: 'cla: yes',
|
||||||
mergeReadyLabel: /^PR action: merge(-assistance)?/,
|
mergeReadyLabel: /^action: merge(-assistance)?/,
|
||||||
caretakerNoteLabel: 'PR action: merge-assistance',
|
caretakerNoteLabel: 'action: merge-assistance',
|
||||||
commitMessageFixupLabel: 'commit message fixup',
|
commitMessageFixupLabel: 'commit message fixup',
|
||||||
labels: [
|
labels: await getDefaultTargetLabelConfiguration(api, github, '@angular/core'),
|
||||||
{
|
|
||||||
pattern: 'PR target: master-only',
|
|
||||||
branches: ['master'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: 'PR target: patch-only',
|
|
||||||
branches: [patch],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: 'PR target: master & patch',
|
|
||||||
branches: ['master', patch],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
requiredBaseCommits: {
|
requiredBaseCommits: {
|
||||||
// PRs that target either `master` or the patch branch, need to be rebased
|
// PRs that target either `master` or the patch branch, need to be rebased
|
||||||
// on top of the latest commit message validation fix.
|
// on top of the latest commit message validation fix.
|
||||||
// These SHAs are the commits that update the required license text in the header.
|
// These SHAs are the commits that update the required license text in the header.
|
||||||
'master': '5aeb9a4124922d8ac08eb73b8f322905a32b0b3a',
|
'master': '5aeb9a4124922d8ac08eb73b8f322905a32b0b3a',
|
||||||
[patch]: '27b95ba64a5d99757f4042073fd1860e20e3ed24'
|
'10.0.x': '27b95ba64a5d99757f4042073fd1860e20e3ed24',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
287
CHANGELOG.md
287
CHANGELOG.md
@ -1,26 +1,175 @@
|
|||||||
<a name="10.1.0-next.6"></a>
|
<a name="11.0.0-next.1"></a>
|
||||||
# 10.1.0-next.6 (2020-08-17)
|
# 11.0.0-next.1 (2020-09-09)
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
### 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)
|
* **compiler-cli:** compute source-mappings for localized strings ([#38645](https://github.com/angular/angular/issues/38645)) ([7e0b3fd](https://github.com/angular/angular/commit/7e0b3fd)), closes [#38588](https://github.com/angular/angular/issues/38588)
|
||||||
* **core:** 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))
|
* **core:** remove CollectionChangeRecord symbol ([#38668](https://github.com/angular/angular/issues/38668)) ([fdea180](https://github.com/angular/angular/commit/fdea180))
|
||||||
* **localize:** render ICU placeholders in extracted translation files ([#38484](https://github.com/angular/angular/issues/38484)) ([81c3e80](https://github.com/angular/angular/commit/81c3e80))
|
* **router:** support lazy loading for empty path named outlets ([#38379](https://github.com/angular/angular/issues/38379)) ([926ffcd](https://github.com/angular/angular/commit/926ffcd)), closes [#12842](https://github.com/angular/angular/issues/12842)
|
||||||
* **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)
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* **core:** CollectionChangeRecord has been removed, use IterableChangeRecord
|
||||||
|
instead
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="10.1.1"></a>
|
||||||
|
## 10.1.1 (2020-09-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **compiler:** correct confusion between field and property names ([#38685](https://github.com/angular/angular/issues/38685)) ([a1c34c6](https://github.com/angular/angular/commit/a1c34c6))
|
||||||
|
* **compiler-cli:** compute source-mappings for localized strings ([#38747](https://github.com/angular/angular/issues/38747)) ([b4eb016](https://github.com/angular/angular/commit/b4eb016)), closes [#38588](https://github.com/angular/angular/issues/38588)
|
||||||
|
* **compiler-cli:** ensure that a declaration is available in type-to-value conversion ([#38684](https://github.com/angular/angular/issues/38684)) ([56d5ff2](https://github.com/angular/angular/commit/56d5ff2)), closes [#38670](https://github.com/angular/angular/issues/38670)
|
||||||
|
* **core:** reset `tView` between tests in Ivy TestBed ([#38659](https://github.com/angular/angular/issues/38659)) ([efc7606](https://github.com/angular/angular/commit/efc7606)), closes [#38600](https://github.com/angular/angular/issues/38600)
|
||||||
|
* **localize:** do not expose NodeJS typings in $localize runtime code ([#38700](https://github.com/angular/angular/issues/38700)) ([4de8dc3](https://github.com/angular/angular/commit/4de8dc3)), closes [#38692](https://github.com/angular/angular/issues/38692)
|
||||||
|
* **localize:** enable whitespace preservation marker in XLIFF files ([#38737](https://github.com/angular/angular/issues/38737)) ([190dca0](https://github.com/angular/angular/commit/190dca0)), closes [#38679](https://github.com/angular/angular/issues/38679)
|
||||||
|
* **localize:** install `[@angular](https://github.com/angular)/localize` in `devDependencies` by default ([#38680](https://github.com/angular/angular/issues/38680)) ([dbab744](https://github.com/angular/angular/commit/dbab744)), closes [#38329](https://github.com/angular/angular/issues/38329)
|
||||||
|
* **localize:** render context of translation file parse errors ([#38673](https://github.com/angular/angular/issues/38673)) ([32f33f0](https://github.com/angular/angular/commit/32f33f0)), closes [#38377](https://github.com/angular/angular/issues/38377)
|
||||||
|
* **localize:** render location in XLIFF 2 even if there is no metadata ([#38713](https://github.com/angular/angular/issues/38713)) ([ab4f953](https://github.com/angular/angular/commit/ab4f953)), closes [#38705](https://github.com/angular/angular/issues/38705)
|
||||||
|
* **ngcc:** use aliased exported types correctly ([#38666](https://github.com/angular/angular/issues/38666)) ([6a28675](https://github.com/angular/angular/commit/6a28675)), closes [#38238](https://github.com/angular/angular/issues/38238)
|
||||||
|
* **router:** If users are using the Alt key when clicking the router links, prioritize browser’s default behavior ([#38375](https://github.com/angular/angular/issues/38375)) ([309709d](https://github.com/angular/angular/commit/309709d))
|
||||||
|
|
||||||
|
|
||||||
### Performance Improvements
|
### 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))
|
* **core:** use `ngDevMode` to tree-shake error messages ([#38612](https://github.com/angular/angular/issues/38612)) ([b084bff](https://github.com/angular/angular/commit/b084bff))
|
||||||
* **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))
|
|
||||||
|
|
||||||
|
<a name="11.0.0-next.0"></a>
|
||||||
|
# 11.0.0-next.0 (2020-09-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **forms:** ensure to emit `statusChanges` on subsequent value update/validations ([#38354](https://github.com/angular/angular/issues/38354)) ([d9fea85](https://github.com/angular/angular/commit/d9fea85)), closes [#20424](https://github.com/angular/angular/issues/20424) [#14542](https://github.com/angular/angular/issues/14542)
|
||||||
|
* **service-worker:** fix condition to check for a cache-busted request ([#36847](https://github.com/angular/angular/issues/36847)) ([5be4edf](https://github.com/angular/angular/commit/5be4edf))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **service-worker:** add `UnrecoverableStateError` ([#36847](https://github.com/angular/angular/issues/36847)) ([036a2fa](https://github.com/angular/angular/commit/036a2fa)), closes [#36539](https://github.com/angular/angular/issues/36539)
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* **forms:** Previously if FormControl, FormGroup and FormArray class instances had async validators
|
||||||
|
defined at initialization time, the status change event was not emitted once async validator
|
||||||
|
completed. After this change the status event is emitted into the `statusChanges` observable.
|
||||||
|
If your code relies on the old behavior, you can filter/ignore this additional status change
|
||||||
|
event.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="10.1.0"></a>
|
||||||
|
# 10.1.0 (2020-09-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **bazel:** provide LinkablePackageInfo from ng_module ([#37623](https://github.com/angular/angular/issues/37623)) ([6898eab](https://github.com/angular/angular/commit/6898eab))
|
||||||
|
* **common:** add ReadonlyMap in place of Map in keyValuePipe ([#37311](https://github.com/angular/angular/issues/37311)) ([3373453](https://github.com/angular/angular/commit/3373453)), closes [#37308](https://github.com/angular/angular/issues/37308)
|
||||||
|
* **compiler-cli:** add `SourceFile.getOriginalLocation()` to sourcemaps package ([#32912](https://github.com/angular/angular/issues/32912)) ([6abb8d0](https://github.com/angular/angular/commit/6abb8d0))
|
||||||
|
* **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))
|
||||||
|
* **compiler-cli:** add support for TypeScript 4.0 ([#38076](https://github.com/angular/angular/issues/38076)) ([0fc44e0](https://github.com/angular/angular/commit/0fc44e0))
|
||||||
|
* **compiler-cli:** explain why an expression cannot be used in AOT compilations ([#37587](https://github.com/angular/angular/issues/37587)) ([712f1bd](https://github.com/angular/angular/commit/712f1bd))
|
||||||
|
* **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)
|
||||||
|
* **core:** rename async to waitForAsync to avoid confusing ([#37583](https://github.com/angular/angular/issues/37583)) ([8f07429](https://github.com/angular/angular/commit/8f07429))
|
||||||
|
* **core:** support injection token as predicate in queries ([#37506](https://github.com/angular/angular/issues/37506)) ([97dc85b](https://github.com/angular/angular/commit/97dc85b)), closes [#21152](https://github.com/angular/angular/issues/21152) [#36144](https://github.com/angular/angular/issues/36144)
|
||||||
|
* **core:** update reference and doc to change `async` to `waitAsync`. ([#37583](https://github.com/angular/angular/issues/37583)) ([8fbf40b](https://github.com/angular/angular/commit/8fbf40b))
|
||||||
|
* **forms:** AbstractControl to store raw validators in addition to combined validators function ([#37881](https://github.com/angular/angular/issues/37881)) ([ad7046b](https://github.com/angular/angular/commit/ad7046b))
|
||||||
|
* **localize:** allow duplicate messages to be handled during extraction ([#38082](https://github.com/angular/angular/issues/38082)) ([cf9a47b](https://github.com/angular/angular/commit/cf9a47b)), closes [#38077](https://github.com/angular/angular/issues/38077)
|
||||||
|
* **localize:** expose `canParse()` diagnostics ([#37909](https://github.com/angular/angular/issues/37909)) ([ec32eba](https://github.com/angular/angular/commit/ec32eba)), closes [#37901](https://github.com/angular/angular/issues/37901)
|
||||||
|
* **localize:** implement message extraction tool ([#32912](https://github.com/angular/angular/issues/32912)) ([190561d](https://github.com/angular/angular/commit/190561d))
|
||||||
|
* **platform-browser:** Allow `sms`-URLs ([#31463](https://github.com/angular/angular/issues/31463)) ([fc5c34d](https://github.com/angular/angular/commit/fc5c34d)), closes [#31462](https://github.com/angular/angular/issues/31462)
|
||||||
|
* **platform-server:** add option for absolute URL HTTP support ([#37539](https://github.com/angular/angular/issues/37539)) ([d37049a](https://github.com/angular/angular/commit/d37049a)), closes [#37071](https://github.com/angular/angular/issues/37071)
|
||||||
|
* **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))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **bazel:** fix integration test for bazel building ([#38629](https://github.com/angular/angular/issues/38629)) ([dd82f2f](https://github.com/angular/angular/commit/dd82f2f))
|
||||||
|
* **common:** date pipe gives wrong week number ([#37632](https://github.com/angular/angular/issues/37632)) ([ef1fb6d](https://github.com/angular/angular/commit/ef1fb6d)), closes [#33961](https://github.com/angular/angular/issues/33961)
|
||||||
|
* **common:** narrow `NgIf` context variables in template type checker ([#36627](https://github.com/angular/angular/issues/36627)) ([9c8bc4a](https://github.com/angular/angular/commit/9c8bc4a))
|
||||||
|
* **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:** ensure source-maps can handle webpack:// protocol ([#32912](https://github.com/angular/angular/issues/32912)) ([decd95e](https://github.com/angular/angular/commit/decd95e))
|
||||||
|
* **compiler-cli:** only read source-map comment from last line ([#32912](https://github.com/angular/angular/issues/32912)) ([07a07e3](https://github.com/angular/angular/commit/07a07e3))
|
||||||
|
* **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))
|
||||||
|
* **compiler:** incorrectly inferring namespace for HTML nodes inside SVG ([#38477](https://github.com/angular/angular/issues/38477)) ([0dda97e](https://github.com/angular/angular/commit/0dda97e)), closes [#37218](https://github.com/angular/angular/issues/37218)
|
||||||
|
* **compiler:** mark `NgModuleFactory` construction as not side effectful ([#38147](https://github.com/angular/angular/issues/38147)) ([7f8c222](https://github.com/angular/angular/commit/7f8c222))
|
||||||
|
* **core:** Allow modification of lifecycle hooks any time before bootstrap ([#35464](https://github.com/angular/angular/issues/35464)) ([737506e](https://github.com/angular/angular/commit/737506e)), closes [#30497](https://github.com/angular/angular/issues/30497)
|
||||||
|
* **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:** determine required DOMParser feature availability ([#36578](https://github.com/angular/angular/issues/36578)) ([#36578](https://github.com/angular/angular/issues/36578)) ([c509243](https://github.com/angular/angular/commit/c509243))
|
||||||
|
* **core:** do not trigger CSP alert/report in Firefox and Chrome ([#36578](https://github.com/angular/angular/issues/36578)) ([#36578](https://github.com/angular/angular/issues/36578)) ([b950d46](https://github.com/angular/angular/commit/b950d46)), closes [#25214](https://github.com/angular/angular/issues/25214)
|
||||||
|
* **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))
|
||||||
|
* **elements:** run strategy methods in correct zone ([#37814](https://github.com/angular/angular/issues/37814)) ([8df888d](https://github.com/angular/angular/commit/8df888d)), closes [#24181](https://github.com/angular/angular/issues/24181)
|
||||||
|
* **forms:** handle form groups/arrays own pending async validation ([#22575](https://github.com/angular/angular/issues/22575)) ([77b62a5](https://github.com/angular/angular/commit/77b62a5)), closes [#10064](https://github.com/angular/angular/issues/10064)
|
||||||
|
* **language-service:** non-existent module format in package output ([#37623](https://github.com/angular/angular/issues/37623)) ([413a0fb](https://github.com/angular/angular/commit/413a0fb))
|
||||||
|
* **localize:** ensure required XLIFF parameters are serialized ([#38575](https://github.com/angular/angular/issues/38575)) ([f0af387](https://github.com/angular/angular/commit/f0af387)), closes [#38570](https://github.com/angular/angular/issues/38570)
|
||||||
|
* **localize:** extract the correct message ids ([#38498](https://github.com/angular/angular/issues/38498)) ([ac461e1](https://github.com/angular/angular/commit/ac461e1))
|
||||||
|
* **localize:** render ICU placeholders in extracted translation files ([#38484](https://github.com/angular/angular/issues/38484)) ([81c3e80](https://github.com/angular/angular/commit/81c3e80))
|
||||||
|
* **localize:** render text of extracted placeholders ([#38536](https://github.com/angular/angular/issues/38536)) ([14e90be](https://github.com/angular/angular/commit/14e90be))
|
||||||
|
* **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)
|
||||||
|
* **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:** fix navigation ignoring logic to compare to the browser url ([#37716](https://github.com/angular/angular/issues/37716)) ([a5ffca0](https://github.com/angular/angular/commit/a5ffca0)), closes [#16710](https://github.com/angular/angular/issues/16710) [#13586](https://github.com/angular/angular/issues/13586)
|
||||||
|
* **router:** properly compare array queryParams for equality ([#37709](https://github.com/angular/angular/issues/37709)) ([#37860](https://github.com/angular/angular/issues/37860)) ([1801d0c](https://github.com/angular/angular/commit/1801d0c))
|
||||||
|
* **router:** remove parenthesis for primary outlet segment after removing auxiliary outlet segment ([#24656](https://github.com/angular/angular/issues/24656)) ([#37163](https://github.com/angular/angular/issues/37163)) ([71f008f](https://github.com/angular/angular/commit/71f008f))
|
||||||
|
* **router:** restore 'history.state' object for navigations coming from Angular router ([#28108](https://github.com/angular/angular/issues/28108)) ([#28176](https://github.com/angular/angular/issues/28176)) ([df76a20](https://github.com/angular/angular/commit/df76a20))
|
||||||
|
|
||||||
### Code Refactoring
|
### Code Refactoring
|
||||||
* **router:** export DefaultRouteReuseStrategy to Router public_api ([#31575](https://github.com/angular/angular/issues/31575)) ([ca79880](https://github.com/angular/angular/commit/ca79880))
|
* **router:** export DefaultRouteReuseStrategy to Router public_api ([#31575](https://github.com/angular/angular/issues/31575)) ([ca79880](https://github.com/angular/angular/commit/ca79880))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 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:** fix regressions in incremental program reuse ([#37641](https://github.com/angular/angular/issues/37641)) ([5103d90](https://github.com/angular/angular/commit/5103d90))
|
||||||
|
* **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))
|
||||||
|
* **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)
|
||||||
|
* **ngcc:** shortcircuit tokenizing in ESM dependency host ([#37639](https://github.com/angular/angular/issues/37639)) ([bd7f440](https://github.com/angular/angular/commit/bd7f440))
|
||||||
|
* **ngcc:** use `EntryPointManifest` to speed up noop `ProgramBaseEntryPointFinder` ([#37665](https://github.com/angular/angular/issues/37665)) ([9318e23](https://github.com/angular/angular/commit/9318e23))
|
||||||
|
* **router:** apply prioritizedGuardValue operator to optimize CanLoad guards ([#37523](https://github.com/angular/angular/issues/37523)) ([d7dd295](https://github.com/angular/angular/commit/d7dd295))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="10.0.14"></a>
|
||||||
|
## 10.0.14 (2020-08-26)
|
||||||
|
|
||||||
|
|
||||||
|
<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.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.0.10"></a>
|
<a name="10.0.10"></a>
|
||||||
## 10.0.10 (2020-08-17)
|
## 10.0.10 (2020-08-17)
|
||||||
|
|
||||||
@ -36,25 +185,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<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>
|
<a name="10.0.9"></a>
|
||||||
## 10.0.9 (2020-08-12)
|
## 10.0.9 (2020-08-12)
|
||||||
|
|
||||||
@ -76,24 +206,6 @@
|
|||||||
* **service-worker:** fix the chrome debugger syntax highlighter ([#38332](https://github.com/angular/angular/issues/38332)) ([f5d5bac](https://github.com/angular/angular/commit/f5d5bac))
|
* **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)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **common:** narrow `NgIf` context variables in template type checker ([#36627](https://github.com/angular/angular/issues/36627)) ([9c8bc4a](https://github.com/angular/angular/commit/9c8bc4a))
|
|
||||||
* **compiler:** mark `NgModuleFactory` construction as not side effectful ([#38147](https://github.com/angular/angular/issues/38147)) ([7f8c222](https://github.com/angular/angular/commit/7f8c222))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **core:** rename async to waitForAsync to avoid confusing ([#37583](https://github.com/angular/angular/issues/37583)) ([8f07429](https://github.com/angular/angular/commit/8f07429))
|
|
||||||
* **core:** update reference and doc to change `async` to `waitAsync`. ([#37583](https://github.com/angular/angular/issues/37583)) ([8fbf40b](https://github.com/angular/angular/commit/8fbf40b))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="10.0.8"></a>
|
<a name="10.0.8"></a>
|
||||||
## 10.0.8 (2020-08-04)
|
## 10.0.8 (2020-08-04)
|
||||||
|
|
||||||
@ -115,16 +227,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="10.1.0-next.3"></a>
|
|
||||||
# 10.1.0-next.3 (2020-07-28)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **elements:** run strategy methods in correct zone ([#37814](https://github.com/angular/angular/issues/37814)) ([8df888d](https://github.com/angular/angular/commit/8df888d)), closes [#24181](https://github.com/angular/angular/issues/24181)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="10.0.6"></a>
|
<a name="10.0.6"></a>
|
||||||
## 10.0.6 (2020-07-28)
|
## 10.0.6 (2020-07-28)
|
||||||
|
|
||||||
@ -138,23 +240,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="10.1.0-next.2"></a>
|
|
||||||
# 10.1.0-next.2 (2020-07-22)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **core:** Allow modification of lifecycle hooks any time before bootstrap ([#35464](https://github.com/angular/angular/issues/35464)) ([737506e](https://github.com/angular/angular/commit/737506e)), closes [#30497](https://github.com/angular/angular/issues/30497)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **common:** add ReadonlyMap in place of Map in keyValuePipe ([#37311](https://github.com/angular/angular/issues/37311)) ([3373453](https://github.com/angular/angular/commit/3373453)), closes [#37308](https://github.com/angular/angular/issues/37308)
|
|
||||||
* **forms:** AbstractControl to store raw validators in addition to combined validators function ([#37881](https://github.com/angular/angular/issues/37881)) ([ad7046b](https://github.com/angular/angular/commit/ad7046b))
|
|
||||||
* **localize:** allow duplicate messages to be handled during extraction ([#38082](https://github.com/angular/angular/issues/38082)) ([cf9a47b](https://github.com/angular/angular/commit/cf9a47b)), closes [#38077](https://github.com/angular/angular/issues/38077)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="10.0.5"></a>
|
<a name="10.0.5"></a>
|
||||||
## 10.0.5 (2020-07-22)
|
## 10.0.5 (2020-07-22)
|
||||||
|
|
||||||
@ -189,62 +274,6 @@
|
|||||||
* **bazel:** provide LinkablePackageInfo from ng_module ([#37778](https://github.com/angular/angular/issues/37778)) ([6cd10a1](https://github.com/angular/angular/commit/6cd10a1)), closes [/github.com/bazelbuild/rules_nodejs/blob/9a5de3728b05bf1647bbb87ad99f54e626604705/internal/linker/link_node_modules.bzl#L144-L146](https://github.com//github.com/bazelbuild/rules_nodejs/blob/9a5de3728b05bf1647bbb87ad99f54e626604705/internal/linker/link_node_modules.bzl/issues/L144-L146)
|
* **bazel:** provide LinkablePackageInfo from ng_module ([#37778](https://github.com/angular/angular/issues/37778)) ([6cd10a1](https://github.com/angular/angular/commit/6cd10a1)), closes [/github.com/bazelbuild/rules_nodejs/blob/9a5de3728b05bf1647bbb87ad99f54e626604705/internal/linker/link_node_modules.bzl#L144-L146](https://github.com//github.com/bazelbuild/rules_nodejs/blob/9a5de3728b05bf1647bbb87ad99f54e626604705/internal/linker/link_node_modules.bzl/issues/L144-L146)
|
||||||
|
|
||||||
|
|
||||||
<a name="10.1.0-next.1"></a>
|
|
||||||
# 10.1.0-next.1 (2020-07-15)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **bazel:** ng_module rule does not expose flat module information in Ivy ([#36971](https://github.com/angular/angular/issues/36971)) ([1550663](https://github.com/angular/angular/commit/1550663))
|
|
||||||
* **compiler:** check more cases for pipe usage inside host bindings ([#37883](https://github.com/angular/angular/issues/37883)) ([9322b9a](https://github.com/angular/angular/commit/9322b9a)), closes [#34655](https://github.com/angular/angular/issues/34655) [#37610](https://github.com/angular/angular/issues/37610)
|
|
||||||
* **compiler-cli:** ensure file_system handles mixed Windows drives ([#37959](https://github.com/angular/angular/issues/37959)) ([6b31155](https://github.com/angular/angular/commit/6b31155)), closes [#36777](https://github.com/angular/angular/issues/36777)
|
|
||||||
* **language-service:** remove completion for string ([#37983](https://github.com/angular/angular/issues/37983)) ([10aba15](https://github.com/angular/angular/commit/10aba15))
|
|
||||||
* **ngcc:** report a warning if ngcc tries to use a solution-style tsconfig ([#38003](https://github.com/angular/angular/issues/38003)) ([b358495](https://github.com/angular/angular/commit/b358495)), closes [#36386](https://github.com/angular/angular/issues/36386)
|
|
||||||
* **router:** ensure duplicate popstate/hashchange events are handled correctly ([#37674](https://github.com/angular/angular/issues/37674)) ([9185c6e](https://github.com/angular/angular/commit/9185c6e)), closes [/github.com/angular/angular/issues/16710#issuecomment-646919529](https://github.com//github.com/angular/angular/issues/16710/issues/issuecomment-646919529) [#16710](https://github.com/angular/angular/issues/16710)
|
|
||||||
* **service-worker:** correctly handle relative base href ([#37922](https://github.com/angular/angular/issues/37922)) ([d19ef65](https://github.com/angular/angular/commit/d19ef65)), closes [#25055](https://github.com/angular/angular/issues/25055) [#25055](https://github.com/angular/angular/issues/25055)
|
|
||||||
* **service-worker:** correctly serve `ngsw/state` with a non-root SW scope ([#37922](https://github.com/angular/angular/issues/37922)) ([2156bee](https://github.com/angular/angular/commit/2156bee)), closes [#30505](https://github.com/angular/angular/issues/30505)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="10.1.0-next.0"></a>
|
|
||||||
# 10.1.0-next.0 (2020-07-08)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **common:** date pipe gives wrong week number ([#37632](https://github.com/angular/angular/issues/37632)) ([ef1fb6d](https://github.com/angular/angular/commit/ef1fb6d)), closes [#33961](https://github.com/angular/angular/issues/33961)
|
|
||||||
* **compiler-cli:** ensure source-maps can handle webpack:// protocol ([#32912](https://github.com/angular/angular/issues/32912)) ([decd95e](https://github.com/angular/angular/commit/decd95e))
|
|
||||||
* **compiler-cli:** only read source-map comment from last line ([#32912](https://github.com/angular/angular/issues/32912)) ([07a07e3](https://github.com/angular/angular/commit/07a07e3))
|
|
||||||
* **core:** determine required DOMParser feature availability ([#36578](https://github.com/angular/angular/issues/36578)) ([#36578](https://github.com/angular/angular/issues/36578)) ([c509243](https://github.com/angular/angular/commit/c509243))
|
|
||||||
* **core:** do not trigger CSP alert/report in Firefox and Chrome ([#36578](https://github.com/angular/angular/issues/36578)) ([#36578](https://github.com/angular/angular/issues/36578)) ([b950d46](https://github.com/angular/angular/commit/b950d46)), closes [#25214](https://github.com/angular/angular/issues/25214)
|
|
||||||
* **forms:** handle form groups/arrays own pending async validation ([#22575](https://github.com/angular/angular/issues/22575)) ([77b62a5](https://github.com/angular/angular/commit/77b62a5)), closes [#10064](https://github.com/angular/angular/issues/10064)
|
|
||||||
* **language-service:** non-existent module format in package output ([#37623](https://github.com/angular/angular/issues/37623)) ([413a0fb](https://github.com/angular/angular/commit/413a0fb))
|
|
||||||
* **router:** fix navigation ignoring logic to compare to the browser url ([#37716](https://github.com/angular/angular/issues/37716)) ([a5ffca0](https://github.com/angular/angular/commit/a5ffca0)), closes [#16710](https://github.com/angular/angular/issues/16710) [#13586](https://github.com/angular/angular/issues/13586)
|
|
||||||
* **router:** properly compare array queryParams for equality ([#37709](https://github.com/angular/angular/issues/37709)) ([#37860](https://github.com/angular/angular/issues/37860)) ([1801d0c](https://github.com/angular/angular/commit/1801d0c))
|
|
||||||
* **router:** remove parenthesis for primary outlet segment after removing auxiliary outlet segment ([#24656](https://github.com/angular/angular/issues/24656)) ([#37163](https://github.com/angular/angular/issues/37163)) ([71f008f](https://github.com/angular/angular/commit/71f008f))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **bazel:** provide LinkablePackageInfo from ng_module ([#37623](https://github.com/angular/angular/issues/37623)) ([6898eab](https://github.com/angular/angular/commit/6898eab))
|
|
||||||
* **compiler-cli:** add `SourceFile.getOriginalLocation()` to sourcemaps package ([#32912](https://github.com/angular/angular/issues/32912)) ([6abb8d0](https://github.com/angular/angular/commit/6abb8d0))
|
|
||||||
* **compiler-cli:** explain why an expression cannot be used in AOT compilations ([#37587](https://github.com/angular/angular/issues/37587)) ([712f1bd](https://github.com/angular/angular/commit/712f1bd))
|
|
||||||
* **core:** support injection token as predicate in queries ([#37506](https://github.com/angular/angular/issues/37506)) ([97dc85b](https://github.com/angular/angular/commit/97dc85b)), closes [#21152](https://github.com/angular/angular/issues/21152) [#36144](https://github.com/angular/angular/issues/36144)
|
|
||||||
* **localize:** expose `canParse()` diagnostics ([#37909](https://github.com/angular/angular/issues/37909)) ([ec32eba](https://github.com/angular/angular/commit/ec32eba)), closes [#37901](https://github.com/angular/angular/issues/37901)
|
|
||||||
* **localize:** implement message extraction tool ([#32912](https://github.com/angular/angular/issues/32912)) ([190561d](https://github.com/angular/angular/commit/190561d))
|
|
||||||
* **platform-browser:** Allow `sms`-URLs ([#31463](https://github.com/angular/angular/issues/31463)) ([fc5c34d](https://github.com/angular/angular/commit/fc5c34d)), closes [#31462](https://github.com/angular/angular/issues/31462)
|
|
||||||
* **platform-server:** add option for absolute URL HTTP support ([#37539](https://github.com/angular/angular/issues/37539)) ([d37049a](https://github.com/angular/angular/commit/d37049a)), closes [#37071](https://github.com/angular/angular/issues/37071)
|
|
||||||
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* **compiler-cli:** fix regressions in incremental program reuse ([#37641](https://github.com/angular/angular/issues/37641)) ([5103d90](https://github.com/angular/angular/commit/5103d90))
|
|
||||||
* **ngcc:** shortcircuit tokenizing in ESM dependency host ([#37639](https://github.com/angular/angular/issues/37639)) ([bd7f440](https://github.com/angular/angular/commit/bd7f440))
|
|
||||||
* **ngcc:** use `EntryPointManifest` to speed up noop `ProgramBaseEntryPointFinder` ([#37665](https://github.com/angular/angular/issues/37665)) ([9318e23](https://github.com/angular/angular/commit/9318e23))
|
|
||||||
* **router:** apply prioritizedGuardValue operator to optimize CanLoad guards ([#37523](https://github.com/angular/angular/issues/37523)) ([d7dd295](https://github.com/angular/angular/commit/d7dd295))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="10.0.3"></a>
|
<a name="10.0.3"></a>
|
||||||
## 10.0.3 (2020-07-08)
|
## 10.0.3 (2020-07-08)
|
||||||
|
|
||||||
|
@ -230,7 +230,6 @@ Must be one of the following:
|
|||||||
* **fix**: A bug fix
|
* **fix**: A bug fix
|
||||||
* **perf**: A code change that improves performance
|
* **perf**: A code change that improves performance
|
||||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
|
||||||
* **test**: Adding missing tests or correcting existing tests
|
* **test**: Adding missing tests or correcting existing tests
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,13 +16,6 @@ import {BuildNums, PrNums, SHA} from './constants';
|
|||||||
|
|
||||||
const logger = new Logger('mock-external-apis');
|
const logger = new Logger('mock-external-apis');
|
||||||
|
|
||||||
const log = (...args: any[]) => {
|
|
||||||
// Filter out non-matching URL checks
|
|
||||||
if (!/^matching.+: false$/.test(args[0])) {
|
|
||||||
logger.log(...args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const AIO_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN');
|
const AIO_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN');
|
||||||
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
||||||
|
|
||||||
@ -91,8 +84,8 @@ const createArchive = (buildNum: number, prNum: number, sha: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create request scopes
|
// Create request scopes
|
||||||
const circleCiApi = nock(CIRCLE_CI_API_HOST).log(log).persist();
|
const circleCiApi = nock(CIRCLE_CI_API_HOST).persist();
|
||||||
const githubApi = nock(GITHUB_API_HOST).log(log).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`);
|
const githubApi = nock(GITHUB_API_HOST).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`);
|
||||||
|
|
||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
|
|
||||||
|
@ -27,28 +27,28 @@
|
|||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"delete-empty": "^3.0.0",
|
"delete-empty": "^3.0.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"jasmine": "^3.5.0",
|
"jasmine": "^3.6.1",
|
||||||
"nock": "^12.0.3",
|
"nock": "^13.0.4",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.1",
|
||||||
"shelljs": "^0.8.4",
|
"shelljs": "^0.8.4",
|
||||||
"source-map-support": "^0.5.19",
|
"source-map-support": "^0.5.19",
|
||||||
"tar-stream": "^2.1.2",
|
"tar-stream": "^2.1.3",
|
||||||
"tslib": "^1.11.1"
|
"tslib": "^2.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/body-parser": "^1.19.0",
|
"@types/body-parser": "^1.19.0",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.8",
|
||||||
"@types/jasmine": "^3.5.10",
|
"@types/jasmine": "^3.5.14",
|
||||||
"@types/nock": "^11.1.0",
|
"@types/nock": "^11.1.0",
|
||||||
"@types/node": "^13.13.2",
|
"@types/node": "^14.6.4",
|
||||||
"@types/node-fetch": "^2.5.7",
|
"@types/node-fetch": "^2.5.7",
|
||||||
"@types/shelljs": "^0.8.7",
|
"@types/shelljs": "^0.8.8",
|
||||||
"@types/supertest": "^2.0.8",
|
"@types/supertest": "^2.0.10",
|
||||||
"nodemon": "^2.0.3",
|
"nodemon": "^2.0.4",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"supertest": "^4.0.2",
|
"supertest": "^4.0.2",
|
||||||
"tslint": "^6.1.1",
|
"tslint": "^6.1.3",
|
||||||
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
|
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^4.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,23 +214,24 @@ describe('GithubApi', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should call \'https.request()\' with the correct options', () => {
|
it('should call \'https.request()\' with the correct options', async () => {
|
||||||
const requestHandler = nock('https://api.github.com')
|
const requestHandler = nock('https://api.github.com')
|
||||||
.intercept('/path', 'method')
|
.intercept('/path', 'method')
|
||||||
.reply(200);
|
.reply(200);
|
||||||
|
|
||||||
(api as any).request('method', '/path');
|
await (api as any).request('method', '/path');
|
||||||
requestHandler.done();
|
requestHandler.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should add the \'Authorization\' header containing the \'githubToken\'', () => {
|
it('should add the \'Authorization\' header containing the \'githubToken\'', async () => {
|
||||||
const requestHandler = nock('https://api.github.com')
|
const requestHandler = nock('https://api.github.com')
|
||||||
.intercept('/path', 'method', undefined, {
|
.intercept('/path', 'method', undefined, {
|
||||||
reqheaders: {Authorization: 'token 12345'},
|
reqheaders: {Authorization: 'token 12345'},
|
||||||
})
|
})
|
||||||
.reply(200);
|
.reply(200);
|
||||||
(api as any).request('method', '/path');
|
|
||||||
|
await (api as any).request('method', '/path');
|
||||||
requestHandler.done();
|
requestHandler.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -244,12 +245,13 @@ describe('GithubApi', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should \'JSON.stringify\' and send the data along with the request', () => {
|
it('should \'JSON.stringify\' and send the data along with the request', async () => {
|
||||||
const data = {key: 'value'};
|
const data = {key: 'value'};
|
||||||
const requestHandler = nock('https://api.github.com')
|
const requestHandler = nock('https://api.github.com')
|
||||||
.intercept('/path', 'method', JSON.stringify(data))
|
.intercept('/path', 'method', JSON.stringify(data))
|
||||||
.reply(200);
|
.reply(200);
|
||||||
(api as any).request('method', '/path', data);
|
|
||||||
|
await (api as any).request('method', '/path', data);
|
||||||
requestHandler.done();
|
requestHandler.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* This example project is special in that it is not a cli app. To run tests appropriate for this
|
||||||
|
* project, the test command is overwritten in `aio/content/examples/observables/example-config.json`.
|
||||||
|
*
|
||||||
|
* This is an empty placeholder file to ensure that `aio/tools/examples/run-example-e2e.js` runs
|
||||||
|
* tests for this project.
|
||||||
|
*
|
||||||
|
* TODO: Fix our infrastructure/tooling, so that this hack is not necessary.
|
||||||
|
*/
|
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"cmd": "yarn",
|
||||||
|
"args": ["tsc", "--project", "tsconfig.spec.json", "--module", "commonjs"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cmd": "yarn",
|
||||||
|
"args": ["jasmine", "out-tsc/**/*.spec.js"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { docRegionChain, docRegionObservable, docRegionUnsubscribe } from './observables';
|
||||||
|
|
||||||
|
describe('observables', () => {
|
||||||
|
it('should print 2', (doneFn: DoneFn) => {
|
||||||
|
const consoleLogSpy = spyOn(console, 'log');
|
||||||
|
const observable = docRegionObservable(console);
|
||||||
|
observable.subscribe(() => {
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(2);
|
||||||
|
doneFn();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the subscription', () => {
|
||||||
|
const subscription = docRegionUnsubscribe();
|
||||||
|
expect(subscription.closed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should chain an observable', (doneFn: DoneFn) => {
|
||||||
|
const observable = docRegionChain();
|
||||||
|
observable.subscribe(value => {
|
||||||
|
expect(value).toBe(4);
|
||||||
|
doneFn();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,20 +1,37 @@
|
|||||||
import { map } from 'rxjs/operators';
|
// #docplaster
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
export function docRegionObservable(console: Console) {
|
||||||
// #docregion observable
|
// #docregion observable
|
||||||
|
|
||||||
// declare a publishing operation
|
// declare a publishing operation
|
||||||
const observable = new Observable<number>(observer => {
|
const observable = new Observable<number>(observer => {
|
||||||
// Subscriber fn...
|
// Subscriber fn...
|
||||||
|
// #enddocregion observable
|
||||||
|
// The below code is used for unit testing only
|
||||||
|
observer.next(2);
|
||||||
|
// #docregion observable
|
||||||
});
|
});
|
||||||
|
|
||||||
// initiate execution
|
// initiate execution
|
||||||
observable.subscribe(() => {
|
observable.subscribe(value => {
|
||||||
// observer handles notifications
|
// observer handles notifications
|
||||||
|
// #enddocregion observable
|
||||||
|
// The below code is used for unit testing only
|
||||||
|
console.log(value);
|
||||||
|
// #docregion observable
|
||||||
});
|
});
|
||||||
|
|
||||||
// #enddocregion observable
|
// #enddocregion observable
|
||||||
|
return observable;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function docRegionUnsubscribe() {
|
||||||
|
const observable = new Observable<number>(() => {
|
||||||
|
// Subscriber fn...
|
||||||
|
});
|
||||||
// #docregion unsubscribe
|
// #docregion unsubscribe
|
||||||
|
|
||||||
const subscription = observable.subscribe(() => {
|
const subscription = observable.subscribe(() => {
|
||||||
@ -24,17 +41,32 @@ const subscription = observable.subscribe(() => {
|
|||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
|
|
||||||
// #enddocregion unsubscribe
|
// #enddocregion unsubscribe
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
// #docregion error
|
export function docRegionError() {
|
||||||
|
const observable = new Observable<number>(() => {
|
||||||
observable.subscribe(() => {
|
// Subscriber fn...
|
||||||
throw Error('my error');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #docregion error
|
||||||
|
observable.subscribe(() => {
|
||||||
|
throw new Error('my error');
|
||||||
|
});
|
||||||
// #enddocregion error
|
// #enddocregion error
|
||||||
|
}
|
||||||
|
|
||||||
|
export function docRegionChain() {
|
||||||
|
let observable = new Observable<number>(observer => {
|
||||||
|
// Subscriber fn...
|
||||||
|
observer.next(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
observable =
|
||||||
// #docregion chain
|
// #docregion chain
|
||||||
|
|
||||||
observable.pipe(map(v => 2 * v));
|
observable.pipe(map(v => 2 * v));
|
||||||
|
|
||||||
// #enddocregion chain
|
// #enddocregion chain
|
||||||
|
return observable;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import { docRegionError, docRegionPromise } from './promises';
|
||||||
|
|
||||||
|
describe('promises', () => {
|
||||||
|
it('should print 2', (doneFn: DoneFn) => {
|
||||||
|
const consoleLogSpy = spyOn(console, 'log');
|
||||||
|
const pr = docRegionPromise(console, 2);
|
||||||
|
pr.then((value) => {
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(2);
|
||||||
|
expect(value).toBe(4);
|
||||||
|
doneFn();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error', (doneFn: DoneFn) => {
|
||||||
|
const promise = docRegionError();
|
||||||
|
promise
|
||||||
|
.then(() => {
|
||||||
|
throw new Error('Promise should be rejected.');
|
||||||
|
},
|
||||||
|
() => doneFn());
|
||||||
|
});
|
||||||
|
});
|
@ -1,25 +1,44 @@
|
|||||||
|
// #docplaster
|
||||||
|
|
||||||
|
export function docRegionPromise(console: Console, inputValue: number) {
|
||||||
// #docregion promise
|
// #docregion promise
|
||||||
// initiate execution
|
// initiate execution
|
||||||
const promise = new Promise<number>((resolve, reject) => {
|
let promise = new Promise<number>((resolve, reject) => {
|
||||||
// Executer fn...
|
// Executer fn...
|
||||||
|
// #enddocregion promise
|
||||||
|
// The below is used in the unit tests.
|
||||||
|
resolve(inputValue);
|
||||||
|
// #docregion promise
|
||||||
});
|
});
|
||||||
|
// #enddocregion promise
|
||||||
|
promise =
|
||||||
|
// #docregion promise
|
||||||
promise.then(value => {
|
promise.then(value => {
|
||||||
// handle result here
|
// handle result here
|
||||||
});
|
|
||||||
|
|
||||||
// #enddocregion promise
|
// #enddocregion promise
|
||||||
|
// The below is used in the unit tests.
|
||||||
|
console.log(value);
|
||||||
|
return value;
|
||||||
|
// #docregion promise
|
||||||
|
});
|
||||||
|
// #enddocregion promise
|
||||||
|
promise =
|
||||||
// #docregion chain
|
// #docregion chain
|
||||||
|
|
||||||
promise.then(v => 2 * v);
|
promise.then(v => 2 * v);
|
||||||
|
|
||||||
// #enddocregion chain
|
// #enddocregion chain
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function docRegionError() {
|
||||||
|
let promise = Promise.resolve();
|
||||||
|
promise =
|
||||||
// #docregion error
|
// #docregion error
|
||||||
|
|
||||||
promise.then(() => {
|
promise.then(() => {
|
||||||
throw Error('my error');
|
throw new Error('my error');
|
||||||
});
|
});
|
||||||
|
|
||||||
// #enddocregion error
|
// #enddocregion error
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
<!-- #enddocregion translated-plural -->
|
<!-- #enddocregion translated-plural -->
|
||||||
<!-- #docregion translated-select -->
|
<!-- #docregion translated-select -->
|
||||||
<!-- #docregion translate-select-1 -->
|
<!-- #docregion translate-select-1 -->
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" datatype="html">
|
<trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" datatype="html">
|
||||||
<source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
|
<source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
|
||||||
<target>L'auteur est <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></target>
|
<target>L'auteur est <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></target>
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { SwUpdate } from '@angular/service-worker';
|
||||||
|
|
||||||
|
function notifyUser(message: string): void { }
|
||||||
|
|
||||||
|
// #docregion sw-unrecoverable-state
|
||||||
|
@Injectable()
|
||||||
|
export class HandleUnrecoverableStateService {
|
||||||
|
constructor(updates: SwUpdate) {
|
||||||
|
updates.unrecoverable.subscribe(event => {
|
||||||
|
notifyUser(
|
||||||
|
`An error occurred that we cannot recover from:\n${event.reason}\n\n` +
|
||||||
|
'Please reload the page.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #enddocregion sw-unrecoverable-state
|
@ -10,8 +10,8 @@ import { Hero } from '../shared/hero.model';
|
|||||||
template: `
|
template: `
|
||||||
<section>
|
<section>
|
||||||
Our list of heroes:
|
Our list of heroes:
|
||||||
<hero-profile *ngFor="let hero of heroes" [hero]="hero">
|
<toh-hero *ngFor="let hero of heroes" [hero]="hero">
|
||||||
</hero-profile>
|
</toh-hero>
|
||||||
Total powers: {{totalPowers}}<br>
|
Total powers: {{totalPowers}}<br>
|
||||||
Average power: {{totalPowers / heroes.length}}
|
Average power: {{totalPowers / heroes.length}}
|
||||||
</section>
|
</section>
|
||||||
|
@ -62,7 +62,7 @@ In the following example, the `@Component()` metadata object and the class const
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-typical',
|
selector: 'app-typical',
|
||||||
template: '<div>A typical component for {{data.name}}</div>'
|
template: '<div>A typical component for {{data.name}}</div>'
|
||||||
)}
|
})
|
||||||
export class TypicalComponent {
|
export class TypicalComponent {
|
||||||
@Input() data: TypicalData;
|
@Input() data: TypicalData;
|
||||||
constructor(private someService: SomeService) { ... }
|
constructor(private someService: SomeService) { ... }
|
||||||
|
@ -17,7 +17,7 @@ An NgModule is defined by a class decorated with `@NgModule()`. The `@NgModule()
|
|||||||
|
|
||||||
* `imports`: Other modules whose exported classes are needed by component templates declared in *this* NgModule.
|
* `imports`: Other modules whose exported classes are needed by component templates declared in *this* NgModule.
|
||||||
|
|
||||||
* `providers`: Creators of [services](guide/architecture-services) that this NgModule contributes to the global collection of services; they become accessible in all parts of the app. (You can also specify providers at the component level, which is often preferred.)
|
* `providers`: Creators of [services](guide/architecture-services) that this NgModule contributes to the global collection of services; they become accessible in all parts of the app. (You can also specify providers at the component level.)
|
||||||
|
|
||||||
* `bootstrap`: The main application view, called the *root component*, which hosts all other app views. Only the *root NgModule* should set the `bootstrap` property.
|
* `bootstrap`: The main application view, called the *root component*, which hosts all other app views. Only the *root NgModule* should set the `bootstrap` property.
|
||||||
|
|
||||||
|
@ -154,7 +154,7 @@ Attributes can be changed by `setAttribute()`, which re-initializes correspondin
|
|||||||
</div>
|
</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.
|
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`.
|
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">
|
<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
|
```html
|
||||||
<input [disabled]="condition ? true : false">
|
<input [disabled]="condition ? true : false">
|
||||||
|
@ -12,7 +12,7 @@ Every application has at least one Angular module, the _root_ module,
|
|||||||
which must be present for bootstrapping the application on launch.
|
which must be present for bootstrapping the application on launch.
|
||||||
By convention and by default, this NgModule is named `AppModule`.
|
By convention and by default, this NgModule is named `AppModule`.
|
||||||
|
|
||||||
When you use the [Angular CLI](cli) command `ng new` to generate an app, the default `AppModule` is as follows.
|
When you use the [Angular CLI](cli) command `ng new` to generate an app, the default `AppModule` looks like the following:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
/* JavaScript imports */
|
/* JavaScript imports */
|
||||||
@ -90,8 +90,6 @@ A declarable can only belong to one module, so only declare it in
|
|||||||
one `@NgModule`. When you need it elsewhere,
|
one `@NgModule`. When you need it elsewhere,
|
||||||
import the module that has the declarable you need in it.
|
import the module that has the declarable you need in it.
|
||||||
|
|
||||||
**Only `@NgModule` references** go in the `imports` array.
|
|
||||||
|
|
||||||
|
|
||||||
### Using directives with `@NgModule`
|
### Using directives with `@NgModule`
|
||||||
|
|
||||||
@ -133,7 +131,7 @@ The module's `imports` array appears exclusively in the `@NgModule` metadata obj
|
|||||||
It tells Angular about other NgModules that this particular module needs to function properly.
|
It tells Angular about other NgModules that this particular module needs to function properly.
|
||||||
|
|
||||||
This list of modules are those that export components, directives, or pipes
|
This list of modules are those that export components, directives, or pipes
|
||||||
that the component templates in this module reference. In this case, the component is
|
that component templates in this module reference. In this case, the component is
|
||||||
`AppComponent`, which references components, directives, or pipes in `BrowserModule`,
|
`AppComponent`, which references components, directives, or pipes in `BrowserModule`,
|
||||||
`FormsModule`, or `HttpClientModule`.
|
`FormsModule`, or `HttpClientModule`.
|
||||||
A component template can reference another component, directive,
|
A component template can reference another component, directive,
|
||||||
|
@ -125,7 +125,7 @@ Emulated is the default and most commonly used view encapsulation. For more info
|
|||||||
|
|
||||||
<div class="alert is-important">
|
<div class="alert is-important">
|
||||||
|
|
||||||
The shadow-piercing descendant combinator is deprecated and [support is being removed from major browsers](https://www.chromestatus.com/features/6750456638341120) and tools.
|
The shadow-piercing descendant combinator is deprecated and [support is being removed from major browsers](https://www.chromestatus.com/feature/6750456638341120) and tools.
|
||||||
As such we plan to drop support in Angular (for all 3 of `/deep/`, `>>>` and `::ng-deep`).
|
As such we plan to drop support in Angular (for all 3 of `/deep/`, `>>>` and `::ng-deep`).
|
||||||
Until then `::ng-deep` should be preferred for a broader compatibility with the tools.
|
Until then `::ng-deep` should be preferred for a broader compatibility with the tools.
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ The `ng generate` command creates the `projects/my-lib` folder in your workspace
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
When you generate a new library, the workspace configuration file, `angular.json`, is updated with a project of type 'library'.
|
When you generate a new library, the workspace configuration file, `angular.json`, is updated with a project of type `library`.
|
||||||
|
|
||||||
<code-example format="json">
|
<code-example format="json">
|
||||||
"projects": {
|
"projects": {
|
||||||
@ -109,7 +109,7 @@ If you want a dropdown that would contain different passed-in values each time,
|
|||||||
|
|
||||||
Suppose you want to read a configuration file and then generate a form based on that configuration.
|
Suppose you want to read a configuration file and then generate a form based on that configuration.
|
||||||
If that form will need additional customization by the developer who is using your library, it might work best as a schematic.
|
If that form will need additional customization by the developer who is using your library, it might work best as a schematic.
|
||||||
However, if the forms will always be the same and not need much customization by developers, then you could create a dynamic component that takes the configuration and generates the form.
|
However, if the form will always be the same and not need much customization by developers, then you could create a dynamic component that takes the configuration and generates the form.
|
||||||
In general, the more complex the customization, the more useful the schematic approach.
|
In general, the more complex the customization, the more useful the schematic approach.
|
||||||
|
|
||||||
To learn more, see [Schematics Overview](guide/schematics) and [Schematics for Libraries](guide/schematics-for-libraries).
|
To learn more, see [Schematics Overview](guide/schematics) and [Schematics for Libraries](guide/schematics-for-libraries).
|
||||||
|
@ -511,9 +511,9 @@ Each script tag has a `type="module"` or `nomodule` attribute. Browsers with nat
|
|||||||
|
|
||||||
To include differential loading in your application builds, you must configure the Browserslist and TypeScript configuration files in your application project.
|
To include differential loading in your application builds, you must configure the Browserslist and TypeScript configuration files in your application project.
|
||||||
|
|
||||||
The following examples show a `browserlistrc` and `tsconfig.json` file for a newly created Angular application. In this configuration, legacy browsers such as IE 9-11 are ignored, and the compilation target is ES2015.
|
The following examples show a `.browserslistrc` and `tsconfig.json` file for a newly created Angular application. In this configuration, legacy browsers such as IE 9-11 are ignored, and the compilation target is ES2015.
|
||||||
|
|
||||||
<code-example language="none" header="browserslistrc">
|
<code-example language="none" header=".browserslistrc">
|
||||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||||
# For additional information regarding the format and rule options, please see:
|
# For additional information regarding the format and rule options, please see:
|
||||||
# https://github.com/browserslist/browserslist#queries
|
# https://github.com/browserslist/browserslist#queries
|
||||||
@ -527,7 +527,7 @@ The following examples show a `browserlistrc` and `tsconfig.json` file for a new
|
|||||||
last 1 Chrome version
|
last 1 Chrome version
|
||||||
last 1 Firefox version
|
last 1 Firefox version
|
||||||
last 2 Edge major versions
|
last 2 Edge major versions
|
||||||
last 2 Safari major version
|
last 2 Safari major versions
|
||||||
last 2 iOS major versions
|
last 2 iOS major versions
|
||||||
Firefox ESR
|
Firefox ESR
|
||||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||||
|
@ -38,7 +38,6 @@ v9 - v12
|
|||||||
| `@angular/bazel` | [`Bazel builder and schematics`](#bazelbuilder) | v10 |
|
| `@angular/bazel` | [`Bazel builder and schematics`](#bazelbuilder) | v10 |
|
||||||
| `@angular/common` | [`ReflectiveInjector`](#reflectiveinjector) | <!--v8--> v11 |
|
| `@angular/common` | [`ReflectiveInjector`](#reflectiveinjector) | <!--v8--> v11 |
|
||||||
| `@angular/common` | [`CurrencyPipe` - `DEFAULT_CURRENCY_CODE`](api/common/CurrencyPipe#currency-code-deprecation) | <!--v9--> v11 |
|
| `@angular/common` | [`CurrencyPipe` - `DEFAULT_CURRENCY_CODE`](api/common/CurrencyPipe#currency-code-deprecation) | <!--v9--> v11 |
|
||||||
| `@angular/core` | [`CollectionChangeRecord`](#core) | <!--v7--> v11 |
|
|
||||||
| `@angular/core` | [`DefaultIterableDiffer`](#core) | <!--v7--> v11 |
|
| `@angular/core` | [`DefaultIterableDiffer`](#core) | <!--v7--> v11 |
|
||||||
| `@angular/core` | [`ReflectiveKey`](#core) | <!--v8--> v11 |
|
| `@angular/core` | [`ReflectiveKey`](#core) | <!--v8--> v11 |
|
||||||
| `@angular/core` | [`RenderComponentType`](#core) | <!--v7--> v11 |
|
| `@angular/core` | [`RenderComponentType`](#core) | <!--v7--> v11 |
|
||||||
@ -89,7 +88,6 @@ Tip: In the [API reference section](api) of this doc site, deprecated APIs are i
|
|||||||
|
|
||||||
| API | Replacement | Deprecation announced | Notes |
|
| API | Replacement | Deprecation announced | Notes |
|
||||||
| --- | ----------- | --------------------- | ----- |
|
| --- | ----------- | --------------------- | ----- |
|
||||||
| [`CollectionChangeRecord`](api/core/CollectionChangeRecord) | [`IterableChangeRecord`](api/core/IterableChangeRecord) | v4 | none |
|
|
||||||
| [`DefaultIterableDiffer`](api/core/DefaultIterableDiffer) | n/a | v4 | Not part of public API. |
|
| [`DefaultIterableDiffer`](api/core/DefaultIterableDiffer) | n/a | v4 | Not part of public API. |
|
||||||
| [`ReflectiveInjector`](api/core/ReflectiveInjector) | [`Injector.create`](api/core/Injector#create) | v5 | See [`ReflectiveInjector`](#reflectiveinjector) |
|
| [`ReflectiveInjector`](api/core/ReflectiveInjector) | [`Injector.create`](api/core/Injector#create) | v5 | See [`ReflectiveInjector`](#reflectiveinjector) |
|
||||||
| [`ReflectiveKey`](api/core/ReflectiveKey) | none | v5 | none |
|
| [`ReflectiveKey`](api/core/ReflectiveKey) | none | v5 | none |
|
||||||
|
@ -76,6 +76,12 @@ All router components must be entry components. Because this would require you t
|
|||||||
|
|
||||||
## The `entryComponents` array
|
## The `entryComponents` array
|
||||||
|
|
||||||
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
|
Since 9.0.0 with Ivy, the `entryComponents` property is no longer necessary. See [deprecations guide](guide/deprecations#entryComponents).
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
Though the `@NgModule` decorator has an `entryComponents` array, most of the time
|
Though the `@NgModule` decorator has an `entryComponents` array, most of the time
|
||||||
you won't have to explicitly set any entry components because Angular adds components listed in `@NgModule.bootstrap` and those in route definitions to entry components automatically. Though these two mechanisms account for most entry components, if your app happens to bootstrap or dynamically load a component by type imperatively,
|
you won't have to explicitly set any entry components because Angular adds components listed in `@NgModule.bootstrap` and those in route definitions to entry components automatically. Though these two mechanisms account for most entry components, if your app happens to bootstrap or dynamically load a component by type imperatively,
|
||||||
you must add it to `entryComponents` explicitly.
|
you must add it to `entryComponents` explicitly.
|
||||||
|
@ -79,7 +79,7 @@ To incorporate the feature module into your app, you have to let the root module
|
|||||||
<code-example path="feature-modules/src/app/app.module.ts" region="app-module" header="src/app/app.module.ts"></code-example>
|
<code-example path="feature-modules/src/app/app.module.ts" region="app-module" header="src/app/app.module.ts"></code-example>
|
||||||
|
|
||||||
|
|
||||||
Now the `AppModule` knows about the feature module. If you were to add any service providers to the feature module, `AppModule` would know about those too, as would any other feature modules. However, NgModules don’t expose their components.
|
Now the `AppModule` knows about the feature module. If you were to add any service providers to the feature module, `AppModule` would know about those too, as would any other feature modules. However, NgModules don’t expose their components by default.
|
||||||
|
|
||||||
|
|
||||||
## Rendering a feature module’s component template
|
## Rendering a feature module’s component template
|
||||||
|
@ -766,9 +766,11 @@ The HTML `base` tag with the `href` attribute specifies the base URI, or URL, fo
|
|||||||
"i18n": {
|
"i18n": {
|
||||||
"sourceLocale": "en-US",
|
"sourceLocale": "en-US",
|
||||||
"locales": {
|
"locales": {
|
||||||
"fr": "src/locale/messages.fr.xlf"
|
"fr": {
|
||||||
|
"translation": "src/locale/messages.fr.xlf",
|
||||||
"baseHref": ""
|
"baseHref": ""
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"architect": {
|
"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
|
The child's template has two controls. The first is an HTML `<input>` with a
|
||||||
[template reference variable](guide/template-reference-variables) , `#newItem`,
|
[template reference variable](guide/template-reference-variables) , `#newItem`,
|
||||||
where the user types in an item name. Whatever the user types
|
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>
|
<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)`.
|
sign is in parentheses, `(click)`.
|
||||||
|
|
||||||
The `(click)` event is bound to the `addNewItem()` method in the child component class which
|
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()`
|
Now the child component has an `@Output()`
|
||||||
for sending data to the parent and a method for raising an event.
|
for sending data to the parent and a method for raising an event.
|
||||||
|
@ -87,7 +87,7 @@ To make one, enter the following command in the terminal, where `customers` is t
|
|||||||
ng generate module customers --route customers --module app.module
|
ng generate module customers --route customers --module app.module
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
This creates a `customers` folder with the new lazy-loadable module `CustomersModule` defined in the `customers.module.ts` file. The command automatically declares the `CustomersComponent` inside the new feature module.
|
This creates a `customers` folder having the new lazy-loadable feature module `CustomersModule` defined in the `customers.module.ts` file and the routing module `CustomersRoutingModule` defined in the `customers-routing.module.ts` file. The command automatically declares the `CustomersComponent` and imports `CustomersRoutingModule` inside the new feature module.
|
||||||
|
|
||||||
Because the new module is meant to be lazy-loaded, the command does NOT add a reference to the new feature module in the application's root module file, `app.module.ts`.
|
Because the new module is meant to be lazy-loaded, the command does NOT add a reference to the new feature module in the application's root module file, `app.module.ts`.
|
||||||
Instead, it adds the declared route, `customers` to the `routes` array declared in the module provided as the `--module` option.
|
Instead, it adds the declared route, `customers` to the `routes` array declared in the module provided as the `--module` option.
|
||||||
|
@ -62,6 +62,8 @@ Angular executes hook methods in the following sequence. You can use them to per
|
|||||||
|
|
||||||
Called before `ngOnInit()` and whenever one or more data-bound input properties change.
|
Called before `ngOnInit()` and whenever one or more data-bound input properties change.
|
||||||
|
|
||||||
|
Note that if your component has no inputs or you use it without providing any inputs, the framework will not call `ngOnChanges()`.
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style='vertical-align:top'>
|
<tr style='vertical-align:top'>
|
||||||
|
@ -101,7 +101,7 @@ should import `BrowserModule` from `@angular/platform-browser`.
|
|||||||
`BrowserModule` provides services that are essential to launch and run a browser app.
|
`BrowserModule` provides services that are essential to launch and run a browser app.
|
||||||
|
|
||||||
`BrowserModule` also re-exports `CommonModule` from `@angular/common`,
|
`BrowserModule` also re-exports `CommonModule` from `@angular/common`,
|
||||||
which means that components in the `AppModule` module also have access to
|
which means that components in the `AppModule` also have access to
|
||||||
the Angular directives every app needs, such as `NgIf` and `NgFor`.
|
the Angular directives every app needs, such as `NgIf` and `NgFor`.
|
||||||
|
|
||||||
Do not import `BrowserModule` in any other module.
|
Do not import `BrowserModule` in any other module.
|
||||||
@ -140,7 +140,7 @@ declared in this NgModule.
|
|||||||
You _can_ export any declarable class—components, directives, and pipes—whether
|
You _can_ export any declarable class—components, directives, and pipes—whether
|
||||||
it's declared in this NgModule or in an imported NgModule.
|
it's declared in this NgModule or in an imported NgModule.
|
||||||
|
|
||||||
You _can_ re-export entire imported NgModules, which effectively re-exports all of their exported classes.
|
You _can_ re-export entire imported NgModules, which effectively re-export all of their exported classes.
|
||||||
An NgModule can even export a module that it doesn't import.
|
An NgModule can even export a module that it doesn't import.
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
@ -192,7 +192,7 @@ Its only purpose is to add http service providers to the application as a whole.
|
|||||||
|
|
||||||
The `forRoot()` static method is a convention that makes it easy for developers to configure services and providers that are intended to be singletons. A good example of `forRoot()` is the `RouterModule.forRoot()` method.
|
The `forRoot()` static method is a convention that makes it easy for developers to configure services and providers that are intended to be singletons. A good example of `forRoot()` is the `RouterModule.forRoot()` method.
|
||||||
|
|
||||||
Apps pass a `Routes` object to `RouterModule.forRoot()` in order to configure the app-wide `Router` service with routes.
|
Apps pass a `Routes` array to `RouterModule.forRoot()` in order to configure the app-wide `Router` service with routes.
|
||||||
`RouterModule.forRoot()` returns a [ModuleWithProviders](api/core/ModuleWithProviders).
|
`RouterModule.forRoot()` returns a [ModuleWithProviders](api/core/ModuleWithProviders).
|
||||||
You add that result to the `imports` list of the root `AppModule`.
|
You add that result to the `imports` list of the root `AppModule`.
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ NgModule metadata does the following:
|
|||||||
* Declares which components, directives, and pipes belong to the module.
|
* Declares which components, directives, and pipes belong to the module.
|
||||||
* Makes some of those components, directives, and pipes public so that other module's component templates can use them.
|
* Makes some of those components, directives, and pipes public so that other module's component templates can use them.
|
||||||
* Imports other modules with the components, directives, and pipes that components in the current module need.
|
* Imports other modules with the components, directives, and pipes that components in the current module need.
|
||||||
* Provides services that the other application components can use.
|
* Provides services that other application components can use.
|
||||||
|
|
||||||
Every Angular app has at least one module, the root module.
|
Every Angular app has at least one module, the root module.
|
||||||
You [bootstrap](guide/bootstrapping) that module to launch the application.
|
You [bootstrap](guide/bootstrapping) that module to launch the application.
|
||||||
|
@ -223,6 +223,6 @@ content harmlessly. The following is the browser output
|
|||||||
of the `evilTitle` examples.
|
of the `evilTitle` examples.
|
||||||
|
|
||||||
<code-example language="bash">
|
<code-example language="bash">
|
||||||
"Template <script>alert("evil never sleeps")</script> Syntax" is the interpolated evil title.
|
"Template <script>alert("evil never sleeps")</script> Syntax" is the interpolated evil title.
|
||||||
"Template alert("evil never sleeps")Syntax" is the property bound evil title.
|
"Template Syntax" is the property bound evil title.
|
||||||
</code-example>
|
</code-example>
|
||||||
|
@ -102,7 +102,7 @@ The following table provides the status for Angular versions under support.
|
|||||||
Version | Status | Released | Active Ends | LTS Ends
|
Version | Status | Released | Active Ends | LTS Ends
|
||||||
------- | ------ | ------------ | ------------ | ------------
|
------- | ------ | ------------ | ------------ | ------------
|
||||||
^10.0.0 | Active | Jun 24, 2020 | Dec 24, 2020 | Dec 24, 2021
|
^10.0.0 | Active | Jun 24, 2020 | Dec 24, 2020 | Dec 24, 2021
|
||||||
^9.0.0 | Active | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
|
^9.0.0 | LTS | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
|
||||||
^8.0.0 | LTS | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020
|
^8.0.0 | LTS | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020
|
||||||
|
|
||||||
Angular versions ^4.0.0, ^5.0.0, ^6.0.0 and ^7.0.0 are no longer under support.
|
Angular versions ^4.0.0, ^5.0.0, ^6.0.0 and ^7.0.0 are no longer under support.
|
||||||
|
@ -67,6 +67,33 @@ Therefore, it is recommended to reload the page once the promise returned by `ac
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
### Handling an unrecoverable state
|
||||||
|
|
||||||
|
In some cases, the version of the app used by the service worker to serve a client might be in a broken state that cannot be recovered from without a full page reload.
|
||||||
|
|
||||||
|
For example, imagine the following scenario:
|
||||||
|
- A user opens the app for the first time and the service worker caches the latest version of the app.
|
||||||
|
Let's assume the app's cached assets include `index.html`, `main.<main-hash-1>.js` and `lazy-chunk.<lazy-hash-1>.js`.
|
||||||
|
- The user closes the app and does not open it for a while.
|
||||||
|
- After some time, a new version of the app is deployed to the server.
|
||||||
|
This newer version includes the files `index.html`, `main.<main-hash-2>.js` and `lazy-chunk.<lazy-hash-2>.js` (note that the hashes are different now, because the content of the files has changed).
|
||||||
|
The old version is no longer available on the server.
|
||||||
|
- In the meantime, the user's browser decides to evict `lazy-chunk.<lazy-hash-1>.js` from its cache.
|
||||||
|
Browsers may decide to evict specific (or all) resources from a cache in order to reclaim disk space.
|
||||||
|
- The user opens the app again.
|
||||||
|
The service worker serves the latest version known to it at this point, namely the old version (`index.html` and `main.<main-hash-1>.js`).
|
||||||
|
- At some later point, the app requests the lazy bundle, `lazy-chunk.<lazy-hash-1>.js`.
|
||||||
|
- The service worker is unable to find the asset in the cache (remember that the browser evicted it).
|
||||||
|
Nor is it able to retrieve it from the server (since the server now only has `lazy-chunk.<lazy-hash-2>.js` from the newer version).
|
||||||
|
|
||||||
|
In the above scenario, the service worker is not able to serve an asset that would normally be cached.
|
||||||
|
That particular app version is broken and there is no way to fix the state of the client without reloading the page.
|
||||||
|
In such cases, the service worker notifies the client by sending an `UnrecoverableStateEvent` event.
|
||||||
|
You can subscribe to `SwUpdate#unrecoverable` to be notified and handle these errors.
|
||||||
|
|
||||||
|
<code-example path="service-worker-getting-started/src/app/handle-unrecoverable-state.service.ts" header="handle-unrecoverable-state.service.ts" region="sw-unrecoverable-state"></code-example>
|
||||||
|
|
||||||
|
|
||||||
## More on Angular service workers
|
## More on Angular service workers
|
||||||
|
|
||||||
You may also be interested in the following:
|
You may also be interested in the following:
|
||||||
|
@ -107,7 +107,7 @@ Notice that all of the files the browser needs to render this application are ca
|
|||||||
<div class="alert is-helpful">
|
<div class="alert is-helpful">
|
||||||
Pay attention to two key points:
|
Pay attention to two key points:
|
||||||
|
|
||||||
1. The generated `ngsw-config.json` includes a limited list of cacheable fonts and images extentions. In some cases, you might want to modify the glob pattern to suit your needs.
|
1. The generated `ngsw-config.json` includes a limited list of cacheable fonts and images extensions. In some cases, you might want to modify the glob pattern to suit your needs.
|
||||||
|
|
||||||
1. If `resourcesOutputPath` or `assets` paths are modified after the generation of configuration file, you need to change the paths manually in `ngsw-config.json`.
|
1. If `resourcesOutputPath` or `assets` paths are modified after the generation of configuration file, you need to change the paths manually in `ngsw-config.json`.
|
||||||
</div>
|
</div>
|
||||||
|
@ -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>
|
<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).
|
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
|
will be referenced without specifying the attribute value, and a `Directive` will not
|
||||||
change the implicit reference (that is, the element).
|
change the implicit reference (that is, the element).
|
||||||
|
|
||||||
|
@ -375,6 +375,5 @@ Some noteworthy observations:
|
|||||||
When you're filtering by CSS selector and only testing properties of a browser's _native element_, the `By.css` approach may be overkill.
|
When you're filtering by CSS selector and only testing properties of a browser's _native element_, the `By.css` approach may be overkill.
|
||||||
|
|
||||||
It's often easier and more clear to filter with a standard `HTMLElement` method
|
It's often easier and more clear to filter with a standard `HTMLElement` method
|
||||||
such as `querySelector()` or `querySelectorAll()`,
|
such as `querySelector()` or `querySelectorAll()`.
|
||||||
as you'll see in the next set of tests.
|
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ It knows who the user is based on a property of the injected `UserService`:
|
|||||||
<code-example path="testing/src/app/welcome/welcome.component.ts" header="app/welcome/welcome.component.ts"></code-example>
|
<code-example path="testing/src/app/welcome/welcome.component.ts" header="app/welcome/welcome.component.ts"></code-example>
|
||||||
|
|
||||||
The `WelcomeComponent` has decision logic that interacts with the service, logic that makes this component worth testing.
|
The `WelcomeComponent` has decision logic that interacts with the service, logic that makes this component worth testing.
|
||||||
Here's the testing module configuration for the spec file, `app/welcome/welcome.component.spec.ts`:
|
Here's the testing module configuration for the spec file:
|
||||||
|
|
||||||
<code-example path="testing/src/app/welcome/welcome.component.spec.ts" region="config-test-module" header="app/welcome/welcome.component.spec.ts"></code-example>
|
<code-example path="testing/src/app/welcome/welcome.component.spec.ts" region="config-test-module" header="app/welcome/welcome.component.spec.ts"></code-example>
|
||||||
|
|
||||||
@ -415,7 +415,7 @@ You do have to call [tick()](api/core/testing/tick) to advance the (virtual) clo
|
|||||||
Calling [tick()](api/core/testing/tick) simulates the passage of time until all pending asynchronous activities finish.
|
Calling [tick()](api/core/testing/tick) simulates the passage of time until all pending asynchronous activities finish.
|
||||||
In this case, it waits for the error handler's `setTimeout()`.
|
In this case, it waits for the error handler's `setTimeout()`.
|
||||||
|
|
||||||
The [tick()](api/core/testing/tick) function accepts milliseconds and tickOptions as parameters, the millisecond (defaults to 0 if not provided) parameter represents how much the virtual clock advances. For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use tick(100) to trigger the fn callback. The tickOptions is an optional parameter with a property called `processNewMacroTasksSynchronously` (defaults to true) represents whether to invoke new generated macro tasks when ticking.
|
The [tick()](api/core/testing/tick) function accepts milliseconds and tickOptions as parameters, the millisecond (defaults to 0 if not provided) parameter represents how much the virtual clock advances. For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use tick(100) to trigger the fn callback. The tickOptions is an optional parameter with a property called `processNewMacroTasksSynchronously` (defaults to true) that represents whether to invoke new generated macro tasks when ticking.
|
||||||
|
|
||||||
<code-example
|
<code-example
|
||||||
path="testing/src/app/demo/async-helper.spec.ts"
|
path="testing/src/app/demo/async-helper.spec.ts"
|
||||||
@ -594,11 +594,6 @@ Then you can assert that the quote element displays the expected text.
|
|||||||
To use `waitForAsync()` functionality, you must import `zone.js/dist/zone-testing` in your test setup file.
|
To use `waitForAsync()` functionality, you must import `zone.js/dist/zone-testing` in your test setup file.
|
||||||
If you created your project with the Angular CLI, `zone-testing` is already imported in `src/test.ts`.
|
If you created your project with the Angular CLI, `zone-testing` is already imported in `src/test.ts`.
|
||||||
|
|
||||||
The `fakeAsync()` utility function has a few limitations.
|
|
||||||
In particular, it won't work if the test body makes an `XMLHttpRequest` (XHR) call.
|
|
||||||
XHR calls within a test are rare so you can generally stick with [`fakeAsync()`](#fake-async).
|
|
||||||
But if you ever do need to call `XMLHttpRequest`, you'll want to know about `waitForAsync()`.
|
|
||||||
|
|
||||||
<div class="alert is-helpful">
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
The `TestBed.compileComponents()` method (see [below](#compile-components)) calls `XHR`
|
The `TestBed.compileComponents()` method (see [below](#compile-components)) calls `XHR`
|
||||||
@ -1231,7 +1226,7 @@ and provide for _all_ services injected in _any_ component in the tree.
|
|||||||
That's too much effort just to answer a few simple questions about links.
|
That's too much effort just to answer a few simple questions about links.
|
||||||
|
|
||||||
This section describes two techniques for minimizing the setup.
|
This section describes two techniques for minimizing the setup.
|
||||||
Use them, alone or in combination, to stay focused on the testing the primary component.
|
Use them, alone or in combination, to stay focused on testing the primary component.
|
||||||
|
|
||||||
{@a stub-component}
|
{@a stub-component}
|
||||||
|
|
||||||
@ -1340,7 +1335,7 @@ The `HostListener` wires the click event of the host element
|
|||||||
Clicking the anchor should trigger the `onClick()` method,
|
Clicking the anchor should trigger the `onClick()` method,
|
||||||
which sets the stub's telltale `navigatedTo` property.
|
which sets the stub's telltale `navigatedTo` property.
|
||||||
Tests inspect `navigatedTo` to confirm that clicking the anchor
|
Tests inspect `navigatedTo` to confirm that clicking the anchor
|
||||||
set the expected route definition.
|
sets the expected route definition.
|
||||||
|
|
||||||
<div class="alert is-helpful">
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
@ -1573,7 +1568,7 @@ calls to other `TestBed` static methods such as `compileComponents()`.
|
|||||||
In this example, the `BannerComponent` is the only component to compile.
|
In this example, the `BannerComponent` is the only component to compile.
|
||||||
Other examples configure the testing module with multiple components
|
Other examples configure the testing module with multiple components
|
||||||
and may import application modules that hold yet more components.
|
and may import application modules that hold yet more components.
|
||||||
Any of them could be require external files.
|
Any of them could require external files.
|
||||||
|
|
||||||
The `TestBed.compileComponents` method asynchronously compiles all components configured in the testing module.
|
The `TestBed.compileComponents` method asynchronously compiles all components configured in the testing module.
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ Here's a summary of the stand-alone functions, in order of likely utility:
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td style="vertical-align: top">
|
<td style="vertical-align: top">
|
||||||
<code>async</code>
|
<code>waitForAsync</code>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
@ -75,7 +75,7 @@ The tests run again, the browser refreshes, and the new test results appear.
|
|||||||
|
|
||||||
The CLI takes care of Jasmine and Karma configuration for you.
|
The CLI takes care of Jasmine and Karma configuration for you.
|
||||||
|
|
||||||
You can fine-tune many options by editing the `karma.conf.js` and
|
You can fine-tune many options by editing the `karma.conf.js` in the root folder of the project and
|
||||||
the `test.ts` files in the `src/` folder.
|
the `test.ts` files in the `src/` folder.
|
||||||
|
|
||||||
The `karma.conf.js` file is a partial Karma configuration file.
|
The `karma.conf.js` file is a partial Karma configuration file.
|
||||||
@ -217,7 +217,7 @@ script:
|
|||||||
- npm run e2e -- --protractor-config=e2e/protractor-ci.conf.js
|
- npm run e2e -- --protractor-config=e2e/protractor-ci.conf.js
|
||||||
```
|
```
|
||||||
|
|
||||||
This does the same things as the Circle CI configuration, except that Travis doesn't come with Chrome, so we use Chromium instead.
|
This does the same things as the CircleCI configuration, except that Travis doesn't come with Chrome, so use Chromium instead.
|
||||||
|
|
||||||
Step 2: Commit your changes and push them to your repository.
|
Step 2: Commit your changes and push them to your repository.
|
||||||
|
|
||||||
|
BIN
aio/content/images/bios/samvloeberghs.jpg
Normal file
BIN
aio/content/images/bios/samvloeberghs.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
aio/content/images/bios/thekiba.jpg
Normal file
BIN
aio/content/images/bios/thekiba.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
@ -53,6 +53,9 @@
|
|||||||
},
|
},
|
||||||
"kyliau": {
|
"kyliau": {
|
||||||
"name": "Keen Yee Liau",
|
"name": "Keen Yee Liau",
|
||||||
|
"twitter": "liauky",
|
||||||
|
"website": "https://github.com/kyliau",
|
||||||
|
"bio": "Keen works on language service and CLI. He also maintains Karma and Protractor.",
|
||||||
"groups": ["Angular"],
|
"groups": ["Angular"],
|
||||||
"lead": "igorminar",
|
"lead": "igorminar",
|
||||||
"picture": "kyliau.jpg"
|
"picture": "kyliau.jpg"
|
||||||
@ -713,9 +716,9 @@
|
|||||||
"santosh": {
|
"santosh": {
|
||||||
"name": "Santosh Yadav",
|
"name": "Santosh Yadav",
|
||||||
"picture": "santoshyadav.jpg",
|
"picture": "santoshyadav.jpg",
|
||||||
"twitter": "Santosh19742211",
|
"twitter": "SantoshYadavDev",
|
||||||
"website": "https://www.santoshyadav.dev",
|
"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"]
|
"groups": ["GDE"]
|
||||||
},
|
},
|
||||||
"josephperrott": {
|
"josephperrott": {
|
||||||
@ -810,5 +813,21 @@
|
|||||||
"website": "kreuzercode.com",
|
"website": "kreuzercode.com",
|
||||||
"bio": "Kevin is a passionate freelance front-end engineer and Google Developer Expert based in Switzerland. He is a JavaScript enthusiast and fascinated by Angular. Kevin always tries to learn new things, expand his knowledge, and share it with others in the form of blog posts, workshops, podcasts, or presentations. He is a writer for various publications and the most active writer on Angular in-depth in 2019. Contributing to multiple projects and maintaining 7 npm packages, Kevin is also a big believer in open source. Furthermore, Kevin is a big football fan. Since his childhood, he has supported Real Madrid, which you might notice in a lot of his blog posts and tutorials.",
|
"bio": "Kevin is a passionate freelance front-end engineer and Google Developer Expert based in Switzerland. He is a JavaScript enthusiast and fascinated by Angular. Kevin always tries to learn new things, expand his knowledge, and share it with others in the form of blog posts, workshops, podcasts, or presentations. He is a writer for various publications and the most active writer on Angular in-depth in 2019. Contributing to multiple projects and maintaining 7 npm packages, Kevin is also a big believer in open source. Furthermore, Kevin is a big football fan. Since his childhood, he has supported Real Madrid, which you might notice in a lot of his blog posts and tutorials.",
|
||||||
"groups": ["GDE"]
|
"groups": ["GDE"]
|
||||||
|
},
|
||||||
|
"samvloeberghs": {
|
||||||
|
"name": "Sam Vloeberghs",
|
||||||
|
"picture": "samvloeberghs.jpg",
|
||||||
|
"groups": ["GDE"],
|
||||||
|
"twitter": "samvloeberghs",
|
||||||
|
"website": "https://samvloeberghs.be",
|
||||||
|
"bio": "Sam is a freelance software architect and Internet entrepreneur, currently focusing on frontend technologies. Co-organiser of the Belgian Angular conference NG-BE and Angular Belgium Meetup group."
|
||||||
|
},
|
||||||
|
"thekiba": {
|
||||||
|
"name": "Andrew Grekov",
|
||||||
|
"picture": "thekiba.jpg",
|
||||||
|
"twitter": "thekiba_io",
|
||||||
|
"website": "https://thekiba.io",
|
||||||
|
"bio": "Andrew is a software engineer using Angular and .NET. He spends most of his spare time staying up-to-date, helping other people, and experimenting with web tech.",
|
||||||
|
"groups": ["GDE"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,10 @@
|
|||||||
"url": "https://dev.to/t/angular",
|
"url": "https://dev.to/t/angular",
|
||||||
"title": "DEV Community"
|
"title": "DEV Community"
|
||||||
},
|
},
|
||||||
"angular-in-depth": {
|
"indepth-dev": {
|
||||||
"desc": "The place where advanced Angular concepts are explained",
|
"desc": "Peer-reviewed Angular articles and tutorials.",
|
||||||
"url": "https://blog.angularindepth.com",
|
"url": "https://indepth.dev/angular/",
|
||||||
"title": "Angular In Depth"
|
"title": "Angular inDepth"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -63,6 +63,12 @@
|
|||||||
"logo": "",
|
"logo": "",
|
||||||
"title": "NgRuAir",
|
"title": "NgRuAir",
|
||||||
"url": "https://github.com/ngRuAir/ngruair"
|
"url": "https://github.com/ngRuAir/ngruair"
|
||||||
|
},
|
||||||
|
"the-deep-dive": {
|
||||||
|
"desc": "The advanced web development podcast about Angular, RxJS, TypeScript and other technologies. English, audio only.",
|
||||||
|
"logo": "https://i.imgur.com/mmE5Feq.jpg",
|
||||||
|
"title": "The Deep Dive",
|
||||||
|
"url": "https://thedeepdive.simplecast.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -429,6 +435,12 @@
|
|||||||
"desc": "Jigsaw provides a set of web components based on Angular. It is supporting the development of all applications of Big Data Product of ZTE (https://www.zte.com.cn).",
|
"desc": "Jigsaw provides a set of web components based on Angular. It is supporting the development of all applications of Big Data Product of ZTE (https://www.zte.com.cn).",
|
||||||
"title": "Awade Jigsaw (Chinese)",
|
"title": "Awade Jigsaw (Chinese)",
|
||||||
"url": "https://jigsaw-zte.gitee.io"
|
"url": "https://jigsaw-zte.gitee.io"
|
||||||
|
},
|
||||||
|
"material-dayjs-adapter": {
|
||||||
|
"desc": "A DayJS implementation of @angular/material's DateAdapter that results in smaller bundle sizes than its MomentJS counterpart.",
|
||||||
|
"rev": true,
|
||||||
|
"title": "material-dayjs-adapter",
|
||||||
|
"url": "https://www.npmjs.com/package/@tabuckner/material-dayjs-adapter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,54 +48,13 @@
|
|||||||
{
|
{
|
||||||
"url": "docs",
|
"url": "docs",
|
||||||
"title": "Introduction",
|
"title": "Introduction",
|
||||||
"tooltip": "Introduction to the Angular documentation",
|
"tooltip": "Welcome to the Angular documentation set.",
|
||||||
"hidden": false
|
"hidden": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Getting Started",
|
"title": "Getting Started",
|
||||||
"tooltip": "Set up your environment and learn basic concepts",
|
"tooltip": "Set up your environment and learn basic concepts",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"url": "guide/setup-local",
|
|
||||||
"title": "Setup",
|
|
||||||
"tooltip": "Setting up for local development with the Angular CLI."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Angular Concepts",
|
|
||||||
"tooltip": "Introduction to basic concepts for Angular applications.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"url": "guide/architecture",
|
|
||||||
"title": "Intro to Basic Concepts",
|
|
||||||
"tooltip": "Basic building blocks of Angular applications."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/architecture-modules",
|
|
||||||
"title": "Intro to Modules",
|
|
||||||
"tooltip": "About NgModules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/architecture-components",
|
|
||||||
"title": "Intro to Components",
|
|
||||||
"tooltip": "About Components, Templates, and Views."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/architecture-services",
|
|
||||||
"title": "Intro to Services and DI",
|
|
||||||
"tooltip": "About services and dependency injection."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/architecture-next-steps",
|
|
||||||
"title": "Next Steps",
|
|
||||||
"tooltip": "Beyond the basics."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/glossary",
|
|
||||||
"title": "Angular Glossary",
|
|
||||||
"tooltip": "Brief definitions of the most important words in the Angular vocabulary."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"title": "Try it",
|
"title": "Try it",
|
||||||
"tooltip": "Examine and work with a ready-made sample app, with no setup.",
|
"tooltip": "Examine and work with a ready-made sample app, with no setup.",
|
||||||
@ -128,68 +87,64 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Tutorial: Tour of Heroes",
|
"url": "guide/setup-local",
|
||||||
"tooltip": "The Tour of Heroes app is used as a reference point in many Angular examples.",
|
"title": "Setup",
|
||||||
"children": [
|
"tooltip": "Setting up for local development with the Angular CLI."
|
||||||
{
|
|
||||||
"url": "tutorial",
|
|
||||||
"title": "Introduction",
|
|
||||||
"tooltip": "Introduction to the Tour of Heroes app and tutorial"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt0",
|
|
||||||
"title": "Create a Project",
|
|
||||||
"tooltip": "Creating the application shell"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt1",
|
|
||||||
"title": "1. The Hero Editor",
|
|
||||||
"tooltip": "Part 1: Build a simple editor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt2",
|
|
||||||
"title": "2. Display a List",
|
|
||||||
"tooltip": "Part 2: Build a master/detail page with a list of heroes."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt3",
|
|
||||||
"title": "3. Create a Feature Component",
|
|
||||||
"tooltip": "Part 3: Refactor the master/detail views into separate components."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt4",
|
|
||||||
"title": "4. Add Services",
|
|
||||||
"tooltip": "Part 4: Create a reusable service to manage hero data."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt5",
|
|
||||||
"title": "5. Add In-app Navigation",
|
|
||||||
"tooltip": "Part 5: Add the Angular router and navigate among the views."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "tutorial/toh-pt6",
|
|
||||||
"title": "6. Get Data from a Server",
|
|
||||||
"tooltip": "Part 6: Use HTTP to retrieve and save hero data."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Fundamentals",
|
"title": "Main Concepts",
|
||||||
"tooltip": "The fundamentals of Angular",
|
"tooltip": "Learn the concepts essential to becoming a proficient Angular developer.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"title": "Components & Templates",
|
"title": "Components",
|
||||||
"tooltip": "Building dynamic views with data binding",
|
"tooltip": "Building dynamic views with data binding",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"url": "guide/displaying-data",
|
"url": "guide/displaying-data",
|
||||||
"title": "Displaying Data",
|
"title": "Data binding",
|
||||||
"tooltip": "Property binding helps show app data in the UI."
|
"tooltip": "Property binding helps show app data in the UI."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Template Syntax",
|
"url": "guide/user-input",
|
||||||
|
"title": "User Input",
|
||||||
|
"tooltip": "User input triggers DOM events. Angular listens to those events with event bindings that funnel updated values back into your app's components and models."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/pipes",
|
||||||
|
"title": "Pipes",
|
||||||
|
"tooltip": "Pipes transform displayed values within a template."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/lifecycle-hooks",
|
||||||
|
"title": "Component Lifecycle",
|
||||||
|
"tooltip": "Angular calls lifecycle hook methods on directives and components as it creates, changes, and destroys them."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/component-interaction",
|
||||||
|
"title": "Component Interaction",
|
||||||
|
"tooltip": "Share information between different directives and components."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/component-styles",
|
||||||
|
"title": "Component Styles",
|
||||||
|
"tooltip": "Add CSS styles that are specific to a component."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/dynamic-component-loader",
|
||||||
|
"title": "Dynamic Components",
|
||||||
|
"tooltip": "Load components dynamically."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/elements",
|
||||||
|
"title": "Angular Elements",
|
||||||
|
"tooltip": "Convert components to Custom Elements."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Templates",
|
||||||
"tooltip": "Syntax to use in templates for binding, expressions, and directives.",
|
"tooltip": "Syntax to use in templates for binding, expressions, and directives.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
@ -232,11 +187,6 @@
|
|||||||
"title": "Two-way binding",
|
"title": "Two-way binding",
|
||||||
"tooltip": "Introductory guide to sharing data between a class and a template."
|
"tooltip": "Introductory guide to sharing data between a class and a template."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"url": "guide/built-in-directives",
|
|
||||||
"title": "Built-in directives",
|
|
||||||
"tooltip": "Introductory guide to some of the most popular built-in directives."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "guide/template-reference-variables",
|
"url": "guide/template-reference-variables",
|
||||||
"title": "Template reference variables",
|
"title": "Template reference variables",
|
||||||
@ -260,9 +210,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/user-input",
|
"title": "Directives",
|
||||||
"title": "User Input",
|
"tooltip": "Control the behavior of elements and the layout of your pages with directives.",
|
||||||
"tooltip": "User input triggers DOM events. Angular listens to those events with event bindings that funnel updated values back into your app's components and models."
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/built-in-directives",
|
||||||
|
"title": "Built-in directives",
|
||||||
|
"tooltip": "Introductory guide to some of the most popular built-in directives."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/attribute-directives",
|
"url": "guide/attribute-directives",
|
||||||
@ -273,164 +227,6 @@
|
|||||||
"url": "guide/structural-directives",
|
"url": "guide/structural-directives",
|
||||||
"title": "Structural Directives",
|
"title": "Structural Directives",
|
||||||
"tooltip": "Structural directives manipulate the layout of the page."
|
"tooltip": "Structural directives manipulate the layout of the page."
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/pipes",
|
|
||||||
"title": "Pipes",
|
|
||||||
"tooltip": "Pipes transform displayed values within a template."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/lifecycle-hooks",
|
|
||||||
"title": "Hook into the Component Lifecycle",
|
|
||||||
"tooltip": "Angular calls lifecycle hook methods on directives and components as it creates, changes, and destroys them."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/component-interaction",
|
|
||||||
"title": "Component Interaction",
|
|
||||||
"tooltip": "Share information between different directives and components."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/component-styles",
|
|
||||||
"title": "Component Styles",
|
|
||||||
"tooltip": "Add CSS styles that are specific to a component."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/dynamic-component-loader",
|
|
||||||
"title": "Dynamic Components",
|
|
||||||
"tooltip": "Load components dynamically."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/elements",
|
|
||||||
"title": "Angular Elements",
|
|
||||||
"tooltip": "Convert components to Custom Elements."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Forms for User Input",
|
|
||||||
"tooltip": "Forms creates a cohesive, effective, and compelling data entry experience.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"url": "guide/forms-overview",
|
|
||||||
"title": "Introduction",
|
|
||||||
"tooltip": "An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/reactive-forms",
|
|
||||||
"title": "Reactive Forms",
|
|
||||||
"tooltip": "Create a reactive form using FormBuilder, groups, and arrays."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/form-validation",
|
|
||||||
"title": "Validate form input",
|
|
||||||
"tooltip": "Validate user's form entries."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/dynamic-form",
|
|
||||||
"title": "Building Dynamic Forms",
|
|
||||||
"tooltip": "Create dynamic form templates using FormGroup."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Observables & RxJS",
|
|
||||||
"tooltip": "Using observables for message passing in Angular.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"url": "guide/observables",
|
|
||||||
"title": "Observables Overview",
|
|
||||||
"tooltip": "Using observables to pass values synchronously or asynchronously."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/rx-library",
|
|
||||||
"title": "The RxJS Library",
|
|
||||||
"tooltip": "A library for reactive programming using observables to compose asynchronous or callback-based code."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/observables-in-angular",
|
|
||||||
"title": "Observables in Angular",
|
|
||||||
"tooltip": "How Angular subsystems use and expect observables."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/practical-observable-usage",
|
|
||||||
"title": "Practical Usage",
|
|
||||||
"tooltip": "Domains in which observables are particularly useful."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/comparing-observables",
|
|
||||||
"title": "Compare to Other Techniques",
|
|
||||||
"tooltip": "How observables compare to promises and other message passing techniques."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "NgModules",
|
|
||||||
"tooltip": "NgModules.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"url": "guide/ngmodules",
|
|
||||||
"title": "NgModules Introduction",
|
|
||||||
"tooltip": "Use NgModules to make your apps efficient."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/ngmodule-vs-jsmodule",
|
|
||||||
"title": "JS Modules vs NgModules",
|
|
||||||
"tooltip": "Differentiate between JavaScript modules and NgModules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/bootstrapping",
|
|
||||||
"title": "Launching Apps with a Root Module",
|
|
||||||
"tooltip": "Tell Angular how to construct and bootstrap the app in the root \"AppModule\"."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/frequent-ngmodules",
|
|
||||||
"title": "Frequently Used NgModules",
|
|
||||||
"tooltip": "Introduction to the most frequently used NgModules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/module-types",
|
|
||||||
"title": "Types of Feature Modules",
|
|
||||||
"tooltip": "Description of the different types of feature modules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/entry-components",
|
|
||||||
"title": "Entry Components",
|
|
||||||
"tooltip": "All about entry components in Angular."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/feature-modules",
|
|
||||||
"title": "Feature Modules",
|
|
||||||
"tooltip": "Create feature modules to organize your code."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/providers",
|
|
||||||
"title": "Providing Dependencies",
|
|
||||||
"tooltip": "Providing dependencies to NgModules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/singleton-services",
|
|
||||||
"title": "Singleton Services",
|
|
||||||
"tooltip": "Creating singleton services."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/lazy-loading-ngmodules",
|
|
||||||
"title": "Lazy Loading Feature Modules",
|
|
||||||
"tooltip": "Lazy load modules to speed up your apps."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/sharing-ngmodules",
|
|
||||||
"title": "Sharing NgModules",
|
|
||||||
"tooltip": "Share NgModules to streamline your apps."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/ngmodule-api",
|
|
||||||
"title": "NgModule API",
|
|
||||||
"tooltip": "Understand the details of NgModules."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/ngmodule-faq",
|
|
||||||
"title": "NgModule FAQs",
|
|
||||||
"tooltip": "Answers to frequently asked questions about NgModules."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -464,28 +260,54 @@
|
|||||||
"tooltip": "Use the injection tree to find parent components."
|
"tooltip": "Use the injection tree to find parent components."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/http",
|
"title": "Built-in Features",
|
||||||
"title": "Access Servers over HTTP",
|
"tooltip": "Learn how to add Angular's built-in features to add functionality to your applications.",
|
||||||
"tooltip": "Use HTTP to talk to a remote server."
|
"children": [
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "guide/router",
|
"url": "guide/router",
|
||||||
"title": "Routing & Navigation",
|
"title": "Routing & Navigation",
|
||||||
"tooltip": "Build in-app navigation among views using the Angular Router."
|
"tooltip": "Build in-app navigation among views using the Angular Router."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/security",
|
"title": "Forms",
|
||||||
"title": "Security",
|
"tooltip": "Forms creates a cohesive, effective, and compelling data entry experience.",
|
||||||
"tooltip": "Developing for content security in Angular applications."
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/forms-overview",
|
||||||
|
"title": "Introduction",
|
||||||
|
"tooltip": "An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/reactive-forms",
|
||||||
|
"title": "Reactive Forms",
|
||||||
|
"tooltip": "Create a reactive form using FormBuilder, groups, and arrays."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/form-validation",
|
||||||
|
"title": "Validate form input",
|
||||||
|
"tooltip": "Validate user's form entries."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/dynamic-form",
|
||||||
|
"title": "Building Dynamic Forms",
|
||||||
|
"tooltip": "Create dynamic form templates using FormGroup."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Techniques",
|
"url": "guide/http",
|
||||||
"tooltip": "Techniques for putting Angular to work in your environment",
|
"title": "HTTP Client",
|
||||||
"children": [
|
"tooltip": "Use HTTP to talk to a remote server."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/i18n",
|
||||||
|
"title": "Internationalization (i18n)",
|
||||||
|
"tooltip": "Translate the app's template text into multiple languages."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Animations",
|
"title": "Animations",
|
||||||
"tooltip": "Enhance the user experience with animation.",
|
"tooltip": "Enhance the user experience with animation.",
|
||||||
@ -518,14 +340,25 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/i18n",
|
"title": "Schematics",
|
||||||
"title": "Internationalization (i18n)",
|
"tooltip": "Understanding schematics.",
|
||||||
"tooltip": "Translate the app's template text into multiple languages."
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/schematics",
|
||||||
|
"title": "Schematics Overview",
|
||||||
|
"tooltip": "Extending CLI generation capabilities."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/accessibility",
|
"url": "guide/schematics-authoring",
|
||||||
"title": "Accessibility",
|
"title": "Authoring Schematics",
|
||||||
"tooltip": "Design apps to be accessible to all users."
|
"tooltip": "Understand the structure of a schematic."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/schematics-for-libraries",
|
||||||
|
"title": "Schematics for Libraries",
|
||||||
|
"tooltip": "Use schematics to integrate your library with the Angular CLI."
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Service Workers & PWA",
|
"title": "Service Workers & PWA",
|
||||||
@ -562,53 +395,27 @@
|
|||||||
"tooltip": "Configuring service worker caching behavior."
|
"tooltip": "Configuring service worker caching behavior."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/web-worker",
|
|
||||||
"title": "Web Workers",
|
|
||||||
"tooltip": "Using web workers for background processing."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/universal",
|
|
||||||
"title": "Server-side Rendering",
|
|
||||||
"tooltip": "Render HTML server-side with Angular Universal."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Dev Workflow",
|
"title": "Best Practices",
|
||||||
"tooltip": "Build, testing, and deployment information.",
|
"tooltip": "Learn how to build robust, scalable applications.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"title": "AOT Compiler",
|
"url": "guide/security",
|
||||||
"tooltip": "Understanding ahead-of-time compilation.",
|
"title": "Security",
|
||||||
"children": [
|
"tooltip": "Developing for content security in Angular applications."
|
||||||
{
|
|
||||||
"url": "guide/aot-compiler",
|
|
||||||
"title": "Ahead-of-Time Compilation",
|
|
||||||
"tooltip": "Learn why and how to use the Ahead-of-Time (AOT) compiler."
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/angular-compiler-options",
|
"url": "guide/accessibility",
|
||||||
"title": "Angular Compiler Options",
|
"title": "Accessibility",
|
||||||
"tooltip": "Configuring AOT compilation."
|
"tooltip": "Design apps to be accessible to all users."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/aot-metadata-errors",
|
"url": "guide/updating",
|
||||||
"title": "AOT Metadata Errors",
|
"title": "Keeping Up-to-Date",
|
||||||
"tooltip": "Troubleshooting AOT compilation."
|
"tooltip": "Information about updating Angular applications and libraries to the latest version."
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/template-typecheck",
|
|
||||||
"title": "Template Type-checking",
|
|
||||||
"tooltip": "Template type-checking in Angular."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/build",
|
|
||||||
"title": "Building & Serving",
|
|
||||||
"tooltip": "Building and serving Angular apps."
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Testing",
|
"title": "Testing",
|
||||||
@ -661,89 +468,48 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Dev Workflow",
|
||||||
|
"tooltip": "Build, and deployment information.",
|
||||||
|
"children": [
|
||||||
{
|
{
|
||||||
"url": "guide/deployment",
|
"url": "guide/deployment",
|
||||||
"title": "Deployment",
|
"title": "Deploying applications",
|
||||||
"tooltip": "Learn how to deploy your Angular app."
|
"tooltip": "Learn how to deploy your Angular app."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Dev Tool Integration",
|
"title": "AOT Compiler",
|
||||||
"tooltip": "Integrate with your development environment and tools.",
|
"tooltip": "Understanding ahead-of-time compilation.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"url": "guide/language-service",
|
"url": "guide/aot-compiler",
|
||||||
"title": "Language Service",
|
"title": "Ahead-of-Time Compilation",
|
||||||
"tooltip": "Use Angular Language Service to speed up dev time."
|
"tooltip": "Learn why and how to use the Ahead-of-Time (AOT) compiler."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/visual-studio-2015",
|
"url": "guide/angular-compiler-options",
|
||||||
"title": "Visual Studio 2015",
|
"title": "Angular Compiler Options",
|
||||||
"tooltip": "Using Angular with Visual Studio 2015.",
|
"tooltip": "Configuring AOT compilation."
|
||||||
"hidden": true
|
},
|
||||||
}
|
{
|
||||||
]
|
"url": "guide/aot-metadata-errors",
|
||||||
|
"title": "AOT Metadata Errors",
|
||||||
|
"tooltip": "Troubleshooting AOT compilation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/template-typecheck",
|
||||||
|
"title": "Template Type-checking",
|
||||||
|
"tooltip": "Template type-checking in Angular."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Configuration",
|
"url": "guide/build",
|
||||||
"tooltip": "Workspace and project file structure and configuration.",
|
"title": "Building & Serving",
|
||||||
"children": [
|
"tooltip": "Building and serving Angular apps."
|
||||||
{
|
|
||||||
"url": "guide/file-structure",
|
|
||||||
"title": "Project File Structure",
|
|
||||||
"tooltip": "How your Angular workspace looks on your filesystem."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/workspace-config",
|
|
||||||
"title": "Workspace Configuration",
|
|
||||||
"tooltip": "The \"angular.json\" file contains workspace and project configuration defaults for Angular CLI commands."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/npm-packages",
|
|
||||||
"title": "npm Dependencies",
|
|
||||||
"tooltip": "Description of npm packages required at development time and at runtime."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/typescript-configuration",
|
|
||||||
"title": "TypeScript Configuration",
|
|
||||||
"tooltip": "TypeScript configuration for Angular developers."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/browser-support",
|
|
||||||
"title": "Browser Support",
|
|
||||||
"tooltip": "Browser support and polyfills guide."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/strict-mode",
|
|
||||||
"title": "Strict mode",
|
|
||||||
"tooltip": "Reference documentation for Angular's strict mode."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "Extending Angular",
|
|
||||||
"tooltip": "Working with libraries and extending the CLI.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"title": "Angular Libraries",
|
|
||||||
"tooltip": "Extending Angular with shared libraries.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"url": "guide/libraries",
|
|
||||||
"title": "Libraries Overview",
|
|
||||||
"tooltip": "Understand how and when to use or create libraries."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/using-libraries",
|
|
||||||
"title": "Using Published Libraries",
|
|
||||||
"tooltip": "Integrate published libraries into an app."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/creating-libraries",
|
|
||||||
"title": "Creating Libraries",
|
|
||||||
"tooltip": "Extend Angular by creating, publishing, and using your own libraries."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "guide/lightweight-injection-tokens",
|
"url": "guide/lightweight-injection-tokens",
|
||||||
"title": "Lightweight Injection Tokens for Libraries",
|
"title": "Lightweight Injection Tokens for Libraries",
|
||||||
@ -752,30 +518,23 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Schematics",
|
"title": "Angular Tools",
|
||||||
"tooltip": "Understanding schematics.",
|
"tooltip": "Tools to help you build your Angular applications.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"url": "guide/schematics",
|
|
||||||
"title": "Schematics Overview",
|
|
||||||
"tooltip": "Extending CLI generation capabilities."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/schematics-authoring",
|
|
||||||
"title": "Authoring Schematics",
|
|
||||||
"tooltip": "Understand the structure of a schematic."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/schematics-for-libraries",
|
|
||||||
"title": "Schematics for Libraries",
|
|
||||||
"tooltip": "Use schematics to integrate your library with the Angular CLI."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "guide/cli-builder",
|
"url": "guide/cli-builder",
|
||||||
"title": "CLI Builders",
|
"title": "CLI Builders",
|
||||||
"tooltip": "Using builders to customize Angular CLI."
|
"tooltip": "Using builders to customize Angular CLI."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/universal",
|
||||||
|
"title": "Server-side Rendering",
|
||||||
|
"tooltip": "Render HTML server-side with Angular Universal."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/language-service",
|
||||||
|
"title": "Language Service",
|
||||||
|
"tooltip": "Use Angular Language Service to speed up dev time."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -803,6 +562,78 @@
|
|||||||
"url": "guide/forms",
|
"url": "guide/forms",
|
||||||
"title": "Building a Template-driven Form",
|
"title": "Building a Template-driven Form",
|
||||||
"tooltip": "Create a template-driven form using directives and Angular template syntax."
|
"tooltip": "Create a template-driven form using directives and Angular template syntax."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Tutorial: Tour of Heroes",
|
||||||
|
"tooltip": "The Tour of Heroes app is used as a reference point in many Angular examples.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "tutorial",
|
||||||
|
"title": "Introduction",
|
||||||
|
"tooltip": "Introduction to the Tour of Heroes app and tutorial"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt0",
|
||||||
|
"title": "Create a Project",
|
||||||
|
"tooltip": "Creating the application shell"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt1",
|
||||||
|
"title": "1. The Hero Editor",
|
||||||
|
"tooltip": "Part 1: Build a simple editor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt2",
|
||||||
|
"title": "2. Display a List",
|
||||||
|
"tooltip": "Part 2: Build a master/detail page with a list of heroes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt3",
|
||||||
|
"title": "3. Create a Feature Component",
|
||||||
|
"tooltip": "Part 3: Refactor the master/detail views into separate components."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt4",
|
||||||
|
"title": "4. Add Services",
|
||||||
|
"tooltip": "Part 4: Create a reusable service to manage hero data."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt5",
|
||||||
|
"title": "5. Add In-app Navigation",
|
||||||
|
"tooltip": "Part 5: Add the Angular router and navigate among the views."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "tutorial/toh-pt6",
|
||||||
|
"title": "6. Get Data from a Server",
|
||||||
|
"tooltip": "Part 6: Use HTTP to retrieve and save hero data."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/web-worker",
|
||||||
|
"title": "Web Workers",
|
||||||
|
"tooltip": "Using web workers for background processing."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Angular Libraries",
|
||||||
|
"tooltip": "Extending Angular with shared libraries.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/libraries",
|
||||||
|
"title": "Libraries Overview",
|
||||||
|
"tooltip": "Understand how and when to use or create libraries."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/using-libraries",
|
||||||
|
"title": "Using Published Libraries",
|
||||||
|
"tooltip": "Integrate published libraries into an app."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/creating-libraries",
|
||||||
|
"title": "Creating Libraries",
|
||||||
|
"tooltip": "Extend Angular by creating, publishing, and using your own libraries."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -810,11 +641,6 @@
|
|||||||
"title": "Release Information",
|
"title": "Release Information",
|
||||||
"tooltip": "Angular release practices, updating, and upgrading.",
|
"tooltip": "Angular release practices, updating, and upgrading.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"url": "guide/updating",
|
|
||||||
"title": "Keeping Up-to-Date",
|
|
||||||
"tooltip": "Information about updating Angular applications and libraries to the latest version."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "guide/releases",
|
"url": "guide/releases",
|
||||||
"title": "Release Practices",
|
"title": "Release Practices",
|
||||||
@ -911,23 +737,181 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Angular Style and Usage",
|
"title": "Reference",
|
||||||
"tooltip": "Summaries of Angular syntax, coding, and doc styles.",
|
"tooltip": "Reference guides for Angular features and tools.",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"url": "guide/cheatsheet",
|
"title": "Conceptual Reference",
|
||||||
"title": "Quick Reference",
|
"tooltip": "Reference documentation that explains how Angular features work.",
|
||||||
"tooltip": "A quick guide to common Angular coding techniques."
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Angular Concepts",
|
||||||
|
"tooltip": "Introduction to basic concepts for Angular applications.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/architecture",
|
||||||
|
"title": "Intro to Basic Concepts",
|
||||||
|
"tooltip": "Basic building blocks of Angular applications."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/styleguide",
|
"url": "guide/architecture-modules",
|
||||||
"title": "Coding Style Guide",
|
"title": "Intro to Modules",
|
||||||
"tooltip": "Guidelines for writing Angular code."
|
"tooltip": "About NgModules."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "guide/docs-style-guide",
|
"url": "guide/architecture-components",
|
||||||
"title": "Documentation Style Guide",
|
"title": "Intro to Components",
|
||||||
"tooltip": "Style guide for documentation authors."
|
"tooltip": "About Components, Templates, and Views."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/architecture-services",
|
||||||
|
"title": "Intro to Services and DI",
|
||||||
|
"tooltip": "About services and dependency injection."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/architecture-next-steps",
|
||||||
|
"title": "Next Steps",
|
||||||
|
"tooltip": "Beyond the basics."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Workspace and project structure",
|
||||||
|
"tooltip": "Workspace and project file structure and configuration.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/file-structure",
|
||||||
|
"title": "Project File Structure",
|
||||||
|
"tooltip": "How your Angular workspace looks on your filesystem."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/workspace-config",
|
||||||
|
"title": "Workspace Configuration",
|
||||||
|
"tooltip": "The \"angular.json\" file contains workspace and project configuration defaults for Angular CLI commands."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/npm-packages",
|
||||||
|
"title": "npm Dependencies",
|
||||||
|
"tooltip": "Description of npm packages required at development time and at runtime."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/typescript-configuration",
|
||||||
|
"title": "TypeScript Configuration",
|
||||||
|
"tooltip": "TypeScript configuration for Angular developers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/browser-support",
|
||||||
|
"title": "Browser Support",
|
||||||
|
"tooltip": "Browser support and polyfills guide."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/strict-mode",
|
||||||
|
"title": "Strict mode",
|
||||||
|
"tooltip": "Reference documentation for Angular's strict mode."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "NgModules",
|
||||||
|
"tooltip": "NgModules.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/ngmodules",
|
||||||
|
"title": "NgModules Introduction",
|
||||||
|
"tooltip": "Use NgModules to make your apps efficient."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/ngmodule-vs-jsmodule",
|
||||||
|
"title": "JS Modules vs NgModules",
|
||||||
|
"tooltip": "Differentiate between JavaScript modules and NgModules."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/bootstrapping",
|
||||||
|
"title": "Launching Apps with a Root Module",
|
||||||
|
"tooltip": "Tell Angular how to construct and bootstrap the app in the root \"AppModule\"."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/frequent-ngmodules",
|
||||||
|
"title": "Frequently Used NgModules",
|
||||||
|
"tooltip": "Introduction to the most frequently used NgModules."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/module-types",
|
||||||
|
"title": "Types of Feature Modules",
|
||||||
|
"tooltip": "Description of the different types of feature modules."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/entry-components",
|
||||||
|
"title": "Entry Components",
|
||||||
|
"tooltip": "All about entry components in Angular."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/feature-modules",
|
||||||
|
"title": "Feature Modules",
|
||||||
|
"tooltip": "Create feature modules to organize your code."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/providers",
|
||||||
|
"title": "Providing Dependencies",
|
||||||
|
"tooltip": "Providing dependencies to NgModules."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/singleton-services",
|
||||||
|
"title": "Singleton Services",
|
||||||
|
"tooltip": "Creating singleton services."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/lazy-loading-ngmodules",
|
||||||
|
"title": "Lazy Loading Feature Modules",
|
||||||
|
"tooltip": "Lazy load modules to speed up your apps."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/sharing-ngmodules",
|
||||||
|
"title": "Sharing NgModules",
|
||||||
|
"tooltip": "Share NgModules to streamline your apps."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/ngmodule-api",
|
||||||
|
"title": "NgModule API",
|
||||||
|
"tooltip": "Understand the details of NgModules."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/ngmodule-faq",
|
||||||
|
"title": "NgModule FAQs",
|
||||||
|
"tooltip": "Answers to frequently asked questions about NgModules."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Observables & RxJS",
|
||||||
|
"tooltip": "Using observables for message passing in Angular.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/observables",
|
||||||
|
"title": "Observables Overview",
|
||||||
|
"tooltip": "Using observables to pass values synchronously or asynchronously."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/rx-library",
|
||||||
|
"title": "The RxJS Library",
|
||||||
|
"tooltip": "A library for reactive programming using observables to compose asynchronous or callback-based code."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/observables-in-angular",
|
||||||
|
"title": "Observables in Angular",
|
||||||
|
"tooltip": "How Angular subsystems use and expect observables."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/practical-observable-usage",
|
||||||
|
"title": "Practical Usage",
|
||||||
|
"tooltip": "Domains in which observables are particularly useful."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/comparing-observables",
|
||||||
|
"title": "Compare to Other Techniques",
|
||||||
|
"tooltip": "How observables compare to promises and other message passing techniques."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -951,6 +935,29 @@
|
|||||||
"title": "API Reference",
|
"title": "API Reference",
|
||||||
"tooltip": "Details of the Angular packages, classes, interfaces, and other types.",
|
"tooltip": "Details of the Angular packages, classes, interfaces, and other types.",
|
||||||
"url": "api"
|
"url": "api"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/glossary",
|
||||||
|
"title": "Angular Glossary",
|
||||||
|
"tooltip": "Brief definitions of the most important words in the Angular vocabulary."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Angular Style and Usage",
|
||||||
|
"tooltip": "Summaries of Angular syntax, coding, and doc styles.",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/cheatsheet",
|
||||||
|
"title": "Quick Reference",
|
||||||
|
"tooltip": "A quick guide to common Angular coding techniques."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/styleguide",
|
||||||
|
"title": "Coding Style Guide",
|
||||||
|
"tooltip": "Guidelines for writing Angular code."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Footer": [
|
"Footer": [
|
||||||
|
@ -40,7 +40,7 @@ A later part of this tutorial, [Use forms for user input](start/start-forms "Try
|
|||||||
|
|
||||||
<code-example header="src/app/cart.service.ts" path="getting-started/src/app/cart.service.1.ts"></code-example>
|
<code-example header="src/app/cart.service.ts" path="getting-started/src/app/cart.service.1.ts"></code-example>
|
||||||
|
|
||||||
<div class="alert is-helpful>
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
The StackBlitz generator might provide the cart service in `app.module.ts` by default. That differs from the example, which uses a bundle-optimization technique, an `@Injectable()` decorator with the `{ providedIn: 'root' }` statement.
|
The StackBlitz generator might provide the cart service in `app.module.ts` by default. That differs from the example, which uses a bundle-optimization technique, an `@Injectable()` decorator with the `{ providedIn: 'root' }` statement.
|
||||||
For more information about services, see [Introduction to Services and Dependency Injection](guide/architecture-services "Concepts > Intro to Services and DI").
|
For more information about services, see [Introduction to Services and Dependency Injection](guide/architecture-services "Concepts > Intro to Services and DI").
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
"build-local-with-viewengine": "yarn ~~build",
|
"build-local-with-viewengine": "yarn ~~build",
|
||||||
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
|
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
|
||||||
"build-local-with-viewengine-ci": "yarn ~~build --progress=false",
|
"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 32391604b",
|
||||||
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
|
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
|
||||||
"test": "yarn check-env && ng test",
|
"test": "yarn check-env && ng test",
|
||||||
"pree2e": "yarn check-env && yarn update-webdriver",
|
"pree2e": "yarn check-env && yarn update-webdriver",
|
||||||
|
@ -214,7 +214,7 @@ code {
|
|||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@include line-height(24);
|
@include line-height(24);
|
||||||
vertical-align: bottom;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,10 +99,11 @@ describe('site App', function() {
|
|||||||
describe('scrolling to the top', () => {
|
describe('scrolling to the top', () => {
|
||||||
it('should scroll to the top when navigating to another page', () => {
|
it('should scroll to the top when navigating to another page', () => {
|
||||||
page.navigateTo('guide/security');
|
page.navigateTo('guide/security');
|
||||||
|
|
||||||
page.scrollTo('bottom');
|
page.scrollTo('bottom');
|
||||||
expect(page.getScrollTop()).toBeGreaterThan(0);
|
expect(page.getScrollTop()).toBeGreaterThan(0);
|
||||||
|
// Navigate to Reference section, then check
|
||||||
|
// Find the navigation item that has the text "api"
|
||||||
|
page.click(page.getNavItem(/reference/i));
|
||||||
page.click(page.getNavItem(/api/i));
|
page.click(page.getNavItem(/api/i));
|
||||||
expect(page.locationPath()).toBe('/api');
|
expect(page.locationPath()).toBe('/api');
|
||||||
expect(page.getScrollTop()).toBe(0);
|
expect(page.getScrollTop()).toBe(0);
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{%- macro renderHeritage(exportDoc) -%}
|
{%- macro renderHeritage(exportDoc) -%}
|
||||||
{%- if exportDoc.extendsClauses.length %} extends {% for clause in exportDoc.extendsClauses -%}
|
{%- if exportDoc.extendsClauses.length %} extends {% for clause in exportDoc.extendsClauses -%}
|
||||||
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}
|
{% if clause.doc.path %}<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% else %}{$ clause.text $}{% endif %}{% if not loop.last %}, {% endif -%}
|
||||||
{% endfor %}{% endif %}
|
{% endfor %}{% endif %}
|
||||||
{%- if exportDoc.implementsClauses.length %} implements {% for clause in exportDoc.implementsClauses -%}
|
{%- if exportDoc.implementsClauses.length %} implements {% for clause in exportDoc.implementsClauses -%}
|
||||||
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}
|
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}
|
||||||
|
@ -8,6 +8,7 @@ ts_library(
|
|||||||
],
|
],
|
||||||
module_name = "@angular/dev-infra-private",
|
module_name = "@angular/dev-infra-private",
|
||||||
deps = [
|
deps = [
|
||||||
|
"//dev-infra/caretaker",
|
||||||
"//dev-infra/commit-message",
|
"//dev-infra/commit-message",
|
||||||
"//dev-infra/format",
|
"//dev-infra/format",
|
||||||
"//dev-infra/pr",
|
"//dev-infra/pr",
|
||||||
|
26
dev-infra/caretaker/BUILD.bazel
Normal file
26
dev-infra/caretaker/BUILD.bazel
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "caretaker",
|
||||||
|
srcs = [
|
||||||
|
"cli.ts",
|
||||||
|
],
|
||||||
|
module_name = "@angular/dev-infra-private/caretaker",
|
||||||
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
|
deps = [
|
||||||
|
"//dev-infra/caretaker/check",
|
||||||
|
"@npm//@types/yargs",
|
||||||
|
"@npm//yargs",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "config",
|
||||||
|
srcs = [
|
||||||
|
"config.ts",
|
||||||
|
],
|
||||||
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
|
deps = [
|
||||||
|
"//dev-infra/utils",
|
||||||
|
],
|
||||||
|
)
|
21
dev-infra/caretaker/check/BUILD.bazel
Normal file
21
dev-infra/caretaker/check/BUILD.bazel
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "check",
|
||||||
|
srcs = glob(["*.ts"]),
|
||||||
|
module_name = "@angular/dev-infra-private/caretaker/service-statuses",
|
||||||
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
|
deps = [
|
||||||
|
"//dev-infra/caretaker:config",
|
||||||
|
"//dev-infra/utils",
|
||||||
|
"@npm//@types/fs-extra",
|
||||||
|
"@npm//@types/node",
|
||||||
|
"@npm//@types/node-fetch",
|
||||||
|
"@npm//@types/yargs",
|
||||||
|
"@npm//multimatch",
|
||||||
|
"@npm//node-fetch",
|
||||||
|
"@npm//typed-graphqlify",
|
||||||
|
"@npm//yaml",
|
||||||
|
"@npm//yargs",
|
||||||
|
],
|
||||||
|
)
|
27
dev-infra/caretaker/check/check.ts
Normal file
27
dev-infra/caretaker/check/check.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* @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 {GitClient} from '../../utils/git';
|
||||||
|
import {getCaretakerConfig} from '../config';
|
||||||
|
|
||||||
|
import {printG3Comparison} from './g3';
|
||||||
|
import {printGithubTasks} from './github';
|
||||||
|
import {printServiceStatuses} from './services';
|
||||||
|
|
||||||
|
|
||||||
|
/** Check the status of services which Angular caretakers need to monitor. */
|
||||||
|
export async function checkServiceStatuses(githubToken: string) {
|
||||||
|
/** The configuration for the caretaker commands. */
|
||||||
|
const config = getCaretakerConfig();
|
||||||
|
/** The GitClient for interacting with git and Github. */
|
||||||
|
const git = new GitClient(githubToken, config);
|
||||||
|
|
||||||
|
await printServiceStatuses();
|
||||||
|
await printGithubTasks(git, config.caretaker);
|
||||||
|
await printG3Comparison(git);
|
||||||
|
}
|
39
dev-infra/caretaker/check/cli.ts
Normal file
39
dev-infra/caretaker/check/cli.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* @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 {addGithubTokenFlag} from '../../utils/yargs';
|
||||||
|
|
||||||
|
import {checkServiceStatuses} from './check';
|
||||||
|
|
||||||
|
|
||||||
|
export interface CaretakerCheckOptions {
|
||||||
|
githubToken: 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 command. */
|
||||||
|
function builder(yargs: Argv) {
|
||||||
|
return addGithubTokenFlag(yargs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handles the command. */
|
||||||
|
async function handler({githubToken}: Arguments<CaretakerCheckOptions>) {
|
||||||
|
await checkServiceStatuses(githubToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** yargs command module for checking status information for the repository */
|
||||||
|
export const CheckModule: CommandModule<{}, CaretakerCheckOptions> = {
|
||||||
|
handler,
|
||||||
|
builder,
|
||||||
|
command: 'check',
|
||||||
|
describe: 'Check the status of information the caretaker manages for the repository',
|
||||||
|
};
|
123
dev-infra/caretaker/check/g3.ts
Normal file
123
dev-infra/caretaker/check/g3.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* @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} from 'fs-extra';
|
||||||
|
import * as multimatch from 'multimatch';
|
||||||
|
import {join} from 'path';
|
||||||
|
import {parse as parseYaml} from 'yaml';
|
||||||
|
|
||||||
|
import {getRepoBaseDir} from '../../utils/config';
|
||||||
|
import {bold, debug, info} from '../../utils/console';
|
||||||
|
import {GitClient} from '../../utils/git';
|
||||||
|
|
||||||
|
/** Compare the upstream master to the upstream g3 branch, if it exists. */
|
||||||
|
export async function printG3Comparison(git: GitClient) {
|
||||||
|
const angularRobotFilePath = join(getRepoBaseDir(), '.github/angular-robot.yml');
|
||||||
|
if (!existsSync(angularRobotFilePath)) {
|
||||||
|
return debug('No angular robot configuration file exists, skipping.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The configuration defined for the angular robot. */
|
||||||
|
const robotConfig = parseYaml(readFileSync(angularRobotFilePath).toString());
|
||||||
|
/** The files to be included in the g3 sync. */
|
||||||
|
const includeFiles = robotConfig?.merge?.g3Status?.include || [];
|
||||||
|
/** The files to be expected in the g3 sync. */
|
||||||
|
const excludeFiles = robotConfig?.merge?.g3Status?.exclude || [];
|
||||||
|
|
||||||
|
if (includeFiles.length === 0 && excludeFiles.length === 0) {
|
||||||
|
debug('No g3Status include or exclude lists are defined in the angular robot configuration,');
|
||||||
|
debug('skipping.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Random prefix to create unique branch names. */
|
||||||
|
const randomPrefix = `prefix${Math.floor(Math.random() * 1000000)}`;
|
||||||
|
/** Ref name of the temporary master branch. */
|
||||||
|
const masterRef = `${randomPrefix}-master`;
|
||||||
|
/** Ref name of the temporary g3 branch. */
|
||||||
|
const g3Ref = `${randomPrefix}-g3`;
|
||||||
|
/** Url of the ref for fetching master and g3 branches. */
|
||||||
|
const refUrl = `https://github.com/${git.remoteConfig.owner}/${git.remoteConfig.name}.git`;
|
||||||
|
/** The result fo the fetch command. */
|
||||||
|
const fetchResult = git.runGraceful(['fetch', refUrl, `master:${masterRef}`, `g3:${g3Ref}`]);
|
||||||
|
|
||||||
|
// If the upstream repository does not have a g3 branch to compare to, skip the comparison.
|
||||||
|
if (fetchResult.status !== 0) {
|
||||||
|
if (fetchResult.stderr.includes(`couldn't find remote ref g3`)) {
|
||||||
|
return debug('No g3 branch exists on upstream, skipping.');
|
||||||
|
}
|
||||||
|
throw Error('Fetch of master and g3 branches for comparison failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The statistical information about the git diff between master and g3. */
|
||||||
|
const stats = getDiffStats(git);
|
||||||
|
|
||||||
|
// Delete the temporarily created mater and g3 branches.
|
||||||
|
git.runGraceful(['branch', '-D', masterRef, g3Ref]);
|
||||||
|
|
||||||
|
info.group(bold('g3 branch check'));
|
||||||
|
info(`${stats.commits} commits between g3 and master`);
|
||||||
|
if (stats.files === 0) {
|
||||||
|
info('✅ No sync is needed at this time');
|
||||||
|
} else {
|
||||||
|
info(`${stats.files} files changed, ${stats.insertions} insertions(+), ${
|
||||||
|
stats.deletions} deletions(-) will be included in the next sync`);
|
||||||
|
}
|
||||||
|
info.groupEnd();
|
||||||
|
info();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get git diff stats between master and g3, for all files and filtered to only g3 affecting
|
||||||
|
* files.
|
||||||
|
*/
|
||||||
|
function getDiffStats(git: GitClient) {
|
||||||
|
/** The diff stats to be returned. */
|
||||||
|
const stats = {
|
||||||
|
insertions: 0,
|
||||||
|
deletions: 0,
|
||||||
|
files: 0,
|
||||||
|
commits: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Determine the number of commits between master and g3 refs. */
|
||||||
|
stats.commits = parseInt(git.run(['rev-list', '--count', `${g3Ref}..${masterRef}`]).stdout, 10);
|
||||||
|
|
||||||
|
// Get the numstat information between master and g3
|
||||||
|
git.run(['diff', `${g3Ref}...${masterRef}`, '--numstat'])
|
||||||
|
.stdout
|
||||||
|
// Remove the extra space after git's output.
|
||||||
|
.trim()
|
||||||
|
// Split each line of git output into array
|
||||||
|
.split('\n')
|
||||||
|
// Split each line from the git output into components parts: insertions,
|
||||||
|
// deletions and file name respectively
|
||||||
|
.map(line => line.split('\t'))
|
||||||
|
// Parse number value from the insertions and deletions values
|
||||||
|
// Example raw line input:
|
||||||
|
// 10\t5\tsrc/file/name.ts
|
||||||
|
.map(line => [Number(line[0]), Number(line[1]), line[2]] as [number, number, string])
|
||||||
|
// Add each line's value to the diff stats, and conditionally to the g3
|
||||||
|
// stats as well if the file name is included in the files synced to g3.
|
||||||
|
.forEach(([insertions, deletions, fileName]) => {
|
||||||
|
if (checkMatchAgainstIncludeAndExclude(fileName, includeFiles, excludeFiles)) {
|
||||||
|
stats.insertions += insertions;
|
||||||
|
stats.deletions += deletions;
|
||||||
|
stats.files += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Determine whether the file name passes both include and exclude checks. */
|
||||||
|
function checkMatchAgainstIncludeAndExclude(
|
||||||
|
file: string, includes: string[], excludes: string[]) {
|
||||||
|
return multimatch(multimatch(file, includes), excludes, {flipNegate: true}).length !== 0;
|
||||||
|
}
|
||||||
|
}
|
56
dev-infra/caretaker/check/github.ts
Normal file
56
dev-infra/caretaker/check/github.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* @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 {alias, params, types} from 'typed-graphqlify';
|
||||||
|
|
||||||
|
import {bold, debug, info} from '../../utils/console';
|
||||||
|
import {GitClient} from '../../utils/git';
|
||||||
|
import {CaretakerConfig} from '../config';
|
||||||
|
|
||||||
|
|
||||||
|
interface GithubInfoQuery {
|
||||||
|
[key: string]: {
|
||||||
|
issueCount: number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieve the number of matching issues for each github query. */
|
||||||
|
export async function printGithubTasks(git: GitClient, config: CaretakerConfig) {
|
||||||
|
if (!config.githubQueries?.length) {
|
||||||
|
debug('No github queries defined in the configuration, skipping.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info.group(bold('Github Tasks'));
|
||||||
|
await getGithubInfo(git, config);
|
||||||
|
info.groupEnd();
|
||||||
|
info();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieve query match counts and log discovered counts to the console. */
|
||||||
|
async function getGithubInfo(git: GitClient, {githubQueries: queries = []}: CaretakerConfig) {
|
||||||
|
/** The query object for graphql. */
|
||||||
|
const graphQlQuery: {[key: string]: {issueCount: number}} = {};
|
||||||
|
/** The Github search filter for the configured repository. */
|
||||||
|
const repoFilter = `repo:${git.remoteConfig.owner}/${git.remoteConfig.name}`;
|
||||||
|
queries.forEach(({name, query}) => {
|
||||||
|
/** The name of the query, with spaces removed to match GraphQL requirements. */
|
||||||
|
const queryKey = alias(name.replace(/ /g, ''), 'search');
|
||||||
|
graphQlQuery[queryKey] = params(
|
||||||
|
{
|
||||||
|
type: 'ISSUE',
|
||||||
|
query: `"${repoFilter} ${query.replace(/"/g, '\\"')}"`,
|
||||||
|
},
|
||||||
|
{issueCount: types.number},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
/** The results of the generated github query. */
|
||||||
|
const results = await git.github.graphql.query(graphQlQuery);
|
||||||
|
Object.values(results).forEach((result, i) => {
|
||||||
|
info(`${queries[i]?.name.padEnd(25)} ${result.issueCount}`);
|
||||||
|
});
|
||||||
|
}
|
79
dev-infra/caretaker/check/services.ts
Normal file
79
dev-infra/caretaker/check/services.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
import {bold, green, info, red} from '../../utils/console';
|
||||||
|
|
||||||
|
/** The status levels for services. */
|
||||||
|
enum ServiceStatus {
|
||||||
|
GREEN,
|
||||||
|
RED
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The results of checking the status of a service */
|
||||||
|
interface StatusCheckResult {
|
||||||
|
status: ServiceStatus;
|
||||||
|
description: string;
|
||||||
|
lastUpdated: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieve and log stasuses for all of the services of concern. */
|
||||||
|
export async function printServiceStatuses() {
|
||||||
|
info.group(bold(`Service Statuses (checked: ${new Date().toLocaleString()})`));
|
||||||
|
logStatus('CircleCI', await getCircleCiStatus());
|
||||||
|
logStatus('Github', await getGithubStatus());
|
||||||
|
logStatus('NPM', await getNpmStatus());
|
||||||
|
logStatus('Saucelabs', await getSaucelabsStatus());
|
||||||
|
info.groupEnd();
|
||||||
|
info();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Log the status of the service to the console. */
|
||||||
|
function logStatus(serviceName: string, status: StatusCheckResult) {
|
||||||
|
serviceName = serviceName.padEnd(15);
|
||||||
|
if (status.status === ServiceStatus.GREEN) {
|
||||||
|
info(`${serviceName} ${green('✅')}`);
|
||||||
|
} else if (status.status === ServiceStatus.RED) {
|
||||||
|
info.group(`${serviceName} ${red('❌')} (Updated: ${status.lastUpdated.toLocaleString()})`);
|
||||||
|
info(` Details: ${status.description}`);
|
||||||
|
info.groupEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the service status information for Saucelabs. */
|
||||||
|
async function getSaucelabsStatus(): Promise<StatusCheckResult> {
|
||||||
|
return getStatusFromStandardApi('https://status.us-west-1.saucelabs.com/api/v2/status.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the service status information for NPM. */
|
||||||
|
async function getNpmStatus(): Promise<StatusCheckResult> {
|
||||||
|
return getStatusFromStandardApi('https://status.npmjs.org/api/v2/status.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the service status information for CircleCI. */
|
||||||
|
async function getCircleCiStatus(): Promise<StatusCheckResult> {
|
||||||
|
return getStatusFromStandardApi('https://status.circleci.com/api/v2/status.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the service status information for Github. */
|
||||||
|
async function getGithubStatus(): Promise<StatusCheckResult> {
|
||||||
|
return getStatusFromStandardApi('https://www.githubstatus.com/api/v2/status.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieve the status information for a service which uses a standard API response. */
|
||||||
|
async function getStatusFromStandardApi(url: string) {
|
||||||
|
const result = await fetch(url).then(result => result.json());
|
||||||
|
const status = result.status.indicator === 'none' ? ServiceStatus.GREEN : ServiceStatus.RED;
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
description: result.status.description,
|
||||||
|
lastUpdated: new Date(result.page.updated_at)
|
||||||
|
};
|
||||||
|
}
|
16
dev-infra/caretaker/cli.ts
Normal file
16
dev-infra/caretaker/cli.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* @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 {Argv} from 'yargs';
|
||||||
|
import {CheckModule} from './check/cli';
|
||||||
|
|
||||||
|
|
||||||
|
/** Build the parser for the caretaker commands. */
|
||||||
|
export function buildCaretakerParser(yargs: Argv) {
|
||||||
|
return yargs.command(CheckModule);
|
||||||
|
}
|
24
dev-infra/caretaker/config.ts
Normal file
24
dev-infra/caretaker/config.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* @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 {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
|
||||||
|
|
||||||
|
export interface CaretakerConfig {
|
||||||
|
githubQueries?: {name: string; query: string;}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieve and validate the config as `CaretakerConfig`. */
|
||||||
|
export function getCaretakerConfig() {
|
||||||
|
// List of errors encountered validating the config.
|
||||||
|
const errors: string[] = [];
|
||||||
|
// The non-validated config object.
|
||||||
|
const config: Partial<NgDevConfig<{caretaker: CaretakerConfig}>> = getConfig();
|
||||||
|
|
||||||
|
assertNoErrors(errors);
|
||||||
|
return config as Required<typeof config>;
|
||||||
|
}
|
@ -13,8 +13,11 @@ import {buildCommitMessageParser} from './commit-message/cli';
|
|||||||
import {buildFormatParser} from './format/cli';
|
import {buildFormatParser} from './format/cli';
|
||||||
import {buildReleaseParser} from './release/cli';
|
import {buildReleaseParser} from './release/cli';
|
||||||
import {buildPrParser} from './pr/cli';
|
import {buildPrParser} from './pr/cli';
|
||||||
|
import {captureLogOutputForCommand} from './utils/console';
|
||||||
|
import {buildCaretakerParser} from './caretaker/cli';
|
||||||
|
|
||||||
yargs.scriptName('ng-dev')
|
yargs.scriptName('ng-dev')
|
||||||
|
.middleware(captureLogOutputForCommand)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.recommendCommands()
|
.recommendCommands()
|
||||||
.command('commit-message <command>', '', buildCommitMessageParser)
|
.command('commit-message <command>', '', buildCommitMessageParser)
|
||||||
@ -23,6 +26,7 @@ yargs.scriptName('ng-dev')
|
|||||||
.command('pullapprove <command>', '', buildPullapproveParser)
|
.command('pullapprove <command>', '', buildPullapproveParser)
|
||||||
.command('release <command>', '', buildReleaseParser)
|
.command('release <command>', '', buildReleaseParser)
|
||||||
.command('ts-circular-deps <command>', '', tsCircularDependenciesBuilder)
|
.command('ts-circular-deps <command>', '', tsCircularDependenciesBuilder)
|
||||||
|
.command('caretaker <command>', '', buildCaretakerParser)
|
||||||
.wrap(120)
|
.wrap(120)
|
||||||
.strict()
|
.strict()
|
||||||
.parse();
|
.parse();
|
||||||
|
@ -4,6 +4,7 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
|||||||
ts_library(
|
ts_library(
|
||||||
name = "commit-message",
|
name = "commit-message",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"builder.ts",
|
||||||
"cli.ts",
|
"cli.ts",
|
||||||
"commit-message-draft.ts",
|
"commit-message-draft.ts",
|
||||||
"config.ts",
|
"config.ts",
|
||||||
@ -12,14 +13,17 @@ ts_library(
|
|||||||
"validate.ts",
|
"validate.ts",
|
||||||
"validate-file.ts",
|
"validate-file.ts",
|
||||||
"validate-range.ts",
|
"validate-range.ts",
|
||||||
|
"wizard.ts",
|
||||||
],
|
],
|
||||||
module_name = "@angular/dev-infra-private/commit-message",
|
module_name = "@angular/dev-infra-private/commit-message",
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
"//dev-infra/utils",
|
"//dev-infra/utils",
|
||||||
|
"@npm//@types/inquirer",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
"@npm//@types/shelljs",
|
"@npm//@types/shelljs",
|
||||||
"@npm//@types/yargs",
|
"@npm//@types/yargs",
|
||||||
|
"@npm//inquirer",
|
||||||
"@npm//shelljs",
|
"@npm//shelljs",
|
||||||
"@npm//yargs",
|
"@npm//yargs",
|
||||||
],
|
],
|
||||||
@ -29,6 +33,7 @@ ts_library(
|
|||||||
name = "test_lib",
|
name = "test_lib",
|
||||||
testonly = True,
|
testonly = True,
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"builder.spec.ts",
|
||||||
"parse.spec.ts",
|
"parse.spec.ts",
|
||||||
"validate.spec.ts",
|
"validate.spec.ts",
|
||||||
],
|
],
|
||||||
|
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');
|
||||||
|
}
|
@ -12,6 +12,7 @@ import {info} from '../utils/console';
|
|||||||
import {restoreCommitMessage} from './restore-commit-message';
|
import {restoreCommitMessage} from './restore-commit-message';
|
||||||
import {validateFile} from './validate-file';
|
import {validateFile} from './validate-file';
|
||||||
import {validateCommitRange} from './validate-range';
|
import {validateCommitRange} from './validate-range';
|
||||||
|
import {runWizard} from './wizard';
|
||||||
|
|
||||||
/** Build the parser for the commit-message commands. */
|
/** Build the parser for the commit-message commands. */
|
||||||
export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||||
@ -41,6 +42,23 @@ export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
|||||||
args => {
|
args => {
|
||||||
restoreCommitMessage(args['file-env-variable'][0], args['file-env-variable'][1] as any);
|
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(
|
.command(
|
||||||
'pre-commit-validate', 'Validate the most recent commit message', {
|
'pre-commit-validate', 'Validate the most recent commit message', {
|
||||||
'file': {
|
'file': {
|
||||||
|
@ -39,36 +39,56 @@ export enum ScopeRequirement {
|
|||||||
|
|
||||||
/** A commit type */
|
/** A commit type */
|
||||||
export interface CommitType {
|
export interface CommitType {
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
scope: ScopeRequirement;
|
scope: ScopeRequirement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The valid commit types for Angular commit messages. */
|
/** The valid commit types for Angular commit messages. */
|
||||||
export const COMMIT_TYPES: {[key: string]: CommitType} = {
|
export const COMMIT_TYPES: {[key: string]: CommitType} = {
|
||||||
build: {
|
build: {
|
||||||
scope: ScopeRequirement.Forbidden,
|
name: 'build',
|
||||||
|
description: 'Changes to local repository build system and tooling',
|
||||||
|
scope: ScopeRequirement.Optional,
|
||||||
},
|
},
|
||||||
ci: {
|
ci: {
|
||||||
|
name: 'ci',
|
||||||
|
description: 'Changes to CI configuration and CI specific tooling',
|
||||||
scope: ScopeRequirement.Forbidden,
|
scope: ScopeRequirement.Forbidden,
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
|
name: 'docs',
|
||||||
|
description: 'Changes which exclusively affects documentation.',
|
||||||
scope: ScopeRequirement.Optional,
|
scope: ScopeRequirement.Optional,
|
||||||
},
|
},
|
||||||
feat: {
|
feat: {
|
||||||
|
name: 'feat',
|
||||||
|
description: 'Creates a new feature',
|
||||||
scope: ScopeRequirement.Required,
|
scope: ScopeRequirement.Required,
|
||||||
},
|
},
|
||||||
fix: {
|
fix: {
|
||||||
|
name: 'fix',
|
||||||
|
description: 'Fixes a previously discovered failure/bug',
|
||||||
scope: ScopeRequirement.Required,
|
scope: ScopeRequirement.Required,
|
||||||
},
|
},
|
||||||
perf: {
|
perf: {
|
||||||
|
name: 'perf',
|
||||||
|
description: 'Improves performance without any change in functionality or API',
|
||||||
scope: ScopeRequirement.Required,
|
scope: ScopeRequirement.Required,
|
||||||
},
|
},
|
||||||
refactor: {
|
refactor: {
|
||||||
|
name: 'refactor',
|
||||||
|
description: 'Refactor without any change in functionality or API (includes style changes)',
|
||||||
scope: ScopeRequirement.Required,
|
scope: ScopeRequirement.Required,
|
||||||
},
|
},
|
||||||
release: {
|
release: {
|
||||||
|
name: 'release',
|
||||||
|
description: 'A release point in the repository',
|
||||||
scope: ScopeRequirement.Forbidden,
|
scope: ScopeRequirement.Forbidden,
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
|
name: 'test',
|
||||||
|
description: 'Improvements or corrections made to the project\'s test suite',
|
||||||
scope: ScopeRequirement.Required,
|
scope: ScopeRequirement.Required,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -82,4 +82,24 @@ describe('commit message parsing:', () => {
|
|||||||
const message2 = buildCommitMessage({prefix: 'squash! '});
|
const message2 = buildCommitMessage({prefix: 'squash! '});
|
||||||
expect(parseCommitMessage(message2).isSquash).toBe(true);
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -36,6 +36,10 @@ const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/;
|
|||||||
|
|
||||||
/** Parse a full commit message into its composite parts. */
|
/** Parse a full commit message into its composite parts. */
|
||||||
export function parseCommitMessage(commitMsg: string): ParsedCommitMessage {
|
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 header = '';
|
||||||
let body = '';
|
let body = '';
|
||||||
let bodyWithoutLinking = '';
|
let bodyWithoutLinking = '';
|
||||||
|
@ -6,9 +6,10 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {info} from 'console';
|
|
||||||
import {writeFileSync} from 'fs';
|
import {writeFileSync} from 'fs';
|
||||||
|
|
||||||
|
import {debug, log} from '../utils/console';
|
||||||
|
|
||||||
import {loadCommitMessageDraft} from './commit-message-draft';
|
import {loadCommitMessageDraft} from './commit-message-draft';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,19 +21,19 @@ import {loadCommitMessageDraft} from './commit-message-draft';
|
|||||||
export function restoreCommitMessage(
|
export function restoreCommitMessage(
|
||||||
filePath: string, source?: 'message'|'template'|'squash'|'commit') {
|
filePath: string, source?: 'message'|'template'|'squash'|'commit') {
|
||||||
if (!!source) {
|
if (!!source) {
|
||||||
info('Skipping commit message restoration attempt');
|
log('Skipping commit message restoration attempt');
|
||||||
if (source === 'message') {
|
if (source === 'message') {
|
||||||
info('A commit message was already provided via the command with a -m or -F flag');
|
debug('A commit message was already provided via the command with a -m or -F flag');
|
||||||
}
|
}
|
||||||
if (source === 'template') {
|
if (source === 'template') {
|
||||||
info('A commit message was already provided via the -t flag or config.template setting');
|
debug('A commit message was already provided via the -t flag or config.template setting');
|
||||||
}
|
}
|
||||||
if (source === 'squash') {
|
if (source === 'squash') {
|
||||||
info('A commit message was already provided as a merge action or via .git/MERGE_MSG');
|
debug('A commit message was already provided as a merge action or via .git/MERGE_MSG');
|
||||||
}
|
}
|
||||||
if (source === 'commit') {
|
if (source === 'commit') {
|
||||||
info('A commit message was already provided through a revision specified via --fixup, -c,');
|
debug('A commit message was already provided through a revision specified via --fixup, -c,');
|
||||||
info('-C or --amend flag');
|
debug('-C or --amend flag');
|
||||||
}
|
}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
|
}
|
@ -6,6 +6,7 @@ ts_library(
|
|||||||
module_name = "@angular/dev-infra-private/pr",
|
module_name = "@angular/dev-infra-private/pr",
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//dev-infra/pr/checkout",
|
||||||
"//dev-infra/pr/discover-new-conflicts",
|
"//dev-infra/pr/discover-new-conflicts",
|
||||||
"//dev-infra/pr/merge",
|
"//dev-infra/pr/merge",
|
||||||
"//dev-infra/pr/rebase",
|
"//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",
|
||||||
|
],
|
||||||
|
)
|
36
dev-infra/pr/checkout/cli.ts
Normal file
36
dev-infra/pr/checkout/cli.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* @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 {addGithubTokenFlag} from '../../utils/yargs';
|
||||||
|
import {checkOutPullRequestLocally} from '../common/checkout-pr';
|
||||||
|
|
||||||
|
export interface CheckoutOptions {
|
||||||
|
prNumber: number;
|
||||||
|
githubToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds the checkout pull request command. */
|
||||||
|
function builder(yargs: Argv) {
|
||||||
|
return addGithubTokenFlag(yargs).positional('prNumber', {type: 'number', demandOption: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handles the checkout pull request command. */
|
||||||
|
async function handler({prNumber, githubToken}: Arguments<CheckoutOptions>) {
|
||||||
|
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 * as yargs from 'yargs';
|
||||||
|
|
||||||
|
import {CheckoutCommandModule} from './checkout/cli';
|
||||||
import {buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand} from './discover-new-conflicts/cli';
|
import {buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand} from './discover-new-conflicts/cli';
|
||||||
import {buildMergeCommand, handleMergeCommand} from './merge/cli';
|
import {buildMergeCommand, handleMergeCommand} from './merge/cli';
|
||||||
import {buildRebaseCommand, handleRebaseCommand} from './rebase/cli';
|
import {buildRebaseCommand, handleRebaseCommand} from './rebase/cli';
|
||||||
@ -24,7 +25,8 @@ export function buildPrParser(localYargs: yargs.Argv) {
|
|||||||
buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand)
|
buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand)
|
||||||
.command(
|
.command(
|
||||||
'rebase <pr-number>', 'Rebase a pending PR and push the rebased commits back to Github',
|
'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) {
|
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();
|
||||||
|
}
|
@ -72,7 +72,7 @@ export async function discoverNewConflictsForPr(
|
|||||||
|
|
||||||
info(`Requesting pending PRs from Github`);
|
info(`Requesting pending PRs from Github`);
|
||||||
/** List of PRs from github currently known as mergable. */
|
/** 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. */
|
/** The PR which is being checked against. */
|
||||||
const requestedPr = allPendingPRs.find(pr => pr.number === newPrNumber);
|
const requestedPr = allPendingPRs.find(pr => pr.number === newPrNumber);
|
||||||
if (requestedPr === undefined) {
|
if (requestedPr === undefined) {
|
||||||
|
@ -8,36 +8,24 @@
|
|||||||
|
|
||||||
import {Arguments, Argv} from 'yargs';
|
import {Arguments, Argv} from 'yargs';
|
||||||
|
|
||||||
import {error, red, yellow} from '../../utils/console';
|
import {addGithubTokenFlag} from '../../utils/yargs';
|
||||||
|
|
||||||
import {GITHUB_TOKEN_GENERATE_URL, mergePullRequest} from './index';
|
import {mergePullRequest} from './index';
|
||||||
|
|
||||||
/** The options available to the merge command via CLI. */
|
/** The options available to the merge command via CLI. */
|
||||||
export interface MergeCommandOptions {
|
export interface MergeCommandOptions {
|
||||||
'github-token'?: string;
|
githubToken: string;
|
||||||
'pr-number': number;
|
'pr-number': number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Builds the options for the merge command. */
|
/** Builds the options for the merge command. */
|
||||||
export function buildMergeCommand(yargs: Argv): Argv<MergeCommandOptions> {
|
export function buildMergeCommand(yargs: Argv): Argv<MergeCommandOptions> {
|
||||||
return yargs.help()
|
return addGithubTokenFlag(yargs).help().strict().positional(
|
||||||
.strict()
|
'pr-number', {demandOption: true, type: 'number'});
|
||||||
.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. */
|
/** Handles the merge command. i.e. performs the merge of a specified pull request. */
|
||||||
export async function handleMergeCommand(args: Arguments<MergeCommandOptions>) {
|
export async function handleMergeCommand(
|
||||||
const githubToken = args['github-token'] || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
{'pr-number': pr, githubToken}: Arguments<MergeCommandOptions>) {
|
||||||
if (!githubToken) {
|
await mergePullRequest(pr, githubToken);
|
||||||
error(red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.'));
|
|
||||||
error(red('Alternatively, pass the `--github-token` command line flag.'));
|
|
||||||
error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await mergePullRequest(args['pr-number'], githubToken);
|
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,11 @@ export async function findActiveVersionBranches(
|
|||||||
latestVersionBranch: string | null,
|
latestVersionBranch: string | null,
|
||||||
releaseCandidateBranch: string | null,
|
releaseCandidateBranch: string | null,
|
||||||
}> {
|
}> {
|
||||||
|
// Version representing the release-train currently in the next phase. Note that we ignore
|
||||||
|
// patch and pre-release segments in order to be able to compare the next release train to
|
||||||
|
// other release trains from version branches (which follow the `N.N.x` pattern).
|
||||||
|
const nextReleaseTrainVersion = semver.parse(`${nextVersion.major}.${nextVersion.minor}.0`)!;
|
||||||
|
|
||||||
let latestVersionBranch: string|null = null;
|
let latestVersionBranch: string|null = null;
|
||||||
let releaseCandidateBranch: string|null = null;
|
let releaseCandidateBranch: string|null = null;
|
||||||
|
|
||||||
@ -177,15 +182,21 @@ export async function findActiveVersionBranches(
|
|||||||
// next version-branch as that one is supposed to be the latest active version-branch. If it
|
// next version-branch as that one is supposed to be the latest active version-branch. If it
|
||||||
// is not, then an error will be thrown due to two FF/RC branches existing at the same time.
|
// is not, then an error will be thrown due to two FF/RC branches existing at the same time.
|
||||||
for (const {name, parsed} of branches) {
|
for (const {name, parsed} of branches) {
|
||||||
// It can happen that version branches that are more recent than the version in the next
|
// It can happen that version branches have been accidentally created which are more recent
|
||||||
// branch (i.e. `master`) have been created. We could ignore such branches silently, but
|
// than the release-train in the next branch (i.e. `master`). We could ignore such branches
|
||||||
// it might actually be symptomatic for an outdated version in the `next` branch, or an
|
// silently, but it might be symptomatic for an outdated version in the `next` branch, or an
|
||||||
// accidentally created branch by the caretaker. In either way we want to raise awareness.
|
// accidentally created branch by the caretaker. In either way we want to raise awareness.
|
||||||
if (semver.gte(parsed, nextVersion)) {
|
if (semver.gt(parsed, nextReleaseTrainVersion)) {
|
||||||
throw Error(
|
throw Error(
|
||||||
`Discovered unexpected version-branch that is representing a minor ` +
|
`Discovered unexpected version-branch "${name}" for a release-train that is ` +
|
||||||
`version more recent than the one in the "${nextBranchName}" branch. Consider ` +
|
`more recent than the release-train currently in the "${nextBranchName}" branch. ` +
|
||||||
`deleting the branch, or check if the version in "${nextBranchName}" is outdated.`);
|
`Please either delete the branch if created by accident, or update the outdated ` +
|
||||||
|
`version in the next branch (${nextBranchName}).`);
|
||||||
|
} else if (semver.eq(parsed, nextReleaseTrainVersion)) {
|
||||||
|
throw Error(
|
||||||
|
`Discovered unexpected version-branch "${name}" for a release-train that is already ` +
|
||||||
|
`active in the "${nextBranchName}" branch. Please either delete the branch if ` +
|
||||||
|
`created by accident, or update the version in the next branch (${nextBranchName}).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = await getVersionOfBranch(repo, name);
|
const version = await getVersionOfBranch(repo, name);
|
||||||
|
@ -280,7 +280,21 @@ describe('default target labels', () => {
|
|||||||
.toBeRejectedWithError('Invalid version detected in following branch: 11.1.x.');
|
.toBeRejectedWithError('Invalid version detected in following branch: 11.1.x.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error if branch more recent than version in "next" branch is found', async () => {
|
it('should error if version-branch more recent than "next" is discovered', async () => {
|
||||||
|
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
||||||
|
interceptBranchVersionRequest('11.3.x', '11.3.0-next.0');
|
||||||
|
interceptBranchVersionRequest('11.1.x', '11.1.5');
|
||||||
|
interceptBranchesListRequest(['11.1.x', '11.3.x']);
|
||||||
|
|
||||||
|
await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
|
||||||
|
.toBeRejectedWithError(
|
||||||
|
'Discovered unexpected version-branch "11.3.x" for a release-train that is ' +
|
||||||
|
'more recent than the release-train currently in the "master" branch. Please ' +
|
||||||
|
'either delete the branch if created by accident, or update the outdated version ' +
|
||||||
|
'in the next branch (master).');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if branch is matching with release-train in the "next" branch', async () => {
|
||||||
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
||||||
interceptBranchVersionRequest('11.2.x', '11.2.0-next.0');
|
interceptBranchVersionRequest('11.2.x', '11.2.0-next.0');
|
||||||
interceptBranchVersionRequest('11.1.x', '11.1.5');
|
interceptBranchVersionRequest('11.1.x', '11.1.5');
|
||||||
@ -288,9 +302,9 @@ describe('default target labels', () => {
|
|||||||
|
|
||||||
await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
|
await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
|
||||||
.toBeRejectedWithError(
|
.toBeRejectedWithError(
|
||||||
'Discovered unexpected version-branch that is representing a minor version more ' +
|
'Discovered unexpected version-branch "11.2.x" for a release-train that is already ' +
|
||||||
'recent than the one in the "master" branch. Consider deleting the branch, or check ' +
|
'active in the "master" branch. Please either delete the branch if created by ' +
|
||||||
'if the version in "master" is outdated.');
|
'accident, or update the version in the next branch (master).');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow merging PR only into patch branch with "target: patch"', async () => {
|
it('should allow merging PR only into patch branch with "target: patch"', async () => {
|
||||||
|
@ -11,13 +11,11 @@ import {getConfig, getRepoBaseDir} from '../../utils/config';
|
|||||||
import {error, green, info, promptConfirm, red, yellow} from '../../utils/console';
|
import {error, green, info, promptConfirm, red, yellow} from '../../utils/console';
|
||||||
import {GitClient} from '../../utils/git';
|
import {GitClient} from '../../utils/git';
|
||||||
import {GithubApiRequestError} from '../../utils/git/github';
|
import {GithubApiRequestError} from '../../utils/git/github';
|
||||||
|
import {GITHUB_TOKEN_GENERATE_URL} from '../../utils/yargs';
|
||||||
|
|
||||||
import {loadAndValidateConfig, MergeConfig, MergeConfigWithRemote} from './config';
|
import {loadAndValidateConfig, MergeConfigWithRemote} from './config';
|
||||||
import {MergeResult, MergeStatus, PullRequestMergeTask} from './task';
|
import {MergeResult, MergeStatus, PullRequestMergeTask} from './task';
|
||||||
|
|
||||||
/** URL to the Github page where personal access tokens can be generated. */
|
|
||||||
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges a given pull request based on labels configured in the given merge configuration.
|
* Merges a given pull request based on labels configured in the given merge configuration.
|
||||||
|
@ -8,39 +8,23 @@
|
|||||||
|
|
||||||
import {Arguments, Argv} from 'yargs';
|
import {Arguments, Argv} from 'yargs';
|
||||||
|
|
||||||
import {error} from '../../utils/console';
|
import {addGithubTokenFlag} from '../../utils/yargs';
|
||||||
|
|
||||||
import {rebasePr} from './index';
|
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`;
|
|
||||||
|
|
||||||
/** The options available to the rebase command via CLI. */
|
/** The options available to the rebase command via CLI. */
|
||||||
export interface RebaseCommandOptions {
|
export interface RebaseCommandOptions {
|
||||||
'github-token'?: string;
|
githubToken: string;
|
||||||
prNumber: number;
|
prNumber: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Builds the rebase pull request command. */
|
/** Builds the rebase pull request command. */
|
||||||
export function buildRebaseCommand(yargs: Argv): Argv<RebaseCommandOptions> {
|
export function buildRebaseCommand(yargs: Argv): Argv<RebaseCommandOptions> {
|
||||||
return yargs
|
return addGithubTokenFlag(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.'
|
|
||||||
})
|
|
||||||
.positional('prNumber', {type: 'number', demandOption: true});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Handles the rebase pull request command. */
|
/** Handles the rebase pull request command. */
|
||||||
export async function handleRebaseCommand(args: Arguments<RebaseCommandOptions>) {
|
export async function handleRebaseCommand(
|
||||||
const githubToken = args['github-token'] || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
{prNumber, githubToken}: Arguments<RebaseCommandOptions>) {
|
||||||
if (!githubToken) {
|
await rebasePr(prNumber, 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.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await rebasePr(args.prNumber, githubToken);
|
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ export async function rebasePr(
|
|||||||
*/
|
*/
|
||||||
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
||||||
/* Get the PR information from Github. */
|
/* 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 headRefName = pr.headRef.name;
|
||||||
const baseRefName = pr.baseRef.name;
|
const baseRefName = pr.baseRef.name;
|
||||||
|
@ -12,13 +12,18 @@ ts_library(
|
|||||||
"@npm//@octokit/graphql",
|
"@npm//@octokit/graphql",
|
||||||
"@npm//@octokit/rest",
|
"@npm//@octokit/rest",
|
||||||
"@npm//@octokit/types",
|
"@npm//@octokit/types",
|
||||||
|
"@npm//@types/fs-extra",
|
||||||
"@npm//@types/inquirer",
|
"@npm//@types/inquirer",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
"@npm//@types/shelljs",
|
"@npm//@types/shelljs",
|
||||||
|
"@npm//@types/yargs",
|
||||||
"@npm//chalk",
|
"@npm//chalk",
|
||||||
|
"@npm//fs-extra",
|
||||||
"@npm//inquirer",
|
"@npm//inquirer",
|
||||||
|
"@npm//inquirer-autocomplete-prompt",
|
||||||
"@npm//shelljs",
|
"@npm//shelljs",
|
||||||
"@npm//tslib",
|
"@npm//tslib",
|
||||||
"@npm//typed-graphqlify",
|
"@npm//typed-graphqlify",
|
||||||
|
"@npm//yargs",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -7,13 +7,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import {prompt} from 'inquirer';
|
import {writeFileSync} from 'fs-extra';
|
||||||
|
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
|
||||||
|
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
||||||
|
import {join} from 'path';
|
||||||
|
import {Arguments} from 'yargs';
|
||||||
|
|
||||||
|
import {getRepoBaseDir} from './config';
|
||||||
|
|
||||||
/** Reexport of chalk colors for convenient access. */
|
/** Reexport of chalk colors for convenient access. */
|
||||||
export const red: typeof chalk = chalk.red;
|
export const red: typeof chalk = chalk.red;
|
||||||
export const green: typeof chalk = chalk.green;
|
export const green: typeof chalk = chalk.green;
|
||||||
export const yellow: typeof chalk = chalk.yellow;
|
export const yellow: typeof chalk = chalk.yellow;
|
||||||
|
export const bold: typeof chalk = chalk.bold;
|
||||||
|
|
||||||
/** Prompts the user with a confirmation question and a specified message. */
|
/** Prompts the user with a confirmation question and a specified message. */
|
||||||
export async function promptConfirm(message: string, defaultValue = false): Promise<boolean> {
|
export async function promptConfirm(message: string, defaultValue = false): Promise<boolean> {
|
||||||
@ -26,6 +32,52 @@ export async function promptConfirm(message: string, defaultValue = false): Prom
|
|||||||
.result;
|
.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.
|
* Supported levels for logging functions.
|
||||||
*
|
*
|
||||||
@ -93,6 +145,7 @@ function runConsoleCommand(loadCommand: () => Function, logLevel: LOG_LEVELS, ..
|
|||||||
if (getLogLevel() >= logLevel) {
|
if (getLogLevel() >= logLevel) {
|
||||||
loadCommand()(...text);
|
loadCommand()(...text);
|
||||||
}
|
}
|
||||||
|
printToLogFile(logLevel, ...text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,3 +161,56 @@ function getLogLevel() {
|
|||||||
}
|
}
|
||||||
return logLevel;
|
return logLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** All text to write to the log file. */
|
||||||
|
let LOGGED_TEXT = '';
|
||||||
|
/** Whether file logging as been enabled. */
|
||||||
|
let FILE_LOGGING_ENABLED = false;
|
||||||
|
/**
|
||||||
|
* The number of columns used in the prepended log level information on each line of the logging
|
||||||
|
* output file.
|
||||||
|
*/
|
||||||
|
const LOG_LEVEL_COLUMNS = 7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable writing the logged outputs to the log file on process exit, sets initial lines from the
|
||||||
|
* command execution, containing information about the timing and command parameters.
|
||||||
|
*
|
||||||
|
* This is expected to be called only once during a command run, and should be called by the
|
||||||
|
* middleware of yargs to enable the file logging before the rest of the command parsing and
|
||||||
|
* response is executed.
|
||||||
|
*/
|
||||||
|
export function captureLogOutputForCommand(argv: Arguments) {
|
||||||
|
if (FILE_LOGGING_ENABLED) {
|
||||||
|
throw Error('`captureLogOutputForCommand` cannot be called multiple times');
|
||||||
|
}
|
||||||
|
/** The date time used for timestamping when the command was invoked. */
|
||||||
|
const now = new Date();
|
||||||
|
/** Header line to separate command runs in log files. */
|
||||||
|
const headerLine = Array(100).fill('#').join('');
|
||||||
|
LOGGED_TEXT += `${headerLine}\nCommand: ${argv.$0} ${argv._.join(' ')}\nRan at: ${now}\n`;
|
||||||
|
|
||||||
|
// On process exit, write the logged output to the appropriate log files
|
||||||
|
process.on('exit', (code: number) => {
|
||||||
|
LOGGED_TEXT += `Command ran in ${new Date().getTime() - now.getTime()}ms`;
|
||||||
|
/** Path to the log file location. */
|
||||||
|
const logFilePath = join(getRepoBaseDir(), '.ng-dev.log');
|
||||||
|
|
||||||
|
writeFileSync(logFilePath, LOGGED_TEXT);
|
||||||
|
|
||||||
|
// For failure codes greater than 1, the new logged lines should be written to a specific log
|
||||||
|
// file for the command run failure.
|
||||||
|
if (code > 1) {
|
||||||
|
writeFileSync(join(getRepoBaseDir(), `.ng-dev.err-${now.getTime()}.log`), LOGGED_TEXT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark file logging as enabled to prevent the function from executing multiple times.
|
||||||
|
FILE_LOGGING_ENABLED = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write the provided text to the log file, prepending each line with the log level. */
|
||||||
|
function printToLogFile(logLevel: LOG_LEVELS, ...text: string[]) {
|
||||||
|
const logLevelText = `${LOG_LEVELS[logLevel]}:`.padEnd(LOG_LEVEL_COLUMNS);
|
||||||
|
LOGGED_TEXT += text.join(' ').split('\n').map(l => `${logLevelText} ${l}\n`).join('');
|
||||||
|
}
|
||||||
|
@ -26,7 +26,7 @@ export class GithubApiRequestError extends Error {
|
|||||||
**/
|
**/
|
||||||
export class GithubClient extends Octokit {
|
export class GithubClient extends Octokit {
|
||||||
/** The Github GraphQL (v4) API. */
|
/** The Github GraphQL (v4) API. */
|
||||||
graqhql: GithubGraphqlClient;
|
graphql: GithubGraphqlClient;
|
||||||
|
|
||||||
/** The current user based on checking against the Github API. */
|
/** The current user based on checking against the Github API. */
|
||||||
private _currentUser: string|null = null;
|
private _currentUser: string|null = null;
|
||||||
@ -42,7 +42,7 @@ export class GithubClient extends Octokit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create authenticated graphql client.
|
// Create authenticated graphql client.
|
||||||
this.graqhql = new GithubGraphqlClient(token);
|
this.graphql = new GithubGraphqlClient(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retrieve the login of the current user from Github. */
|
/** Retrieve the login of the current user from Github. */
|
||||||
@ -51,7 +51,7 @@ export class GithubClient extends Octokit {
|
|||||||
if (this._currentUser !== null) {
|
if (this._currentUser !== null) {
|
||||||
return this._currentUser;
|
return this._currentUser;
|
||||||
}
|
}
|
||||||
const result = await this.graqhql.query({
|
const result = await this.graphql.query({
|
||||||
viewer: {
|
viewer: {
|
||||||
login: types.string,
|
login: types.string,
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ class GithubGraphqlClient {
|
|||||||
// Set the default headers to include authorization with the provided token for all
|
// Set the default headers to include authorization with the provided token for all
|
||||||
// graphQL calls.
|
// graphQL calls.
|
||||||
if (token) {
|
if (token) {
|
||||||
this.graqhql.defaults({headers: {authorization: `token ${token}`}});
|
this.graqhql = this.graqhql.defaults({headers: {authorization: `token ${token}`}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ export class GitCommandError extends Error {
|
|||||||
/**
|
/**
|
||||||
* Common client for performing Git interactions.
|
* Common client for performing Git interactions.
|
||||||
*
|
*
|
||||||
* Takes in two optional arguements:
|
* Takes in two optional arguments:
|
||||||
* _githubToken: the token used for authentifation in github interactions, by default empty
|
* _githubToken: the token used for authentifation in github interactions, by default empty
|
||||||
* allowing readonly actions.
|
* allowing readonly actions.
|
||||||
* _config: The dev-infra configuration containing GitClientConfig information, by default
|
* _config: The dev-infra configuration containing GitClientConfig information, by default
|
||||||
@ -86,10 +86,10 @@ export class GitClient {
|
|||||||
/**
|
/**
|
||||||
* Spawns a given Git command process. Does not throw if the command fails. Additionally,
|
* Spawns a given Git command process. Does not throw if the command fails. Additionally,
|
||||||
* if there is any stderr output, the output will be printed. This makes it easier to
|
* if there is any stderr output, the output will be printed. This makes it easier to
|
||||||
* debug failed commands.
|
* info failed commands.
|
||||||
*/
|
*/
|
||||||
runGraceful(args: string[], options: SpawnSyncOptions = {}): SpawnSyncReturns<string> {
|
runGraceful(args: string[], options: SpawnSyncOptions = {}): SpawnSyncReturns<string> {
|
||||||
// To improve the debugging experience in case something fails, we print all executed
|
// To improve the infoging experience in case something fails, we print all executed
|
||||||
// Git commands. Note that we do not want to print the token if is contained in the
|
// Git commands. Note that we do not want to print the token if is contained in the
|
||||||
// command. It's common to share errors with others if the tool failed.
|
// command. It's common to share errors with others if the tool failed.
|
||||||
info('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
info('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
||||||
@ -150,6 +150,25 @@ export class GitClient {
|
|||||||
return value.replace(this._githubTokenRegex, '<TOKEN>');
|
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
|
* Assert the GitClient instance is using a token with permissions for the all of the
|
||||||
* provided OAuth scopes.
|
* provided OAuth scopes.
|
||||||
|
@ -6,29 +6,15 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {GitClient} from './git';
|
||||||
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}`,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Get a PR from github */
|
/** Get a PR from github */
|
||||||
export async function getPr<PrSchema>(
|
export async function getPr<PrSchema>(prSchema: PrSchema, prNumber: number, git: GitClient) {
|
||||||
prSchema: PrSchema, prNumber: number, {owner, name}: GithubConfig) {
|
/** 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(
|
const PR_QUERY = params(
|
||||||
{
|
{
|
||||||
$number: 'Int!', // The PR number
|
$number: 'Int!', // The PR number
|
||||||
@ -41,14 +27,15 @@ export async function getPr<PrSchema>(
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const result =
|
const result = (await git.github.graphql.query(PR_QUERY, {number: prNumber, owner, name}));
|
||||||
await graphql(graphqlQuery(PR_QUERY), {number: prNumber, owner, name}) as typeof PR_QUERY;
|
|
||||||
return result.repository.pullRequest;
|
return result.repository.pullRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get all pending PRs from github */
|
/** Get all pending PRs from github */
|
||||||
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, {owner, name}: GithubConfig) {
|
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, git: GitClient) {
|
||||||
// The GraphQL query object to get a page of pending PRs
|
/** 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(
|
const PRS_QUERY = params(
|
||||||
{
|
{
|
||||||
$first: 'Int', // How many entries to get with each request
|
$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);
|
/** The current cursor */
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
let cursor: string|undefined;
|
let cursor: string|undefined;
|
||||||
// If an additional page of members is expected
|
/** If an additional page of members is expected */
|
||||||
let hasNextPage = true;
|
let hasNextPage = true;
|
||||||
// Array of pending PRs
|
/** Array of pending PRs */
|
||||||
const prs: Array<PrSchema> = [];
|
const prs: Array<PrSchema> = [];
|
||||||
|
|
||||||
// For each page of the response, get the page and add it to the
|
// For each page of the response, get the page and add it to the list of PRs
|
||||||
// list of PRs
|
|
||||||
while (hasNextPage) {
|
while (hasNextPage) {
|
||||||
const {query, params} = queryBuilder(100, cursor);
|
const params = {
|
||||||
const results = await graphql(query, params) as typeof PRS_QUERY;
|
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);
|
prs.push(...results.repository.pullRequests.nodes);
|
||||||
hasNextPage = results.repository.pullRequests.pageInfo.hasNextPage;
|
hasNextPage = results.repository.pullRequests.pageInfo.hasNextPage;
|
||||||
cursor = results.repository.pullRequests.pageInfo.endCursor;
|
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;
|
||||||
|
}
|
37
dev-infra/utils/yargs.ts
Normal file
37
dev-infra/utils/yargs.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @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 {Argv} from 'yargs';
|
||||||
|
import {error, red, yellow} from './console';
|
||||||
|
|
||||||
|
export type ArgvWithGithubToken = Argv<{githubToken: string}>;
|
||||||
|
|
||||||
|
export function addGithubTokenFlag(yargs: Argv): ArgvWithGithubToken {
|
||||||
|
return yargs
|
||||||
|
// 'github-token' is casted to 'githubToken' to properly set up typings to reflect the key in
|
||||||
|
// the Argv object being camelCase rather than kebob case due to the `camel-case-expansion`
|
||||||
|
// config: https://github.com/yargs/yargs-parser#camel-case-expansion
|
||||||
|
.option('github-token' as 'githubToken', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Github token. If not set, token is retrieved from the environment variables.',
|
||||||
|
coerce: (token: string) => {
|
||||||
|
const githubToken = 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.'));
|
||||||
|
error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return githubToken;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.default('github-token' as 'githubToken', '', '<LOCAL TOKEN>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** URL to the Github page where personal access tokens can be generated. */
|
||||||
|
export const GITHUB_TOKEN_GENERATE_URL = 'https://github.com/settings/tokens/new';
|
@ -4,71 +4,23 @@ Caretaker is responsible for merging PRs into the individual branches and intern
|
|||||||
|
|
||||||
## Responsibilities
|
## Responsibilities
|
||||||
|
|
||||||
- Draining the queue of PRs ready to be merged. (PRs with [`PR action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22PR+action%3A+merge%22) label)
|
- Draining the queue of PRs ready to be merged. (PRs with [`action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22action%3A+merge%22) label)
|
||||||
- Assigning [new issues](https://github.com/angular/angular/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) to individual component authors.
|
- Assigning [new issues](https://github.com/angular/angular/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) to individual component authors.
|
||||||
|
|
||||||
## Merging the PR
|
## Merging the PR
|
||||||
|
|
||||||
A PR needs to have `PR action: merge` and `PR target: *` labels to be considered
|
A PR needs to have `action: merge` and `target: *` labels to be considered
|
||||||
ready to merge. Merging is performed by running `merge-pr` with a PR number to merge.
|
ready to merge. Merging is performed by running `ng-dev pr merge` with a PR number to merge.
|
||||||
|
|
||||||
|
The tooling automatically verifies the given PR is ready for merge. If the PR passes the tests, the
|
||||||
|
tool will automatically merge it based on the applied target label.
|
||||||
|
|
||||||
To merge a PR run:
|
To merge a PR run:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ ./scripts/github/merge-pr 1234
|
$ yarn ng-dev pr merge <pr number>
|
||||||
```
|
```
|
||||||
|
|
||||||
The `merge-pr` script will:
|
|
||||||
- Ensure that all appropriate labels are on the PR.
|
|
||||||
- Fetches the latest PR code from the `angular/angular` repo.
|
|
||||||
- It will `cherry-pick` all of the SHAs from the PR into the current corresponding branches `master` and or `?.?.x` (patch).
|
|
||||||
- It will rewrite commit history by automatically adding `Close #1234` and `(#1234)` into the commit message.
|
|
||||||
|
|
||||||
NOTE: The `merge-pr` will land the PR on `master` and or `?.?.x` (patch) as described by `PR target: *` label.
|
|
||||||
|
|
||||||
### Recovering from failed `merge-pr` due to conflicts
|
### Recovering from failed `merge-pr` due to conflicts
|
||||||
|
|
||||||
When running `merge-pr` the script will output the commands which it is about to run.
|
The `ng-dev pr merge` tool will automatically restore to the previous git state when a merge fails.
|
||||||
|
|
||||||
```
|
|
||||||
$ ./scripts/github/merge-pr 1234
|
|
||||||
======================
|
|
||||||
GitHub Merge PR Steps
|
|
||||||
======================
|
|
||||||
git cherry-pick angular/pr/1234~1..angular/pr/1234
|
|
||||||
git filter-branch -f --msg-filter "/home/misko/angular/scripts/github/utils/github.closes 1234" HEAD~1..HEAD
|
|
||||||
```
|
|
||||||
|
|
||||||
If the `cherry-pick` command fails than resolve conflicts and use `git cherry-pick --continue` once ready. After the `cherry-pick` is done cut&paste and run the `filter-branch` command to properly rewrite the messages
|
|
||||||
|
|
||||||
## Cherry-picking PRs into patch branch
|
|
||||||
|
|
||||||
In addition to merging PRs into the master branch, many PRs need to be also merged into a patch branch.
|
|
||||||
Follow these steps to get patch branch up to date.
|
|
||||||
|
|
||||||
1. Check out the most recent patch branch: `git checkout 4.3.x`
|
|
||||||
2. Get a list of PRs merged into master: `git log master --oneline -n10`
|
|
||||||
3. For each PR number in the commit message run: `./scripts/github/merge-pr 1234`
|
|
||||||
- The PR will only merge if the `PR target:` matches the branch.
|
|
||||||
|
|
||||||
Once all of the PRs are in patch branch, push the all branches and tags to github using `push-upstream` script.
|
|
||||||
|
|
||||||
|
|
||||||
## Pushing merged PRs into github
|
|
||||||
|
|
||||||
Use `push-upstream` script to push all of the branch and tags to github.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ./scripts/github/push-upstream
|
|
||||||
git push git@github.com:angular/angular.git master:master 4.3.x:4.3.x
|
|
||||||
Counting objects: 25, done.
|
|
||||||
Delta compression using up to 6 threads.
|
|
||||||
Compressing objects: 100% (17/17), done.
|
|
||||||
Writing objects: 100% (25/25), 2.22 KiB | 284.00 KiB/s, done.
|
|
||||||
Total 25 (delta 22), reused 8 (delta 7)
|
|
||||||
remote: Resolving deltas: 100% (22/22), completed with 18 local objects.
|
|
||||||
To github.com:angular/angular.git
|
|
||||||
079d884b6..d1c4a94bb master -> master
|
|
||||||
git push --tags -f git@github.com:angular/angular.git patch_sync:patch_sync
|
|
||||||
Everything up-to-date
|
|
||||||
```
|
|
||||||
|
@ -12,7 +12,7 @@ Change approvals in our monorepo are managed via [PullApprove](https://docs.pull
|
|||||||
# Merging
|
# Merging
|
||||||
|
|
||||||
Once a change has all of the required approvals, either the last approver or the PR author (if PR author has the project collaborator status)
|
Once a change has all of the required approvals, either the last approver or the PR author (if PR author has the project collaborator status)
|
||||||
should mark the PR with the `PR action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
|
should mark the PR with the `action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
|
||||||
This signals to the caretaker that the PR should be merged. See [merge instructions](CARETAKER.md).
|
This signals to the caretaker that the PR should be merged. See [merge instructions](CARETAKER.md).
|
||||||
|
|
||||||
# Who is the Caretaker?
|
# Who is the Caretaker?
|
||||||
|
@ -154,9 +154,7 @@ available as a long-term distribution mechanism, but they are guaranteed to be a
|
|||||||
time of the build.
|
time of the build.
|
||||||
|
|
||||||
You can access the artifacts for a specific CI run by going to the workflow page, clicking on the
|
You can access the artifacts for a specific CI run by going to the workflow page, clicking on the
|
||||||
`publish_packages_as_artifacts` job and then switching to the "Artifacts" tab.
|
`publish_packages_as_artifacts` job and then switching to the "ARTIFACTS" tab.
|
||||||
(If you happen to know the build number of the job, the URL will be something like:
|
|
||||||
`https://circleci.com/gh/angular/angular/<build-number>#artifacts`)
|
|
||||||
|
|
||||||
#### Archives for each Package
|
#### Archives for each Package
|
||||||
On the "Artifacts" tab, there is a list of links to compressed archives for Angular packages. The
|
On the "Artifacts" tab, there is a list of links to compressed archives for Angular packages. The
|
||||||
|
@ -125,28 +125,28 @@ Triaging PRs is the same as triaging issues, except that the labels `frequency:
|
|||||||
|
|
||||||
PRs also have additional label categories that should be used to signal their state.
|
PRs also have additional label categories that should be used to signal their state.
|
||||||
|
|
||||||
Every triaged PR must have a `PR action` label assigned to it:
|
Every triaged PR must have a `action: *` label assigned to it:
|
||||||
|
|
||||||
* `PR action: discuss`: Discussion is needed, to be led by the author.
|
* `action: discuss`: Discussion is needed, to be led by the author.
|
||||||
* _**Who adds it:** Typically the PR author._
|
* _**Who adds it:** Typically the PR author._
|
||||||
* _**Who removes it:** Whoever added it._
|
* _**Who removes it:** Whoever added it._
|
||||||
* `PR action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
|
* `action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
|
||||||
* _**Who adds it:** Any team member. The caretaker can use it to differentiate PRs pending review from merge-ready PRs._
|
* _**Who adds it:** Any team member. The caretaker can use it to differentiate PRs pending review from merge-ready PRs._
|
||||||
* _**Who removes it:** Whoever added it or the reviewer adding the last missing review._
|
* _**Who removes it:** Whoever added it or the reviewer adding the last missing review._
|
||||||
* `PR action: cleanup`: More work is needed from the author.
|
* `action: cleanup`: More work is needed from the author.
|
||||||
* _**Who adds it:** The reviewer requesting changes to the PR._
|
* _**Who adds it:** The reviewer requesting changes to the PR._
|
||||||
* _**Who removes it:** Either the author (after implementing the requested changes) or the reviewer (after confirming the requested changes have been implemented)._
|
* _**Who removes it:** Either the author (after implementing the requested changes) or the reviewer (after confirming the requested changes have been implemented)._
|
||||||
* `PR action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
|
* `action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
|
||||||
* _**Who adds it:** Typically the PR author._
|
* _**Who adds it:** Typically the PR author._
|
||||||
* _**Who removes it:** Whoever added it._
|
* _**Who removes it:** Whoever added it._
|
||||||
|
|
||||||
|
|
||||||
In addition, PRs can have the following states:
|
In addition, PRs can have the following states:
|
||||||
|
|
||||||
* `PR state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
|
* `state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
|
||||||
* _**Who adds it:** The PR author._
|
* _**Who adds it:** The PR author._
|
||||||
* _**Who removes it:** Whoever added it._
|
* _**Who removes it:** Whoever added it._
|
||||||
* `PR state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
|
* `state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
|
||||||
* _**Who adds it:** Any team member._
|
* _**Who adds it:** Any team member._
|
||||||
* _**Who removes it:** Any team member._
|
* _**Who removes it:** Any team member._
|
||||||
|
|
||||||
@ -162,13 +162,27 @@ This decision is then honored when the PR is being merged by the caretaker.
|
|||||||
|
|
||||||
To communicate the target we use the following labels:
|
To communicate the target we use the following labels:
|
||||||
|
|
||||||
* `PR target: master & patch`: the PR should me merged into the master branch and cherry-picked into the most recent patch branch. All PRs with fixes, docs and refactorings should use this target.
|
Targeting an active release train:
|
||||||
* `PR target: master-only`: the PR should be merged only into the `master` branch. All PRs with new features, API changes or high-risk changes should use this target.
|
|
||||||
* `PR target: patch-only`: the PR should be merged only into the most recent patch branch (e.g. 5.0.x). This target is useful if a `master & patch` PR can't be cleanly cherry-picked into the stable branch and a new PR is needed.
|
|
||||||
* `PR target: LTS-only`: the PR should be merged only into the active LTS branch(es). Only security and critical fixes are allowed in these branches. Always send a new PR targeting just the LTS branch and request review approval from @IgorMinar.
|
|
||||||
* `PR target: TBD`: the target is yet to be determined.
|
|
||||||
|
|
||||||
If a PR is missing the `PR target: *` label, or if the label is set to "TBD" when the PR is sent to the caretaker, the caretaker should reject the PR and request the appropriate target label to be applied before the PR is merged.
|
* `target: major`: Any breaking change
|
||||||
|
* `target: minor`: Any new feature
|
||||||
|
* `target: patch`: Bug fixes, refactorings, documentation changes, etc. that pose no or very low risk of adversely
|
||||||
|
affecting existing applications.
|
||||||
|
|
||||||
|
Special Cases:
|
||||||
|
* `target: rc`: A critical fix for an active release-train while it is in a feature freeze or RC phase
|
||||||
|
* `target: lts`: A criticial fix for a specific release-train that is still within the long term support phase
|
||||||
|
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- To land a change only in a patch/RC branch, without landing it in any other active release-train branch (such
|
||||||
|
as `master`), the patch/RC branch can be targeted in the Github UI with the appropriate
|
||||||
|
`target: patch`/`target: rc` label.
|
||||||
|
- `target: lts` PRs must target the specific LTS branch they would need to merge into in the Github UI, in
|
||||||
|
cases which a change is desired in multiple LTS branches, individual PRs for each LTS branch must be created
|
||||||
|
|
||||||
|
|
||||||
|
If a PR is missing the `target:*` label, it will be marked as pending by the angular robot status checks.
|
||||||
|
|
||||||
|
|
||||||
## PR Approvals
|
## PR Approvals
|
||||||
@ -182,7 +196,7 @@ In any case, the reviewer should actually look through the code and provide feed
|
|||||||
|
|
||||||
Note that approved state does not mean a PR is ready to be merged.
|
Note that approved state does not mean a PR is ready to be merged.
|
||||||
For example, a reviewer might approve the PR but request a minor tweak that doesn't need further review, e.g., a rebase or small uncontroversial change.
|
For example, a reviewer might approve the PR but request a minor tweak that doesn't need further review, e.g., a rebase or small uncontroversial change.
|
||||||
Only the `PR action: merge` label means that the PR is ready for merging.
|
Only the `action: merge` label means that the PR is ready for merging.
|
||||||
|
|
||||||
|
|
||||||
## Special Labels
|
## Special Labels
|
||||||
@ -201,7 +215,7 @@ Only issues with `cla:yes` should be merged into master.
|
|||||||
|
|
||||||
Applying this label to a PR makes the angular.io preview available regardless of the author. [More info](../aio/aio-builds-setup/docs/overview--security-model.md)
|
Applying this label to a PR makes the angular.io preview available regardless of the author. [More info](../aio/aio-builds-setup/docs/overview--security-model.md)
|
||||||
|
|
||||||
### `PR action: merge-assistance`
|
### `action: merge-assistance`
|
||||||
* _**Who adds it:** Any team member._
|
* _**Who adds it:** Any team member._
|
||||||
* _**Who removes it:** Any team member._
|
* _**Who removes it:** Any team member._
|
||||||
|
|
||||||
@ -211,7 +225,7 @@ The comment should be formatted like this: `merge-assistance: <explain what kind
|
|||||||
|
|
||||||
For example, the PR owner might not be a Googler and needs help to run g3sync; or one of the checks is failing due to external causes and the PR should still be merged.
|
For example, the PR owner might not be a Googler and needs help to run g3sync; or one of the checks is failing due to external causes and the PR should still be merged.
|
||||||
|
|
||||||
### `PR action: rerun CI at HEAD`
|
### `action: rerun CI at HEAD`
|
||||||
* _**Who adds it:** Any team member._
|
* _**Who adds it:** Any team member._
|
||||||
* _**Who removes it:** The Angular Bot, once it triggers the CI rerun._
|
* _**Who removes it:** The Angular Bot, once it triggers the CI rerun._
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user